chore(wip): checkpoint P8/C avant chantier Codex inter-agents

Sauvegarde de l'arbre de travail en cours (persistance P8, conversations
C-series, write-portal frontend, médiation d'entrée) avant d'attaquer le
support de la délégation inter-agents pour les profils Codex.

Le round-trip inter-agent question/réponse est couvert sans tokens par
les tests loopback existants (state::mcp_e2e_loopback_tests).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-13 21:42:53 +02:00
parent 4509f0db9d
commit fdcf16c387
76 changed files with 3783 additions and 1404 deletions

View File

@ -3,11 +3,14 @@
* {@link TerminalGateway} port (or a custom opener), and fits it to its container:
*
* - PTY output (gateway `onData`) → `term.write(bytes)`.
* - xterm `onData` (keystrokes) → `handle.write(bytes)` **only for a plain
* (non-agent) cell** (a raw shell). In **agent mode** (`agentMode`, lot F2,
* cadrage §4.2) keystrokes are NOT written to the PTY: input is mediated by
* IdeA through {@link MediatedInput}. The PTY **output** path stays live and
* INCHANGED in both modes (xterm is always the raw output view).
* - xterm `onData` (keystrokes) → `handle.write(bytes)` in **both** modes: the
* agent cell is a **native terminal** (ARCHITECTURE §20), so human keystrokes
* (Enter included) reach the PTY unconditionally, exactly like a raw shell.
* In agent mode the keystrokes are additionally reported to the write-portal
* (a {@link WritePortal} supplied via `portal`) which (a) keeps a *human line*
* counter (+1 per printable, reset on Enter/Ctrl-C) and (b) can momentarily
* **suspend** the keystroke relay while it injects a delegation. The PTY
* **output** path stays live and unchanged in both modes.
* - container resize (fit addon) → `handle.resize(rows, cols)`.
*
* Pure presentation: it only knows the port, never `invoke()`/`Channel`
@ -40,6 +43,7 @@ import type {
OpenTerminalOptions,
ReattachResult,
TerminalHandle,
WritePortal,
} from "@/ports";
interface TerminalViewProps {
@ -76,14 +80,21 @@ interface TerminalViewProps {
*/
onSessionId?: (sessionId: string) => void;
/**
* Agent mode (lot F2, cadrage §4.2). When `true` the cell hosts an agent, so
* keystrokes (`term.onData`) are **not** forwarded to the PTY — input is
* mediated by IdeA through {@link MediatedInput} rendered under the terminal.
* The PTY **output** stays live and unchanged (xterm remains the raw output
* view). When `false`/absent the cell is a plain shell: keystrokes go straight
* to the PTY (current behaviour). Defaults to `false`.
* Agent mode (ARCHITECTURE §20). When `true` the cell hosts an agent and the
* terminal is **native**: keystrokes (`term.onData`) reach the PTY exactly
* like a plain shell, AND are reported to the {@link WritePortal} (`portal`)
* for line counting / suspension. When `false`/absent the cell is a plain
* shell with no portal. Defaults to `false`.
*/
agentMode?: boolean;
/**
* The write-portal for this agent cell (ARCHITECTURE §20). Only meaningful in
* agent mode: it receives keystroke reports, gates the relay via
* {@link WritePortal.isSuspended}, and is given the live handle so it can
* inject delegations through the same single PTY writer. Absent for plain
* cells.
*/
portal?: WritePortal;
}
export function TerminalView({
@ -93,6 +104,7 @@ export function TerminalView({
sessionId,
onSessionId,
agentMode = false,
portal,
}: TerminalViewProps) {
const { terminal } = useGateways();
const containerRef = useRef<HTMLDivElement | null>(null);
@ -116,6 +128,8 @@ export function TerminalView({
terminalRef.current = terminal;
const agentModeRef = useRef(agentMode);
agentModeRef.current = agentMode;
const portalRef = useRef(portal);
portalRef.current = portal;
useEffect(() => {
const container = containerRef.current;
@ -151,14 +165,22 @@ export function TerminalView({
let handle: TerminalHandle | null = null;
const encoder = new TextEncoder();
// Keystroke → PTY path. In **agent mode** (F2) keystrokes are mediated by
// IdeA (MediatedInput) and must NOT reach the PTY here; we drop them so the
// raw terminal is output-only for the human. In **plain mode** keystrokes go
// straight to the PTY (raw shell), buffering any that arrive before the PTY
// finished opening. The output path below is unchanged in both modes.
// Keystroke → PTY path. The agent cell is a **native terminal**
// (ARCHITECTURE §20): keystrokes reach the PTY exactly like a plain shell.
// In agent mode we additionally (1) report the keystroke to the write-portal
// for line counting, and (2) honour the portal's `isSuspended()` flag, which
// is raised while the portal injects a delegation so the human keystroke does
// not race the injected text. Keystrokes arriving before the PTY opened are
// buffered. The output path below is unchanged in both modes.
let pending = "";
const onKey = term.onData((data) => {
if (agentModeRef.current) return;
const portal = portalRef.current;
if (agentModeRef.current && portal) {
// Always report for counting (even while suspended — the portal decides
// what counts), then drop the relay while the portal is injecting.
portal.onHumanData(data);
if (portal.isSuspended()) return;
}
if (handle) void handle.write(encoder.encode(data));
else pending += data;
});
@ -176,6 +198,9 @@ export function TerminalView({
return;
}
handle = h;
// Hand the live handle to the write-portal so it can inject delegations
// through the SAME single PTY writer (no second physical writer).
if (agentModeRef.current) portalRef.current?.bindHandle(h);
if (pending) {
void h.write(encoder.encode(pending));
pending = "";
@ -268,6 +293,7 @@ export function TerminalView({
if (rafId) cancelAnimationFrame(rafId);
ro.disconnect();
onKey.dispose();
portalRef.current?.unbindHandle();
// DETACH, never close: tearing the view down (navigation / layout change)
// must leave the backend PTY running so the AI isn't cut off. Killing the
// PTY is an explicit user action handled elsewhere.