diff --git a/.ideai/tickets/216/carnet.md b/.ideai/tickets/216/carnet.md
index d62684d5..b42f27de 100644
--- a/.ideai/tickets/216/carnet.md
+++ b/.ideai/tickets/216/carnet.md
@@ -1,8 +1,8 @@
---
issueRef: "#216"
-version: 4
+version: 6
updatedBy: {"kind":"agent","agent_id":"a6c6ea12-bfc6-4bdc-8031-324102dfa34d"}
-updatedAt: 1786813039107
+updatedAt: 1786813140292
---
## Triage 2026-08-15
- Symptôme rapporté: dans la CLI custom, impression qu'après le premier `Final`, il n'y a plus de progress remontés.
@@ -38,3 +38,10 @@ updatedAt: 1786813039107
- Tests QA:
- `npm test -- CustomAgentChatView.test.tsx` => OK
- `npm run typecheck` => OK
+
+## Git
+- Commit fix: `3582b25` — `fix(cli): affiche les progress reçus après Final sans rouvrir le tour — #216`
+- Commit bookkeeping: `7fd47b3` — `chore(idea): bookkeeping #216 QA verte + clôtures #214/#215`
+- Merge local: `02715a0` — `merge(cli): progress après Final dans la CLI custom — #216 (QA verte)`
+- Branche finale active: `develop`
+- Aucun push effectué.
diff --git a/.ideai/tickets/216/issue.md b/.ideai/tickets/216/issue.md
index 84857476..e5d07c7d 100644
--- a/.ideai/tickets/216/issue.md
+++ b/.ideai/tickets/216/issue.md
@@ -2,7 +2,7 @@
id: "81ef20fa-57bf-468c-b396-4f1ee2c61c08"
number: 216
title: "CLI custom: plus de progress remontés après le premier Final"
-status: "qa"
+status: "closed"
priority: "high"
sprint: null
links: []
@@ -11,7 +11,7 @@ attachments: []
createdBy: {"kind":"agent","agent_id":"a6c6ea12-bfc6-4bdc-8031-324102dfa34d"}
updatedBy: {"kind":"agent","agent_id":"a6c6ea12-bfc6-4bdc-8031-324102dfa34d"}
createdAt: 1786812380073
-updatedAt: 1786813039107
-version: 4
+updatedAt: 1786813140292
+version: 6
---
Investigation et correction du comportement où la CLI custom semble ne plus afficher/remonter les événements de progression après un premier message Final. Vérifier si le bug est purement d'affichage ou si le flux de progress s'arrête réellement.
\ No newline at end of file
diff --git a/.ideai/tickets/217/carnet.md b/.ideai/tickets/217/carnet.md
new file mode 100644
index 00000000..e26a100d
--- /dev/null
+++ b/.ideai/tickets/217/carnet.md
@@ -0,0 +1,49 @@
+---
+issueRef: "#217"
+version: 6
+updatedBy: {"kind":"agent","agent_id":"a6c6ea12-bfc6-4bdc-8031-324102dfa34d"}
+updatedAt: 1786815105743
+---
+## Triage 2026-08-15
+- Symptôme rapporté: la conversation disparaît parfois quand l'utilisateur passe de son layout agent au layout Git Graph puis revient.
+- Cadrage UX: un switch de layout ne doit jamais réinitialiser la conversation; au retour, il faut restaurer transcript connu + état de réattachement explicite au lieu d'une surface vide.
+- Cadrage Architect: cause probable côté projection frontend / corrélation de réattache (`conversationId`/`sessionId`) plus qu'une perte backend réelle.
+- Couches à inspecter d'abord: `frontend/src/features/layout/LayoutGrid.tsx`, `frontend/src/features/agents/CustomAgentChatView.tsx`, hooks layout frontend, puis `reattach_*` côté app-tauri si nécessaire.
+- Branche locale créée par Git: `fix/217-layout-gitgraph-conversation-loss` depuis `develop` @ `02715a0`.
+
+## Diagnostic DevFrontend
+- Bug confirmé côté frontend.
+- Cause: au retour de layout, `CustomAgentChatView` savait reprendre via `sessionId`, mais pas via `conversationId` seul. Si le layout revenait avec conversation persistée mais sans session réattachable, la vue repartait vide et la reprise n'appliquait pas le scrollback.
+- Correctif initial appliqué:
+ - cache UI ajouté par `conversationId`, en plus du cache par `sessionId`
+ - restauration initiale depuis le cache conversation quand `sessionId` manque
+ - reprise au retour avec `conversationId` connu et scrollback appliqué, sans relance froide implicite
+- QA a trouvé un gap: la vue repeignait depuis le cache mais relançait encore `launchAgentChat` quand `sessionId` était nul dans les props.
+- Correctif complémentaire livré:
+ - le cache conversation conserve aussi le `sessionId`
+ - le cache n'écrase plus une entrée utile par un état vide sans session
+ - au retour, si le cache conversation porte un `sessionId`, la vue réattache cette session et sort sans appeler `launchAgentChat`
+- Fichiers modifiés:
+ - `frontend/src/features/agents/CustomAgentChatView.tsx`
+ - `frontend/src/features/agents/CustomAgentChatView.test.tsx`
+ - `frontend/src/features/layout/LayoutGrid.chat.test.tsx`
+- Tests DevFrontend finaux:
+ - `npm test -- CustomAgentChatView.test.tsx` => OK (50)
+ - `npm test -- LayoutGrid.chat.test.tsx` => OK (14)
+ - `npm run typecheck` => OK
+
+## Diagnostic DevBackend
+- Pas de bug backend/app-tauri trouvé.
+- `detach_agent_chat` préserve session + scrollback; `reattach_agent_chat` relit le scrollback; `set_active_layout` ne détruit pas les conversations/layouts; la perte apparente ne vient pas du backend selon les tests ciblés.
+- Tests DevBackend:
+ - `cargo test -p app-tauri --test chat_bridge reattach` => OK (4)
+ - `cargo test -p app-tauri --test dto_layouts git_graph` => OK (2)
+ - `cargo test -p application relaunch_other_cell_with_conversation_id_rebinds` => OK (1)
+ - `cargo test -p application launch_other_cell_with_conversation_id_rebinds_no_respawn` => OK (1)
+ - `cargo test -p application attach_` => OK (5)
+ - `cargo test -p application --test layout_usecases` => OK (27)
+- Conclusion: correction frontend, pas backend.
+
+## QA rouge 2026-08-15
+- `npm test -- CustomAgentChatView.test.tsx` => ROUGE sur le scénario `conversationId` connu / `sessionId` nul qui déclenchait encore `launchAgentChat`.
+- Ce gap a été corrigé dans l'itération frontend suivante.
diff --git a/.ideai/tickets/217/issue.md b/.ideai/tickets/217/issue.md
new file mode 100644
index 00000000..28f5c603
--- /dev/null
+++ b/.ideai/tickets/217/issue.md
@@ -0,0 +1,17 @@
+---
+id: "6ea02306-17ba-4085-a8fb-df9e7d061e35"
+number: 217
+title: "Conversation disparaît parfois au switch layout agent vers Git Graph"
+status: "qa"
+priority: "high"
+sprint: null
+links: []
+agentRefs: []
+attachments: []
+createdBy: {"kind":"agent","agent_id":"a6c6ea12-bfc6-4bdc-8031-324102dfa34d"}
+updatedBy: {"kind":"agent","agent_id":"a6c6ea12-bfc6-4bdc-8031-324102dfa34d"}
+createdAt: 1786814259134
+updatedAt: 1786815105743
+version: 6
+---
+Investigation et correction du cas où une conversation disparaît parfois lorsqu'on passe du layout agent au layout Git Graph, puis retour vers le layout agent. Vérifier si le problème vient de la persistance UI, de la projection layout, ou d'un problème de réattache conversation/session.
\ No newline at end of file
diff --git a/.ideai/tickets/counter.json b/.ideai/tickets/counter.json
index 3e7801df..c9cd9bd7 100644
--- a/.ideai/tickets/counter.json
+++ b/.ideai/tickets/counter.json
@@ -1,3 +1,3 @@
{
- "nextNumber": 217
+ "nextNumber": 218
}
\ No newline at end of file
diff --git a/.ideai/tickets/index.json b/.ideai/tickets/index.json
index 39ab9f1d..b163e7b3 100644
--- a/.ideai/tickets/index.json
+++ b/.ideai/tickets/index.json
@@ -2918,6 +2918,20 @@
"issueRef": "#216",
"path": "216",
"title": "CLI custom: plus de progress remontés après le premier Final",
+ "status": "closed",
+ "priority": "high",
+ "sprint": null,
+ "assignedAgentIds": [],
+ "createdBy": {
+ "kind": "agent",
+ "agent_id": "a6c6ea12-bfc6-4bdc-8031-324102dfa34d"
+ },
+ "updatedAt": 1786813140292
+ },
+ {
+ "issueRef": "#217",
+ "path": "217",
+ "title": "Conversation disparaît parfois au switch layout agent vers Git Graph",
"status": "qa",
"priority": "high",
"sprint": null,
@@ -2926,7 +2940,7 @@
"kind": "agent",
"agent_id": "a6c6ea12-bfc6-4bdc-8031-324102dfa34d"
},
- "updatedAt": 1786813039107
+ "updatedAt": 1786815105743
}
]
}
\ No newline at end of file
diff --git a/frontend/src/features/agents/CustomAgentChatView.test.tsx b/frontend/src/features/agents/CustomAgentChatView.test.tsx
index f9d45687..18e15003 100644
--- a/frontend/src/features/agents/CustomAgentChatView.test.tsx
+++ b/frontend/src/features/agents/CustomAgentChatView.test.tsx
@@ -522,6 +522,85 @@ describe("CustomAgentChatView", () => {
expect(screen.getByLabelText(/message CLI custom/).hasAttribute("disabled")).toBe(true);
});
+ it("restores visible conversation from conversationId cache when layout returns before sessionId", async () => {
+ let scrollback: ReplyChunk[] = [
+ { kind: "userPrompt" as const, text: "Initial prompt" },
+ { kind: "final" as const, content: "Initial reply" },
+ ];
+ const secondReattach = deferred<{ sessionId: string; scrollback: ReplyChunk[] }>();
+ const agent = {
+ launchAgentChat: vi.fn(),
+ reattachAgentChat: vi
+ .fn()
+ .mockImplementationOnce(async (sessionId: string) => ({
+ sessionId,
+ scrollback,
+ }))
+ .mockImplementationOnce(() => secondReattach.promise),
+ detachAgentChat: vi.fn(async () => {}),
+ sendAgentChat: vi.fn(async () => {}),
+ cancelAgentChat: vi.fn(async () => {}),
+ closeAgentChat: vi.fn(async () => {}),
+ };
+ const props = {
+ projectId: "project-1",
+ agentId: "agent-1",
+ agentName: "Worker",
+ profile,
+ cwd: "/repo",
+ nodeId: "node-1",
+ sessionId: "chat-session-1" as string | null,
+ conversationId: "conversation-1",
+ onSessionId: vi.fn(),
+ onConversationId: vi.fn(),
+ };
+
+ const first = render(
+ null) },
+ } as unknown as Gateways}
+ >
+
+ ,
+ );
+
+ expect(await screen.findByText("Initial reply")).toBeTruthy();
+ first.unmount();
+ await waitFor(() => expect(agent.detachAgentChat).toHaveBeenCalledWith("chat-session-1"));
+
+ scrollback = [
+ { kind: "userPrompt" as const, text: "Initial prompt" },
+ { kind: "final" as const, content: "Initial reply" },
+ { kind: "userPrompt" as const, text: "Question during graph view" },
+ { kind: "final" as const, content: "Answer restored from replay" },
+ ];
+
+ render(
+ null) },
+ } as unknown as Gateways}
+ >
+
+ ,
+ );
+
+ expect(screen.getByText("Initial reply")).toBeTruthy();
+ expect(agent.launchAgentChat).not.toHaveBeenCalled();
+ expect(screen.getByRole("button", { name: "Envoyer" }).hasAttribute("disabled")).toBe(true);
+
+ act(() => {
+ secondReattach.resolve({ sessionId: "chat-session-1", scrollback });
+ });
+
+ expect(await screen.findByText("Answer restored from replay")).toBeTruthy();
+ expect(screen.getByText("Question during graph view")).toBeTruthy();
+ expect(agent.launchAgentChat).not.toHaveBeenCalled();
+ });
+
it("drops a stale no-response chunk when replay contains a later real answer (#190)", async () => {
const agent = {
launchAgentChat: vi.fn(),
diff --git a/frontend/src/features/agents/CustomAgentChatView.tsx b/frontend/src/features/agents/CustomAgentChatView.tsx
index 113f9ada..6439ac46 100644
--- a/frontend/src/features/agents/CustomAgentChatView.tsx
+++ b/frontend/src/features/agents/CustomAgentChatView.tsx
@@ -67,15 +67,18 @@ interface CachedChatView {
turns: ChatTurn[];
progressIndex: Map;
chunkCount: number;
+ sessionId?: string;
}
const chatSessionViewCache = new Map();
+const chatConversationViewCache = new Map();
const EMPTY_MODEL_RESPONSE_MESSAGE = "Le modèle n'a renvoyé aucune réponse.";
const COMPOSER_RESIZE_STEP = 12;
const FALLBACK_COMPOSER_HEIGHT = 64;
export function __clearCustomAgentChatViewCacheForTests() {
chatSessionViewCache.clear();
+ chatConversationViewCache.clear();
}
interface AttachmentDraft {
@@ -310,6 +313,14 @@ function emptyChatViewState(): ChatViewState {
return { turns: [], progressIndex: new Map() };
}
+function conversationCacheKey(
+ projectId: string,
+ agentId: string,
+ conversationId: string | null,
+): string | null {
+ return conversationId ? `${projectId}::${agentId}::${conversationId}` : null;
+}
+
function appendProgress(state: ChatViewState, progress: ReplyProgress): ChatViewState {
const key = progressUpsertKey(progress);
const existingIndex = state.turns.findIndex(
@@ -711,10 +722,27 @@ export function CustomAgentChatView({
}: CustomAgentChatViewProps) {
const { agent, profile: profileGateway, system } = useGateways();
const pluginRuntime = usePluginRuntime();
- const [turns, setTurns] = useState(
- () => (sessionId ? (chatSessionViewCache.get(sessionId)?.turns ?? []) : []),
+ const initialConversationCacheKey = conversationCacheKey(
+ projectId,
+ agentId,
+ conversationId,
+ );
+ const [turns, setTurns] = useState(
+ () =>
+ sessionId
+ ? (chatSessionViewCache.get(sessionId)?.turns ?? [])
+ : initialConversationCacheKey
+ ? (chatConversationViewCache.get(initialConversationCacheKey)?.turns ?? [])
+ : [],
+ );
+ const [progressIndex, setProgressIndex] = useState