From 7ef479639a0715b29e1713d73b2a2977ebac347c Mon Sep 17 00:00:00 2001 From: indifferentketchup Date: Sun, 7 Jun 2026 22:40:27 +0000 Subject: [PATCH] feat(booterm): add PTY session registry + listing endpoint In-memory SessionMeta registry tracks active terminal sessions with paneId, sessionId, projectPath, title, createdAt, lastActivityAt. GET /api/term/sessions returns all active sessions as JSON array. Registry is updated on WS attach and cleaned up on disconnect. --- apps/booterm/src/index.ts | 2 ++ apps/booterm/src/pty/registry.ts | 44 +++++++++++++++++++++++++++++ apps/booterm/src/routes/sessions.ts | 18 ++++++++++++ apps/booterm/src/ws/attach.ts | 4 +++ 4 files changed, 68 insertions(+) create mode 100644 apps/booterm/src/pty/registry.ts create mode 100644 apps/booterm/src/routes/sessions.ts diff --git a/apps/booterm/src/index.ts b/apps/booterm/src/index.ts index 55b075a..317aa61 100644 --- a/apps/booterm/src/index.ts +++ b/apps/booterm/src/index.ts @@ -4,6 +4,7 @@ import { loadConfig } from './config.js'; import { getPool, closeDb } from './db.js'; import { registerHealthRoutes } from './routes/health.js'; import { registerTerminalRoutes } from './routes/terminals.js'; +import { registerSessionRoutes } from './routes/sessions.js'; import { registerWsAttachRoute } from './ws/attach.js'; async function main(): Promise { @@ -33,6 +34,7 @@ async function main(): Promise { registerHealthRoutes(app); registerTerminalRoutes(app, config.TMUX_CONF_PATH); + registerSessionRoutes(app); registerWsAttachRoute(app, config.TMUX_CONF_PATH); const shutdown = async (signal: string) => { diff --git a/apps/booterm/src/pty/registry.ts b/apps/booterm/src/pty/registry.ts new file mode 100644 index 0000000..e989a7e --- /dev/null +++ b/apps/booterm/src/pty/registry.ts @@ -0,0 +1,44 @@ +export interface SessionMeta { + paneId: string; + sessionId: string; + projectPath: string; + title?: string; + createdAt: Date; + lastActivityAt: Date; +} + +const sessions = new Map(); + +export function register( + sessionId: string, + paneId: string, + projectPath: string, + title?: string, +): void { + const now = new Date(); + const existing = sessions.get(paneId); + if (existing) { + existing.lastActivityAt = now; + return; + } + sessions.set(paneId, { + paneId, + sessionId, + projectPath, + title, + createdAt: now, + lastActivityAt: now, + }); +} + +export function unregister(paneId: string): void { + sessions.delete(paneId); +} + +export function list(): SessionMeta[] { + return Array.from(sessions.values()); +} + +export function get(paneId: string): SessionMeta | undefined { + return sessions.get(paneId); +} diff --git a/apps/booterm/src/routes/sessions.ts b/apps/booterm/src/routes/sessions.ts new file mode 100644 index 0000000..a53bd9d --- /dev/null +++ b/apps/booterm/src/routes/sessions.ts @@ -0,0 +1,18 @@ +import type { FastifyInstance } from 'fastify'; +import { list } from '../pty/registry.js'; + +export function registerSessionRoutes(app: FastifyInstance): void { + app.get('/api/term/sessions', async (_req, reply) => { + const active = list(); + return reply.code(200).send({ + sessions: active.map((s) => ({ + paneId: s.paneId, + sessionId: s.sessionId, + projectPath: s.projectPath, + title: s.title ?? null, + createdAt: s.createdAt.toISOString(), + lastActivityAt: s.lastActivityAt.toISOString(), + })), + }); + }); +} diff --git a/apps/booterm/src/ws/attach.ts b/apps/booterm/src/ws/attach.ts index 5aa9201..b51ffa1 100644 --- a/apps/booterm/src/ws/attach.ts +++ b/apps/booterm/src/ws/attach.ts @@ -9,6 +9,7 @@ import { } from '../pty/manager.js'; import { attachPty } from '../pty/pty.js'; import { getUser } from '../auth.js'; +import { register, unregister } from '../pty/registry.js'; export function registerWsAttachRoute(app: FastifyInstance, tmuxConfPath: string): void { app.get<{ @@ -57,6 +58,8 @@ export function registerWsAttachRoute(app: FastifyInstance, tmuxConfPath: string return; } + register(sid, pid, session.project_path); + let handle: IPty; try { handle = attachPty({ @@ -157,6 +160,7 @@ export function registerWsAttachRoute(app: FastifyInstance, tmuxConfPath: string // teardown happens via the /kill route called from the frontend when the // user closes the pane. socket.on('close', () => { + unregister(pid); try { handle.kill(); } catch {