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:
@ -112,7 +112,7 @@ interface LeafViewProps {
|
||||
projectId: string;
|
||||
}
|
||||
|
||||
function LeafView({ id, agent, cwd, vm, parentSplit, projectId }: LeafViewProps) {
|
||||
function LeafView({ id, session, agent, cwd, vm, parentSplit, projectId }: LeafViewProps) {
|
||||
const canMerge = parentSplit !== null && parentSplit.siblings > 1;
|
||||
const { agent: agentGateway } = useGateways();
|
||||
|
||||
@ -133,6 +133,12 @@ function LeafView({ id, agent, cwd, vm, parentSplit, projectId }: LeafViewProps)
|
||||
? (opts: Parameters<typeof agentGateway.launchAgent>[2], onData: (bytes: Uint8Array) => void) =>
|
||||
agentGateway.launchAgent(projectId, agentId, opts, onData)
|
||||
: undefined;
|
||||
// Agent cells re-attach through the agent gateway; plain cells fall back to
|
||||
// the terminal gateway's reattach (handled by TerminalView's default).
|
||||
const reattachOpener = agentGateway && agentId
|
||||
? (sessionId: string, onData: (bytes: Uint8Array) => void) =>
|
||||
agentGateway.reattach(sessionId, onData)
|
||||
: undefined;
|
||||
|
||||
return (
|
||||
<div
|
||||
@ -215,8 +221,17 @@ function LeafView({ id, agent, cwd, vm, parentSplit, projectId }: LeafViewProps)
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
{/* Re-key terminal when the agent changes so xterm re-mounts with the right opener. */}
|
||||
<TerminalView key={`${id}-${agentId ?? "plain"}`} cwd={cwd} open={terminalOpener} />
|
||||
{/* Re-key terminal when the agent changes so xterm re-mounts with the right opener.
|
||||
The cell's persisted session id drives reattach-vs-open so navigating
|
||||
(layout/tab switch) never kills the PTY. */}
|
||||
<TerminalView
|
||||
key={`${id}-${agentId ?? "plain"}`}
|
||||
cwd={cwd}
|
||||
open={terminalOpener}
|
||||
reattach={reattachOpener}
|
||||
sessionId={session}
|
||||
onSessionId={(sid) => void vm.setSession(id, sid)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user