feat(ui): cellule agent stoppé — état explicite + reopen from scratch (#197, QA verte)

Surface UI du LayoutGrid pour un agent de cellule stoppé:

- Affichage d'un état explicite quand runtimeState vaut « stopped » ou « failedStart », avec actions refresh / reopen from scratch (freshStart) et état restarting pendant la relance.

- theme.css: styles .stopped-agent-cell (boutons primary/secondary, états disabled).

Consomme le contrat runtime issu de #196 (AgentRuntimeState stopped/failedStart, freshStart) déjà présent dans develop; aucune modification runtime ici.

Tests QA verts: LayoutGrid.test.tsx, LayoutGrid.chat.test.tsx (rejoué aussi: agent_lifecycle launch_fresh_start_ignores_existing_conversation_id_and_mints_new_one).
This commit is contained in:
2026-08-12 15:34:53 +02:00
parent 7b7b2cec85
commit cdb395307b
4 changed files with 689 additions and 5 deletions

View File

@ -51,6 +51,7 @@ import {
MockProfileGateway,
MockSystemGateway,
MockTerminalGateway,
MockWorkStateGateway,
} from "@/adapters/mock";
import { DIProvider } from "@/app/di";
import { leaves } from "./layout";
@ -222,6 +223,75 @@ describe("LayoutGrid custom agent CLI (#147)", () => {
expect(screen.queryByText(/déjà actif dans une autre cellule/)).toBeNull();
});
it("relaunches a stopped custom CLI agent from scratch (#197)", async () => {
const layout = new MockLayoutGateway();
const agent = new MockAgentGateway();
const profileGateway = new MockProfileGateway();
const terminal = new MockTerminalGateway();
const system = new MockSystemGateway();
const workState = new MockWorkStateGateway();
await profileGateway.configureProfiles([structuredProfile]);
const created = await agent.createAgent("p1", {
name: "Worker",
profileId: structuredProfile.id,
});
const tree = await layout.loadLayout("p1");
const leafId = leaves(tree)[0].id;
await layout.mutateLayout("p1", {
type: "setCellAgent",
target: leafId,
agent: created.id,
});
await layout.mutateLayout("p1", {
type: "setSession",
target: leafId,
session: "dead-structured-session",
});
await layout.mutateLayout("p1", {
type: "setCellConversation",
target: leafId,
conversationId: "old-structured-conversation",
});
window.localStorage.setItem(`idea.agent-cell-mode.p1.${leafId}`, "custom");
workState._setProjectWorkState("p1", {
agents: [
{
agentId: created.id,
name: "Worker",
profileId: structuredProfile.id,
runtimeState: "stopped",
busy: { state: "idle" },
},
],
});
const launchChat = vi.spyOn(agent, "launchAgentChat");
renderGrid({
layout,
agent,
profile: profileGateway,
terminal,
system,
workState,
} as unknown as Gateways);
fireEvent.click(await screen.findByRole("button", { name: "Relancer" }));
await waitFor(() => expect(launchChat).toHaveBeenCalledTimes(1));
expect(launchChat.mock.calls[0]?.[2]).toMatchObject({
cwd: "/home/me/proj",
nodeId: leafId,
freshStart: true,
});
expect(launchChat.mock.calls[0]?.[2].conversationId).toBeUndefined();
await waitFor(async () => {
const updated = await layout.loadLayout("p1");
const leaf = leaves(updated)[0];
expect(leaf.session).toMatch(/^mock-agent-chat-/);
expect(leaf.conversationId).toMatch(/^mock-conversation-/);
});
});
it("closes the structured chat session by id before switching back to native TUI (#170)", async () => {
const layout = new MockLayoutGateway();
const agent = new MockAgentGateway();