feat(agent): conversation par paire + entrée médiée + pivot terminal/MCP

Coeur inter-agents consolidé et surface front réalignée sur la décision
"terminal natif PTY, pas d'UI chat" (Option 1).

Domaine
- nouveaux modules conversation, mailbox, input, fileguard (ports + types)
- orchestrator/profile/events étendus (conversation par paire, FIFO)

Application / Infrastructure
- orchestrator/service + context_guard : sérialisation FIFO par agent,
  garde RW mémoire/contexte, dispatch ask/reply
- adapters in-memory conversation / mailbox / input / fileguard
- registry session + lifecycle agent durcis (1 agent = 1 session vivante)
- outils MCP idea_* alignés sur le nouveau dispatch

Frontend
- MediatedInput + useAgentBusy : entrée utilisateur médiée par IdeA,
  terminal = vue sortie inchangée
- suppression de la vue chat structurée (AgentChatView) — abandonnée
- adapter input + ports mis à jour

Divers
- .ideai/ : mémoire projet + briefs de cadrage versionnés ;
  requests/ runtime ignoré ; agents projet réels (DevBackend/DevFrontend/QA)

Tests : Rust (domain/application/infrastructure/app-tauri) + front (346) verts.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-12 07:33:04 +02:00
parent 5f45c22941
commit eca2ba95c4
61 changed files with 9491 additions and 2512 deletions

View File

@ -1,26 +1,27 @@
/**
* D5 — `LayoutGrid` cell-kind routing (§17.6): an agent cell whose launch reports
* `cellKind:"chat"` must render an {@link AgentChatView}; every other cell (plain
* or a `pty` agent) keeps the unchanged {@link TerminalView}. Wired through the
* real {@link DIProvider} with the in-memory mocks, exactly like
* `LayoutGrid.test.tsx`.
* F-1 — `LayoutGrid` cell routing (Option 1, Terminal + MCP): **every** agent
* cell renders the raw {@link TerminalView}; no structured chat view is ever
* mounted. This replaces the former §17.6 `cellKind:"chat"` routing — the human
* view is now the native interactive PTY, and cross-model delegation flows
* through MCP tools, not a chat view. Wired through the real {@link DIProvider}
* with the in-memory mocks, exactly like `LayoutGrid.test.tsx`.
*
* Routing to chat is *derived from the launch* (the leaf starts as a terminal
* and swaps once the handle's `cellKind` arrives). Under jsdom xterm's `open`
* may bail, so the terminal opener that triggers the launch might not run; these
* tests therefore assert the *reachable* outcomes without depending on xterm:
* - a plain cell renders the terminal view and NEVER a chat view (hard);
* - when the launch does run, a chat agent ends up rendering the chat view.
* The decisive case: an agent cell always renders the terminal and never swaps
* to a chat view (the structured chat surface was removed in the F-2 cleanup).
*
* Under jsdom xterm's `open` may bail, so the opener that triggers the launch
* might not run; we therefore stub xterm (as in the original test) so the launch
* does fire and we genuinely exercise the post-launch routing — which must stay
* on the terminal regardless of the reported kind.
*/
import { describe, it, expect, vi } from "vitest";
import { render, screen, waitFor as rtlWaitFor } from "@testing-library/react";
// Make xterm "wire up" under jsdom: the real `Terminal.open` throws without a
// layout engine, which makes `TerminalView`'s effect bail before it ever calls
// the opener — so the launch (and thus the chat-routing swap) would never fire.
// A minimal stub lets `term.open` succeed, the opener run, and the leaf derive
// its `cellKind` from the launch exactly as in the app. We do NOT stub the chat
// view or the routing — only xterm, the headless-only obstacle.
// the opener — so the launch would never fire. A minimal stub lets `term.open`
// succeed and the opener run, so the launch (and any routing it could trigger)
// is genuinely exercised. We do NOT stub the routing — only xterm.
vi.mock("@xterm/xterm", () => ({
Terminal: class {
loadAddon() {}
@ -64,16 +65,17 @@ import { leaves } from "./layout";
import { LayoutGrid } from "./LayoutGrid";
/** Seeds an agent in the gateway and pins it onto the (single) leaf cell. */
async function seedPinnedAgent(opts: {
chat: boolean;
}): Promise<{ gateways: Gateways; layout: MockLayoutGateway; agentGateway: MockAgentGateway }> {
async function seedPinnedAgent(): Promise<{
gateways: Gateways;
layout: MockLayoutGateway;
agentGateway: MockAgentGateway;
}> {
const layout = new MockLayoutGateway();
const agentGateway = new MockAgentGateway();
const terminal = new MockTerminalGateway();
const system = new MockSystemGateway();
const agent = await agentGateway.createAgent("p1", { name: "Worker", profileId: "claude" });
if (opts.chat) agentGateway._setChatAgents([agent.id]);
// Pin the agent onto the single leaf.
const tree = await layout.loadLayout("p1");
@ -92,7 +94,7 @@ function renderGrid(gateways: Gateways) {
);
}
describe("LayoutGrid cell-kind routing (§17.6)", () => {
describe("LayoutGrid cell routing (F-1, Terminal + MCP)", () => {
it("a plain (agent-less) cell renders the terminal view, never a chat view", async () => {
const layout = new MockLayoutGateway();
const gateways = {
@ -105,34 +107,49 @@ describe("LayoutGrid cell-kind routing (§17.6)", () => {
renderGrid(gateways);
await rtlWaitFor(() => expect(screen.getByTestId("layout-leaf")).toBeTruthy());
// Non-regression: the plain path is the terminal, and there is no chat view.
expect(screen.getByTestId("terminal-view")).toBeTruthy();
expect(screen.queryByTestId("agent-chat-view")).toBeNull();
});
it("a pty agent cell renders the terminal view, never a chat view", async () => {
const { gateways } = await seedPinnedAgent({ chat: false });
const { gateways } = await seedPinnedAgent();
renderGrid(gateways);
await rtlWaitFor(() => expect(screen.getByTestId("layout-leaf")).toBeTruthy());
// A pty agent keeps the unchanged terminal path; the chat view never appears.
expect(screen.getByTestId("terminal-view")).toBeTruthy();
expect(screen.queryByTestId("agent-chat-view")).toBeNull();
});
it("a chat agent cell routes to the chat view once its launch reports cellKind 'chat'", async () => {
const { gateways } = await seedPinnedAgent({ chat: true });
it("re-mounting a known agent cell (persisted session) repaints as a terminal, never chat", async () => {
const layout = new MockLayoutGateway();
const agentGateway = new MockAgentGateway();
const terminal = new MockTerminalGateway();
const system = new MockSystemGateway();
const agent = await agentGateway.createAgent("p1", { name: "Worker", profileId: "claude" });
// Seed a persisted session on the leaf — the pre-F-1 path would have re-mounted
// such a known agent cell as a chat view; now it must always be a terminal.
const tree = await layout.loadLayout("p1");
const leafId = leaves(tree)[0].id;
await layout.mutateLayout("p1", { type: "setCellAgent", target: leafId, agent: agent.id });
await layout.mutateLayout("p1", {
type: "setSession",
target: leafId,
session: "running-session",
});
const gateways = { layout, agent: agentGateway, terminal, system } as unknown as Gateways;
// First mount.
const { unmount } = renderGrid(gateways);
await rtlWaitFor(() => expect(screen.getByTestId("terminal-view")).toBeTruthy());
expect(screen.queryByTestId("agent-chat-view")).toBeNull();
unmount();
// Re-mount (as after a tab/layout navigation): the known agent cell with its
// persisted session must repaint as a terminal — never a chat view.
renderGrid(gateways);
await rtlWaitFor(() => expect(screen.getByTestId("layout-leaf")).toBeTruthy());
// The leaf starts as a terminal and swaps to the chat view once the derived
// `cellKind` ("chat") arrives from the launch (xterm is stubbed so the
// opener actually runs). This is the real §17.6 routing.
await rtlWaitFor(() =>
expect(screen.queryByTestId("agent-chat-view")).toBeTruthy(),
);
// Once routed to chat, the terminal view is gone for that cell.
expect(screen.queryByTestId("terminal-view")).toBeNull();
await rtlWaitFor(() => expect(screen.getByTestId("terminal-view")).toBeTruthy());
expect(screen.queryByTestId("agent-chat-view")).toBeNull();
});
});