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:
@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user