Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
132 changes: 131 additions & 1 deletion apps/code/src/renderer/utils/session.test.ts
Original file line number Diff line number Diff line change
@@ -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", () => {
Expand Down Expand Up @@ -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",
]);
});
});
18 changes: 10 additions & 8 deletions apps/code/src/renderer/utils/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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}]`);
}
}
}
Expand Down
Loading