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:
@ -19,8 +19,10 @@ import type {
|
||||
AgentGateway,
|
||||
CreateAgentInput,
|
||||
OpenTerminalOptions,
|
||||
ReattachResult,
|
||||
TerminalHandle,
|
||||
} from "@/ports";
|
||||
import { makeTerminalHandle } from "./terminal";
|
||||
|
||||
/** Wire shape returned by the `launch_agent` command (mirrors `open_terminal`). */
|
||||
interface LaunchAgentResponse {
|
||||
@ -87,22 +89,26 @@ export class TauriAgentGateway implements AgentGateway {
|
||||
onOutput: channel,
|
||||
});
|
||||
|
||||
const sessionId = res.sessionId;
|
||||
return makeTerminalHandle(res.sessionId, channel);
|
||||
}
|
||||
|
||||
async reattach(
|
||||
sessionId: string,
|
||||
onData: (bytes: Uint8Array) => void,
|
||||
): Promise<ReattachResult> {
|
||||
// Agent sessions reattach through the same session-based `reattach_terminal`
|
||||
// command as plain terminals (the PTY is identified by its session id).
|
||||
const channel = new Channel<number[]>();
|
||||
channel.onmessage = (chunk) => onData(Uint8Array.from(chunk));
|
||||
|
||||
const res = await invoke<{ sessionId: string; scrollback: number[] }>(
|
||||
"reattach_terminal",
|
||||
{ sessionId, onOutput: channel },
|
||||
);
|
||||
|
||||
return {
|
||||
sessionId,
|
||||
async write(data: Uint8Array): Promise<void> {
|
||||
await invoke("write_terminal", {
|
||||
request: { sessionId, data: Array.from(data) },
|
||||
});
|
||||
},
|
||||
async resize(rows: number, cols: number): Promise<void> {
|
||||
await invoke("resize_terminal", {
|
||||
request: { sessionId, rows, cols },
|
||||
});
|
||||
},
|
||||
async close(): Promise<void> {
|
||||
await invoke("close_terminal", { sessionId });
|
||||
},
|
||||
handle: makeTerminalHandle(res.sessionId, channel),
|
||||
scrollback: Uint8Array.from(res.scrollback),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user