qwen-code 终端内联图片渲染:display_image 工具、Kitty/chafa 三级降级与 E2E 验证指南
【免费下载链接】qwen-codeAn open-source AI coding agent that lives in your terminal.项目地址: https://gitcode.com/GitHub_Trending/qw/qwen-code
本文以仓库 .qwen/e2e-tests/terminal-inline-images.md 的 E2E 测试计划为主体,系统讲解 qwen-code 在交互式终端中显示图片的完整方案:display_image工具的文件路径渲染、AI 回复与工具结果中的内联inlineData图片渲染、Kitty/Ghostty 原生协议与 chafa ANSI 回退的降级链路,以及占位符、无障碍模式与流生命周期下的验证要点。读完本文,你将掌握这套终端图片渲染的架构边界、安全约束与可执行的回归验证步骤。
背景:为什么要做终端内联图片
qwen-code 是一款运行在终端中的开源 AI 编码 Agent(见 README.md)。终端本身没有浏览器那样的 DOM 可以承载图片,要在 CLI 里让用户看到模型输出的图片、或让模型调用工具展示工作区里的 PNG,就必须依赖终端协议能力。仓库的方案由两条输入通道组成:
- 文件路径通道:模型调用
display_image工具,传入工作区内 PNG 的绝对路径,由终端渲染器读取文件并显示(已有能力,见 display-image.ts)。 - 内联数据通道:模型回复或工具结果的
parts中直接携带inlineData(base64 编码的image/png),CLI 需要把图片从"模型事件流"提取出来,按顺序渲染进转录(transcript),这是 .qwen/e2e-tests/terminal-inline-images.md 重点验证的新入口点。
E2E 计划的开篇 "Baseline" 明确了当前基线:在main分支上,助手回复中text -> inlineData(image/png) -> text的图片会被省略,工具functionResponse.parts中的 PNG 会被降级为文本;而 #8217 引入的display_image渲染器已能显示工作区 PNG。因此验证的核心就是:复用display_image的渲染器,补齐内联数据入口与转录生命周期,确保两条通道行为一致。
渲染架构:一个共享渲染器,两条数据入口
渲染器分层(三级降级)
终端图片渲染的统一实现位于 packages/cli/src/ui/utils/terminal-image-renderer.ts,按能力降级为三级:
- Kitty 图像协议(原生):当满足
supportsKittyImageProtocol()的条件时(标准输出为 TTY、无TMUX/SSH_TTY/SSH_CLIENT环境变量、TERM含kitty或TERM_PROGRAM含ghostty,且非 Warp 终端),渲染器把 PNG 编码为 Kitty 虚拟图像序列encodeKittyVirtualImage,并在屏幕上布局对应行数的 Unicode 占位符buildKittyPlaceholder。终端负责从占位符单元格重绘图像,因此同一图像只需传输一次 base64 载荷——这正是 E2E 计划中"remounting a history row does not retransmit an already-written payload"的实现基础(源码见wasKittyImageWritten/markKittyImageWritten与TRANSMITTED_KEY_LIMIT = 256的会话级已写集合,见 terminal-image-renderer.ts)。 - chafa ANSI 回退:在不支持 Kitty 协议的环境中,若
findExecutable('chafa')能在 PATH 中找到 chafa,则调用chafa --animate=off --colors=256 --format=symbols --symbols=block --size=WxH生成 ANSI 符号行。图片数据通过 stdin(input)传入,文件路径仅用于--size之外的显式参数,模型可控值绝不允许作为命令参数拼接,且通过CMD_SHELL_METACHARACTERS正则与shouldRunThroughShell防御 Windows.cmd/.batshim 的命令注入(见 terminal-image-renderer.ts 与 renderWithChafa)。chafa 渲染有 8 秒超时、2 MiB 输出上限。 - 纯文本占位符:两者都不可用时(或无障碍模式开启时),输出确定性的占位符文本
[image: <width>x<height> png]。
文件路径通道:display_image 工具
display_image工具定义在 packages/core/src/tools/display-image.ts,它是一个只读声明式工具,完整校验链为:
file_path必须是绝对路径、经过unescapePath清洗、非空;- 路径必须位于当前工作区内(
isPathWithinWorkspace校验); - 执行时校验:文件存在、是普通文件而非目录、大小不超过
MAX_TERMINAL_IMAGE_BYTES(即 8 MiB,常量定义见 packages/core/src/tools/tools.ts); - 读取 24 字节 PNG 头,校验
89504e470d0a1a0a签名,截断或非 PNG 文件直接报错; - 只能由主 Agent 执行:
isInForkExecution()时返回EXECUTION_DENIED; - 渲染支持探测通过后返回
TerminalImageDisplay(type: 'terminal_image'),CLI 侧的FileTerminalImage组件渲染它。
该工具是纯展示性的:llmContent明确告知模型"此工具不向模型提供图片内容,需要理解图片请用 read_file"。CLI 组件还会二次校验路径仍在工作区内,越界直接拒绝显示。
内联数据通道:parts 提取与顺序保持
内联图片的提取逻辑位于 packages/cli/src/ui/utils/inline-image-parts.ts:
getInlineImageData(part):仅接受mimeType以image/开头、data为字符串且长度不超过MAX_INLINE_IMAGE_ENCODED_LENGTH的 part(编码上限由 8 MiB 原始字节按 base64 膨胀 4/3 换算并向上取整);collectInlineImages(parts):遍历顶层 part 与functionResponse.parts中的嵌套 part,每个条目最多渲染 4 张图片(MAX_INLINE_IMAGES_PER_ITEM = 4),超出部分计入omittedImageCount;extractInlineContentRuns(parts):把 parts 流折叠为text -> image -> text交替的 run 序列,thoughtpart 被跳过,保证转录顺序为text -> image -> text,溢出图片在末尾折叠为[+N more images](对应计划中"six images render four and the row ends with[+2 more images]")。
CLI 侧 TerminalImage.tsx 根据 props 是否含image字段自动分派到InlineTerminalImage或FileTerminalImage,两者共用RenderedTerminalImage:Kitty 序列通过writeRaw写入、chafa 行进入MaxSizedBox、不可用时显示占位符。渲染结果还有两层缓存:40 条/32 MiB 的渲染结果 LRU 与 4 条的内联解码缓存,避免 resize 与历史行重挂时重复读盘、重复派生 chafa。
内联图片的完整性校验与占位符语义
内联数据来自模型输出,属于不可信输入,必须在渲染前做完整性校验。getDecodedInlinePng与readValidatedInlinePngSize实现了以下检查(见 terminal-image-renderer.ts):
- base64 语法校验:去除空白后非空、长度
% 4 != 1、字符集合法、解码后重新编码必须与原文一致(防"宽松解码"歧义); - 解码后字节数不得超过 8 MiB;
- PNG 结构校验:至少 24 字节、IHDR 块长度必须为 13、块类型必须为
IHDR; - 尺寸合理性:宽高为正整数、单边不超过
MAX_INLINE_IMAGE_DIMENSION(1,000,000)、像素总数不超过MAX_INLINE_IMAGE_PIXELS(64,000,000); - 长度或编码上限超限的载荷在进入 UI 历史前就被丢弃(返回
null,不产生渲染结果);而通过校验但畸形的数据(如伪造 IHDR)与超出 8 MiB 的载荷则走确定性占位符路径。
这套语义正是 E2E 计划 "Placeholders and accessibility" 一节要回归验证的内容:malformed base64、超过 8 MiB 的载荷、非法 IHDR 尺寸、非 PNG MIME 类型四种负例,必须"不写任何原始图像序列"。
无障碍模式与流生命周期
无障碍模式
开启无障碍模式后,内联图片只输出占位符。开启方式有两种等价途径:
- CLI 启动参数:
--screen-reader; - 配置文件:
ui.accessibility.screenReader: true(对应配置项定义见 packages/cli/src/config/config.test.ts 的 screenReader 配置测试)。
实现上,InlineTerminalImage通过useIsScreenReaderEnabled()感知该模式,并将其作为disabled传入prepareInlineTerminalImage——渲染结果直接置空,仅保留[image: WxH png]占位文本,避免原始图像序列对读屏器产生噪音。
流生命周期边界
模型输出是流式的,因此 E2E 计划 "Stream lifecycle" 一节要求覆盖六类边界:全新重试(fresh retry)、续传重试(continuation retry)、模型回退(fallback)、取消、流边界(stream boundary)与工具调用边界(tool call boundary)。需要验证的三条核心不变量:
- 全新尝试丢弃一切:失败尝试中暂存的图片/文本 run 全部作废,不残留到 UI;
- 续传保留部分输出:continuation 场景必须保留已提交的部分输出;
- 边界先提交再继续:每个正常边界,先提交之前的 run,再渲染后续状态行或工具行,确保
text -> image -> text顺序在历史中稳定。
会话恢复(/resume)语义
计划明确了两类持久化差异:
- 成功工具行的图片:从持久化的
functionResponse.parts中重建,顺序保持; - 助手内联图片:当前 Core 的 recorder不持久化助手的内联图片,恢复会话时助手输出以持久化的文本继续。
这意味着 E2E 验证/resume(或--continue)后,工具图片必须完整还原,而助手图片按"文本续接"预期处理即可。
按环境分组的回归验证步骤
共享渲染器回归(display_image 既有能力不被破坏)
- 让模型对工作区内 PNG 调用
display_image; - 在直接运行的 Kitty 或 Ghostty 中,确认原生预览仍能渲染;
- 在安装了 chafa 的非原生终端中,确认 ANSI 预览仍能渲染;
- 确认文件路径工具仍保留工作区、文件大小与 PNG 校验行为(对应 display-image.ts 的完整校验链)。
内联助手图片与工具图片
- 在两条助手文本 part 之间返回一张 1x1 PNG,确认转录顺序为
text -> image -> text; - 在成功工具的顶层与嵌套
functionResponse.parts中返回 PNG,确认成功工具行保留图片; - 用
/resume(或--continue)恢复会话,确认成功工具图片顺序从持久化 parts 重建; - 一次输出六张图片(五张助手 + 一张工具),确认前四张渲染、行尾出现
[+2 more images]。
Kitty/Ghostty 原生协议
- 在未套 tmux、未走 SSH 的直接 Kitty/Ghostty 中启动 CLI;
- 重复助手与工具用例;
- 确认内联 PNG 与
display_image使用相同的虚拟布局与 Unicode 占位符; - 确认历史行重挂(remount)不会重传已写入的图像载荷(由
transmittedKeys会话级集合保证)。
chafa 回退环境
- 在 Warp、iTerm2、tmux、SSH 或其他非原生环境(已安装 chafa)启动 CLI;
- 重复助手与工具用例;
- 确认 PNG 字节渲染为 ANSI 符号行且滚动时保持对齐;
- 确认图像数据通过 stdin 提供,任何模型可控值都不作为命令参数使用。
占位符与无障碍
- 在既非直接 Kitty/Ghostty、又未安装 chafa 的环境中重复用例;
- 确认有效 PNG 显示
[image: <width>x<height> png]; - 分别用畸形 base64、超过 8 MiB 的载荷、非法 IHDR 尺寸、非 PNG MIME 类型验证:不写原始图像序列;超限载荷在进入 UI 历史前被丢弃;可解析的畸形/非 PNG 数据输出确定性占位符;
- 以
--screen-reader(或ui.accessibility.screenReader: true)重复,确认只输出占位符。
流生命周期用例
依次演练全新重试、续传重试、模型回退、取消、流边界、工具调用边界与 goal-state 事件展示,按前述三条不变量核对输出。
自动化证据清单
E2E 计划的 "Automated Evidence" 要求完成并记录以下检查项,作为人工终端验证之外的机器证据:
- 聚焦测试:CLI 渲染器(如 TerminalImage.test.tsx)、组件、流、工具、resume 与压缩(compaction)相关测试;
- Core 测试:
Turn、display_image(见 display-image.test.ts)、配置、调度器与工具测试; - 工程门禁:
npm run lint:ci、npm run typecheck、npm run build、npm run check:serve-fast-path-bundle。
关于真实终端验证:当本地环境没有 Kitty/Ghostty 会话时,真实终端输出属于评审者的硬件步骤。由于本方案复用 #8217 的渲染器,其既有的 Kitty、Ghostty、cmux、Warp 手动证据已覆盖终端协议层;本计划把新的手动验证聚焦于内联数据入口与转录生命周期即可,无需重复整套协议验证。
关键源码索引
- E2E 计划原文:.qwen/e2e-tests/terminal-inline-images.md
- 渲染器核心实现:packages/cli/src/ui/utils/terminal-image-renderer.ts
- 内联 parts 提取:packages/cli/src/ui/utils/inline-image-parts.ts
- 共享 UI 组件:packages/cli/src/ui/components/TerminalImage.tsx
display_image工具:packages/core/src/tools/display-image.ts 及其测试 packages/core/src/tools/display-image.test.ts- 8 MiB 上限常量:packages/core/src/tools/tools.ts
- 无障碍配置测试:packages/cli/src/config/config.test.ts
【免费下载链接】qwen-codeAn open-source AI coding agent that lives in your terminal.项目地址: https://gitcode.com/GitHub_Trending/qw/qwen-code
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考