From 863d9b727741fae801315e1c9b45bd6d30d78bc8 Mon Sep 17 00:00:00 2001 From: Blomios Date: Mon, 3 Aug 2026 14:35:36 +0200 Subject: [PATCH] fix(agents): unwrap read_agent_context DTO content (#140) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Le contexte projet d'un agent s'affichait "[object Object]" et se vidait à la réouverture du panneau : read_agent_context renvoie déjà {content} côté backend, mais les gateways front traitaient la réponse comme une string brute. Les deux gateways (Tauri + HTTP) unwrap maintenant .content ; type AgentContextDocument ajouté au domaine. Co-Authored-By: Claude Sonnet 5 --- .ideai/tickets/140/carnet.md | 6 +++ .ideai/tickets/140/issue.md | 17 ++++++++ .ideai/tickets/counter.json | 2 +- .ideai/tickets/index.json | 15 +++++++ frontend/src/adapters/agent.test.ts | 5 ++- frontend/src/adapters/agent.ts | 6 ++- .../src/adapters/http/agentGateway.test.ts | 43 ++++++++++++++++++- frontend/src/adapters/http/streamGateways.ts | 5 ++- frontend/src/domain/index.ts | 5 +++ frontend/src/features/agents/agents.test.tsx | 37 ++++++++++++++++ 10 files changed, 135 insertions(+), 6 deletions(-) create mode 100644 .ideai/tickets/140/carnet.md create mode 100644 .ideai/tickets/140/issue.md diff --git a/.ideai/tickets/140/carnet.md b/.ideai/tickets/140/carnet.md new file mode 100644 index 0000000..690e594 --- /dev/null +++ b/.ideai/tickets/140/carnet.md @@ -0,0 +1,6 @@ +--- +issueRef: "#140" +version: 3 +updatedBy: {"kind":"user"} +updatedAt: 1785760014957 +--- diff --git a/.ideai/tickets/140/issue.md b/.ideai/tickets/140/issue.md new file mode 100644 index 0000000..d3b6b3d --- /dev/null +++ b/.ideai/tickets/140/issue.md @@ -0,0 +1,17 @@ +--- +id: "353aa2ae-cf98-4a63-b4fb-a15fdb801a0a" +number: 140 +title: "[Bug] je ne peux pas editer le context projet d'un agent a la main" +status: "open" +priority: "medium" +sprint: null +links: [] +agentRefs: [{"agentId":"a6c6ea12-bfc6-4bdc-8031-324102dfa34d","role":"assigned"}] +attachments: [] +createdBy: {"kind":"user"} +updatedBy: {"kind":"user"} +createdAt: 1785759952774 +updatedAt: 1785760014957 +version: 3 +--- +Quand je suis dans le panneau des agents, que je selectionne un agent, le context projet de l'agent s'affiche mal (il n'affiche que [object Object]) et si je l'edit, que je save, et que je le réouvre il estd e nouveau vide \ No newline at end of file diff --git a/.ideai/tickets/counter.json b/.ideai/tickets/counter.json index 03e32ab..ef4f3b5 100644 --- a/.ideai/tickets/counter.json +++ b/.ideai/tickets/counter.json @@ -1,3 +1,3 @@ { - "nextNumber": 140 + "nextNumber": 141 } \ No newline at end of file diff --git a/.ideai/tickets/index.json b/.ideai/tickets/index.json index d955d21..e8ffed0 100644 --- a/.ideai/tickets/index.json +++ b/.ideai/tickets/index.json @@ -1807,6 +1807,21 @@ "agent_id": "a6c6ea12-bfc6-4bdc-8031-324102dfa34d" }, "updatedAt": 1785709504027 + }, + { + "issueRef": "#140", + "path": "140", + "title": "[Bug] je ne peux pas editer le context projet d'un agent a la main", + "status": "open", + "priority": "medium", + "sprint": null, + "assignedAgentIds": [ + "a6c6ea12-bfc6-4bdc-8031-324102dfa34d" + ], + "createdBy": { + "kind": "user" + }, + "updatedAt": 1785760014957 } ] } \ No newline at end of file diff --git a/frontend/src/adapters/agent.test.ts b/frontend/src/adapters/agent.test.ts index 4f77b6c..669f3df 100644 --- a/frontend/src/adapters/agent.test.ts +++ b/frontend/src/adapters/agent.test.ts @@ -42,12 +42,13 @@ describe("TauriAgentGateway invoke payloads", () => { }); }); - it("list_agents / read / delete pass top-level args (no request wrapper)", async () => { + it("list_agents / read / delete pass top-level args and unwrap read context DTO", async () => { const gw = new TauriAgentGateway(); await gw.listAgents("p"); expect(invoke).toHaveBeenCalledWith("list_agents", { projectId: "p" }); - await gw.readContext("p", "a"); + invoke.mockResolvedValueOnce({ content: "# context" }); + await expect(gw.readContext("p", "a")).resolves.toBe("# context"); expect(invoke).toHaveBeenCalledWith("read_agent_context", { projectId: "p", agentId: "a", diff --git a/frontend/src/adapters/agent.ts b/frontend/src/adapters/agent.ts index 4ff3d07..f416ec4 100644 --- a/frontend/src/adapters/agent.ts +++ b/frontend/src/adapters/agent.ts @@ -16,6 +16,7 @@ import { Channel, invoke } from "@tauri-apps/api/core"; import type { Agent, + AgentContextDocument, EffortSelection, ResumableAgent, TerminalSession, @@ -117,7 +118,10 @@ export class TauriAgentGateway implements AgentGateway { } readContext(projectId: string, agentId: string): Promise { - return invoke("read_agent_context", { projectId, agentId }); + return invoke("read_agent_context", { + projectId, + agentId, + }).then((res) => res.content); } async updateContext( diff --git a/frontend/src/adapters/http/agentGateway.test.ts b/frontend/src/adapters/http/agentGateway.test.ts index 948864d..9b21c31 100644 --- a/frontend/src/adapters/http/agentGateway.test.ts +++ b/frontend/src/adapters/http/agentGateway.test.ts @@ -8,7 +8,7 @@ import { describe, it, expect, vi } from "vitest"; import { HttpAgentGateway } from "./streamGateways"; -import { HttpInvoker } from "./httpInvoker"; +import { HttpInvoker, type FetchLike } from "./httpInvoker"; import { WsLiveClient, type WebSocketLike } from "./wsLiveClient"; import { bytesToBase64 } from "./frames"; @@ -45,6 +45,27 @@ function gateway(): { gw: HttpAgentGateway; sockets: FakeSocket[] } { return { gw, sockets }; } +function httpAgentGateway( + fetchImpl: FetchLike, +): { gw: HttpAgentGateway; calls: { url: string; init: unknown }[] } { + const calls: { url: string; init: unknown }[] = []; + const recordingFetch: FetchLike = async (url, init) => { + calls.push({ url, init }); + return fetchImpl(url, init); + }; + const ws = new WsLiveClient({ + wsUrl: "wss://host", + socketFactory: () => new FakeSocket(), + }); + return { + gw: new HttpAgentGateway( + new HttpInvoker({ baseUrl: "https://host", fetchImpl: recordingFetch }), + ws, + ), + calls, + }; +} + async function replyToLast( socket: FakeSocket, index: number, @@ -78,6 +99,26 @@ function attachedAck( const OPTS = { cwd: "/srv/app", rows: 24, cols: 80, nodeId: "node-1" }; describe("HttpAgentGateway WS round-trip (B6 frames)", () => { + it("readContext unwraps the read_agent_context DTO content over HTTP", async () => { + const { gw, calls } = httpAgentGateway(async () => ({ + ok: true, + status: 200, + json: async () => ({ content: "## Restored context" }), + text: async () => JSON.stringify({ content: "## Restored context" }), + })); + + await expect(gw.readContext("proj-1", "agent-1")).resolves.toBe( + "## Restored context", + ); + + expect(calls).toHaveLength(1); + const init = calls[0].init as { body: string }; + expect(JSON.parse(init.body)).toEqual({ + command: "read_agent_context", + args: { projectId: "proj-1", agentId: "agent-1" }, + }); + }); + it("launch → attached: conforming agent.launch frame, assignedConversationId consumed", async () => { const { gw, sockets } = gateway(); const chunks: Uint8Array[] = []; diff --git a/frontend/src/adapters/http/streamGateways.ts b/frontend/src/adapters/http/streamGateways.ts index b1edfd9..4e32fe7 100644 --- a/frontend/src/adapters/http/streamGateways.ts +++ b/frontend/src/adapters/http/streamGateways.ts @@ -21,6 +21,7 @@ import type { Agent, + AgentContextDocument, AppExitWorkGuardState, DomainEvent, EffortSelection, @@ -234,7 +235,9 @@ export class HttpAgentGateway implements AgentGateway { }); } readContext(projectId: string, agentId: string): Promise { - return this.http.invoke("read_agent_context", { projectId, agentId }); + return this.http + .invoke("read_agent_context", { projectId, agentId }) + .then((res) => res.content); } async updateContext(projectId: string, agentId: string, content: string): Promise { await this.http.invoke("update_agent_context", { request: { projectId, agentId, content } }); diff --git a/frontend/src/domain/index.ts b/frontend/src/domain/index.ts index 92382e3..bb0fe73 100644 --- a/frontend/src/domain/index.ts +++ b/frontend/src/domain/index.ts @@ -1277,6 +1277,11 @@ export interface Agent { effort?: EffortSelection; } +/** Response DTO returned by `read_agent_context`; adapters unwrap `content`. */ +export interface AgentContextDocument { + content: string; +} + /** * A terminal/PTY session as returned by the backend (mirror of * `TerminalSessionDto`, camelCase wire format). Surfaced by `changeAgentProfile` diff --git a/frontend/src/features/agents/agents.test.tsx b/frontend/src/features/agents/agents.test.tsx index 978dbe6..4c68b22 100644 --- a/frontend/src/features/agents/agents.test.tsx +++ b/frontend/src/features/agents/agents.test.tsx @@ -241,6 +241,43 @@ describe("AgentsPanel (with MockAgentGateway)", () => { }); }); + it("reopening the agents panel reloads the saved context as textarea text", async () => { + const agent = new MockAgentGateway(); + await agent.createAgent(PROJECT_ID, { + name: "Reopen", + profileId: "p1", + initialContent: "initial", + }); + const firstRender = renderPanel(agent); + await waitForIdle(); + + let buttons = screen.getAllByRole("button", { name: /reopen/i }); + let rowBtn = buttons.find((b) => b.hasAttribute("aria-pressed"))!; + fireEvent.click(rowBtn); + + let textarea = await screen.findByLabelText("agent context"); + fireEvent.change(textarea, { target: { value: "persisted after reopen" } }); + fireEvent.click(screen.getByRole("button", { name: "Save" })); + + await waitFor(async () => { + const agents = await agent.listAgents(PROJECT_ID); + await expect(agent.readContext(PROJECT_ID, agents[0].id)).resolves.toBe( + "persisted after reopen", + ); + }); + + firstRender.unmount(); + renderPanel(agent); + await waitForIdle(); + + buttons = screen.getAllByRole("button", { name: /reopen/i }); + rowBtn = buttons.find((b) => b.hasAttribute("aria-pressed"))!; + fireEvent.click(rowBtn); + + textarea = await screen.findByLabelText("agent context"); + expect((textarea as HTMLTextAreaElement).value).toBe("persisted after reopen"); + }); + it("deleting an agent removes it from the list", async () => { const agent = new MockAgentGateway(); await agent.createAgent(PROJECT_ID, {