Add realtime terminal tab badges

This commit is contained in:
Federico Jaramillo Martinez
2026-05-11 10:45:56 +02:00
parent 045f4e247a
commit 80cf879e0f
16 changed files with 219 additions and 27 deletions
+5 -1
View File
@@ -1,4 +1,4 @@
import type { GlobalSessionEvent, SessionUiEvent } from "../../shared/apiTypes.js";
import type { GlobalSessionEvent, RealtimeEvent, SessionUiEvent } from "../../shared/apiTypes.js";
import type { WebSocket } from "ws";
export class SessionEventHub {
@@ -30,6 +30,10 @@ export class SessionEventHub {
}
publishGlobal(event: GlobalSessionEvent): void {
this.publishRealtime(event);
}
publishRealtime(event: RealtimeEvent): void {
const payload = JSON.stringify(event);
for (const socket of this.globalSockets) {
if (socket.readyState === socket.OPEN) socket.send(payload);
+1 -1
View File
@@ -15,7 +15,7 @@ await app.register(fastifyWebsocket);
const eventHub = new SessionEventHub();
const sessions = new PiSessionService(eventHub);
const terminals = new TerminalService();
const terminals = new TerminalService(eventHub);
registerSessionRoutes(app, sessions, eventHub);
registerTerminalRoutes(app, terminals);
@@ -26,6 +26,10 @@ export function registerSessionProxyRoutes(app: FastifyInstance, daemon = new Se
bridgeSockets(socket, daemon.connectWebSocket("/sessions/events"));
});
app.get("/api/events", { websocket: true }, (socket) => {
bridgeSockets(socket, daemon.connectWebSocket("/events"));
});
app.all("/api/sessions", (request, reply) => proxy(request, reply));
app.all("/api/sessions/*", (request, reply) => proxy(request, reply));
}
+4
View File
@@ -167,6 +167,10 @@ export function registerSessionRoutes(app: FastifyInstance, sessions: PiSessionS
app.get(`${prefix}/sessions/events`, { websocket: true }, (socket) => {
eventHub.addGlobal(socket);
});
app.get(`${prefix}/events`, { websocket: true }, (socket) => {
eventHub.addGlobal(socket);
});
}
function optionalField<T>(key: string, value: T | undefined): Record<string, T> | object {
+13 -1
View File
@@ -1,6 +1,8 @@
import { EventEmitter } from "node:events";
import { randomUUID } from "node:crypto";
import * as pty from "node-pty";
import type { TerminalUiEvent } from "../../shared/apiTypes.js";
import type { SessionEventHub } from "../realtime/sessionEventHub.js";
const MAX_REPLAY_BUFFER = 200_000;
@@ -22,6 +24,8 @@ interface TerminalRecord extends TerminalInfo {
export class TerminalService {
private readonly terminals = new Map<string, TerminalRecord>();
constructor(private readonly events?: SessionEventHub) {}
list(cwd: string): TerminalInfo[] {
return [...this.terminals.values()]
.filter((terminal) => terminal.cwd === cwd)
@@ -59,9 +63,12 @@ export class TerminalService {
record.exited = true;
record.exitCode = exitCode;
record.events.emit("exit", exitCode);
this.publish({ type: "terminal.exited", terminal: toInfo(record) });
});
this.terminals.set(id, record);
return toInfo(record);
const info = toInfo(record);
this.publish({ type: "terminal.created", terminal: info });
return info;
}
get(id: string): TerminalInfo | undefined {
@@ -101,6 +108,7 @@ export class TerminalService {
this.terminals.delete(id);
terminal.events.removeAllListeners();
if (!terminal.exited) terminal.pty.kill();
this.publish({ type: "terminal.closed", terminalId: id, cwd: terminal.cwd });
}
dispose(): void {
@@ -112,6 +120,10 @@ export class TerminalService {
if (terminal === undefined) throw new Error("Terminal not found");
return terminal;
}
private publish(event: TerminalUiEvent): void {
this.events?.publishRealtime(event);
}
}
function toInfo(record: TerminalRecord): TerminalInfo {