diff --git a/apps/code/src/renderer/utils/session.test.ts b/apps/code/src/renderer/utils/session.test.ts index be1a0ac82..8f62f80fa 100644 --- a/apps/code/src/renderer/utils/session.test.ts +++ b/apps/code/src/renderer/utils/session.test.ts @@ -1,6 +1,9 @@ +import type { ContentBlock } from "@agentclientprotocol/sdk"; +import type { AcpMessage } from "@shared/types/session-events"; import { describe, expect, it } from "vitest"; -import { isFatalSessionError } from "./session"; +import { makeAttachmentUri } from "./promptContent"; +import { extractUserPromptsFromEvents, isFatalSessionError } from "./session"; describe("isFatalSessionError", () => { it("detects fatal 'Internal error' pattern", () => { @@ -37,3 +40,130 @@ describe("isFatalSessionError", () => { expect(isFatalSessionError("")).toBe(false); }); }); + +function promptEvent(prompt: ContentBlock[], ts = 1): AcpMessage { + return { + type: "acp_message", + ts, + message: { + jsonrpc: "2.0", + id: ts, + method: "session/prompt", + params: { prompt }, + }, + }; +} + +describe("extractUserPromptsFromEvents", () => { + it("extracts text from a plain text prompt", () => { + const events = [promptEvent([{ type: "text", text: "fix the bug" }])]; + expect(extractUserPromptsFromEvents(events)).toEqual(["fix the bug"]); + }); + + it("skips hidden text blocks", () => { + const events = [ + promptEvent([ + { + type: "text", + text: "hidden context", + _meta: { ui: { hidden: true } }, + } as ContentBlock, + { type: "text", text: "visible prompt" }, + ]), + ]; + expect(extractUserPromptsFromEvents(events)).toEqual(["visible prompt"]); + }); + + it("returns attachment labels when prompt has no text", () => { + const uri = makeAttachmentUri("/tmp/screenshot.png"); + const events = [ + promptEvent([ + { + type: "resource", + resource: { uri, text: "", mimeType: "image/png" }, + }, + ]), + ]; + expect(extractUserPromptsFromEvents(events)).toEqual([ + "[Attached files: screenshot.png]", + ]); + }); + + it("returns text when prompt has both text and attachments", () => { + const uri = makeAttachmentUri("/tmp/data.csv"); + const events = [ + promptEvent([ + { type: "text", text: "analyze this" }, + { type: "resource", resource: { uri, text: "", mimeType: "text/csv" } }, + ]), + ]; + expect(extractUserPromptsFromEvents(events)).toEqual(["analyze this"]); + }); + + it("joins multiple attachment labels with commas", () => { + const uri1 = makeAttachmentUri("/tmp/a.png"); + const uri2 = makeAttachmentUri("/tmp/b.pdf"); + const events = [ + promptEvent([ + { + type: "resource", + resource: { uri: uri1, text: "", mimeType: "image/png" }, + }, + { + type: "resource", + resource: { uri: uri2, text: "", mimeType: "application/pdf" }, + }, + ]), + ]; + expect(extractUserPromptsFromEvents(events)).toEqual([ + "[Attached files: a.png, b.pdf]", + ]); + }); + + it("falls back to attachment labels when all text blocks are hidden", () => { + const uri = makeAttachmentUri("/tmp/report.md"); + const events = [ + promptEvent([ + { + type: "text", + text: "hidden", + _meta: { ui: { hidden: true } }, + } as ContentBlock, + { + type: "resource", + resource: { uri, text: "", mimeType: "text/markdown" }, + }, + ]), + ]; + expect(extractUserPromptsFromEvents(events)).toEqual([ + "[Attached files: report.md]", + ]); + }); + + it("skips events with empty prompt arrays", () => { + const events = [promptEvent([])]; + expect(extractUserPromptsFromEvents(events)).toEqual([]); + }); + + it("collects prompts from multiple events in order", () => { + const uri = makeAttachmentUri("/tmp/logo.svg"); + const events = [ + promptEvent([{ type: "text", text: "first" }], 1), + promptEvent( + [ + { + type: "resource", + resource: { uri, text: "", mimeType: "image/svg+xml" }, + }, + ], + 2, + ), + promptEvent([{ type: "text", text: "third" }], 3), + ]; + expect(extractUserPromptsFromEvents(events)).toEqual([ + "first", + "[Attached files: logo.svg]", + "third", + ]); + }); +}); diff --git a/apps/code/src/renderer/utils/session.ts b/apps/code/src/renderer/utils/session.ts index d1400dc75..6809b35a1 100644 --- a/apps/code/src/renderer/utils/session.ts +++ b/apps/code/src/renderer/utils/session.ts @@ -182,14 +182,16 @@ export function extractUserPromptsFromEvents(events: AcpMessage[]): string[] { if (isJsonRpcRequest(msg) && msg.method === "session/prompt") { const params = msg.params as { prompt?: ContentBlock[] }; if (params?.prompt?.length) { - // Find first visible text block (skip hidden context blocks) - const textBlock = params.prompt.find((b) => { - if (b.type !== "text") return false; - const meta = (b as { _meta?: { ui?: { hidden?: boolean } } })._meta; - return !meta?.ui?.hidden; - }); - if (textBlock && textBlock.type === "text") { - prompts.push(textBlock.text); + const { text, attachments } = extractPromptDisplayContent( + params.prompt, + { filterHidden: true }, + ); + + if (text) { + prompts.push(text); + } else if (attachments.length > 0) { + const labels = attachments.map((a) => a.label).join(", "); + prompts.push(`[Attached files: ${labels}]`); } } }