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:
@@ -165,7 +165,10 @@ export type PtyCommand =
|
||||
| { type: 'pty:init'; id: string; sessionId: string; config: PtyInitConfig }
|
||||
| { type: 'pty:input'; id: string; sessionId: string; data: string }
|
||||
| { type: 'pty:resize'; id: string; sessionId: string; cols: number; rows: number }
|
||||
| { type: 'pty:close'; id: string; sessionId: string };
|
||||
| { type: 'pty:close'; id: string; sessionId: string }
|
||||
// Enumerate live shells. A panel keeps its session id across unmounts so it can re-attach, which means a
|
||||
// panel deleted for good leaves its shell running with nothing pointing at it. This is how you find one.
|
||||
| { type: 'pty:list'; id: string };
|
||||
|
||||
// PTY events (PTY sidecar → API)
|
||||
export type PtyEvent =
|
||||
@@ -174,4 +177,17 @@ export type PtyEvent =
|
||||
// Scrollback sent on re-attach, which the client may already be showing in part — distinct from
|
||||
// `pty:output` so it can rebuild the screen rather than append a second copy of it.
|
||||
| { type: 'pty:replay'; sessionId: string; data: string }
|
||||
| { type: 'pty:exit'; sessionId: string; exitCode: number; signal?: number };
|
||||
| { type: 'pty:exit'; sessionId: string; exitCode: number; signal?: number }
|
||||
| { type: 'pty:sessions'; id: string; sessions: PtySessionInfo[] };
|
||||
|
||||
/** A live shell, as reported by `pty:list`. `title` is whatever the shell set via OSC 0/2 — usually the
|
||||
* running command — which is what makes an orphan identifiable rather than just a uuid. */
|
||||
export type PtySessionInfo = {
|
||||
sessionId: string;
|
||||
cols: number;
|
||||
rows: number;
|
||||
createdAt: number;
|
||||
lastActivityAt: number;
|
||||
title?: string;
|
||||
pid?: number;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user