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