terminals: re-attach on reopen, and make orphaned shells findable
Closing a terminal panel abandoned its shell. TerminalWrapper deleted the panel -> session mapping on *unmount*, so any layout or route change generated a fresh uuid on the way back and left the old shell running: alive, unreachable, and never killed, because nothing has ever sent pty:close. The mapping now outlives the mount, so reopening a panel re-attaches to the shell you left — which is also what finally makes the sidecar's replay buffer worth having. It is persisted dashboard state, so this survives a reload too. That trades an invisible leak for a visible one: a panel deleted for good still leaves its shell behind. So `pty:list` now enumerates live sessions, and GET /api/terminal/sessions + DELETE /api/terminal/sessions/:id expose them. pty:close finally has a sender. Each session carries createdAt, lastActivityAt, pid, and the title the shell sets for itself via OSC 0/2 — usually the running command, which is what turns "some uuid" into "the one running claude" when you are deciding what to kill. Killing on unmount is still not an option: it needs the panel system to distinguish a real close from an incidental remount, which it cannot currently do. Also raises the sidecar replay buffer from 50KB to 512KB — 50KB was about one long agent turn, so reconnecting mid-task showed you the tail and nothing before it — and cuts the buffer on a line boundary rather than a byte offset. A blind slice can land inside an escape sequence, and the replay then opens with the tail of a colour or cursor-move code, which xterm renders as garbage or applies as a real instruction. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,32 @@
|
||||
import { createRouter } from '../../create-router';
|
||||
import { sendPtyCommandAsync, sendPtyCommand, isTerminalConnected } from '@@/sidecar-registry';
|
||||
|
||||
// Same shape as the bridge's own counter next door — correlation ids only need to be unique per process.
|
||||
let idCounter = 0;
|
||||
const nextId = (): string => `pty_${Date.now()}_${++idCounter}`;
|
||||
|
||||
// Live shells, and a way to kill one.
|
||||
//
|
||||
// A terminal panel keeps its session id across unmounts so reopening it re-attaches to the shell you left
|
||||
// running (TerminalWrapper). The cost of that is a panel deleted for good leaves its shell alive with
|
||||
// nothing pointing at it — `pty:close` has a handler but, until this router, had no sender at all. These
|
||||
// two endpoints are how an orphan becomes visible and killable instead of just leaking.
|
||||
export const terminalRouter = createRouter();
|
||||
|
||||
// GET /terminal/sessions → { sessions: PtySessionInfo[] }
|
||||
terminalRouter.get('/sessions', async (ctx) => {
|
||||
if (!isTerminalConnected()) return ctx.json({ sessions: [] });
|
||||
|
||||
const res = await sendPtyCommandAsync({ type: 'pty:list', id: nextId() });
|
||||
if (res.type !== 'pty:sessions') return ctx.json({ error: 'unexpected response' }, 502);
|
||||
return ctx.json({ sessions: res.sessions });
|
||||
});
|
||||
|
||||
// DELETE /terminal/sessions/:sessionId — kills the shell. Fire-and-forget: the sidecar answers the death
|
||||
// on the `pty:exit` broadcast that any attached client is already listening to, not as a reply here.
|
||||
terminalRouter.delete('/sessions/:sessionId', (ctx) => {
|
||||
if (!isTerminalConnected()) return ctx.json({ error: 'terminal sidecar not available' }, 503);
|
||||
|
||||
sendPtyCommand({ type: 'pty:close', id: nextId(), sessionId: ctx.req.param('sessionId') });
|
||||
return ctx.json({ ok: true });
|
||||
});
|
||||
Reference in New Issue
Block a user