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:
@ -73,6 +73,15 @@ export function AgentsPanel({ projectId, projectRoot = "" }: AgentsPanelProps) {
|
||||
*/
|
||||
const [activeAgentId, setActiveAgentId] = useState<string | null>(null);
|
||||
|
||||
/**
|
||||
* Live PTY session id of the running agent terminal, keyed by agent id. Lets
|
||||
* the terminal re-attach (rather than re-launch) when the panel re-mounts the
|
||||
* view, so navigating away never kills the agent.
|
||||
*/
|
||||
const [agentSessions, setAgentSessions] = useState<Record<string, string>>(
|
||||
{},
|
||||
);
|
||||
|
||||
const canCreate = newName.trim().length > 0 && !vm.busy;
|
||||
|
||||
async function handleCreate(e: React.FormEvent) {
|
||||
@ -325,10 +334,18 @@ export function AgentsPanel({ projectId, projectRoot = "" }: AgentsPanelProps) {
|
||||
<div className="border-t border-border p-4">
|
||||
<div className="h-96 overflow-hidden rounded-lg border border-border bg-surface">
|
||||
<TerminalView
|
||||
key={activeAgentId}
|
||||
cwd={projectRoot}
|
||||
open={(opts, onData) =>
|
||||
vm.launchAgent(activeAgentId, opts, onData)
|
||||
}
|
||||
reattach={(sessionId, onData) =>
|
||||
gateways.agent.reattach(sessionId, onData)
|
||||
}
|
||||
sessionId={agentSessions[activeAgentId] ?? null}
|
||||
onSessionId={(sid) =>
|
||||
setAgentSessions((prev) => ({ ...prev, [activeAgentId]: sid }))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user