| 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,12 +962,14 @@ const ThreadWorkLogRow = memo(function ThreadWorkLogRow( |
| 962 | : row.getFullDetail() | 962 | : row.getFullDetail() |
| 963 | : null; | 963 | : null; |
| 964 | const outputImages = expanded && fetchedItem ? turnItemOutputImages(fetchedItem) : []; | 964 | const outputImages = expanded && fetchedItem ? turnItemOutputImages(fetchedItem) : []; |
| 965 | const viewedImagePath = workEntryViewedImagePath(row.workEntry); | ||
| 965 | const fetchedOutput = !expanded | 966 | const fetchedOutput = !expanded |
| 966 | ? null | 967 | ? null |
| 967 | : shownItem.type === "file_search" || shownItem.type === "web_search" | 968 | : shownItem.type === "file_search" || shownItem.type === "web_search" |
| 968 | ? turnItemOutputText(shownItem) | 969 | ? turnItemOutputText(shownItem) |
| 969 | : fetchedItem | 970 | : fetchedItem |
| 970 | ? (turnItemOutputText(fetchedItem) ?? (outputImages.length > 0 ? null : "No output.")) | 971 | ? (turnItemOutputText(fetchedItem) ?? |
| 972 | (outputImages.length > 0 || viewedImagePath ? null : "No output.")) | ||
| 971 | : fetchedDetail.error | 973 | : fetchedDetail.error |
| 972 | ? `Couldn't load output: ${fetchedDetail.error}` | 974 | ? `Couldn't load output: ${fetchedDetail.error}` |
| 973 | : row.fetchesDetail | 975 | : row.fetchesDetail |
| ... | @@ -975,7 +977,6 @@ const ThreadWorkLogRow = memo(function ThreadWorkLogRow( | ... | @@ -975,7 +977,6 @@ const ThreadWorkLogRow = memo(function ThreadWorkLogRow( |
| 975 | ? "Output is no longer available." | 977 | ? "Output is no longer available." |
| 976 | : "Loading output…" | 978 | : "Loading output…" |
| 977 | : null; | 979 | : null; |
| 978 | const viewedImagePath = workEntryViewedImagePath(row.workEntry); | ||
| 979 | const toolPresentation = resolveWorkEntryToolPresentation(row.workEntry); | 980 | const toolPresentation = resolveWorkEntryToolPresentation(row.workEntry); |
| 980 | const previewText = workEntryRowLabel(row.workEntry); | 981 | const previewText = workEntryRowLabel(row.workEntry); |
| 981 | const answerPreview = row.workEntry.questionAnswer | 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,4 +52,42 @@ describe("tool output images", () => { |
| 52 | expect(turnItemOutputImages({ ...screenshot, output })).toEqual([]); | 52 | expect(turnItemOutputImages({ ...screenshot, output })).toEqual([]); |
| 53 | expect(turnItemOutputText({ ...screenshot, output })).toBe("[image]"); | 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,8 +65,10 @@ function prettyJsonText(text: string): string { |
| 65 | } | 65 | } |
| 66 | 66 | ||
| 67 | /** Formats a tool input or output for display: text blocks as text, the rest as JSON. */ | 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 | if (value === undefined || value === null) return null; | 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 | const text = textFromBlocks(value, 0); | 72 | const text = textFromBlocks(value, 0); |
| 71 | if (text !== null) return text.trim() ? prettyJsonText(text) : null; | 73 | if (text !== null) return text.trim() ? prettyJsonText(text) : null; |
| 72 | let json: string | undefined; | 74 | let json: string | undefined; |
| ... | @@ -171,7 +173,9 @@ export function turnItemOutputText(item: OrchestrationV2TurnItem): string | null | ... | @@ -171,7 +173,9 @@ export function turnItemOutputText(item: OrchestrationV2TurnItem): string | null |
| 171 | case "command_execution": | 173 | case "command_execution": |
| 172 | return item.output?.trim() ? commandOutputText(item.output) || null : null; | 174 | return item.output?.trim() ? commandOutputText(item.output) || null : null; |
| 173 | case "dynamic_tool": | 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 | case "file_search": | 179 | case "file_search": |
| 176 | return item.results?.length | 180 | return item.results?.length |
| 177 | ? item.results | 181 | ? item.results |