Repository navigation
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 3 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
Some comments aren't visible on the classic Files Changed page.
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,92 @@ | ||
| # 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 tool's top-level and nested `functionResponse.parts`. | ||
| 4. Confirm both successful and failed/cancelled tool rows retain their images. | ||
| 5. Open Ctrl+O and resume the session; confirm assistant and tool image order is | ||
| reconstructed from persisted parts. | ||
|
|
||
| ### 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 and a deterministic placeholder | ||
| remains visible. | ||
| 5. Repeat with `INK_SCREEN_READER=true`; confirm only the placeholder is | ||
| emitted. | ||
|
|
||
| ### 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,108 @@ | ||
| # 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 and restored assistant PNGs through the #8217 component and | ||
| renderer; | ||
| - render PNGs nested in successful, failed, or cancelled 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; | ||
| - 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. | ||
|
tlysanhuo marked this conversation as resolved.
Resolved
|
||
|
|
||
| ## 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. | ||
| 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. | ||
|
|
||
| Recorded assistant messages already retain their original parts. Resume logic | ||
| reconstructs ordered text/image runs instead of flattening images away. | ||
|
|
||
| ### 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`. | ||
|
|
||
| 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 or dimensions above 1,000,000 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 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. | ||
|
|
||
| ## 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 tool responses expose nested images. | ||
| - Verify restored assistant history preserves text/image ordering. | ||
| - 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
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.