Skip to content

Commit 4bbd029

Browse files
authored
refactor(renderer): render read tool results directly without labels (#13)
- Update ToolCall to render read file content directly via CommandOutputViewport - Omit the standard args and result section headers for read tool calls - Update rendering and artifact tests to query disclosure body selectors directly
1 parent e5775a5 commit 4bbd029

3 files changed

Lines changed: 38 additions & 31 deletions

File tree

src/renderer/components/thread/ChatPane/parts/items/ToolCall.renderArtifact.test.tsx

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -51,14 +51,11 @@ describe.skipIf(!enabled)("ToolCall — render artifact", () => {
5151
fireEvent.click(trigger);
5252

5353
const viewport = await waitFor(() => {
54-
const headers = Array.from(document.querySelectorAll("div")).filter(
55-
(el) => el.textContent?.trim() === "result",
56-
);
57-
const header = headers[0];
58-
if (!header) throw new Error("result header not found");
59-
const sibling = header.nextElementSibling;
54+
const body = document.querySelector('[data-slot="disclosure-body"]');
55+
if (!(body instanceof HTMLElement)) throw new Error("disclosure body not found");
56+
const sibling = body.querySelector(".lc-shiki, pre");
6057
if (!(sibling instanceof HTMLElement) || !sibling.classList.contains("lc-shiki")) {
61-
throw new Error("result viewport not yet highlighted");
58+
throw new Error("read viewport not yet highlighted");
6259
}
6360
return sibling;
6461
});

src/renderer/components/thread/ChatPane/parts/items/ToolCall.test.tsx

Lines changed: 26 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import type { RuntimeChatItem } from "@/renderer/state/slices/runtimeEventSlice"
55
import { ToolCall } from "./ToolCall";
66

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

3131
const resultViewport = await waitFor(() => {
32-
const viewport = getSectionViewport("result");
32+
const viewport = findRichViewport();
3333
if (!viewport.classList.contains("lc-shiki")) {
34-
throw new Error("result viewport not yet highlighted");
34+
throw new Error("read viewport not yet highlighted");
3535
}
3636
return viewport;
3737
});
3838

39+
// No labeled args/result headers — only the rich view of the file body.
40+
expect(findSectionHeader("args")).toBeNull();
41+
expect(findSectionHeader("result")).toBeNull();
42+
3943
// The "1: " / "2: " line-number prefixes that the read tool emits should
4044
// be stripped before highlighting.
4145
expect(resultViewport.textContent).toContain("import { useEffect }");
@@ -72,16 +76,19 @@ describe("ToolCall — Claude View (Read) rich rendering", () => {
7276
fireEvent.click(getDisclosureTrigger());
7377

7478
const resultViewport = await waitFor(() => {
75-
const viewport = getSectionViewport("result");
79+
const viewport = findRichViewport();
7680
if (!viewport.textContent?.includes("plain note body")) {
77-
throw new Error("result viewport not populated yet");
81+
throw new Error("read viewport not populated yet");
7882
}
7983
return viewport;
8084
});
8185

82-
// Result viewport for an unknown language stays in a plain <pre>, not the
83-
// Shiki container. The args section (JSON) may still be highlighted —
84-
// that's expected and irrelevant to this assertion.
86+
// No labeled args/result headers — only the rich view of the file body.
87+
expect(findSectionHeader("args")).toBeNull();
88+
expect(findSectionHeader("result")).toBeNull();
89+
90+
// A read result with an unknown language renders in a plain <pre>, not the
91+
// Shiki container.
8592
expect(resultViewport.tagName.toLowerCase()).toBe("pre");
8693
expect(resultViewport.classList.contains("lc-shiki")).toBe(false);
8794
});
@@ -95,17 +102,17 @@ function getDisclosureTrigger(): HTMLElement {
95102
return trigger;
96103
}
97104

98-
function getSectionViewport(label: string): HTMLElement {
99-
const headers = Array.from(document.querySelectorAll("div")).filter(
100-
(el) => el.textContent?.trim() === label,
105+
function findSectionHeader(label: string): HTMLElement | null {
106+
return (
107+
Array.from(document.querySelectorAll("div")).find((el) => el.textContent?.trim() === label) ??
108+
null
101109
);
102-
const header = headers[0];
103-
if (!header) {
104-
throw new Error(`section label "${label}" not found`);
105-
}
106-
const viewport = header.nextElementSibling;
107-
if (!(viewport instanceof HTMLElement)) {
108-
throw new Error(`viewport sibling for section "${label}" not found`);
109-
}
110+
}
111+
112+
function findRichViewport(): HTMLElement {
113+
const body = document.querySelector('[data-slot="disclosure-body"]');
114+
if (!(body instanceof HTMLElement)) throw new Error("disclosure body not found");
115+
const viewport = body.querySelector(".lc-shiki, pre");
116+
if (!(viewport instanceof HTMLElement)) throw new Error("rich viewport not found");
110117
return viewport;
111118
}

src/renderer/components/thread/ChatPane/parts/items/ToolCall.tsx

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -32,21 +32,22 @@ export const ToolCall = memo(function ToolCall({ item }: ToolCallProps) {
3232
? { path: lazyReadPath, projectLocation: paneActions.projectLocation }
3333
: null;
3434
const fetched = useReadAbsoluteFile(isExpanded ? fetchTarget : null);
35+
const readResultPart =
36+
payload?.kind === "read" && !lazyReadPath ? extractReadFileResultPart(payload) : undefined;
37+
const hasReadResult = !!readResultPart && readResultPart.text.length > 0;
3538
const sections = useMemo<ToolCallSection[]>(() => {
3639
if (!isExpanded || !payload) return [];
37-
if (lazyReadPath) return [];
40+
if (lazyReadPath || hasReadResult) return [];
3841
const isSkill = isSkillTool(payload);
39-
const resultPart =
40-
payload.kind === "read" ? extractReadFileResultPart(payload) : extractAcpResultPart(payload);
4142
return [
4243
{ label: "args", part: extractAcpArgsPart(payload) },
4344
{
4445
label: "result",
45-
part: resultPart,
46+
part: extractAcpResultPart(payload),
4647
...(isSkill ? { renderAsMarkdown: true } : {}),
4748
},
4849
];
49-
}, [isExpanded, payload, lazyReadPath]);
50+
}, [isExpanded, payload, lazyReadPath, hasReadResult]);
5051
if (!payload?.name) return null;
5152
if (isContextCompactionToolCall(item)) return <ContextCompaction item={item} />;
5253
if (isPlanProposalToolCall(item)) return <PlanProposal item={item} />;
@@ -76,6 +77,8 @@ export const ToolCall = memo(function ToolCall({ item }: ToolCallProps) {
7677
) : (
7778
<FileContentPlaceholder state={fetched.state} reason={fetched.reason} />
7879
)
80+
) : readResultPart && hasReadResult ? (
81+
<CommandOutputViewport text={readResultPart.text} language={readResultPart.language} />
7982
) : (
8083
<ToolCallSections sections={sections} />
8184
)}

0 commit comments

Comments
 (0)