-
Notifications
You must be signed in to change notification settings - Fork 3k
feat(cli): render inline terminal images #8305
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
wenshao
merged 15 commits into
QwenLM:main
from
tlysanhuo:tlysanhuo/inline-terminal-images
Aug 6, 2026
Merged
Changes from all commits
Commits
Show all changes
15 commits
Select commit
Hold shift + click to select a range
7b772c9
feat(cli): render inline terminal images
tlysanhuo f502e95
Merge remote-tracking branch 'upstream/main' into tlysanhuo/inline-te…
tlysanhuo e5f892e
Merge remote-tracking branch 'upstream/main' into tlysanhuo/inline-te…
tlysanhuo f74d954
fix(cli): preserve text around hidden citations
tlysanhuo 447bccb
fix(cli): clear pending image state on reset
tlysanhuo d253fd7
Merge branch 'main' into tlysanhuo/inline-terminal-images
wenshao 0edfa6c
fix(cli): bound inline image rendering
tlysanhuo cf7f9e7
fix(cli): clear compacted image overflow markers
tlysanhuo ff4284b
Merge branch 'main' into tlysanhuo/inline-terminal-images
wenshao f97d233
docs(cli): clarify inline image resume scope
tlysanhuo 29298b9
Merge branch 'main' into tlysanhuo/inline-terminal-images
wenshao ac7d564
fix(cli): address inline image review feedback
tlysanhuo 1e1a6e8
Merge remote-tracking branch 'upstream/main' into tlysanhuo/inline-te…
tlysanhuo fcd0eb7
Merge branch 'main' into tlysanhuo/inline-terminal-images
wenshao b9ebfdd
Merge branch 'main' into tlysanhuo/inline-terminal-images
wenshao File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,97 @@ | ||
| # Terminal Inline Images E2E Plan | ||
|
|
||
| ## Baseline | ||
|
|
||
| 1. Start the interactive CLI from `main`. | ||
| 2. Return an assistant response containing | ||
| `text -> inlineData(image/png) -> text`. | ||
| 3. Run a tool whose `functionResponse.parts` contains an `image/png`. | ||
| 4. Confirm that `main` omits the assistant image and reduces tool media to | ||
| text, while the separate `display_image` tool from #8217 can display a | ||
| workspace PNG path. | ||
|
|
||
| The global `qwen` executable is unavailable in the current environment, so the | ||
| baseline is grounded in issue #8090 and the unchanged `main` event mapping. | ||
|
|
||
| ## Verification | ||
|
|
||
| ### Shared renderer regression | ||
|
|
||
| 1. Ask the model to call `display_image` for a workspace PNG. | ||
| 2. In direct Kitty or Ghostty, confirm the existing native preview still | ||
| renders. | ||
| 3. In a non-native terminal with `chafa` installed, confirm the existing ANSI | ||
| preview still renders. | ||
| 4. Confirm the file-path tool retains its workspace, file-size, and PNG | ||
| validation behavior. | ||
|
|
||
| ### Inline assistant and tool images | ||
|
|
||
| 1. Return a 1x1 PNG between two assistant text parts. | ||
| 2. Confirm the transcript order is `text -> image -> text`. | ||
| 3. Return a PNG in a successful tool's top-level and nested | ||
| `functionResponse.parts`. | ||
| 4. Confirm the successful tool row retains its images. | ||
| 5. Open Ctrl+O and resume the session; confirm successful tool image order is | ||
| reconstructed from persisted parts. Assistant output resumes its persisted | ||
| text; assistant inline images are not persisted by the current Core recorder. | ||
| 6. Return six images in one assistant output and one tool response; confirm the | ||
| first four render and the row ends with `[+2 more images]`. | ||
|
|
||
| ### Kitty/Ghostty | ||
|
|
||
| 1. Start the CLI in direct Kitty or Ghostty without tmux or SSH. | ||
| 2. Repeat the assistant and tool cases. | ||
| 3. Confirm inline PNGs use the same virtual placement and Unicode placeholders | ||
| as `display_image`. | ||
| 4. Confirm remounting a history row does not retransmit an already-written | ||
| payload. | ||
|
|
||
| ### chafa | ||
|
|
||
| 1. Start the CLI in Warp, iTerm2, tmux, SSH, or another non-native environment | ||
| with `chafa` installed. | ||
| 2. Repeat the assistant and tool cases. | ||
| 3. Confirm PNG bytes render as ANSI symbol rows and stay aligned during normal | ||
| scrolling. | ||
| 4. Confirm image data is supplied through stdin and no model-controlled value | ||
| is used as a command argument. | ||
|
|
||
| ### Placeholders and accessibility | ||
|
|
||
| 1. Repeat without `chafa` and outside direct Kitty/Ghostty. | ||
| 2. Confirm a valid PNG displays `[image: <width>x<height> png]`. | ||
| 3. Repeat with malformed base64, a payload above 8 MiB, invalid IHDR | ||
| dimensions, and a non-PNG MIME type. | ||
| 4. Confirm no raw image sequence is written. Confirm the oversized payload is | ||
| dropped before UI history, while admitted malformed/non-PNG data uses a | ||
| deterministic placeholder. | ||
| 5. Repeat with `INK_SCREEN_READER=true`; confirm only the placeholder is | ||
| emitted. | ||
|
tlysanhuo marked this conversation as resolved.
|
||
|
|
||
| ### Stream lifecycle | ||
|
|
||
| 1. Exercise a fresh retry, continuation retry, model fallback, cancellation, | ||
| stream boundary, tool call boundary, and displayed goal-state event. | ||
| 2. Confirm fresh attempts discard all staged image/text runs from the failed | ||
| attempt. | ||
| 3. Confirm continuations preserve partial output. | ||
| 4. Confirm every normal boundary commits earlier runs before later status or | ||
| tool rows. | ||
|
|
||
| ## Automated Evidence | ||
|
|
||
| Record final results for: | ||
|
|
||
| - focused CLI renderer, component, stream, tool, resume, and compaction tests; | ||
| - Core `Turn`, `display_image`, config, scheduler, and tool tests; | ||
| - `npm run lint:ci`; | ||
| - `npm run typecheck`; | ||
| - `npm run build`; | ||
| - `npm run check:serve-fast-path-bundle`. | ||
|
|
||
| Real-terminal output remains a reviewer hardware step when the local environment | ||
| has no Kitty/Ghostty session. Reusing #8217's renderer means its existing manual | ||
| Kitty, Ghostty, cmux, and Warp evidence continues to cover the terminal protocol | ||
| layer; this plan focuses new manual verification on the inline-data entry point | ||
| and transcript lifecycle. | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,138 @@ | ||
| # Terminal Inline Images | ||
|
|
||
| ## Problem | ||
|
|
||
| The interactive CLI drops model `inlineData` image parts at the | ||
| `Turn`-to-TUI boundary. Images nested in tool `functionResponse.parts` | ||
| survive in model history, but the tool display reduces them to text. As a | ||
| result, image-generating models and screenshot-producing tools cannot show | ||
| their output in the conversation. | ||
|
|
||
| PR #8217 introduced the path-based `display_image` tool and established the | ||
| project's terminal image infrastructure: `TerminalImage`, | ||
| `terminal-image-renderer`, native Kitty/Ghostty placement, and `chafa` | ||
| symbol output. This change extends that infrastructure to in-memory model and | ||
| tool image parts instead of adding another renderer. | ||
|
|
||
| ## Scope | ||
|
|
||
| This is the render-and-forget slice requested by issue #8090: | ||
|
|
||
| - preserve ordered text and image parts on content events without changing the | ||
| existing concatenated `value` contract; | ||
| - render live assistant PNGs and restored successful tool PNGs through the | ||
| #8217 component and renderer; | ||
| - render PNGs nested in successful tool responses; | ||
| - keep text/image ordering across retry, model fallback, cancellation, stream | ||
| boundaries, and goal-state events; | ||
| - bound retained image payloads during UI history compaction; | ||
| - render at most four images per assistant output or tool row and collapse the | ||
| remainder into a `[+K more images]` marker; | ||
| - show a deterministic text placeholder when an image cannot be rendered. | ||
|
|
||
| Kitty deletion, resize-driven replacement, terminal cell pixel queries, and | ||
| global scroll lifecycle ownership remain out of scope and are tracked in | ||
| #8520. | ||
|
|
||
| ## Data Flow | ||
|
|
||
| ### Model output | ||
|
|
||
| `ServerGeminiContentEvent.value` remains the concatenated text consumed by | ||
| existing clients. When a response chunk contains image `inlineData`, the | ||
| event also carries an optional ordered `parts` field containing displayable | ||
| non-thought text and image parts. | ||
|
|
||
| Only the interactive TUI reads `parts`. It stages text and image history | ||
| items in their original order. A fresh retry or model fallback discards the | ||
| failed attempt's staged output, while a normal response boundary commits it. | ||
| The TUI admits at most four images for one assistant output and represents any | ||
| remaining image parts with a small overflow marker. Visible status rows end the | ||
| current assistant display block, so later text starts with the normal assistant | ||
| prefix instead of being attached to the status row as a continuation. | ||
| Text-only events keep their existing runtime shape, so non-interactive output, | ||
| SDK, ACP, daemon, channel, Web UI, and VS Code consumers continue using | ||
| `value` unchanged. | ||
|
|
||
| After a thrown stream, staged output remains transient so an explicit retry can | ||
| discard the failed attempt. If an out-of-band shell or slash-command item is | ||
| added before the next model submit, that item can enter committed history before | ||
| the staged output. The next model submit commits the staged output; history | ||
| clear, resume, branch, restore, rewind, and Ctrl+L paths discard it instead. | ||
|
|
||
| Resume logic reconstructs ordered text/image runs when persisted parts contain | ||
| them. Tool responses retain nested image parts in the session record. The | ||
| current Core recorder flattens assistant output to text, so live assistant | ||
| images are not restored by `--continue`; assistant-image persistence is outside | ||
| this slice and tracked in #8521. | ||
|
|
||
| ### Tool output | ||
|
|
||
| Tool media is stored in `functionResponse.parts`. A CLI extractor reads image | ||
| `inlineData` from top-level and nested response parts. Live scheduler mapping | ||
| and resume mapping attach the images to the existing | ||
| `IndividualToolCallDisplay`. Each successful tool row keeps the first four | ||
| images and an overflow count for the rest. Failed and cancelled tool records | ||
| currently do not carry inline image parts from Core, so their image handling is | ||
| defensive rather than a supported output path in this slice. | ||
|
|
||
| Tools carrying images render individually even when their text-only form would | ||
| normally collapse into a read/search summary. `ToolMessage` routes the images | ||
| through the same `TerminalImage` component used by assistant messages. | ||
|
|
||
| ## Rendering | ||
|
|
||
| The existing #8217 file-path entry point is unchanged. The shared renderer | ||
| adds an in-memory PNG entry point that: | ||
|
|
||
| 1. validates bounded base64 before decoding; | ||
| 2. verifies the PNG signature and IHDR dimensions; | ||
| 3. rejects payloads above 8 MiB, dimensions above 1,000,000 pixels, or images | ||
| above 64 million total pixels; | ||
| 4. reuses the existing terminal sizing and bounded render cache; | ||
| 5. uses native Kitty placement in direct Kitty/Ghostty sessions; | ||
| 6. passes PNG bytes to `chafa` over stdin in other supported environments; | ||
| 7. returns a text placeholder when rendering is unavailable. | ||
|
|
||
| No temporary file is created. The inline payload is never used as a command | ||
| argument, and `chafa` receives the same allowlisted environment as the | ||
| path-based renderer. | ||
|
|
||
| The fallback format is `[image: <width>x<height> png]`. Invalid PNG data | ||
| becomes `[image: png]`; unsupported image MIME types retain their sanitized | ||
| format label, such as `[image: jpeg]`. Screen-reader mode always uses the text | ||
| placeholder and emits no raw image sequence. | ||
|
|
||
| The same encoded-length limit is applied before inline data enters CLI history | ||
| or tool-display state. Payloads that exceed the renderer's 8 MiB decoded-image | ||
| budget are dropped before rendering and do not produce a placeholder. | ||
|
|
||
| The first slice renders validated PNG data only. Other image MIME types remain | ||
| visible as deterministic placeholders rather than entering a second protocol | ||
| or decoding path. | ||
|
|
||
| ## Memory | ||
|
|
||
| Encoded images are much larger than ordinary history text. UI compaction drops | ||
| payloads from old assistant image items while retaining the 20 most recent | ||
| items. Cleared images leave a visible marker instead of becoming blank rows. | ||
| Tool image payloads participate in the existing tool-result compaction limit. | ||
| The four-image admission cap also bounds synchronous Kitty/chafa rendering for | ||
| each assistant output and tool row. | ||
|
|
||
| ## Test Plan | ||
|
|
||
| - Keep every #8217 renderer and `display_image` test green. | ||
| - Verify inline PNG validation, Kitty rendering, `chafa` stdin rendering, | ||
| screen-reader output, and unavailable-renderer placeholders. | ||
| - Verify `Turn` preserves mixed `text -> image -> text` ordering while | ||
| retaining the old `value` and text-only event shape. | ||
| - Verify live TUI ordering across retry, fallback, cancellation, stream | ||
| boundaries, and goal-state events. | ||
| - Verify live and restored successful tool responses expose nested images. | ||
| - Verify the resume parser preserves assistant text/image ordering for records | ||
| that already contain persisted parts; the current recorder's assistant-image | ||
| persistence gap remains outside this slice. | ||
| - Verify live assistant and successful tool output enforce the image cap and | ||
| expose the overflow count; oversized payloads are dropped before UI history. | ||
| - Verify memory compaction clears old assistant and tool image payloads. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.