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>
156 lines
5.9 KiB
TypeScript
156 lines
5.9 KiB
TypeScript
/**
|
|
* 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`.
|
|
*
|
|
* 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 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() {}
|
|
open() {}
|
|
onData() {
|
|
return { dispose() {} };
|
|
}
|
|
onResize() {
|
|
return { dispose() {} };
|
|
}
|
|
write() {}
|
|
dispose() {}
|
|
get cols() {
|
|
return 80;
|
|
}
|
|
get rows() {
|
|
return 24;
|
|
}
|
|
},
|
|
}));
|
|
vi.mock("@xterm/addon-fit", () => ({
|
|
FitAddon: class {
|
|
fit() {}
|
|
},
|
|
}));
|
|
vi.mock("@xterm/xterm/css/xterm.css", () => ({}));
|
|
|
|
// jsdom has no ResizeObserver; TerminalView installs one after `term.open`.
|
|
if (typeof globalThis.ResizeObserver === "undefined") {
|
|
globalThis.ResizeObserver = class {
|
|
observe() {}
|
|
unobserve() {}
|
|
disconnect() {}
|
|
} as unknown as typeof ResizeObserver;
|
|
}
|
|
|
|
import type { Gateways } from "@/ports";
|
|
import { MockAgentGateway, MockLayoutGateway, MockSystemGateway, MockTerminalGateway } from "@/adapters/mock";
|
|
import { DIProvider } from "@/app/di";
|
|
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(): 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" });
|
|
|
|
// Pin the agent onto the single leaf.
|
|
const tree = await layout.loadLayout("p1");
|
|
const leafId = leaves(tree)[0].id;
|
|
await layout.mutateLayout("p1", { type: "setCellAgent", target: leafId, agent: agent.id });
|
|
|
|
const gateways = { layout, agent: agentGateway, terminal, system } as unknown as Gateways;
|
|
return { gateways, layout, agentGateway };
|
|
}
|
|
|
|
function renderGrid(gateways: Gateways) {
|
|
return render(
|
|
<DIProvider gateways={gateways}>
|
|
<LayoutGrid projectId="p1" cwd="/home/me/proj" />
|
|
</DIProvider>,
|
|
);
|
|
}
|
|
|
|
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 = {
|
|
layout,
|
|
agent: new MockAgentGateway(),
|
|
terminal: new MockTerminalGateway(),
|
|
system: new MockSystemGateway(),
|
|
} as unknown as Gateways;
|
|
|
|
renderGrid(gateways);
|
|
|
|
await rtlWaitFor(() => expect(screen.getByTestId("layout-leaf")).toBeTruthy());
|
|
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();
|
|
renderGrid(gateways);
|
|
|
|
await rtlWaitFor(() => expect(screen.getByTestId("layout-leaf")).toBeTruthy());
|
|
expect(screen.getByTestId("terminal-view")).toBeTruthy();
|
|
expect(screen.queryByTestId("agent-chat-view")).toBeNull();
|
|
});
|
|
|
|
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("terminal-view")).toBeTruthy());
|
|
expect(screen.queryByTestId("agent-chat-view")).toBeNull();
|
|
});
|
|
});
|