diff --git a/src/renderer/components/thread/ChatPane/parts/items/ToolCall.renderArtifact.test.tsx b/src/renderer/components/thread/ChatPane/parts/items/ToolCall.renderArtifact.test.tsx index dea1b85..4d70ec8 100644 --- a/src/renderer/components/thread/ChatPane/parts/items/ToolCall.renderArtifact.test.tsx +++ b/src/renderer/components/thread/ChatPane/parts/items/ToolCall.renderArtifact.test.tsx @@ -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; }); diff --git a/src/renderer/components/thread/ChatPane/parts/items/ToolCall.test.tsx b/src/renderer/components/thread/ChatPane/parts/items/ToolCall.test.tsx index efbff43..1fecf2a 100644 --- a/src/renderer/components/thread/ChatPane/parts/items/ToolCall.test.tsx +++ b/src/renderer/components/thread/ChatPane/parts/items/ToolCall.test.tsx @@ -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", @@ -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 }"); @@ -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
, 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 
, not the
+    // Shiki container.
     expect(resultViewport.tagName.toLowerCase()).toBe("pre");
     expect(resultViewport.classList.contains("lc-shiki")).toBe(false);
   });
@@ -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;
 }
diff --git a/src/renderer/components/thread/ChatPane/parts/items/ToolCall.tsx b/src/renderer/components/thread/ChatPane/parts/items/ToolCall.tsx
index 1e8d63a..f6d7d78 100644
--- a/src/renderer/components/thread/ChatPane/parts/items/ToolCall.tsx
+++ b/src/renderer/components/thread/ChatPane/parts/items/ToolCall.tsx
@@ -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(() => {
     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 ;
   if (isPlanProposalToolCall(item)) return ;
@@ -76,6 +77,8 @@ export const ToolCall = memo(function ToolCall({ item }: ToolCallProps) {
         ) : (
           
         )
+      ) : readResultPart && hasReadResult ? (
+        
       ) : (
         
       )}