feat(frontend): surface agent web sur WebSocket (cellule agent) (#13)

Lot F4 du chantier server/client mode : le client web expose une cellule
agent branchée sur le PTY WebSocket, en face de agent.launch (B6).

- wsLiveClient.ts : launchAgent sur le transport WebSocket.
- streamGateways.ts : gateway agent alignée sur le contrat serveur B6.
- WebAgentCell.tsx : cellule agent web, câblée dans WebWorkspace et index.
- Tests : agentGateway.test.ts, WebApp.test.tsx.

Validé : frontend 749 tests verts, contrat B6↔F4 aligné (aucun écart de
frame), desktop non régressé.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-15 23:17:24 +02:00
parent 6391d1c75a
commit 5254f16095
7 changed files with 359 additions and 32 deletions

View File

@ -0,0 +1,76 @@
/**
* Web agent cell — ticket #13, lot F4. Thin wiring (no new terminal logic) that
* reuses the existing, transport-neutral {@link TerminalView} to render a CLI
* agent over the WebSocket in web mode.
*
* The agent's CLI runs server-side (B6); the browser is display-only. This
* component just supplies `TerminalView` with the DI **agent gateway** as the
* opener/reattacher:
* - `open` → `agent.launchAgent(projectId, agentId, …)` (frame `agent.launch`,
* unified `terminal.attached` ack; the minted conversation id is carried on the
* returned handle),
* - `reattach` → `agent.reattach(sessionId, …)` (frame `terminal.attach`, no
* relaunch, bounded scrollback repainted) — so a browser reload/reconnect
* resumes the surviving server-side PTY.
*
* The session id is persisted in component state so a re-mount re-attaches rather
* than relaunching, matching the desktop cell's lifecycle. A structured agent is
* refused server-side (`UNSUPPORTED`) and surfaced by `TerminalView`'s own error
* banner — no crash. It touches only gateways via DI (no `@tauri-apps/api`).
*/
import { useCallback, useState } from "react";
import type {
OpenTerminalOptions,
ReattachResult,
TerminalHandle,
} from "@/ports";
import { useGateways } from "@/app/di";
import { TerminalView } from "@/features/terminals";
interface WebAgentCellProps {
/** Owning project id (resolved server-side by `agent.launch`). */
projectId: string;
/** Agent to launch/attach. */
agentId: string;
/** Working directory for the cell (typically the project root). */
cwd: string;
/** Layout leaf id, when the caller tracks one (drives the singleton guard). */
nodeId?: string;
}
export function WebAgentCell({ projectId, agentId, cwd, nodeId }: WebAgentCellProps) {
const { agent } = useGateways();
const [sessionId, setSessionId] = useState<string | null>(null);
const open = useCallback(
(options: OpenTerminalOptions, onData: (bytes: Uint8Array) => void): Promise<TerminalHandle> =>
agent.launchAgent(
projectId,
agentId,
nodeId ? { ...options, nodeId } : options,
onData,
),
[agent, projectId, agentId, nodeId],
);
const reattach = useCallback(
(sid: string, onData: (bytes: Uint8Array) => void): Promise<ReattachResult> =>
agent.reattach(sid, onData),
[agent],
);
return (
<div data-testid="web-agent-cell" className="h-64 w-full">
<TerminalView
cwd={cwd}
agentMode
open={open}
reattach={reattach}
sessionId={sessionId}
onSessionId={setSessionId}
/>
</div>
);
}