| author | |
| committer | |
| log | 511d20c6f20c2ed6ff787aba068d7d673e2949b9 |
| tree | 905a01943f719acb0a1dbb6c28e725c05004f139 |
| parent | 0e7346ccd9427c06161e222319e718e6bd70887f |
| signature | Signed by PGP key B5690EEEBB952194 |
3 files changed, 47 insertions(+), 4 deletions(-)
apps/mobile/src/features/threads/thread-work-log.tsx+3-2| ... | ... | @@ -962,12 +962,14 @@ const ThreadWorkLogRow = memo(function ThreadWorkLogRow( |
| 962 | 962 | : row.getFullDetail() |
| 963 | 963 | : null; |
| 964 | 964 | const outputImages = expanded && fetchedItem ? turnItemOutputImages(fetchedItem) : []; |
| 965 | const viewedImagePath = workEntryViewedImagePath(row.workEntry); | |
| 965 | 966 | const fetchedOutput = !expanded |
| 966 | 967 | ? null |
| 967 | 968 | : shownItem.type === "file_search" || shownItem.type === "web_search" |
| 968 | 969 | ? turnItemOutputText(shownItem) |
| 969 | 970 | : fetchedItem |
| 970 | ? (turnItemOutputText(fetchedItem) ?? (outputImages.length > 0 ? null : "No output.")) | |
| 971 | ? (turnItemOutputText(fetchedItem) ?? | |
| 972 | (outputImages.length > 0 || viewedImagePath ? null : "No output.")) | |
| 971 | 973 | : fetchedDetail.error |
| 972 | 974 | ? `Couldn't load output: ${fetchedDetail.error}` |
| 973 | 975 | : row.fetchesDetail |
| ... | ... | @@ -975,7 +977,6 @@ const ThreadWorkLogRow = memo(function ThreadWorkLogRow( |
| 975 | 977 | ? "Output is no longer available." |
| 976 | 978 | : "Loading output…" |
| 977 | 979 | : null; |
| 978 | const viewedImagePath = workEntryViewedImagePath(row.workEntry); | |
| 979 | 980 | const toolPresentation = resolveWorkEntryToolPresentation(row.workEntry); |
| 980 | 981 | const previewText = workEntryRowLabel(row.workEntry); |
| 981 | 982 | const answerPreview = row.workEntry.questionAnswer |
packages/client-runtime/src/work-log/itemDetail.test.ts+38| ... | ... | @@ -52,4 +52,42 @@ describe("tool output images", () => { |
| 52 | 52 | expect(turnItemOutputImages({ ...screenshot, output })).toEqual([]); |
| 53 | 53 | expect(turnItemOutputText({ ...screenshot, output })).toBe("[image]"); |
| 54 | 54 | }); |
| 55 | ||
| 56 | it("omits Claude Read's image placeholder when the file has a preview", () => { | |
| 57 | const output = { | |
| 58 | type: "image", | |
| 59 | file: { filePath: "/tmp/blk.png", type: "image/png", sizeBytes: 1024 }, | |
| 60 | }; | |
| 61 | const item = { ...screenshot, toolName: "Read", viewedImagePath: "/tmp/blk.png", output }; | |
| 62 | ||
| 63 | expect(turnItemOutputImages(item)).toEqual([]); | |
| 64 | expect(turnItemOutputText(item)).toBeNull(); | |
| 65 | expect(turnItemOutputText({ ...item, viewedImagePath: undefined })).toBe("[image]"); | |
| 66 | }); | |
| 67 | ||
| 68 | it("keeps accompanying text and literal image markers when the file has a preview", () => { | |
| 69 | const output = { | |
| 70 | content: [ | |
| 71 | { type: "text", text: "Read /tmp/blk.png. Literal marker: [image]" }, | |
| 72 | screenshot.output.content[1], | |
| 73 | ], | |
| 74 | }; | |
| 75 | expect(turnItemOutputText({ ...screenshot, viewedImagePath: "/tmp/blk.png", output })).toBe( | |
| 76 | "Read /tmp/blk.png. Literal marker: [image]", | |
| 77 | ); | |
| 78 | }); | |
| 79 | ||
| 80 | it("keeps placeholders for unserved images in a list despite a file preview", () => { | |
| 81 | const blocks = [ | |
| 82 | { type: "text", text: "Read /tmp/blk.png." }, | |
| 83 | { type: "image", file: { type: "image/png", sizeBytes: 1024 } }, | |
| 84 | screenshot.output.content[1], | |
| 85 | { type: "image", mimeType: "image/svg+xml" }, | |
| 86 | ]; | |
| 87 | for (const output of [blocks, { content: blocks }, { content: blocks, isError: false }]) { | |
| 88 | const item = { ...screenshot, viewedImagePath: "/tmp/blk.png", output }; | |
| 89 | expect(turnItemOutputText(item)).toBe("Read /tmp/blk.png.\n[image]\n[image]"); | |
| 90 | expect(turnItemOutputImages(item)).toHaveLength(1); | |
| 91 | } | |
| 92 | }); | |
| 55 | 93 | }); |
packages/client-runtime/src/work-log/itemDetail.ts+6-2| ... | ... | @@ -65,8 +65,10 @@ function prettyJsonText(text: string): string { |
| 65 | 65 | } |
| 66 | 66 | |
| 67 | 67 | /** Formats a tool input or output for display: text blocks as text, the rest as JSON. */ |
| 68 | function formatToolValue(value: unknown): string | null { | |
| 68 | function formatToolValue(value: unknown, hasImagePreview = false): string | null { | |
| 69 | 69 | if (value === undefined || value === null) return null; |
| 70 | // Claude Read returns one structured image; lists may contain other images. | |
| 71 | if (hasImagePreview && isRecord(value) && value.type === "image") return null; | |
| 70 | 72 | const text = textFromBlocks(value, 0); |
| 71 | 73 | if (text !== null) return text.trim() ? prettyJsonText(text) : null; |
| 72 | 74 | let json: string | undefined; |
| ... | ... | @@ -171,7 +173,9 @@ export function turnItemOutputText(item: OrchestrationV2TurnItem): string | null |
| 171 | 173 | case "command_execution": |
| 172 | 174 | return item.output?.trim() ? commandOutputText(item.output) || null : null; |
| 173 | 175 | case "dynamic_tool": |
| 174 | return item.outputOmitted === true ? null : formatToolValue(item.output); | |
| 176 | return item.outputOmitted === true | |
| 177 | ? null | |
| 178 | : formatToolValue(item.output, item.viewedImagePath !== undefined); | |
| 175 | 179 | case "file_search": |
| 176 | 180 | return item.results?.length |
| 177 | 181 | ? item.results |