From 023d483ba2a0114629bb6caf4deca2a2d7d9ec66 Mon Sep 17 00:00:00 2001 From: Blomios Date: Thu, 20 Aug 2026 15:10:41 +0200 Subject: [PATCH 1/2] =?UTF-8?q?fix(web):=20=C3=A9met=20chat.attached=20pou?= =?UTF-8?q?r=20les=20sessions=20agents=20structur=C3=A9es=20au=20lieu=20de?= =?UTF-8?q?=20UNSUPPORTED=20=E2=80=94=20#229?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/web-server/src/lib.rs | 70 ++++++++++---- .../agents/CustomAgentChatView.test.tsx | 92 +++++++++++++++++++ .../features/agents/CustomAgentChatView.tsx | 74 ++++++++++++--- frontend/src/features/layout/LayoutGrid.tsx | 1 + frontend/src/features/web/WebAgentCell.tsx | 1 + 5 files changed, 203 insertions(+), 35 deletions(-) diff --git a/crates/web-server/src/lib.rs b/crates/web-server/src/lib.rs index 8175c65c..4157938b 100644 --- a/crates/web-server/src/lib.rs +++ b/crates/web-server/src/lib.rs @@ -1559,11 +1559,19 @@ async fn send_launch_agent_attached( owned: &Arc>>, output: application::LaunchAgentOutput, ) -> Result<(), ErrorDto> { - if output.structured.is_some() { - return Err(ErrorDto { - code: "UNSUPPORTED".to_owned(), - message: "structured agent sessions do not stream over the PTY websocket".to_owned(), - }); + if let Some(structured) = output.structured.as_ref() { + tx.send(ServerFrame::chat_attached( + &frame.id, + structured, + output.assigned_conversation_id.as_deref(), + output.engine_session_id.as_deref(), + )) + .await + .map_err(|_| ErrorDto { + code: "WS_CLOSED".to_owned(), + message: "websocket output closed".to_owned(), + })?; + return Ok(()); } let session_id = output.session.id; let dto = TerminalSessionDto::from(output); @@ -5430,6 +5438,28 @@ impl ServerFrame { } } + fn chat_attached( + request_id: &str, + session: &application::StructuredSessionDescriptor, + assigned_conversation_id: Option<&str>, + engine_session_id: Option<&str>, + ) -> Self { + Self { + kind: "chat.attached".to_owned(), + reply_to: Some(request_id.to_owned()), + payload: json!({ + "sessionId": session.session_id.to_string(), + "session": { + "sessionId": session.session_id.to_string(), + "nodeId": session.node_id.to_string(), + }, + "cellKind": "chat", + "assignedConversationId": assigned_conversation_id, + "engineSessionId": engine_session_id, + }), + } + } + fn output(session_id: SessionId, seq: u64, bytes: Vec) -> Self { Self { kind: "terminal.output".to_owned(), @@ -7043,7 +7073,7 @@ mod tests { } #[tokio::test] - async fn websocket_launch_agent_structured_is_unsupported() { + async fn websocket_launch_agent_structured_emits_chat_attached() { let state = state(); let (tx, mut rx) = mpsc::channel(4); let owned = Arc::new(Mutex::new(Vec::new())); @@ -7073,25 +7103,25 @@ mod tests { profile: None, }; - if let Err(err) = send_launch_agent_attached(&frame, &state, &tx, &owned, output).await { - tx.send(ServerFrame::error( - Some(frame.id), - payload_session_id(&frame.payload), - err.code, - err.message, - )) + send_launch_agent_attached(&frame, &state, &tx, &owned, output) .await .unwrap(); - } - let error = recv_server_frame(&mut rx).await; + let attached = recv_server_frame(&mut rx).await; - assert_eq!(error.kind, "error"); - assert_eq!(error.reply_to.as_deref(), Some("agent-structured")); - assert_eq!(error.payload["code"], "UNSUPPORTED"); + assert_eq!(attached.kind, "chat.attached"); + assert_eq!(attached.reply_to.as_deref(), Some("agent-structured")); + assert_eq!(attached.payload["sessionId"], session_id.to_string()); assert_eq!( - error.payload["message"], - "structured agent sessions do not stream over the PTY websocket" + attached.payload["session"]["sessionId"], + session_id.to_string() ); + assert_eq!(attached.payload["session"]["nodeId"], node_id.to_string()); + assert_eq!(attached.payload["cellKind"], "chat"); + assert_eq!( + attached.payload["assignedConversationId"], + "pair-conversation" + ); + assert_eq!(attached.payload["engineSessionId"], "engine-session"); assert_eq!(state.ws_pty_bridge.active_sessions(), 0); } diff --git a/frontend/src/features/agents/CustomAgentChatView.test.tsx b/frontend/src/features/agents/CustomAgentChatView.test.tsx index 554811d6..8166067d 100644 --- a/frontend/src/features/agents/CustomAgentChatView.test.tsx +++ b/frontend/src/features/agents/CustomAgentChatView.test.tsx @@ -89,6 +89,98 @@ describe("CustomAgentChatView", () => { __resetProjectCliInteractionActivityForTests(); }); + it("shows the technical open failure on mobile with retry and native TUI fallback", async () => { + const openNativeTui = vi.fn(); + const agent = { + launchAgentChat: vi + .fn() + .mockRejectedValueOnce(new Error("spawn codex ENOENT")) + .mockResolvedValueOnce({ sessionId: "chat-session-1" }), + reattachAgentChat: vi.fn(async (sessionId: string) => ({ + sessionId, + scrollback: [], + })), + sendAgentChat: vi.fn(async () => {}), + cancelAgentChat: vi.fn(async () => {}), + closeAgentChat: vi.fn(async () => {}), + }; + + render( + null) }, + } as unknown as Gateways} + > + + , + ); + + const alert = await screen.findByRole("alert"); + expect(within(alert).getByText("Le CLI custom n'a pas pu démarrer.")).toBeTruthy(); + expect(within(alert).getByText("Cause: spawn codex ENOENT")).toBeTruthy(); + expect(screen.queryByText(/Vérifiez la configuration du profil/)).toBeNull(); + + fireEvent.click(within(alert).getByRole("button", { name: "Ouvrir en TUI native" })); + expect(openNativeTui).toHaveBeenCalledTimes(1); + + fireEvent.click(within(alert).getByRole("button", { name: "Réessayer" })); + await waitFor(() => expect(agent.launchAgentChat).toHaveBeenCalledTimes(2)); + await waitFor(() => expect(screen.queryByRole("alert")).toBeNull()); + }); + + it("uses the same technical open failure on desktop", async () => { + const agent = { + launchAgentChat: vi.fn(async () => { + throw new Error("permission denied: /opt/codex"); + }), + reattachAgentChat: vi.fn(), + sendAgentChat: vi.fn(async () => {}), + cancelAgentChat: vi.fn(async () => {}), + closeAgentChat: vi.fn(async () => {}), + }; + + render( + null) }, + } as unknown as Gateways} + > + + , + ); + + const alert = await screen.findByRole("alert"); + expect(within(alert).getByText("Cause: permission denied: /opt/codex")).toBeTruthy(); + expect(screen.getByRole("button", { name: "Réessayer" })).toBeTruthy(); + expect(screen.queryByRole("button", { name: "Ouvrir en TUI native" })).toBeNull(); + }); + it("repaints attachment labels from user prompt scrollback", async () => { const agent = { launchAgentChat: vi.fn(), diff --git a/frontend/src/features/agents/CustomAgentChatView.tsx b/frontend/src/features/agents/CustomAgentChatView.tsx index 78647140..b66f3524 100644 --- a/frontend/src/features/agents/CustomAgentChatView.tsx +++ b/frontend/src/features/agents/CustomAgentChatView.tsx @@ -52,6 +52,7 @@ export interface CustomAgentChatViewProps { onComposerHeightPxChange?: (heightPx: number) => void; draftScope?: ChatDraftScope | null; mobile?: boolean; + onOpenNativeTui?: () => void; onSessionId: (sessionId: string | null) => void; onConversationId: (conversationId: string | null) => void; } @@ -118,6 +119,11 @@ interface SlashCommandInvocation { arguments: unknown[]; } +interface ChatErrorState { + message: string; + openFailure: boolean; +} + function describe(e: unknown): string { if (e && typeof e === "object" && "message" in e) { return String((e as GatewayError).message); @@ -125,9 +131,9 @@ function describe(e: unknown): string { return String(e); } -function describeOpenFailure(e: unknown, mobile: boolean): string { - if (!mobile) return describe(e); - return "Le CLI custom n'a pas pu démarrer. Vérifiez la configuration du profil ou utilisez la TUI native."; +function describeOpenFailure(e: unknown): string { + const message = describe(e).trim(); + return message || "Cause technique non disponible."; } function instrumentationError(e: unknown): Record { @@ -734,6 +740,7 @@ export function CustomAgentChatView({ onComposerHeightPxChange, draftScope: explicitDraftScope, mobile = false, + onOpenNativeTui, onSessionId, onConversationId, }: CustomAgentChatViewProps) { @@ -770,9 +777,10 @@ export function CustomAgentChatView({ const [profileDialog, setProfileDialog] = useState(null); const [opening, setOpening] = useState(Boolean(sessionId || conversationId)); + const [openRetryNonce, setOpenRetryNonce] = useState(0); const [busy, setBusy] = useState(false); const [stopping, setStopping] = useState(false); - const [error, setError] = useState(null); + const [error, setError] = useState(null); const scrollRef = useRef(null); const composerRef = useRef(null); const chatRootRef = useRef(null); @@ -1371,7 +1379,9 @@ export function CustomAgentChatView({ sessionId: externalSessionId, error: instrumentationError(e), }); - if (!cancelled) setError(describeOpenFailure(e, mobile)); + if (!cancelled) { + setError({ message: describeOpenFailure(e), openFailure: true }); + } } finally { if (!cancelled) setOpening(false); } @@ -1390,7 +1400,7 @@ export function CustomAgentChatView({ applyChatViewState, currentConversationCacheKey, conversationId, - mobile, + openRetryNonce, ]); const canSend = useMemo( @@ -1490,7 +1500,10 @@ export function CustomAgentChatView({ async function executeSlashCommand(invocation: SlashCommandInvocation) { if (!agent.executeSlashCommand) { - setError(`Commande ${invocation.name} indisponible dans ce runtime.`); + setError({ + message: `Commande ${invocation.name} indisponible dans ce runtime.`, + openFailure: false, + }); return; } setDraft(""); @@ -1546,7 +1559,7 @@ export function CustomAgentChatView({ } } catch (e) { const message = `Commande ${invocation.name} échouée: ${describe(e)}`; - setError(message); + setError({ message, openFailure: false }); updateChatViewState((prev) => ({ turns: [...prev.turns, { role: "error", text: message, receivedAtMs: Date.now() }], progressIndex: prev.progressIndex, @@ -1633,7 +1646,7 @@ export function CustomAgentChatView({ ); } } catch (e) { - setError(describe(e)); + setError({ message: describe(e), openFailure: false }); } } @@ -1704,7 +1717,7 @@ export function CustomAgentChatView({ } catch (recoveryError) { finishInteraction(); setBusy(false); - setError(describe(recoveryError)); + setError({ message: describe(recoveryError), openFailure: false }); updateChatViewState((prev) => ({ turns: [ ...prev.turns, @@ -1717,7 +1730,7 @@ export function CustomAgentChatView({ } finishInteraction(); setBusy(false); - setError(describe(e)); + setError({ message: describe(e), openFailure: false }); updateChatViewState((prev) => ({ turns: [...prev.turns, { role: "error", text: describe(e), receivedAtMs: Date.now() }], progressIndex: prev.progressIndex, @@ -1779,7 +1792,7 @@ export function CustomAgentChatView({ }); return; } - setError(describe(e)); + setError({ message: describe(e), openFailure: false }); updateChatViewState((prev) => ({ turns: [ ...prev.turns, @@ -1821,9 +1834,40 @@ export function CustomAgentChatView({

)} {error && ( -

- {error} -

+
+ {error.openFailure ? ( +
+
Le CLI custom n'a pas pu démarrer.
+
+ Cause: {error.message} +
+
+ + {onOpenNativeTui && ( + + )} +
+
+ ) : ( + error.message + )} +
)} {profileDialog && (
requestMode("tui")} onSessionId={(sid) => void setSessionWithInstrumentation("custom", sid)} onConversationId={(cid) => void vm.setCellConversation(id, cid)} /> diff --git a/frontend/src/features/web/WebAgentCell.tsx b/frontend/src/features/web/WebAgentCell.tsx index 6f875f68..50b2e8a6 100644 --- a/frontend/src/features/web/WebAgentCell.tsx +++ b/frontend/src/features/web/WebAgentCell.tsx @@ -208,6 +208,7 @@ export function WebAgentCell({ conversationId={customConversationId} draftScope={draftScope} mobile + onOpenNativeTui={() => setMode("tui")} onSessionId={setCustomSessionId} onConversationId={setCustomConversationId} /> From 0a2a577d2572c765a10ce0b1008e20cf4f44cb9e Mon Sep 17 00:00:00 2001 From: Blomios Date: Thu, 20 Aug 2026 15:10:49 +0200 Subject: [PATCH 2/2] =?UTF-8?q?chore(idea):=20bookkeeping=20#229=20QA=20ve?= =?UTF-8?q?rte=20et=20cl=C3=B4ture=20(+=20ticket=20#228=20retardataire)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .ideai/tickets/228/carnet.md | 17 +++++++++++++++++ .ideai/tickets/228/issue.md | 17 +++++++++++++++++ .ideai/tickets/229/carnet.md | 13 +++++++++++++ .ideai/tickets/229/issue.md | 17 +++++++++++++++++ .ideai/tickets/counter.json | 2 +- .ideai/tickets/index.json | 28 ++++++++++++++++++++++++++++ 6 files changed, 93 insertions(+), 1 deletion(-) create mode 100644 .ideai/tickets/228/carnet.md create mode 100644 .ideai/tickets/228/issue.md create mode 100644 .ideai/tickets/229/carnet.md create mode 100644 .ideai/tickets/229/issue.md diff --git a/.ideai/tickets/228/carnet.md b/.ideai/tickets/228/carnet.md new file mode 100644 index 00000000..403144dc --- /dev/null +++ b/.ideai/tickets/228/carnet.md @@ -0,0 +1,17 @@ +--- +issueRef: "#228" +version: 4 +updatedBy: {"kind":"agent","agent_id":"a6c6ea12-bfc6-4bdc-8031-324102dfa34d"} +updatedAt: 1787229737762 +--- +## 2026-08-20 +- Rebuild AppImage demandé par l'utilisateur. +- Ticket créé et passé en `inProgress`. +- Décision Git préparée avant tagging : branche `develop`, HEAD `1b8534f`, tag à créer après build vert `v0.18.0`. +- Frontend build exécuté avec succès. +- Bundle Tauri AppImage exécuté avec succès via `CARGO_HOME=/tmp/idea-cargo-home APPIMAGE_EXTRACT_AND_RUN=1 NO_STRIP=1`. +- Artefact produit : `/home/anthony/Documents/Projects/IdeA/target/release/bundle/appimage/IdeA_0.3.0_amd64.AppImage`. +- Vérification artefact : taille `118745592` bytes, horodatage `2026-08-20 14:41:34 +0200`. +- Git a créé localement le tag annoté `v0.18.0` sur le commit `1b8534f` puis l'a vérifié avec `git tag --contains` et `git describe --tags`. +- Aucun push effectué. +- Hors périmètre volontaire : remplacement manuel de l'AppImage installée et relance d'IdeA. diff --git a/.ideai/tickets/228/issue.md b/.ideai/tickets/228/issue.md new file mode 100644 index 00000000..e04499c1 --- /dev/null +++ b/.ideai/tickets/228/issue.md @@ -0,0 +1,17 @@ +--- +id: "ce5b03a6-deaa-4b4d-9eff-49bff8e6a79c" +number: 228 +title: "Rebuild AppImage Linux" +status: "closed" +priority: "medium" +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: 1787229516097 +updatedAt: 1787229737762 +version: 4 +--- +Reconstruction de l'AppImage IdeA depuis le project root, puis tag de version du commit construit selon la règle Main (vx sur main, vy sur develop, vz sur autres branches). \ No newline at end of file diff --git a/.ideai/tickets/229/carnet.md b/.ideai/tickets/229/carnet.md new file mode 100644 index 00000000..c2b96e4c --- /dev/null +++ b/.ideai/tickets/229/carnet.md @@ -0,0 +1,13 @@ +--- +issueRef: "#229" +version: 4 +updatedBy: {"kind":"agent","agent_id":"1ff94b51-3e17-4a39-8543-7715d1ff0f80"} +updatedAt: 1787231430402 +--- +2026-08-20 +- Symptôme utilisateur: via l'interface web, le profil CLI custom affiche "Le CLI custom n'a pas pu démarrer. Vérifiez la configuration du profil ou utilisez la TUI native". +- Ticket créé car aucun ticket ouvert ne correspondait. +- UX: ne pas masquer l'erreur réelle sur web/mobile; afficher un échec d'ouverture avec détail exploitable, action Réessayer, fallback TUI secondaire. +- Architect: root cause probable = contrat cassé côté web-server. Le frontend web lance `cellKind: chat`, mais le web-server rejette encore les sessions structurées avec `UNSUPPORTED`; le message générique frontend masque la vraie cause. +- Git: branche locale décidée `fix/229-web-custom-cli-startup`, basée sur `develop` (tip `1b8534f`), créée sans checkout. +- À implémenter: backend web-server pour honorer `agent.launch` structuré sur web + frontend pour exposer le diagnostic utile au lieu du message générique. \ No newline at end of file diff --git a/.ideai/tickets/229/issue.md b/.ideai/tickets/229/issue.md new file mode 100644 index 00000000..f3aa7cc8 --- /dev/null +++ b/.ideai/tickets/229/issue.md @@ -0,0 +1,17 @@ +--- +id: "a993f30b-b128-4b26-94ba-6301279c1bd4" +number: 229 +title: "CLI custom indisponible via l'interface web" +status: "closed" +priority: "high" +sprint: null +links: [] +agentRefs: [] +attachments: [] +createdBy: {"kind":"agent","agent_id":"a6c6ea12-bfc6-4bdc-8031-324102dfa34d"} +updatedBy: {"kind":"agent","agent_id":"1ff94b51-3e17-4a39-8543-7715d1ff0f80"} +createdAt: 1787230509856 +updatedAt: 1787231430402 +version: 4 +--- +Le profil CLI custom ne demarre pas depuis l'interface web et affiche: "le cli custom n'a pas pu demarrer. Verifiez la configuration du profil ou utilisez la TUI native". A diagnostiquer et corriger selon le cycle UX/Architect/Dev/QA. \ No newline at end of file diff --git a/.ideai/tickets/counter.json b/.ideai/tickets/counter.json index 5468b951..389096c9 100644 --- a/.ideai/tickets/counter.json +++ b/.ideai/tickets/counter.json @@ -1,3 +1,3 @@ { - "nextNumber": 228 + "nextNumber": 230 } \ No newline at end of file diff --git a/.ideai/tickets/index.json b/.ideai/tickets/index.json index 1bb8e7f5..85d09d20 100644 --- a/.ideai/tickets/index.json +++ b/.ideai/tickets/index.json @@ -3056,6 +3056,34 @@ "agent_id": "a6c6ea12-bfc6-4bdc-8031-324102dfa34d" }, "updatedAt": 1787227664912 + }, + { + "issueRef": "#228", + "path": "228", + "title": "Rebuild AppImage Linux", + "status": "closed", + "priority": "medium", + "sprint": null, + "assignedAgentIds": [], + "createdBy": { + "kind": "agent", + "agent_id": "a6c6ea12-bfc6-4bdc-8031-324102dfa34d" + }, + "updatedAt": 1787229737762 + }, + { + "issueRef": "#229", + "path": "229", + "title": "CLI custom indisponible via l'interface web", + "status": "closed", + "priority": "high", + "sprint": null, + "assignedAgentIds": [], + "createdBy": { + "kind": "agent", + "agent_id": "a6c6ea12-bfc6-4bdc-8031-324102dfa34d" + }, + "updatedAt": 1787231430402 } ] } \ No newline at end of file