Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -51,14 +51,11 @@ describe.skipIf(!enabled)("ToolCall — render artifact", () => {
fireEvent.click(trigger);

const viewport = await waitFor(() => {
const headers = Array.from(document.querySelectorAll("div")).filter(
(el) => el.textContent?.trim() === "result",
);
const header = headers[0];
if (!header) throw new Error("result header not found");
const sibling = header.nextElementSibling;
const body = document.querySelector('[data-slot="disclosure-body"]');
if (!(body instanceof HTMLElement)) throw new Error("disclosure body not found");
const sibling = body.querySelector(".lc-shiki, pre");
if (!(sibling instanceof HTMLElement) || !sibling.classList.contains("lc-shiki")) {
throw new Error("result viewport not yet highlighted");
throw new Error("read viewport not yet highlighted");
}
return sibling;
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import type { RuntimeChatItem } from "@/renderer/state/slices/runtimeEventSlice"
import { ToolCall } from "./ToolCall";

describe("ToolCall — Claude View (Read) rich rendering", () => {
it("syntax-highlights the file body and strips the LLM line-number prefixes", async () => {
it("renders the rich file body directly without args/result labels", async () => {
const item: RuntimeChatItem = {
id: "toolu_read",
type: "tool_call",
Expand All @@ -29,13 +29,17 @@ describe("ToolCall — Claude View (Read) rich rendering", () => {
fireEvent.click(getDisclosureTrigger());

const resultViewport = await waitFor(() => {
const viewport = getSectionViewport("result");
const viewport = findRichViewport();
if (!viewport.classList.contains("lc-shiki")) {
throw new Error("result viewport not yet highlighted");
throw new Error("read viewport not yet highlighted");
}
return viewport;
});

// No labeled args/result headers — only the rich view of the file body.
expect(findSectionHeader("args")).toBeNull();
expect(findSectionHeader("result")).toBeNull();

// The "1: " / "2: " line-number prefixes that the read tool emits should
// be stripped before highlighting.
expect(resultViewport.textContent).toContain("import { useEffect }");
Expand Down Expand Up @@ -72,16 +76,19 @@ describe("ToolCall — Claude View (Read) rich rendering", () => {
fireEvent.click(getDisclosureTrigger());

const resultViewport = await waitFor(() => {
const viewport = getSectionViewport("result");
const viewport = findRichViewport();
if (!viewport.textContent?.includes("plain note body")) {
throw new Error("result viewport not populated yet");
throw new Error("read viewport not populated yet");
}
return viewport;
});

// Result viewport for an unknown language stays in a plain <pre>, not the
// Shiki container. The args section (JSON) may still be highlighted —
// that's expected and irrelevant to this assertion.
// No labeled args/result headers — only the rich view of the file body.
expect(findSectionHeader("args")).toBeNull();
expect(findSectionHeader("result")).toBeNull();

// A read result with an unknown language renders in a plain <pre>, not the
// Shiki container.
expect(resultViewport.tagName.toLowerCase()).toBe("pre");
expect(resultViewport.classList.contains("lc-shiki")).toBe(false);
});
Expand All @@ -95,17 +102,17 @@ function getDisclosureTrigger(): HTMLElement {
return trigger;
}

function getSectionViewport(label: string): HTMLElement {
const headers = Array.from(document.querySelectorAll("div")).filter(
(el) => el.textContent?.trim() === label,
function findSectionHeader(label: string): HTMLElement | null {
return (
Array.from(document.querySelectorAll("div")).find((el) => el.textContent?.trim() === label) ??
null
);
const header = headers[0];
if (!header) {
throw new Error(`section label "${label}" not found`);
}
const viewport = header.nextElementSibling;
if (!(viewport instanceof HTMLElement)) {
throw new Error(`viewport sibling for section "${label}" not found`);
}
}

function findRichViewport(): HTMLElement {
const body = document.querySelector('[data-slot="disclosure-body"]');
if (!(body instanceof HTMLElement)) throw new Error("disclosure body not found");
const viewport = body.querySelector(".lc-shiki, pre");
if (!(viewport instanceof HTMLElement)) throw new Error("rich viewport not found");
return viewport;
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,21 +32,22 @@ export const ToolCall = memo(function ToolCall({ item }: ToolCallProps) {
? { path: lazyReadPath, projectLocation: paneActions.projectLocation }
: null;
const fetched = useReadAbsoluteFile(isExpanded ? fetchTarget : null);
const readResultPart =
payload?.kind === "read" && !lazyReadPath ? extractReadFileResultPart(payload) : undefined;
const hasReadResult = !!readResultPart && readResultPart.text.length > 0;
const sections = useMemo<ToolCallSection[]>(() => {
if (!isExpanded || !payload) return [];
if (lazyReadPath) return [];
if (lazyReadPath || hasReadResult) return [];
const isSkill = isSkillTool(payload);
const resultPart =
payload.kind === "read" ? extractReadFileResultPart(payload) : extractAcpResultPart(payload);
return [
{ label: "args", part: extractAcpArgsPart(payload) },
{
label: "result",
part: resultPart,
part: extractAcpResultPart(payload),
...(isSkill ? { renderAsMarkdown: true } : {}),
},
];
}, [isExpanded, payload, lazyReadPath]);
}, [isExpanded, payload, lazyReadPath, hasReadResult]);
if (!payload?.name) return null;
if (isContextCompactionToolCall(item)) return <ContextCompaction item={item} />;
if (isPlanProposalToolCall(item)) return <PlanProposal item={item} />;
Expand Down Expand Up @@ -76,6 +77,8 @@ export const ToolCall = memo(function ToolCall({ item }: ToolCallProps) {
) : (
<FileContentPlaceholder state={fetched.state} reason={fetched.reason} />
)
) : readResultPart && hasReadResult ? (
<CommandOutputViewport text={readResultPart.text} language={readResultPart.language} />
) : (
<ToolCallSections sections={sections} />
)}
Expand Down