fix(application,frontend): restauration correcte d'une cellule agent en CLI custom après changement de projet — #322 (QA verte)
Symptôme: après changement de projet puis retour, une cellule en CLI custom retombait en TUI native et un nouveau switch CLI custom repartait à neuf (aucune conversation reprenable) car le mode préféré de la cellule n'était pas persisté dans l'arbre de layout. - domaine frontend: type PreferredView (tui | chat) et champ preferredView optionnel sur LeafCell; opération de layout setCellPreferredView persistant la surface préférée (tui = défaut, champ omis pour garder les arbres existants compacts) - LayoutGrid: le mode de présentation d'une cellule agent dérive de preferredView persisté; le cache localStorage du mode custom n'est plus qu'un cache de migration secondaire; remount par clé projectId:layoutId:nodeId à chaque changement de projet pour éviter tout état résiduel inter-projets; la conversation/engine session/ session/agentWasRunning de la cellule sont préservés par les bascules chat <-> TUI - useLayout: setCellPreferredView exposé au view model - tests: layout_usecases (persistance chat puis retour TUI en conservant session/conversation/engine/running), layout.test (setCellPreferredView chat persiste, tui omet le champ), LayoutGrid.chat et cellControlsLayering alignés QA verte sur fix/batch-322-323-session-restore-prompt-history. Aucun push.
This commit is contained in:
@ -55,7 +55,7 @@ import {
|
||||
} from "@/adapters/mock";
|
||||
import { DIProvider } from "@/app/di";
|
||||
import { leaves } from "./layout";
|
||||
import { LayoutGrid, shouldFallbackCustomCliMode } from "./LayoutGrid";
|
||||
import { LayoutGrid } from "./LayoutGrid";
|
||||
|
||||
const structuredProfile: AgentProfile = {
|
||||
id: "mock-structured",
|
||||
@ -119,10 +119,11 @@ async function seeded(profile: AgentProfile): Promise<Gateways> {
|
||||
} as unknown as Gateways;
|
||||
}
|
||||
|
||||
function renderGrid(gateways: Gateways) {
|
||||
function renderGrid(gateways: Gateways, projectId = "p1") {
|
||||
const cwd = projectId === "p1" ? "/home/me/proj" : `/home/me/${projectId}`;
|
||||
return render(
|
||||
<DIProvider gateways={gateways}>
|
||||
<LayoutGrid projectId="p1" cwd="/home/me/proj" />
|
||||
<LayoutGrid projectId={projectId} cwd={cwd} />
|
||||
</DIProvider>,
|
||||
);
|
||||
}
|
||||
@ -182,6 +183,109 @@ describe("LayoutGrid custom agent CLI (#147)", () => {
|
||||
expect(screen.getAllByText(/mock-attachment.txt/).length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it("restores preferredView=chat ahead of stale localStorage without a resume warning", async () => {
|
||||
const gateways = await seeded(structuredProfile);
|
||||
const layout = gateways.layout as MockLayoutGateway;
|
||||
const tree = await layout.loadLayout("p1");
|
||||
const leafId = leaves(tree)[0].id;
|
||||
await layout.mutateLayout("p1", {
|
||||
type: "setCellPreferredView",
|
||||
target: leafId,
|
||||
preferredView: "chat",
|
||||
});
|
||||
window.localStorage.setItem(`idea.agent-cell-mode.p1.${leafId}`, "tui");
|
||||
|
||||
renderGrid(gateways);
|
||||
|
||||
expect(await screen.findByTestId("custom-agent-chat-view")).toBeTruthy();
|
||||
expect(screen.queryByRole("alertdialog", { name: "Confirmer le changement de CLI" }))
|
||||
.toBeNull();
|
||||
expect(screen.queryByText(/Aucune conversation reprenable/)).toBeNull();
|
||||
expect(window.localStorage.getItem(`idea.agent-cell-mode.p1.${leafId}`)).toBe("custom");
|
||||
});
|
||||
|
||||
it("restores a project custom CLI cell after switching away and back without rewriting durable ids", 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 agentA = await agent.createAgent("p1", {
|
||||
name: "Worker A",
|
||||
profileId: structuredProfile.id,
|
||||
});
|
||||
const treeA = await layout.loadLayout("p1");
|
||||
const leafA = leaves(treeA)[0];
|
||||
await layout.mutateLayout("p1", {
|
||||
type: "setCellAgent",
|
||||
target: leafA.id,
|
||||
agent: agentA.id,
|
||||
});
|
||||
await layout.mutateLayout("p1", {
|
||||
type: "setCellPreferredView",
|
||||
target: leafA.id,
|
||||
preferredView: "chat",
|
||||
});
|
||||
|
||||
const agentB = await agent.createAgent("p2", {
|
||||
name: "Worker B",
|
||||
profileId: structuredProfile.id,
|
||||
});
|
||||
const treeB = await layout.loadLayout("p2");
|
||||
const leafB = leaves(treeB)[0];
|
||||
await layout.mutateLayout("p2", {
|
||||
type: "setCellAgent",
|
||||
target: leafB.id,
|
||||
agent: agentB.id,
|
||||
});
|
||||
|
||||
const gateways = {
|
||||
layout,
|
||||
agent,
|
||||
profile: profileGateway,
|
||||
terminal,
|
||||
system,
|
||||
workState,
|
||||
} as unknown as Gateways;
|
||||
const view = renderGrid(gateways, "p1");
|
||||
|
||||
expect(await screen.findByTestId("custom-agent-chat-view")).toBeTruthy();
|
||||
await waitFor(async () => {
|
||||
const updated = await layout.loadLayout("p1");
|
||||
const leaf = leaves(updated)[0];
|
||||
expect(leaf.preferredView).toBe("chat");
|
||||
expect(leaf.session).toMatch(/^mock-agent-chat-/);
|
||||
expect(leaf.conversationId).toMatch(/^mock-conversation-mock-agent-chat-/);
|
||||
});
|
||||
const persistedA = leaves(await layout.loadLayout("p1"))[0];
|
||||
|
||||
view.rerender(
|
||||
<DIProvider gateways={gateways}>
|
||||
<LayoutGrid projectId="p2" cwd="/home/me/p2" />
|
||||
</DIProvider>,
|
||||
);
|
||||
await waitFor(() => expect(screen.queryByTestId("custom-agent-chat-view")).toBeNull());
|
||||
|
||||
view.rerender(
|
||||
<DIProvider gateways={gateways}>
|
||||
<LayoutGrid projectId="p1" cwd="/home/me/proj" />
|
||||
</DIProvider>,
|
||||
);
|
||||
|
||||
expect(await screen.findByTestId("custom-agent-chat-view")).toBeTruthy();
|
||||
expect(screen.queryByTestId("terminal-view")).toBeNull();
|
||||
expect(screen.queryByRole("alertdialog", { name: "Confirmer le changement de CLI" }))
|
||||
.toBeNull();
|
||||
expect(screen.queryByText(/Aucune conversation reprenable/)).toBeNull();
|
||||
const restoredA = leaves(await layout.loadLayout("p1"))[0];
|
||||
expect(restoredA.preferredView).toBe("chat");
|
||||
expect(restoredA.session).toBe(persistedA.session);
|
||||
expect(restoredA.conversationId).toBe(persistedA.conversationId);
|
||||
});
|
||||
|
||||
it("keeps custom CLI visible while busy and locks only the composer (#247)", async () => {
|
||||
const gateways = await seeded(structuredProfile);
|
||||
const layout = gateways.layout as MockLayoutGateway;
|
||||
@ -553,6 +657,9 @@ describe("LayoutGrid custom agent CLI (#147)", () => {
|
||||
);
|
||||
expect(stopLive).not.toHaveBeenCalled();
|
||||
await waitFor(() => expect(screen.getByTestId("terminal-view")).toBeTruthy());
|
||||
const updated = await layout.loadLayout("p1");
|
||||
expect(leaves(updated)[0].preferredView).toBeUndefined();
|
||||
expect(window.localStorage.getItem(`idea.agent-cell-mode.p1.${leafId}`)).toBe("tui");
|
||||
});
|
||||
|
||||
it("switches a live native TUI session to custom CLI without stopping it first (#189)", async () => {
|
||||
@ -695,7 +802,7 @@ describe("LayoutGrid custom agent CLI (#147)", () => {
|
||||
await waitFor(() => expect(agent.listAgents).toHaveBeenCalled());
|
||||
expect(window.localStorage.getItem(`idea.agent-cell-mode.p1.${leafId}`)).toBe("custom");
|
||||
expect(screen.queryByTestId("custom-agent-chat-view")).toBeNull();
|
||||
expect(screen.getByTestId("terminal-view")).toBeTruthy();
|
||||
expect(screen.getByTestId("custom-agent-chat-unavailable")).toBeTruthy();
|
||||
|
||||
staleCatalog = false;
|
||||
system.emit({
|
||||
@ -710,7 +817,7 @@ describe("LayoutGrid custom agent CLI (#147)", () => {
|
||||
expect(window.localStorage.getItem(`idea.agent-cell-mode.p1.${leafId}`)).toBe("custom");
|
||||
});
|
||||
|
||||
it("falls back to native TUI once the catalog confirms the pinned profile is incompatible", async () => {
|
||||
it("keeps the custom preference and shows a non-destructive state when the profile is incompatible", async () => {
|
||||
const layout = new MockLayoutGateway();
|
||||
const agent = new MockAgentGateway();
|
||||
const profileGateway = new MockProfileGateway();
|
||||
@ -758,29 +865,16 @@ describe("LayoutGrid custom agent CLI (#147)", () => {
|
||||
profilesLoaded.resolve();
|
||||
|
||||
await waitFor(() =>
|
||||
expect(window.localStorage.getItem(`idea.agent-cell-mode.p1.${leafId}`)).toBe("tui"),
|
||||
expect(screen.getByTestId("custom-agent-chat-unavailable")).toBeTruthy(),
|
||||
);
|
||||
expect(screen.getByTestId("terminal-view")).toBeTruthy();
|
||||
expect(window.localStorage.getItem(`idea.agent-cell-mode.p1.${leafId}`)).toBe("custom");
|
||||
const updated = await layout.loadLayout("p1");
|
||||
expect(leaves(updated)[0].preferredView).toBe("chat");
|
||||
expect(screen.queryByTestId("terminal-view")).toBeNull();
|
||||
expect(screen.queryByTestId("custom-agent-chat-view")).toBeNull();
|
||||
});
|
||||
|
||||
it("does not downgrade a validated custom CLI on a contradictory refetch without a profile-change event (#149)", () => {
|
||||
expect(
|
||||
shouldFallbackCustomCliMode({
|
||||
agentsLoaded: true,
|
||||
profilesLoaded: true,
|
||||
cellMode: "custom",
|
||||
hasPinnedAgent: true,
|
||||
hasPinnedProfile: true,
|
||||
customCliAvailable: false,
|
||||
effectiveCustomCliAvailable: true,
|
||||
hasTrustedCustomCli: true,
|
||||
hasLegitProfileChange: false,
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("falls back to native TUI after a legitimate profile-change event downgrades the profile (#149)", async () => {
|
||||
it("does not overwrite the custom preference after a profile-change downgrade", async () => {
|
||||
const layout = new MockLayoutGateway();
|
||||
const agent = new MockAgentGateway();
|
||||
const profileGateway = new MockProfileGateway();
|
||||
@ -820,9 +914,12 @@ describe("LayoutGrid custom agent CLI (#147)", () => {
|
||||
});
|
||||
|
||||
await waitFor(() =>
|
||||
expect(window.localStorage.getItem(`idea.agent-cell-mode.p1.${leafId}`)).toBe("tui"),
|
||||
expect(screen.getByTestId("custom-agent-chat-unavailable")).toBeTruthy(),
|
||||
);
|
||||
expect(screen.getByTestId("terminal-view")).toBeTruthy();
|
||||
expect(window.localStorage.getItem(`idea.agent-cell-mode.p1.${leafId}`)).toBe("custom");
|
||||
const updated = await layout.loadLayout("p1");
|
||||
expect(leaves(updated)[0].preferredView).toBe("chat");
|
||||
expect(screen.queryByTestId("terminal-view")).toBeNull();
|
||||
expect(screen.queryByTestId("custom-agent-chat-view")).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user