fix(terminals): decouple PTY lifecycle from view lifecycle (no kill on navigation)
Navigating (layout/tab switch) tore the xterm view down and called handle.close(), killing the backend PTY and cutting off running AIs. Now the view's cleanup only detaches; only an explicit user action kills a PTY. Backend: - PortablePtyAdapter: per-session scrollback ring buffer (~100KB, most recent) + re-subscribable fan-out broadcast replacing the single-take output_rx. Reader thread feeds both the ring buffer and current subscribers; on EOF it closes subscribers (streams end) while keeping scrollback for late re-attach. - PtyPort: new scrollback() method; subscribe_output is now re-subscribable (all impls + test fakes updated). - reattach_terminal IPC command: returns scrollback and re-wires a fresh output channel on the live session without re-spawning. - CloseRequested hook kills all live PTYs cleanly on app shutdown. - TerminalSessions::handles() to enumerate live sessions at shutdown. Frontend: - TerminalHandle.detach(); TerminalGateway/AgentGateway.reattach() + mocks. - TerminalView cleanup detaches (never close); on mount it re-attaches to a persisted session (repainting scrollback) instead of opening a new PTY. - LayoutGrid persists the cell's session id via setSession; AgentsPanel tracks per-agent session ids — both drive reattach-vs-open. Tests: ring buffer bounds to 100KB keeping newest bytes; scrollback retained; re-subscription delivers post-reattach output; TerminalView detaches (not closes) on unmount and reattaches with a known session; mock detach/reattach. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@ -72,6 +72,16 @@ export interface AgentGateway {
|
||||
options: OpenTerminalOptions,
|
||||
onData: (bytes: Uint8Array) => void,
|
||||
): Promise<TerminalHandle>;
|
||||
/**
|
||||
* Re-attaches to an agent's already-running PTY (same backend mechanism as
|
||||
* {@link TerminalGateway.reattach}; agent sessions share the session-based
|
||||
* terminal commands). Used when an agent cell's view re-mounts after a
|
||||
* navigation/layout change, so the agent is never killed.
|
||||
*/
|
||||
reattach(
|
||||
sessionId: string,
|
||||
onData: (bytes: Uint8Array) => void,
|
||||
): Promise<ReattachResult>;
|
||||
}
|
||||
|
||||
/** Options for opening a terminal. */
|
||||
@ -98,7 +108,19 @@ export interface TerminalHandle {
|
||||
write(data: Uint8Array): Promise<void>;
|
||||
/** Resizes the PTY. */
|
||||
resize(rows: number, cols: number): Promise<void>;
|
||||
/** Kills the PTY and stops the output stream. */
|
||||
/**
|
||||
* Detaches the **view** from the PTY without killing it: stops the local
|
||||
* output subscription so a torn-down view (navigation / layout change) stops
|
||||
* receiving bytes, while the backend PTY keeps running. The session can later
|
||||
* be re-attached via {@link TerminalGateway.reattach}.
|
||||
*
|
||||
* This is the lifecycle the view's cleanup must use — never {@link close}.
|
||||
*/
|
||||
detach(): void;
|
||||
/**
|
||||
* Kills the PTY and stops the output stream. Reserved for an **explicit** user
|
||||
* action (closing the terminal); navigation must never call this.
|
||||
*/
|
||||
close(): Promise<void>;
|
||||
}
|
||||
|
||||
@ -115,6 +137,27 @@ export interface TerminalGateway {
|
||||
options: OpenTerminalOptions,
|
||||
onData: (bytes: Uint8Array) => void,
|
||||
): Promise<TerminalHandle>;
|
||||
/**
|
||||
* Re-attaches to an **already-running** PTY identified by `sessionId` (after a
|
||||
* view was torn down by navigation/layout change). Returns the live handle and
|
||||
* the retained scrollback, which the caller repaints into xterm before the new
|
||||
* output stream (`onData`) starts delivering subsequent bytes. Does NOT
|
||||
* re-spawn the process.
|
||||
*
|
||||
* Rejects if the session is no longer alive (the caller then opens fresh).
|
||||
*/
|
||||
reattach(
|
||||
sessionId: string,
|
||||
onData: (bytes: Uint8Array) => void,
|
||||
): Promise<ReattachResult>;
|
||||
}
|
||||
|
||||
/** The outcome of {@link TerminalGateway.reattach}. */
|
||||
export interface ReattachResult {
|
||||
/** The live terminal handle for the re-attached session. */
|
||||
handle: TerminalHandle;
|
||||
/** The retained scrollback bytes to repaint before the live stream resumes. */
|
||||
scrollback: Uint8Array;
|
||||
}
|
||||
|
||||
/** Projects: create/open/close/list (L2). */
|
||||
|
||||
Reference in New Issue
Block a user