docs/design/ctrl-o-detail-expand/design.md
feat/ctrl-o-detail-expand<worktree-path>实现状态对照(当前 PR):
部分 状态 删除全局 compactMode(context/settings/toggle/i18n key、 mergeCompactToolGroups)✅ 已实现 fullDetail管线(HistoryItemDisplay/ToolGroupMessage,并入forceExpandAll/forceShowResult/思考块 expanded)✅ 已实现 fullDetail不被ToolGroupMessage两个 early return 绕过(纯并行/ memory-only 守!fullDetail)✅ 已实现 + 回归测试 TranscriptView+ alt-screen 接入(Ctrl+O 开关、Esc/q/Ctrl+C 关闭、双段冻结、退出重绘、后台确认自动关闭、消息队列守卫)✅ 已实现 基于 #5661 type-based partition 的 rebase(已合入 main) ✅ 已实现 AlternateScreen的process.stdout.isTTYguard(§4.2)✅ 已实现 + 测试 i18n 旧 compact 文案清理(9 语言)+ KeyboardShortcuts ctrl+o → view transcript文案(§5)✅ 已实现 read/search/list 完整明细透传到 transcript(§4.9: detailedDisplay提取 helper + 渲染拆分 + live/resume/replay)✅ 已实现 + 测试(方案 Y:core getToolResponseDisplayText+ live/resume 派生 +ToolMessage数据源切换;ACP 经transformPartsToToolCallContent已带全文,无需新增协议字段;截图 §3.4 待重录)鼠标点击工具 block 就地展开(§4.8,follow-up) ⏭️ follow-up(独立 PR,不在本 PR)——理由见 §4.8:type-based 下无 per-tool 点击目标、~250–400 行、SGR 选区风险
qwen-code 当前把 Ctrl+O 绑定为 TOGGLE_COMPACT_MODE:一个全局二态开关(compactMode,持久化到 settings.ui.compactMode)。开启后:
Thought for …;refreshStatic() 重挂 <Static>)。这造成了"精简模式 vs 详细模式"的全局割裂:同一段历史会因为一个全局开关在两种完全不同的形态间整体跳变,心智负担大、视觉抖动明显,且与上游 gemini-cli、与 Claude Code 的设计哲学都背离。
本方案叠加在 #5661 的 partition 基线之上(已合入 main)。 #5661 重构了工具组的默认渲染:把
CompactToolGroupDisplay扩展为按类别分区的摘要渲染器(ToolCategory/TOOL_NAME_TO_CATEGORY/CATEGORY_ORDER/getToolCategory/buildToolSummary),并把ToolGroupMessage的折叠决策改为 type-based partition:用forceExpandAll逆向门控,把工具按类型拆成collapsibleTools(read/search/list,经isCollapsibleTool(name))→ 折叠成CompactToolGroupDisplay分区摘要,与nonCollapsibleTools(edit/write/command/agent 及 Canceled)→ 始终逐个ToolMessage。注意:#5661 与compactMode无关——compactMode不再影响工具渲染,分区折叠纯由工具类型 +forceExpandAll决定。本 PR 不重建工具渲染基线——它在 #5661 的 partition 基线 + #5751 的鼠标基础设施之上,叠加 (1) 删除残留的全局compactMode、(2) Ctrl+O transcript 全详情屏、(3) 鼠标点击就地展开工具块。详见 §3.1(partition 基线)、§4.1 / §5(删除清单)、§9(栈式 commit 拆分)。修订说明(rebase 到 #5661 合入态):本文档早期版本基于 #5661 的早期 state-based 快照(
showCompact = (compactMode || allComplete)、整组完成即整组折叠)撰写。#5661 在 review 中演进为上述 type-based partition 并已合入 main。§3.1 / §4.1 / §4.5 / 附录已据真实合入实现改写:核心符号是isCollapsibleTool/forceExpandAll(它们确实存在),transcript 的fullDetail直接置forceExpandAll=true(而非改一个已不存在的showCompact)。
彻底取消"精简/详细"全局模式区别,对齐 Claude Code:
(ctrl+o to expand) 提示作为"这里还有更多内容、按 Ctrl+O 去 transcript 看全貌"的指引。用户明确指示:直接对齐 Claude Code 即可。本方案以"忠实还原 Claude Code 的 ctrl+o = toggleTranscript 模型"为准绳。鼠标点击展开是在此基础上叠加的第二交互入口(键盘 + 鼠标双通道)。
| 维度 | qwen-code(现状/#5661 底座) | Claude Code(真实) | gemini-cli(上游) |
|---|---|---|---|
| Ctrl+O 绑定 | TOGGLE_COMPACT_MODE | app:toggleTranscript | SHOW_MORE_LINES + EXPAND_PASTE |
| 核心模型 | 全局精简/详细二态(持久化)+ #5661 的 partition 自动折叠 | 全局 transcript 屏 + 块级 per-block 展开 | 全局 constrainHeight + per-tool 展开 |
| 主视图影响 | 一键回溯重渲染全历史 | 主视图恒定;transcript 是独立屏 | 切换高度约束、展开"最后一轮" |
| 块级状态 | ❌ 无 | ✅ expandedKeys(按 tool_use_id/uuid) | ✅ ToolActionsContext.toggleExpansion |
| 退出方式 | 再按 Ctrl+O 切回 | 再按 Ctrl+O / Esc 回 prompt | 再按 Ctrl+O 收起 |
qwen 的"新现状底座"= #5661 的 type-based partition 模型(本方案的起点,不是要推翻的旧基线): #5661 把工具组默认渲染从"靠 compactMode 全显/全隐"演进为 按工具类型分区折叠——forceExpandAll 为假时,collapsibleTools(read/search/list,经 isCollapsibleTool(name),非 Canceled)折叠成 CompactToolGroupDisplay 分区摘要行(按 CATEGORY_ORDER:search/read/list/command/edit/write/agent/other 聚合,如 Read 3 files, edited 2 files),nonCollapsibleTools(edit/write/command/agent + Canceled)始终逐个 ToolMessage;force 条件(确认/错误/聚焦 shell/用户发起/终端子代理)令 forceExpandAll=true → 全部逐个展开。该模型与 compactMode 无关——compactMode 在 #5661 中已不影响工具渲染(仅残留影响思考块)。本方案在此底座上保留整个 type-based partition 机制不动,只删除残留的全局 compactMode 开关,并叠加 transcript 的 fullDetail(置 forceExpandAll=true)。
Claude Code 的实际机制(来自 claude-code 打包源码取证):
defaultBindings.ts:'ctrl+o': 'app:toggleTranscript'。useGlobalKeybindings.tsx:setScreen(s => (s === 'transcript' ? 'prompt' : 'transcript')),并打点 tengu_toggle_transcript。REPL.tsx:screen === 'transcript' 时用虚拟滚动渲染全部历史,且 verbose={true}(强制完整展开);prompt 模式则有显示条数上限。CtrlOToExpand.tsx:被截断的块尾部渲染 (ctrl+o to expand),点/按则进入 transcript 看全貌。expandedKeys(per-message),但主交互入口就是 transcript 屏。gemini-cli 的 per-block 模型(取证补充):gemini-cli 走 per-tool 展开——ToolActionsContext 的 expandedTools / toggleExpansion 按单个工具维护展开态。这与 qwen main 已有的 Alt+T per-block 思考展开(ThoughtExpandedContext)属同一思路:都解决"就地展开单个块"。而本方案的 transcript 解决的是不同维度——"全会话的完整回顾"(alt-screen 冻结快照、全部块 fullDetail、可滚动),两者正交互补(详见 §4.7)。
关键利好:qwen-code 本来就具备实现 transcript 屏所需的全部底座(ScrollableList/VirtualizedList + 已落地的 AlternateScreen.tsx),见 §4.4。
主视图对所有历史项采用单一、稳定的渲染规则,不存在任何全局开关切换它。该基线不是本方案自建,而是 #5661 已落地的 type-based partition(按工具类型分区折叠)模型——本方案保留它不动,仅删除与之无关的全局 compactMode 开关:
| 块类型 | 默认基线渲染(#5661 type-based partition 模型) | 是否在 transcript 才看全 |
|---|---|---|
| 思考(gemini_thought / _content) | 单行摘要 ✻ Thought for 3s (ctrl+o to expand);streaming 中实时显示,落定后收成摘要 | 是 |
| collapsible 工具(read/search/list,非 force) | 经 isCollapsibleTool(name) 归入 collapsibleTools,折叠成 CompactToolGroupDisplay 分区摘要行(按 CATEGORY_ORDER 聚合,如 Read 3 files, edited 2 files, ran 1 command) | 逐个工具的明细在 transcript 看全 |
| non-collapsible 工具(edit/write/command/agent / Canceled) | 归入 nonCollapsibleTools,始终逐个 ToolMessage 完整渲染(其输出本身就是答案)——即使整组已完成也不折叠成摘要 | —— |
| 混合组(collapsible + non-collapsible 并存) | 摘要行 + 逐个工具并存:collapsible 部分 → 一行 CompactToolGroupDisplay 摘要;non-collapsible 部分 → 逐个 ToolMessage。不是整组折叠 | 部分 |
| 已完成 collapsible 工具的 string/ansi 结果 | 默认折叠(shouldCollapseResult = !forceShowResult && Success && isCollapsibleTool(name) && (string|ansi) → 结果区不渲染);Shell/Edit 等 non-collapsible 结果始终显示;diff/plan/todo/task 各自 renderer | 在 transcript 看全 |
| 出错 / 待确认 / 用户发起 / 聚焦 shell / 终端子代理 | 令 forceExpandAll=true → 全组逐个 ToolMessage;对应触发工具收到 forceShowResult=true 解除结果折叠 | —— |
| 普通文本消息 | 完整显示 | —— |
要点:
forceExpandAll + isCollapsibleTool 驱动——ToolGroupMessage 中 forceExpandAll = hasConfirmingTool || hasSubagentPendingConfirmation || hasErrorTool || isEmbeddedShellFocused || isUserInitiated || hasTerminalSubagent(不含 compactMode/allComplete)。forceExpandAll 为假时按 isCollapsibleTool(name)(read/search/list,非 Canceled)拆出 collapsibleTools → CompactToolGroupDisplay 摘要,其余进 nonCollapsibleTools → 逐个 ToolMessage;为真时所有工具进 nonCollapsibleTools。shouldCollapseResult gate 驱动——ToolMessage 中 shouldCollapseResult = !forceShowResult && status === Success && isCollapsibleTool(name) && (renderer.type === 'string' || 'ansi')。注意额外的 isCollapsibleTool(name) 守卫:只有 read/search/list 的 string/ansi 结果会折叠,Shell/Edit/Agent 等 non-collapsible 工具的结果始终可见。ToolGroupMessage 在 force 场景给触发工具逐个传 forceShowResult=true(isUserInitiated || Confirming || Error || pending-agent || 终端子代理)。forceExpandAll 不被分区折叠、且 non-collapsible 工具的结果天然不折叠(外加触发工具的 forceShowResult)。核心符号 forceExpandAll / isCollapsibleTool 确实存在(#5661 合入实现);不需要独立的 shouldForceFullDetail.ts / COLLAPSIBLE_CATEGORIES——force 语义内联在 forceExpandAll,结果折叠门控内联在 shouldCollapseResult(见 §4.5)。fullDetail 必须先于 ToolGroupMessage 的两个 early return 生效(实现要点,勿回归)——ToolGroupMessage 在算 forceExpandAll 之前有两个提前返回会绕开分区逻辑:(1) 纯并行 agent 组 → InlineParallelAgentsDisplay 密集面板;(2) 已完成 memory-only 组 → Recalled/Wrote N memories 徽章。这两条都会让 transcript fullDetail 下仍不是完整展示。因此两个 early return 均以 !fullDetail 守卫:fullDetail 为真时跳过它们,让每个工具/agent 落到逐个 ToolMessage(forceExpandAll=true + forceShowResult=true + 不截断)。已有回归测试覆盖(memory-only 组在 fullDetail 下逐个渲染而非徽章)。忠实还原 Claude Code 的 transcript(已从 claude-code 源码取证):
1049,\x1b[?1049h)接管整屏,渲染一个冻结快照:定格进入那一刻的历史,解除 UI 层高度/行数截断(思考全文、工具输出尽量完整),支持上下/翻页/Home/End 滚动。⚠️ "完整"只指 UI 层——core 层 truncateToolOutput 已截断的内容无法从 UI history 恢复(见 §4.4),不是字面"全文"。history: HistoryItem[](UIStateContext.tsx:45)与流式进行中的 pendingHistoryItems(:123,渲染时以负 id 拼接,MainContent.tsx:456-461)。Claude Code 的 freeze 实际只存两个数字 { messagesLength, streamingToolUsesLength }、render 时 slice,而非 entry-time 克隆。qwen-code 据此同时冻结两段,但用最省的形式:已落定 history 只存长度 historyLength(render 时 history.slice(0, historyLength),不克隆整个 history),流式 pendingItems: [...pendingHistoryItems] 存浅副本(pending 是临时区、会被后续重写或清空,必须副本才能定格那一刻形态)。transcript 渲染 history.slice(0, historyLength) 拼接进入那一刻定格的 pending 快照。后台后续新增的 history / pending 均不进入 transcript,保证定格不抖动。AlternateScreen 卸载写 EXIT_ALT_SCREEN 还原 normal buffer,再经一次 refreshStatic() 把当前完整 history 重绘到主屏(见 §4.4——不是字面"原样不动",而是退出时统一重绘一次,保证无重复/无缺失/scrollback 不破坏)。Esc / q(less 风格)/ Ctrl+C 关闭;再按 Ctrl+O 亦 toggle 关闭。退出后回到主屏,可看到 transcript 打开期间后台新增的流式内容(主屏 Static 一直在追加,只是被 alt-screen 暂时遮住)。(ctrl+o to expand) 提示语义统一为"按 Ctrl+O 进入 transcript 查看完整上下文",而非"此处被截断"。注意思考块摘要恒带该提示(无论原文长短),工具输出仅在被高度约束截断时带 +N lines——两者提示触发条件不同,属预期(见 §7 #7)。取证:claude-code
ink/components/AlternateScreen.tsx、termio/dec.ts:16(ALT_SCREEN_CLEAR: 1049)、screens/REPL.tsx:1325/4184/4381(frozenTranscriptState + slice)、keybindings/defaultBindings.ts:160-169(escape/q/ctrl+c → transcript:exit)。
SHOW_MORE_LINES / constrainHeight)的关系SHOW_MORE_LINES(当前 Ctrl+S)维持不变:它解除当前 pending 区的高度约束,属于"流式输出时临时看更多行",与 transcript 是正交的两件事。SHOW_MORE_LINES 也并入 transcript,但本期不做。)下列两张为本分支构建(node dist/cli.js --yolo)在固定虚拟终端(1400×900 / FontSize 14)下、对同一会话先后捕获的 before/after,经 qwen-code-mac-autotest skill 录制(session.tape 可复现)。会话提示为:列文件 → 读 README.md → grep export → 一句话总结,触发 list/read/grep 三个可折叠工具。
主视图(默认基线,§3.1)——三个 read/search/list 工具折叠为单行分区摘要 ✔ Searched 1 pattern, read 1 file, listed 1 directory,思考块折叠为 Thought for Ns (option+t to expand):
Ctrl+O Transcript 屏(§3.2 + §4.5 fullDetail)——alt-screen 全屏,header「完整记录」、footer「Esc/q 关闭 ↑↓ 滚动 PgUp/PgDn Ctrl+Home/End」;同一会话下三个工具从主视图的单行合并摘要拆为各自独立的行,思考块解除折叠(option+t to collapse)并显示全文:
⚠️ 该截图为 §4.9 实现前的状态:此处 read/search/list 工具仍只显示摘要级结果(
Listed 3 item(s)/Found 4 matches/读取文件 README.md),尚未显示完整明细(目录条目、grep 命中行、文件全文)。即 §4.5 的fullDetail只解除了"分区折叠 / 结果折叠 gate / 高度约束",但 collapsible 工具的returnDisplay本身只是摘要——完整明细的数据层透传是 §4.9(merge blocker),落地后将重新录制该截图以反映真正的"全详情"。对比要点:
fullDetail经forceExpandAll一处并入即同时解除分区折叠、逐工具结果折叠 gate 与高度约束(§4.5),无需触碰已不存在的showCompact。i18n 中文键(完整记录/关闭/滚动/列出文件/读取文件)均正确渲染。
范围澄清:#5661 已经把工具渲染基线切到 type-based partition 模型,且
compactMode在 #5661 中已不影响工具渲染(forceExpandAll不引用compactMode;compactToggleHasVisualEffect已被 #5661 改为只探测gemini_thought,不再探测tool_group)。因此本 PR 的删除范围比早期设想更小:只删 #5661 之后仍然残留的全局二态开关(context/settings/binding/i18n),不触碰ToolGroupMessage的分区决策(无showCompact、无compactMode ||项可删),并保留CompactToolGroupDisplay与整个 type-based partition 机制。
删除以下概念及其全部引用(清单见 §5):
CompactModeContext / CompactModeProvider / useCompactModecompactMode / compactInline / setCompactMode 状态与 settings 项(settingsSchema.ts;保留 web shell 侧 ui.compactMode 透传——它是独立 surface)Command.TOGGLE_COMPACT_MODE 命令与 Ctrl+O 旧绑定compactToggleHasVisualEffect 及其所在的 mergeCompactToolGroups.ts(移除全局 compact 后无人引用 → 整文件删除)compactMode 对思考块展开的残留影响(HistoryItemDisplay 把 expanded 从依赖 compactMode 改为依赖 fullDetail/thoughtExpanded,§4.5/§4.7)保留(属 #5661 type-based partition 基线,本 PR 不动):
CompactToolGroupDisplay 及其 ToolCategory / TOOL_NAME_TO_CATEGORY / CATEGORY_ORDER / getToolCategory / buildToolSummary / isCollapsibleTool 等分区符号;ToolGroupMessage 的 forceExpandAll + collapsibleTools/nonCollapsibleTools 分区决策(不删、不改,仅在其上叠加 fullDetail → forceExpandAll=true);ToolMessage 的 forceShowResult prop 与 shouldCollapseResult(含 isCollapsibleTool(name) 守卫)折叠 gate。注意:#5661 的
forceExpandAll = hasConfirmingTool || hasSubagentPendingConfirmation || hasErrorTool || isEmbeddedShellFocused || isUserInitiated || hasTerminalSubagent已经承载了"出错/待确认/用户发起/聚焦 shell/终端子代理 必须完整展示"的安全语义——本 PR 保留不动,无需再迁移到任何新文件。
alt-screen 能力已有现成组件可复用:qwen-code 用的是上游官方 ink ^7.0.3(注意:与 gemini-cli 不同包不同大版本——gemini-cli 用 fork npm:@jrichman/[email protected](v6);不要再把两者当同版本看待)。更关键的是,main 已落地可直接复用的 packages/cli/src/ui/components/AlternateScreen.tsx(PR #5627),无需新建、无需移植 hook、无需引入 ink fork:
AlternateScreen.tsx 在 useEffect 中 writeRaw(ENTER_ALT_SCREEN + CLEAR + HIDE_CURSOR),卸载/process.on('exit') 时 writeRaw(SHOW_CURSOR + EXIT_ALT_SCREEN);内部用 useTerminalOutput()/useTerminalSize()。transcript 只需用 <AlternateScreen> 包裹 TranscriptView 即可获得"进入时进 alt-screen、卸载时回 normal buffer"的完整生命周期。render() 的 alternateScreen: true 整应用选项——那会让整个 app 常驻 alt-screen,丢掉 qwen-code 默认主视图赖以为生的终端原生 scrollback,不符合"主屏保持干净、仅 transcript 接管整屏"的需求。useTerminalBuffer)已常驻 alt-screen,必须用 disabled prop 避免 double-enter:当 settings.merged.ui?.useTerminalBuffer 开启时,ink root 自身已通过 render() 占有 alt-screen(gemini.tsx:367 const useVP = settings.merged.ui?.useTerminalBuffer ?? false;,:379 alternateScreen: useVP)。此时 transcript 若再写一次 ?1049h 就会 double-enter,破坏 buffer 状态。AlternateScreen.tsx 正为此带了 disabled?: boolean prop(其注释:"Skip escape writes when the root Ink renderer already owns the alt screen (VP mode)")。因此 transcript 一律以 <AlternateScreen disabled={useVP}> 包裹:
useVP=false):组件正常写 ENTER_ALT_SCREEN/EXIT_ALT_SCREEN,进出 alt-screen;useVP=true):传 disabled 跳过转义写入,因为 ink root 已在 alt-screen,transcript 直接在该 buffer 内以替换主内容树的方式渲染。isAltScreenSupported() 启发式判定。判定收敛为两条明确依据——(1) 是否已在 alt-screen 由 useVP 决定(决定是否传 disabled);(2) 非 TTY 防护。⚠️ 现状澄清(取证):AlternateScreen.tsx 当前并没有 process.stdout.isTTY 防护(useEffect 内无条件 writeRaw(ENTER_ALT_SCREEN…))。但 TUI 本身只有 interactive 为真才渲染,无 prompt 时 interactive = process.stdin.isTTY ?? false(config.ts:1532)——非 TTY 默认根本不进交互渲染,TranscriptView/AlternateScreen 不挂载;唯一边角是显式 -i(强制 interactive 而 stdout 可能非 TTY)。待实现:给 AlternateScreen 补一个 process.stdout.isTTY guard(写转义前判定,非 TTY 不接管整屏、退化为普通 buffer 内渲染),对齐仓库既有约定(startInteractiveUI.tsx:77/81、notificationService.ts:53 等均在写终端转义前判 isTTY)。改动极小、属"对齐约定的兜底",并补对应单测。状态(single source of truth:AppContainer 的 useState,本地持有、顶层消费——不 surface 到 broad UIStateContext):
设计取舍(取证:与 ThinkingViewer 一致 + claude-code REPL-local + gemini-cli 先例):transcript 是"由全局键 Ctrl+O 开、在顶层 layout 消费"的单一 UI 态,没有深层消费者。本仓库最近的同类 overlay
ThinkingViewer即把 canonical 放在AppContainer本地useState(thinkingViewerData),只把最小openaction 经专用ThinkingViewerContext下传,不塞进 broadUIStateContext;claude-code 的 transcript 屏状态也留在REPL顶层本地。故 transcript 同样留AppContainer本地,在AppContainer顶层 JSX 里像渲染<ThinkingViewer>一样条件渲染<TranscriptView>,不经UIStateContext/UIActionsContext投影。(注:实现代码已如此——transcriptFreeze是AppContainer本地 useState,UIStateContext内无任何 transcript 字段;早期文档写"surface 到 UIStateContext"有误,此处纠正。)
AppContainer 用 useState 承载 transcriptFreeze: { historyLength: number; pendingItems: HistoryItemWithoutId[] } | null(HistoryItemWithoutId 即 pendingHistoryItems 的元素类型),isTranscriptOpen = transcriptFreeze != null 直接派生。openTranscript()(拍双段快照:setTranscriptFreeze({ historyLength: history.length, pendingItems: [...pendingHistoryItems] }))/ closeTranscript()(置 null)/ toggleTranscript()——均为 AppContainer 本地回调,由全局键处理(§4.3)直接调用,无需经 UIActionsContext。openTranscript() 都重新拍当前快照;closeTranscript() 清空。即 transcript 定格在"本次打开那一刻",关闭再开会刷新到最新——不是永久定格在首次。注意快照对已落定 history 只存长度(historyLength,render 时 history.slice(0, historyLength),对齐 Claude Code 存 messagesLength 而非克隆),对流式 pending 区存浅副本([...pendingHistoryItems],因 pending 是临时区会被后续重写/清空,必须存副本才能定格那一刻的形态)。不是克隆整个 history。isTranscriptOpen 不并入 dialogsVisible 聚合(见 §4.3 的 Esc/键位分析——并入会误伤"Responding 且无 dialog 才能 Esc 取消请求"等逻辑);composer 的屏蔽由 transcript 走 alt-screen 接管整屏天然达成,无需依赖 dialogsVisible。AppContainer.handleGlobalKeypress 中:
// 删除:TOGGLE_COMPACT_MODE 分支
// 新增:
} else if (keyMatchers[Command.TOGGLE_TRANSCRIPT](key)) {
toggleTranscript(); // open <-> close
}
Command.TOGGLE_TRANSCRIPT = 'toggleTranscript',默认绑定 [{ key: 'o', ctrl: true }]。键位归属(避免双重处理竞态):KeypressContext 是广播、无 consumed flag(KeypressContext.tsx:655,所有 useKeypress 订阅者都会收到同一按键)。因此必须单一 owner,规则如下:
handleGlobalKeypress 处理(toggle 语义对开/关都成立)。TranscriptView 自身的 useKeypress 绝不处理 Ctrl+O,否则一次按键被两处响应 → 关了又开的竞态。handleGlobalKeypress 处理,且必须是 handleGlobalKeypress 的【最最前面、第一个】分支——⚠️ 注意现有第一个分支就是 Command.QUIT(Ctrl+C,AppContainer.tsx:3104),第二个是 Command.EXIT(Ctrl+D,:3121),第三个才是 ESCAPE(:3132),而 ESCAPE 分支顶部还有 vim 守卫 if (vimEnabled && vimMode==='INSERT') return;。因此 transcript 关闭分支必须早于全部这些——早于 QUIT/Ctrl+C、早于 EXIT、早于 ESCAPE 分支及其 vim INSERT 守卫——短路:
const handleGlobalKeypress = (key) => {
// 必须是整个 handleGlobalKeypress 的第一个分支:
// 早于 QUIT(Ctrl+C) / EXIT(Ctrl+D) / ESCAPE 分支(及其 vim INSERT 守卫)
if (isTranscriptOpenRef.current &&
(key.name === 'escape' || key.name === 'q' || (key.ctrl && key.name === 'c'))) {
closeTranscript(); return;
}
if (keyMatchers[Command.QUIT](key)) { ... } // 现有 :3104
...
ctrlCPressedOnce;按 Esc 会被 vim INSERT 守卫吞掉而关不掉 transcript。因为本分支由 isTranscriptOpenRef 守卫,仅在 transcript 打开时生效,对 vim 正常编辑无影响。测试:Ctrl+C closes transcript and does NOT set quit/ctrlCPressedOnce;Esc closes transcript even when vim INSERT mode is active。
q(普通字母键)为何安全:该分支被 isTranscriptOpenRef 守卫,仅在 transcript 打开(此时 composer 被 alt-screen 接管、无文本输入焦点)时匹配;transcript 关闭时 q 直接落到正常输入流,不受影响。无需新增 Command 绑定,inline 匹配即可。isTranscriptOpen 不并入 dialogsVisible,所以不走 useDialogClose;transcript 的 Esc 在上面的全局前置分支里独立处理(useDialogClose 保持原样不动)。isTranscriptOpenRef(仿现有 dialogsVisibleRef 模式,AppContainer.tsx:2425)。新增 components/TranscriptView.tsx,外层包复用现有的 <AlternateScreen disabled={useVP}>(§4.2:VP 模式下 ink root 已占 alt-screen,传 disabled 跳过转义写入;非 VP 模式正常进出 alt-screen;非 TTY 由待补的 process.stdout.isTTY guard 退化为普通 buffer 内渲染,见 §4.2):
[...history.slice(0, freeze.historyLength), ...freeze.pendingItems] —— history 前缀 + 进入那一刻定格的 pending 副本(见 §3.2)。后台后续新增项不进入,避免滚动抖动。ScrollableList/VirtualizedList 已存在于 main(标准 Ink 7 组件,非 Ink fork;ScrollableList.tsx 具备 scrollBy/scrollTo/scrollToEnd/scrollToIndex 与 PageUp/Down/Home/End/滚轮),但当前仅在 useTerminalBuffer(VP/virtual-viewport 模式)下被 MainContent 使用——默认主视图走 <Static> + pending,不用它们。transcript 无条件复用这两个组件(与 useTerminalBuffer 解耦,自管滚动容器),因此不受默认 Static 路径限制。⚠️ 这些组件相对较新,长会话下的滚动性能、键盘滚动、resize 重排须纳入测试(§8),不能假设"零成本复用"。estimatedItemHeight(虚拟滚动估高,必须调大/自适应):MainContent 当前对 VirtualizedList 用恒定 estimatedItemHeight=3。transcript 以 fullDetail 渲染(思考全文、工具全输出),每项远高于 3 行,若沿用 3 会导致滚动条/定位失真、PageUp/Down 跳幅错乱。transcript 必须用更大或自适应的 estimatedItemHeight(按内容类型估算,或交由 VirtualizedList 的实测高度回填机制修正)。该估高纳入测试(§8)。fullDetail prop):为渲染路径引入显式 fullDetail 替代原先靠 !compactMode 推导。fullDetail=true 时:思考块 expanded={true};工具输出同时满足两点才算真正不截断——(a) availableTerminalHeight={undefined}(验证 ToolGroupMessage.tsx:357-365 据此使 availableTerminalHeightPerToolMessage 为 undefined);(b) 关闭 MaxSizedBox 的高度约束、sliceTextForMaxHeight、shell 的 shellStringCapHeight/shellOutputMaxLines(ToolMessage.tsx:67-74,750-756)。⚠️ 保留按字符数的性能上限(qwen-code 侧为工具输出截断阈值 DEFAULT_TRUNCATE_TOOL_OUTPUT_THRESHOLD ~25000,非 gemini-cli 的 SlicingMaxSizedBox)——区分"行高截断(为显示,transcript 解除)"与"字符上限(为性能,始终保留)",避免单条超大输出拖垮虚拟滚动。truncateToolOutput(packages/core/src/utils/truncation.ts,被 shell.ts/mcp-tool.ts 调用)在工具产出时就按 truncateToolOutputThreshold/Lines 截断,写进 history 的 resultDisplay 已是截断后的,原文可能仅以临时 output 文件存在;(2) UI 层 MaxSizedBox/sliceTextForMaxHeight 等按终端高度截断。transcript 只能解除 UI 层——core 层已丢弃的内容 UI 拿不回来。规则:transcript 对 core-截断项保留其 truncation marker(如"… output truncated, N lines omitted"),明示不可恢复;"读取 core 保存的 output 文件并展示"列为后续可选增强,不在本期范围。i18n/文案不得宣称"查看完整工具输出",改为"查看完整上下文(不含已被 core 截断的部分)"。useKeypress(isActive: isTranscriptOpen)只处理滚动键(上下/翻页/Home/End)。关闭键(Esc/q/Ctrl+C/Ctrl+O)一律由全局 handleGlobalKeypress 处理(§4.3),TranscriptView 不碰,杜绝广播双响应。<AlternateScreen disabled={useVP}> 包裹的 TranscriptView 替代主内容树(MainContent 从渲染中卸载,不再绘制);后台对话/流式只更新数据层(history/pendingHistoryItems 继续增长),但不被绘制。退出时:AlternateScreen 卸载写 EXIT_ALT_SCREEN(VP 模式由 disabled 跳过)回到 normal buffer(其中仍是进入前那帧 <Static> 旧内容)→ 必须再调用一次 refreshStatic()(清屏 + 重挂 Static key)把当前完整 history 一次性重绘,从而保证退出后主屏无重复回放、无缺失、无错位。这是 alt-screen + Static append-only 模型下的正确收尾,不是"原样不动"。refreshStatic(避免污染主屏 scrollback):useResizeSettleRepaint 等内部路径(如 resize)可能在 transcript 打开期间触发 refreshStatic——若放任,它会向 normal-buffer 的 scrollback 写入/重排主内容,而此刻屏幕正被 alt-screen 占据,导致退出后主屏错位或 scrollback 被污染。规则:用 isTranscriptOpenRef 守卫 refreshStatic,transcript 打开期间一律跳过;退出 transcript 时再统一做一次 refreshStatic() 重绘主屏(即上一条)。如此可澄清"主屏 normal-buffer scrollback 不被 alt-screen 期间的写入污染"。测试:打开 transcript 期间后台完成一轮工具调用 / 触发 resize,退出后主屏该轮内容恰好出现一次、scrollback 不被破坏。Transcript — ↑↓ scroll · Ctrl+O/Esc/q to close),初始 initialScrollIndex 滚到底部(对齐 Claude Code 打开即在最新处)。fullDetail 联动(transcript 路径置 forceExpandAll=true)#5661 的 type-based partition 基线已经把"哪些工具折叠成分区摘要"(forceExpandAll + isCollapsibleTool 分区)与"是否折叠已完成结果"(shouldCollapseResult gate)这两层决策做好了。本 PR 不重写这两层,只新增一个显式的 fullDetail 信号,让 transcript 路径能整体解除 partition 折叠 + 结果折叠 + 高度约束。关键利好:type-based 基线下,transcript 只需把 fullDetail 并入 forceExpandAll(forceExpandAll = fullDetail || hasConfirmingTool || ...)——一处即同时禁用分区(所有工具进 nonCollapsibleTools 逐个渲染)并使逐工具 forceShowResult 联动解除结果折叠,不需要去改一个已不存在的 showCompact。
ToolGroupMessage 的 forceExpandAll(fullDetail || hasConfirmingTool || hasSubagentPendingConfirmation || hasErrorTool || isEmbeddedShellFocused || isUserInitiated || hasTerminalSubagent)为真 → 跳过 type-based partition,所有工具逐个 ToolMessage;为假 → 按 isCollapsibleTool 拆分 collapsible(摘要)/ non-collapsible(逐个)。ToolMessage 的 forceShowResult prop 经 shouldCollapseResult = !forceShowResult && status === Success && isCollapsibleTool(name) && (string|ansi) 决定已完成 collapsible 工具的 string/ansi 结果是否折叠。ToolGroupMessage 在 force/fullDetail 场景对工具传 forceShowResult={fullDetail || isUserInitiated || Confirming || Error || pending-agent || 终端子代理}。shouldForceFullDetail.ts,不迁移任何判定。HistoryItemDisplay 把思考块 expanded 从依赖 compactMode 改为 resolvedThoughtExpanded = fullDetail || (thoughtExpanded ?? contextThoughtExpanded)(§4.7)——transcript 路径 fullDetail、main 的 Alt+T per-block 开关任一为真即展开;主视图常态 fullDetail=false。fullDetail 的联动(transcript 路径 = true 时同时生效):
fullDetail=true(transcript)时同时做到以下几点,才算真正"逐个工具、完整不截断":
forceExpandAll=true(把 fullDetail 并入 forceExpandAll)——解除 #5661 的 type-based partition,所有工具进 nonCollapsibleTools 逐个渲染 ToolMessage(而非把 collapsible 部分聚合成分区摘要行);forceShowResult=true(把 fullDetail 并入 forceShowResult)——shouldCollapseResult 的 !forceShowResult 不再命中,已完成 collapsible 工具的 string/ansi 结果也展开(non-collapsible 结果本就显示);MaxSizedBox 高度约束——ToolGroupMessage 在 fullDetail 时给每个 ToolMessage 传 availableTerminalHeight={undefined};ToolMessage 内 availableHeight 随之为 undefined,MaxSizedBox maxHeight 无界、shell 的 isCappingShell(含 !forceShowResult)解除。⚠️ 保留按字符数的性能上限(core 层 truncateToolOutput)——区分"行高截断(为显示,transcript 解除)"与"字符上限(为性能/core 层,始终保留)";expanded——transcript 路径 resolvedThoughtExpanded 的 fullDetail 分量为 true(见上)。fullDetail 数据流(谁算、谁传、默认值):
fullDetail 是 HistoryItemDisplay / ToolGroupMessage 的显式 prop,由父级按渲染上下文计算并传入,不从 context 读(避免再造一个隐式全局态)。MainContent):不传 fullDetail(默认 false)。"哪些工具组必须完整展示"完全由 #5661 已内联在 ToolGroupMessage 的 forceExpandAll/forceShowResult 条件决定(embeddedShellFocused/activeShellPtyId 等输入 HistoryItemDisplay 本就持有并下传),无需在 MainContent 另算 force 布尔。TranscriptView):对所有 item 恒传 fullDetail={true},触发上面联动。这样"主视图 vs transcript"的差异收敛为一个干净的布尔
fullDetail:主视图沿用 #5661 的 type-based partition + force 安全语义,transcript 用fullDetail(并入forceExpandAll+forceShowResult)把这两层连同高度约束一并解除。不引入新文件、不迁移判定、不改 #5661 的分区逻辑。
transcript 走 alt-screen 接管整屏,但后台对话仍在跑——这带来几个必须明确的交互规则:
WaitingForConfirmation 一种——DialogManager.tsx 还渲染 shellConfirmationRequest(ShellConfirmationDialog)、loopDetectionConfirmationRequest(LoopDetectionConfirmation)、confirmationRequest(ConsentPrompt)、confirmUpdateExtensionRequests(ConsentPrompt)、providerUpdateRequest(ProviderUpdatePrompt) 等。任一种被 alt-screen 遮住都会让用户看不到、无法响应 → 死锁。规则:当任一阻塞确认/对话框需要用户输入时自动 closeTranscript()(在 AppContainer 加 useEffect 监听这些请求状态 + streamingState===WaitingForConfirmation 的并集,任一为真即退出 alt-screen),让用户看到并响应。这比"在 alt-screen 内重渲染各种确认框"简单且无歧义。测试需逐一覆盖上述每种阻塞确认触发自动关闭(§8)。VirtualizedList 需按新宽度重排、重算行高。复用其既有 resize 响应即可;退出 transcript 时 §4.4 的 refreshStatic() 也会让主屏重排。测试需覆盖"transcript 内 resize 后滚动位置不崩"。AppContainer.tsx 的消息队列排空(drain)逻辑有 if (dialogsVisible) return; 守卫,避免 dialog 打开时静默自动提交排队消息。由于 isTranscriptOpen 不并入 dialogsVisible(§4.2/§4.3),该守卫不会覆盖 transcript 打开态。规则:在该 drain 逻辑补 || isTranscriptOpenRef.current(或等价条件),确保 transcript 打开期间队列消息不被自动发送,待退出 transcript 后再正常排空。测试:transcript 打开期间排队消息不被自动提交,退出后恢复排空。合并上游 main 后,仓库已存在一套块级(per-block)思考展开机制,与本方案的全会话 transcript 互补共存、各解决不同诉求:
ThoughtExpandedContext —— Alt+T(Command.TOGGLE_THINKING_EXPANDED)就地切换单个思考块的展开/折叠;ThinkingViewer / ThinkingViewerContext —— 查看单个思考块的全文;thoughtExpanded / thinkingFullText props、buildThinkingFullTextMap、ClickableThinkMessage。expanded = isPending || fullDetail || resolvedThoughtExpanded——三个来源任一为真即展开:
isPending:流式中实时全文(§4.5);fullDetail:transcript 路径恒为 true(§4.5);resolvedThoughtExpanded:main 的 Alt+T per-block 开关。
本方案的 fullDetail 改造只接管前两者,不触碰 resolvedThoughtExpanded——per-block 机制原样保留。📌 范围已定:本功能为 follow-up,不在当前 PR 交付。 当前 PR 只交付 Ctrl+O transcript。鼠标点击展开移到后续独立 PR(VP-only MVP)。理由(评估自真实代码):
- 没有 per-tool 点击目标:#5661 type-based partition 把折叠的 read/search 工具聚合成单行摘要——折叠态下不存在"单个工具块"可点。点击粒度须从"per-tool"重定为"点摘要行→展开整组"(
forceExpandAll),这是设计变更不是直接照搬。- 工程量 ~250–400 行 / 4–5 文件:
ToolExpandedContext+ AppContainer 接线 +ClickableToolMessage(不能在.map()里调useMouseEvents,须抽组件,模板ClickableThinkMessage即 59 行) + ToolGroupMessage 接线 + 鼠标命中测试(需 mockmeasureElementPosition)。- 已知风险:SGR 鼠标追踪与终端原生文本选择冲突([[project_vp_text_selection]]),transcript/主视图内是否启用点击需单独权衡。
下文保留设计草案供后续 PR 用;当前 PR 的 §1 目标#4、状态表与 §9 末注已把鼠标点击展开标注为 follow-up(§4.9 / commit 4 现为完整明细透传,非鼠标)。
除键盘(Ctrl+O→transcript、Alt+T→思考块)外,(后续 PR)提供鼠标点击作为就地展开的第二通道。以下为该 follow-up 的设计草案。
与 #5751 的分工(依赖关系,不重复造轮子):
ScrollableList/VirtualizedList 增加滚轮滚动与滚动条点击拖拽。涉及 useMouseEvents.ts、ScrollableList.tsx、VirtualizedList.tsx。本设计基于其已合入的基础设施。ClickableThinkMessage + #5751 提供(点击折叠思考行→打开 ThinkingViewer),本 PR 不重做,仅确保与新基线/transcript 共存。(follow-up 设计草案)点击工具调用 block 就地展开/折叠明细
复用 main 已有的成熟范式(ClickableThinkMessage + measureElementPosition + useMouseEvents),把它从"思考块"推广到"工具块"。对齐点:per-tool 点击展开 = 就地把该工具/组切到 forceExpandAll=true(不被 partition 折叠)+ forceShowResult=true(用一个 per-id 展开态,命中后 toggle),复用 #5661 已有的 forceExpandAll / forceShowResult 机制,不另造渲染分支:
ToolActionsContext / main ThoughtExpandedContext 的形态):
expandedToolGroupIds: ReadonlySet<string>(按 tool_group id 或 callId)。canonical 落在 AppContainer useState。ToolActionsContext / 本仓库 ThinkingViewerContext/ThoughtExpandedContext 的形态),暴露 toggleToolExpanded(id) / isToolExpanded(id)——不塞进 broad UIStateContext/UIActionsContext。与 transcript(§4.2,纯本地、顶层消费、无深层消费者)不同:per-tool 展开确有跨层生产者(深层工具块点击 set)+消费者(ToolGroupMessage 读 isToolExpanded 并入 forceExpandAll),故下传是正当的,但用专用 context 而非污染全局 UI 态聚合。ToolGroupMessage:把 isToolExpanded(id) 并入 forceExpandAll 的"为真"分量(forceExpandAll = fullDetail || isToolExpanded(id) || hasConfirmingTool || ...)→ 解除 type-based partition,逐个 ToolMessage;ToolMessage:对该工具传 forceShowResult=true(shouldCollapseResult 的 !forceShowResult 不再命中),并配合解除高度约束。forceExpandAll=true + forceShowResult=true"的同一对开关(§4.5 #1/#2),只是作用域是单个被点工具/组、而非全会话。与思考块 expanded 的多源合成(§4.7)对称。ref + measureElementPosition 命中测试,照搬 ClickableThinkMessage 的 left-press + 坐标包含判定)。命中 → toggleToolExpanded(id)。
isActive 守卫:仅在该工具已完成且其结果被 shouldCollapse 折叠或输出被高度截断(即存在"更多可看")时才挂点击监听,避免给无可展开内容的块挂无效监听;transcript 内不需要(已全展开)。measureElementPosition 直接可用(与 ClickableThinkMessage 同一前提);Static 模式因 append-only 不参与点击展开(点击展开主要服务 VP/可视区,Static 仍可用 Ctrl+O 走 transcript)。ClickableThinkMessage + #5751 提供,本 PR 不重做,仅确保与新基线/transcript 共存。依赖说明:本功能的可用性前置 = #5751 的鼠标引用计数修复(否则 VP 下鼠标可能被某个折叠块的卸载误关)。设计与实现以"#5751 已在 main"为前提;commit 顺序见 §9。
背景:本 PR 把 Ctrl+O 定义为"transcript 全详情屏"(§3.2)——主视图保持 #5661 的精简分区摘要,Ctrl+O 随时调出"完整记录",所有 item 以 fullDetail 渲染。设计承诺 transcript 内工具输出完整展开、不折叠、不按高度截断;实现上由单一 fullDetail 信号并入 forceExpandAll + 逐工具 forceShowResult + availableTerminalHeight=undefined 达成(§4.5)。
问题(验收实测,§3.4):用户的"三层折叠"模型卡在第二层——第一层(主视图)✔ read 1 file, listed 1 dir 分区摘要应保持;第二层(Ctrl+O 当前)每个工具一行 + 简短摘要(列出文件 Listed 3 items / Grep Found 4 matches / 读取文件 README.md);而最详细层缺失(实际目录条目、grep 命中行、文件全文)。read_file / ls / grep / glob 这类可折叠只读工具在 transcript 下只到摘要级,违背 "全详情" 语义。
根因(数据层,非折叠开关):经取证,§4.5 的 fullDetail → forceExpandAll / forceShowResult / availableTerminalHeight=undefined 链路全部正确生效;问题在于前端根本拿不到完整明细:
ToolResult 有两路内容(packages/core/src/tools/tools.ts:443+):llmContent("factual outcome",完整)与 returnDisplay(注释明确为 "user-friendly summary")。ls / grep / ripGrep / read_file 的 returnDisplay 只装摘要(Listed N / Found N / ''),完整内容只进 llmContent(取证:ls.ts / grep.ts / ripGrep.ts / read-file.ts 的返回)。IndividualToolCallDisplay(packages/cli/src/ui/types.ts:65)只有 resultDisplay,无完整内容字段;转换点 useReactToolScheduler.ts:330 只取 response.resultDisplay(摘要)。ToolCallResponseInfo.responseParts(turn.ts:116)到达前端,但那是 functionResponse 包装,partToString / getResponseTextFromParts 都只取 part.text、读不到 functionResponse.response.output(partUtils.ts:61-69、generateContentResponseUtilities.ts:14)——前端拿到的 parts 里有完整内容,但没有现成 helper 把它取出来。关键发现:完整明细其实已天然持久化(决定方案走向)
convertToFunctionResponse(...)(coreToolScheduler.ts:679-714)把完整 llmContent 写进 functionResponse.response.output(string 直接放;array 提取全部 text 拼接,media 进 parts)——即工具结果 parts 内已含完整明细。ChatRecord.message 存的就是 response.responseParts(取证:recordToolResult(...) 调用点 coreToolScheduler.ts:4009、Session.ts:3334/4404 等均传 responseParts),是完整 functionResponse,非摘要。trackedCall.response.responseParts)/ resume(record.message.parts,resumeHistoryUtils tool_result case)/ ACP replay(HistoryReplayer.replayToolResult 已把 message: record.message.parts 传给 emitResult)。"持久化"在数据层天然满足,无需新增持久化字段(该前提以 §8 的"方案 Y 前提保护"测试守卫——断言 message.parts 的完整 output 经 recording / loadSession / resume / replay 四段不丢失、不误走 API compressedHistory;若失败则回退方案 X)。方案 Y(单一完整数据源 + 显示层提取,对齐 claude code)
claude code 的机制是"存储层保留完整、显示层按 verbose 截断",不拆 llmContent/returnDisplay,resume 后 transcript 仍全详情(取证:claude-code/src/screens/REPL.tsx:4185-4194,4381-4382,4402 frozen transcript = 内存 messages 快照 + verbose=true;src/utils/toolResultStorage.ts、sessionStorage.ts 持久化完整内容/文件引用)。qwen 的完整内容既已在 parts 持久化,采用同构思路——不新增持久化字段,从已存在的 functionResponse 集中提取完整文本来显示。
contentForDisplay 字段贯穿 core→序列化→回放→前端,8 处):与已持久化的 responseParts 内容冗余(同一完整内容存两份)、扩大持久化 schema 与迁移面。returnDisplay 携带完整):违背 returnDisplay="summary" 语义、改多个工具。改动点(方案 Y):
getToolResponseDisplayText(parts: Part[]): string | undefined(与 getResponseTextFromParts 并列)。可实现的优先级规则——媒体挂在 nested functionResponse.parts(非顶层,取证:coreToolScheduler.ts:661-744、postCompactAttachments.ts:149-161、compactionInputSlimming.ts:205-213):遍历顶层 parts → 对每个 functionResponse 读非空 response.output 文本拼接;再遍历其 nested functionResponse.parts,对 inlineData/fileData 输出占位(如 <image: mimeType>)、对 nested {text} 占位也保留(compaction slimmer 会产生该形态);若无 output 但有 nested media → 返回占位;output 与 media 都无 → 返回 undefined 让 UI 降级摘要(避免 media-only read_file 显示空白或误用 "Tool execution succeeded")。单一提取入口,live/resume/replay 共用。IndividualToolCallDisplay(types.ts:65)新增 detailedDisplay?: string——派生值、不持久化(每次从已持久化的 parts 提取)。useReactToolScheduler success 分支:detailedDisplay = getToolResponseDisplayText(trackedCall.response.responseParts)(不二次截断,见下"内存与上限")。resumeHistoryUtils 的 tool_result case(:411-431,已持有 record.message.parts)用同一 helper 提取。HistoryReplayer.replayToolResult(:259)已把 message: record.message.parts 传给 emitResult;在 ACP→display 映射处(resumeHistoryUtils / ToolCallEmitter)用同一 helper 派生。无需新增 ACP 协议字段。forceShowResult 泄漏):给 ToolMessage 显式传 fullDetail(由 ToolGroupMessage 下传)。仅当 fullDetail && isCollapsibleTool(name) && detailedDisplay 时,用 detailedDisplay 取代摘要 resultDisplay。
forceShowResult/forceExpandAll,可由 isUserInitiated / Confirming / Error / pending-agent / terminal-subagent 触发,见 ToolGroupMessage.tsx:469-475)与"切换到完整数据源"(仅 fullDetail)分离。否则主视图里 user-initiated/error 等 force 场景会误显完整明细,违反"主视图不变"。内存与上限(不二次截断,符合"全详情"语义):detailedDisplay 不走 compactStringForHistory 的 32k 截断——那会把 Ctrl+O 变成"32k bounded preview"而非"全详情",与 §3.2/§3.4 承诺冲突(尤其 read_file:maxOutputChars=Infinity、自管理分页、免于 scheduler char 截断,见 read-file.ts:385-390,合法大读取会超 32k;此时 functionResponse.response.output 内仍有 >32k 完整内容,不应被 UI 再裁)。detailedDisplay 直接 = getToolResponseDisplayText(parts) 的完整文本,其边界即 core 已施加的 truncateToolOutput/工具自带分页(UI 拿不回 core 已丢弃内容,§4.5)——UI 层不再叠加字符上限。detailedDisplay 是派生值、不入库;内容已存在于 responseParts,仅多一份字符串引用。极大输出由 transcript ScrollableList 虚拟滚动按视口渲染(有界),与 claude code "存储留完整、显示层 verbose 决定" 同构。
范围(按类别判定,不硬编码名单):
isCollapsibleTool(name) 为准——类别级 read/search/list,含 glob/FindFiles(getToolCategory 把 GLOB 归 search,见 CompactToolGroupDisplay.tsx:74-95;glob.ts:267-280 的 returnDisplay 同样只是 Found N matching file(s))。不写死 read_file/ls/grep。fullDetail)、run_shell_command(returnDisplay = result.output,完整)/ edit / write(完整 diff)不变。测试点(并入 §8):
functionResponse.response.output 提取、media 占位、空/缺失降级;detailedDisplay,尤其 resume/回放后 transcript 仍全详情;ACP 路经 transformPartsToToolCallContent 已带全文(非 TUI transcript,无需 detailedDisplay);ToolMessage:fullDetail && collapsible && detailedDisplay 用完整;forceShowResult 但非 fullDetail(主视图 user-initiated/error)仍用摘要(回归主视图不变);glob(或 legacy search displayName),不只 read/grep/list;detailedDisplay 提取自 parts,旧记录的 parts 同样在 → 自然全详情;极旧无 parts 记录降级摘要。完整符号清单来自代码取证,下列为需改动文件与动作;行号以当前 main 为准,实现时以实际为准。
依赖基线(#5661,本 PR 不重做):#5661 已把工具渲染切到 type-based partition 模型。
MainContent维持mergedHistory = visibleHistory(无 cross-group 合并、无absorbedCallIds/summary 吸收机制);tool_use_summary在HistoryItemDisplay中渲染为独立的● <summary>行(不吸收进表头)。CompactToolGroupDisplay已被扩展为分区摘要渲染器并保留。本 PR 只在此之上删除残留的全局compactMode(context/settings/i18n),并删除随之失去引用的mergeCompactToolGroups.ts(含compactToggleHasVisualEffect)。 已由合并 main 处理、本设计无需再单列的项:旧 compact 渲染里的isDim暗淡样式已在实现侧随相关重构移除(合并 main 后不再存在),无需在改动清单中单独追踪。
| 文件 | 动作 |
|---|---|
packages/cli/src/ui/contexts/CompactModeContext.tsx | 删除整个文件(CompactModeProvider/useCompactMode) |
不删
CompactToolGroupDisplay:它是 #5661 partition 基线的核心摘要渲染器(ToolCategory/CATEGORY_ORDER/buildToolSummary),本 PR 保留。 删mergeCompactToolGroups.ts:移除全局 compactMode 后,compactToggleHasVisualEffect(其唯一 import 点是已被删除的 compact toggle 机制)与整文件不再被引用 → 整文件删除(含其测试)。#5661 的 type-based partition 不依赖此文件。 不新增shouldForceFullDetail.ts:force 安全语义已内联在 #5661 的forceExpandAll/forceShowResult,无需抽出新文件(见 §4.5)。该文件从未实际存在。
| 文件 | 动作 |
|---|---|
packages/cli/src/config/keyBindings.ts | 删 TOGGLE_COMPACT_MODE;加 TOGGLE_TRANSCRIPT='toggleTranscript',默认绑 Ctrl+O;保留 SHOW_MORE_LINES(Ctrl+S) 不动。启动迁移检测当前不适用——代码库无用户可配置 keybinding 覆盖(keyMatchers 恒用硬编码默认值),无残留绑定可扫描(见 §6) |
packages/cli/src/ui/keyMatchers.ts | 同步增删 matcher |
packages/cli/src/ui/AppContainer.tsx | 删 compactMode/compactInline/setCompactMode 状态、CompactModeProvider、Ctrl+O 旧分支、compactToggleHasVisualEffect 调用;加 isTranscriptOpen(canonical useState,§4.2)+ isTranscriptOpenRef + transcriptFreeze + toggleTranscript/openTranscript/closeTranscript;全局 Ctrl+O→toggleTranscript;Esc/q/Ctrl+C handleGlobalKeypress 第一分支关闭(早于 QUIT/EXIT/ESCAPE 及 vim INSERT 守卫,§4.3);useEffect 监听全部阻塞确认/对话框自动关闭(§4.6 #1);消息队列 drain 守卫加 || isTranscriptOpenRef.current(§4.6 #3);refreshStatic用isTranscriptOpenRef守卫、退出时重绘一次(§4.4)。不并入 dialogsVisible,不改 useDialogClose |
packages/cli/src/ui/contexts/UIStateContext.tsx | 不改:transcript 态留 AppContainer 本地、顶层消费,不 surface(取证:ThinkingViewer / claude-code REPL-local 先例,§4.2)。实现代码已如此 |
packages/cli/src/ui/contexts/UIActionsContext.tsx | 不改:toggleTranscript/closeTranscript 为 AppContainer 本地回调,由全局键处理直接调用,不经此 context(§4.2) |
packages/cli/src/ui/hooks/useDialogClose.ts | 不改动(transcript 不走此路径,Esc 在全局前置分支处理,见 §4.3) |
packages/cli/src/ui/layouts/DefaultAppLayout.tsx(及 ScreenReaderAppLayout) | 加顶层条件:isTranscriptOpen 时渲染 <TranscriptView/>(<AlternateScreen disabled={useVP}> 接管)替代主内容。无独立 overlay 路径——非 TTY 由 AlternateScreen 的 isTTY guard 退化为普通 buffer 内渲染(§4.2),VP 由 disabled 复用 ink root 的 alt-screen |
packages/cli/src/ui/components/MainContent.tsx | #5661 维持 mergedHistory = visibleHistory(无 cross-group 合并、无 getCompactLabel/absorbedCallIds/isSummaryAbsorbed)。本 PR 不改动:主视图不传 fullDetail(默认 false),force 语义全由 ToolGroupMessage 内联(§4.5) |
packages/cli/src/ui/components/HistoryItemDisplay.tsx | 引入 fullDetail prop(父级传入,默认 false):折入 resolvedThoughtExpanded = fullDetail || (thoughtExpanded ?? contextThoughtExpanded)(§4.7,两个思考块分支自动生效);下传 fullDetail 给 ToolGroupMessage。tool_use_summary 维持渲染为独立 ● <summary> 行(跟齐 main,无吸收机制) |
packages/cli/src/ui/components/messages/ConversationMessages.tsx | 不改:ThinkMessage/Content 的 expanded 由 HistoryItemDisplay 传入的 resolvedThoughtExpanded(已含 fullDetail 分量)决定,无需在此处引用 compactMode/fullDetail |
packages/cli/src/ui/components/messages/ToolMessage.tsx | 保留不动 #5661 的 forceShowResult prop + shouldCollapseResult(含 isCollapsibleTool(name) 守卫)折叠 gate。fullDetail/点击展开经 ToolGroupMessage 传入的 forceShowResult=true + availableTerminalHeight=undefined 即自动解除结果折叠与 MaxSizedBox/shell 高度约束(§4.5 #2/#3)。§4.9 改:新增显式 fullDetail prop(由 ToolGroupMessage 下传),仅当 fullDetail && isCollapsibleTool(name) && detailedDisplay 时以 detailedDisplay 取代摘要数据源 resultDisplay;把"解折叠/高度"(forceShowResult,可由 user-initiated/error 触发)与"切换完整数据源"(仅 fullDetail)分离,避免主视图 force 场景误显完整明细(修审计 #2);点击命中由 ToolGroupMessage 包裹(§4.8) |
packages/cli/src/ui/components/messages/ToolGroupMessage.tsx | 保留 forceExpandAll + collapsibleTools/nonCollapsibleTools type-based 分区。本 PR:加 fullDetail prop → 并入 forceExpandAll = fullDetail || ...(解除分区)+ 逐工具 forceShowResult = fullDetail || ... + fullDetail 时 availableTerminalHeight=undefined(§4.5 #1/#2/#3);加点击命中 isToolExpanded(id) 同样并入 forceExpandAll(§4.8)。无 showCompact/compactMode 可删;§4.9:把 fullDetail 显式下传给 ToolMessage 作数据源开关 |
packages/cli/src/ui/components/SettingsDialog.tsx | 删 ui.compactMode 的特殊 setCompactMode 同步逻辑 |
packages/cli/src/config/settingsSchema.ts | 移除 ui.compactMode/ui.compactInline(见 §6 迁移策略) |
packages/cli/src/serve/routes/workspace-settings.ts | 保留 WEB_SHELL_SETTINGS 中的 ui.compactMode——web-shell 有独立的 CompactModeContext(packages/web-shell/client/App.tsx 读 'ui.compactMode'),即使 CLI settingsSchema 不再定义该键,serve 层仍需透传给 web-shell,否则破坏其 compact。web-shell 自身 compact 去留另案评估、不在本 PR 范围(§6/§7 #5) |
packages/cli/src/ui/components/KeyboardShortcuts.tsx | Ctrl+O 文案改为 to view transcript |
packages/cli/src/services/tips/tipRegistry.ts | 删/改 id: 'compact-mode' 的启动提示(:183-185 "Press Ctrl+O to toggle compact mode …")——否则实现后仍向用户提示旧行为;改为 transcript 提示或移除 |
packages/cli/src/i18n/locales/{en,zh,zh-TW,ca,de,fr,ja,pt,ru}.js | 全部 9 个语言文件都要改/删 compact 文案、加 transcript 文案(PR #3100 先例就改了 de/ja/ru/pt)。只改 en/zh 会导致多语言用户看到残留的 "toggle compact mode" 旧文案 |
packages/web-shell/client/i18n.tsx | 同步 compact→transcript 文案(web-shell 行为另议,见 §7) |
§4.9(方案 Y,merge blocker)的跨文件改动(详见 §4.9 改动点 1–6,上表
ToolMessage/ToolGroupMessage行已含渲染拆分):
- 新增 core helper
packages/core/src/utils/generateContentResponseUtilities.ts的getToolResponseDisplayText(parts)(读functionResponse.response.output+ 遍历 nestedfunctionResponse.parts媒体占位、空/缺失返回undefined;不二次截断;规则见 §4.9 改动点 1);- 改
packages/cli/src/ui/types.ts:IndividualToolCallDisplay加detailedDisplay?: string(派生、不持久化);- 改
packages/cli/src/ui/hooks/useReactToolScheduler.ts(live 提取,success分支派生detailedDisplay)、packages/cli/src/ui/utils/resumeHistoryUtils.ts(resume 提取,tool_result分支从responseParts ?? message.parts派生)、packages/cli/src/ui/components/messages/ToolMessage.tsx+ToolGroupMessage.tsx(渲染拆分:ToolGroupMessage下传fullDetail,ToolMessage仅fullDetail && isCollapsibleTool && detailedDisplay切数据源);- 不改
packages/cli/src/acp-integration/session/history-replayer.ts/emitters/tool-call-emitter.ts——ACPcontent[]已含完整output(见上表),TUI transcript 不经此路,无需改动;- 不改 持久化 schema(
serializeToolResponse/chatRecordingService/ ACP 协议字段)——完整明细已天然存于responseParts,新字段为派生值。
| 文件 | 内容 |
|---|---|
packages/cli/src/ui/components/TranscriptView.tsx | Transcript 全详情滚动屏(用 <AlternateScreen disabled={useVP}> 包裹 + 双段冻结快照 + ScrollableList/VirtualizedList(调大/自适应 estimatedItemHeight)+ fullDetail={true} 渲染) |
packages/cli/src/ui/components/TranscriptView.test.tsx | 渲染/滚动/关闭/快照定格测试 |
packages/cli/src/ui/contexts/ToolExpandedContext.tsx(follow-up,鼠标点击展开) | 专用小 context(仿 gemini-cli ToolActionsContext/本仓库 ThinkingViewerContext):暴露 toggleToolExpanded(id)/isToolExpanded(id);canonical expandedToolGroupIds 仍在 AppContainer useState(§4.8)。不塞进 broad UIStateContext |
复用现有组件,不再新增 AlternateScreen:
packages/cli/src/ui/components/AlternateScreen.tsx(PR #5627)已实现 DEC 1049 进出 +disabledprop,直接复用(§4.2)。原"新增 AlternateScreen/useAlternateBuffer"提法已删除。
MainContent.test.tsx、HistoryItemDisplay.test.tsx、ToolGroupMessage.test.tsx、ToolMessage.test.tsx、SettingsDialog.test.tsx 中所有 CompactModeProvider 包装与 compactMode 用例需删除或改写为基线/transcript 用例。
settingsSchema.ts),但 WEB_SHELL_SETTINGS(workspace-settings.ts:36)保留 ui.compactMode——web-shell 有独立的 CompactModeContext(packages/web-shell/client/App.tsx 读 'ui.compactMode'),serve 层须继续透传,否则破坏 web-shell(见 #12 / §7 #5)。已验证设置系统对未知键宽容——getSettingsFileKeyWarnings(settings.ts:250-309,未知键处理在 :290-306,debugLogger.warn 在 :303-305)仅 debug 记录、不报错不阻断。因此用户旧的 CLI 配置可安全读取(被 CLI 忽略),新 CLI 设置 UI/API 不再列出该项。CLI 侧不保留向后语义——模式概念整体删除,保留也无处生效;web-shell 侧的 compact 去留另案评估。keyMatchers(keyMatchers.ts)始终由硬编码的 defaultKeyBindings 构建,settingsSchema 也无 keybinding 项(仅 vimMode)。因此不存在用户持久化的 toggleCompactMode 绑定会被静默丢弃,启动时迁移检测无可扫描对象、当前不适用。一旦将来引入用户可配置 keybinding,再补"检测残留 toggleCompactMode 绑定并一次性提示改绑 toggleTranscript"。当前 PR 仅需 release note 提示 Ctrl+O 语义已变更为 transcript(docs/users/reference/keyboard-shortcuts.md 已更新)。toggle_transcript 事件(可选)。VirtualizedList)按视口有界;区分两类上限——只解除"行高"截断(为显示,transcript 解除),但保留"字符数"上限(为性能,始终保留)。注意 qwen-code 不存在 SlicingMaxSizedBox(那是 gemini-cli 的);qwen 侧的字符级保护是工具输出截断阈值 DEFAULT_TRUNCATE_TOOL_OUTPUT_THRESHOLD(~25000)。实现时验证万行级历史滚动流畅度。AlternateScreen.tsx(PR #5627)进出 alt-screen,而非自行手写 ?1049h/l 时序,风险显著低于初版设计。剩余需处理的两点:(a) VP 模式 double-enter——useTerminalBuffer 开启时 ink root 已占 alt-screen,必须传 disabled={useVP} 跳过转义(§4.2);(b) 退出收尾重绘——退出时主屏经 refreshStatic() 清屏重绘一次(§4.4),且 transcript 期间 refreshStatic 被 isTranscriptOpenRef 守卫跳过,避免污染 normal-buffer scrollback。注意旧 compact 是每次 toggle 都 refreshStatic,本方案只在 transcript 关闭时一次,频率低得多。仍需在 tmux / iTerm2 / VSCode / Apple Terminal 验证。forceExpandAll 前缀加 fullDetail || isToolExpanded(id) || 时,务必保留其余 hasConfirmingTool || hasSubagentPendingConfirmation || hasErrorTool || isEmbeddedShellFocused || isUserInitiated || hasTerminalSubagent,以及 ToolMessage 的 shouldCollapseResult(含 isCollapsibleTool 守卫)gate——这些是 #5661 已就位的"出错/待确认/用户发起/聚焦 shell/子代理 必须完整展示"安全语义(§4.5),误删会导致"出错的工具被折叠看不全"的回归。不需要迁移到任何新文件。AlternateScreen 与 VP root 既有机制走,无需额外手写。CompactModeContext(packages/web-shell/client/App.tsx 读 'ui.compactMode'),与 CLI 的 compact 是两套实现。为不破坏它,本 PR 在 WEB_SHELL_SETTINGS 中保留 ui.compactMode(即使 CLI schema 不再定义该键,serve 层仍透传,见 #12 / §6)。本期 CLI 仅做自身文案与设置项清理;web-shell 自身 compact 的去留另案评估,不在本设计实现范围。SHOW_MORE_LINES 是否并入:本期保留独立,避免改动面失控;列为后续可选优化。(ctrl+o to expand) 提示语义(已定):思考块摘要恒带该提示(指引"进 transcript 看完整上下文",非"此处被截断");工具输出仅在被高度约束截断时带 +N lines。两者触发条件不同是预期行为,i18n 文案需体现"查看完整上下文"而非"展开被截断内容",避免误导。AlternateScreen(复用现有组件 + 新补 isTTY guard 后的接入测试):非 VP 模式 enter/exit 写 ENTER_ALT_SCREEN/EXIT_ALT_SCREEN;VP 模式(disabled=true)路径跳过转义写入、不 double-enter;非 TTY(process.stdout.isTTY 假)跳过转义写入(待补 guard,§4.2)。TranscriptView:打开渲染全历史 + 进入时刻 pending 快照、思考/工具全展开不截断、滚动 API、双段冻结(后台新增 history/pending 不进入视图)、estimatedItemHeight 调大/自适应后滚动定位不失真(§4.4)。ToolGroupMessage/ToolMessage(type-based partition 基线 + fullDetail 联动):无 force 时 collapsible(read/search/list)折叠成 CompactToolGroupDisplay 摘要、non-collapsible(edit/write/command/agent)逐个 ToolMessage、混合组摘要行 + 逐个并存;force 组(出错/确认/聚焦 shell/用户发起/终端子代理)forceExpandAll=true → 全部逐个、触发工具 forceShowResult=true;fullDetail=true(transcript/点击展开)时 forceExpandAll=true + forceShowResult=true + availableTerminalHeight=undefined 解除高度约束;non-collapsible 工具结果在非 force 下也始终可见(shouldCollapseResult 的 isCollapsibleTool 守卫);思考摘要含 (ctrl+o to expand);思考块 resolvedThoughtExpanded = fullDetail || (thoughtExpanded ?? contextThoughtExpanded)(与 main 的 Alt+T per-block 开关共存,§4.7)。ctrlCPressedOnce(验证短路早于 QUIT 分支,§4.3);Esc 关 transcript 即使处于 vim INSERT 模式(验证早于 ESCAPE 分支的 vim 守卫,§4.3);Esc/q 关 transcript 且不取消后台请求;Ctrl+S 仍切 constrainHeight、互不干扰。WaitingForConfirmation、shellConfirmationRequest、loopDetectionConfirmationRequest、confirmationRequest、confirmUpdateExtensionRequests、providerUpdateRequest 任一 → 自动 closeTranscript()(§4.6 #1)。refreshStatic 守卫:打开 transcript 期间后台完成一轮工具调用 / 触发 resize,transcript 期间 refreshStatic 被 isTranscriptOpenRef 守卫跳过、退出后重绘一次——该轮内容恰好出现一次(无重复回放/无缺失/scrollback 不破坏,§4.4)。functionResponse.response.output 提取 + media 占位 + 空/缺失降级;detailedDisplay(TUI 专属、IndividualToolCallDisplay 派生字段)在 live(useReactToolScheduler)+ resume(resumeHistoryUtils)两路都产出,尤其 resume / 回放后 Ctrl+O transcript 仍是全详情(不回退摘要);ACP 路无需 detailedDisplay——ToolCallEmitter.transformPartsToToolCallContent 早已把同一 functionResponse.response.output 完整文本写进 ACP content[](其 SSE 客户端自行渲染,非 TUI transcript),故复用 message: record.message.parts、不新增协议字段即满足 §4.9;ToolMessage 仅 fullDetail && isCollapsibleTool && detailedDisplay 用完整明细,而 forceShowResult 但非 fullDetail(主视图 user-initiated/error 等)仍用摘要 resultDisplay(回归"主视图不变");覆盖 glob(或 legacy search displayName),不只 read/grep/list;不二次截断(read_file 超 32k 的完整 output 在 Ctrl+O 仍完整、不被 UI 再裁,仅受 core 已施加的 truncateToolOutput/分页边界);极旧无-parts 记录降级摘要。functionResponse.response.output 长度 > MAX_RETAINED_TOOL_RESULT_DISPLAY_CHARS 的 tool_result + 旁置 toolCallResult.resultDisplay 摘要 + 后续 chat_compression record;断言 record.message.parts[0].functionResponse.response.output 在 recording / loadSession / resumeHistoryUtils / HistoryReplayer 四段都保留完整 string,且 detailedDisplay 从 message.parts 派生(非 resultDisplay、非 API compressedHistory)。此用例失败即触发回退方案 X(新增持久化 display 字段)。源码上前提成立:serializeToolResponse/summarizeBatchResponsePart 仅用于 post-tool-batch hook payload(coreToolScheduler.ts:819-870)、不碰 chat recording;ChatRecordingService.recordToolResult 以 createUserContent(message) 原样写 record.message、只 sanitize resultDisplay(chatRecordingService.ts:1077-1109);TUI resume / ACP replay 走 conversation.messages、非 API compressedHistory(AppContainer.tsx:642-652、acpAgent.ts:6355-6357)。grep -rE 'TOGGLE_COMPACT_MODE|useCompactMode|CompactModeContext|compactInline|compactToggleHasVisualEffect' 在 packages/cli/src 非测试源应为空;compactMode 仅保留 web-shell 透传 WEB_SHELL_SETTINGS 与 ToolConfirmationMessage 的本地 layout prop);注意 CompactToolGroupDisplay 与 partition 符号(getToolCategory/CATEGORY_ORDER/isCollapsibleTool 等)应仍存在(属 #5661 基线,不在删除范围);mergeCompactToolGroups.ts 应已删除;typecheck/lint/test 全绿。compactMode 影响(始终走 type-based partition)。需重录 qwen-code-mac-autotest:默认 partition 基线(collapsible→摘要、non-collapsible→逐个、混合组并存)、含出错工具(force 逐个展示)、含长 shell 输出(non-collapsible 结果可见)、点击工具块就地展开、打开 transcript、transcript 内滚动到顶/底、transcript 内 resize、退出后主屏恢复。AlternateScreen 的进出(含 VP disabled 路径与非 VP 写转义路径)在 tmux / iTerm2 / VSCode 集成终端 / Apple Terminal 下逐一验证(重绘、退出 refreshStatic 收尾、resize)。本 PR 实现按 commit 拆分以便 review/回滚(下表为实际/计划状态,非纯 design-first 的"先文档后实现")。依赖基线 #5661(partition 基线)+ #5751(鼠标基础设施)均已合入 main,本分支已 rebase 其上。每个 commit 必须自身可编译可测:
commit 1 — 删残留全局 compactMode(保留 type-based partition 基线):删 CompactModeContext/useCompactMode/compactMode settings(schema)/i18n,删失去引用的 mergeCompactToolGroups.ts(含 compactToggleHasVisualEffect)。不动 ToolGroupMessage 的 forceExpandAll/分区决策(无 showCompact/compactMode || 可删)。同步引入 fullDetail prop(默认 false)穿过 HistoryItemDisplay/ToolGroupMessage,并把 fullDetail 并入 forceExpandAll/forceShowResult、思考块折入 resolvedThoughtExpanded;主视图不传(沿用 #5661 的 type-based partition)。完成后主视图即为"#5661 type-based partition 基线、无全局开关",Ctrl+O 暂为 no-op。commit 2 — 接入 alt-screen 能力:复用现有 AlternateScreen.tsx(PR #5627),验证 disabled={useVP} 跳过 double-enter、退出 refreshStatic() 重绘 + isTranscriptOpenRef 守卫期间 refreshStatic、非 TTY 退化。仅加能力、不接线。commit 3 — 新增 TranscriptView(<AlternateScreen disabled={useVP}> + 双段冻结快照 + 虚拟滚动 + 调大/自适应 estimatedItemHeight + fullDetail={true}),fullDetail 联动落地:transcript 路径 forceExpandAll=true + forceShowResult=true + availableTerminalHeight=undefined 解除高度约束 + 思考块 expanded(§4.5);TOGGLE_TRANSCRIPT 绑定、全局键位接线(Ctrl+O toggle / Esc·Ctrl+C·q 第一分支关闭,早于 QUIT/vim 守卫)、全部阻塞确认自动关闭、消息队列 drain 守卫、顶层 layout 条件渲染。commit 4(§4.9,本 PR merge blocker)— read/search/list 完整明细透传(方案 Y):core 新增 getToolResponseDisplayText(读 functionResponse.response.output);IndividualToolCallDisplay.detailedDisplay(派生、不持久化);live(useReactToolScheduler)/resume(resumeHistoryUtils)/ACP replay(HistoryReplayer+ToolCallEmitter) 三路用同一 helper 派生;ToolMessage 显式收 fullDetail、仅 fullDetail && isCollapsibleTool && detailedDisplay 切换数据源(与 forceShowResult 解折叠分离,修审计 #2)。完成后 Ctrl+O 才是真正"全详情",并重录 §3.4 截图。commit 5 — i18n 文案(9 语言 compact→transcript)、settings 清理、KeyboardShortcuts/Help、测试与 TUI 快照重录。鼠标点击就地展开 = follow-up(独立 PR,VP-only MVP):新增
ToolExpandedContext(toggleToolExpanded/isToolExpanded)+ClickableToolMessage命中区,点击折叠摘要行 → 该组forceExpandAll=true(§4.8)。不在本 PR commit 序列;理由与点击粒度重定见 §4.8 顶部 banner。
commit 1 删残留 compact、保留 type-based partition、引入 fullDetail 管线(默认 false);commit 2 仅加 alt-screen 能力;commit 3 接线点亮 Ctrl+O transcript + fullDetail 联动(此时 collapsible 工具仍只到摘要级);commit 4 补全 read/search/list 完整明细透传(§4.9,merge blocker)——Ctrl+O 此后才是真正"全详情";commit 5 收尾文案/设置与测试、重录截图。每步可编译。鼠标点击就地展开 = follow-up(独立 PR),见 §4.8,不在本 commit 序列。
前序工作 PR #3100 已产出 docs/design/compact-mode/compact-mode-design.md(284 行竞品分析)。本方案与之的关系:
frozenTranscriptState + messages.slice(0, len))。本方案据真实实现采用冻结快照,并在 §3.2 标注来源。forceExpandAll 条件 + forceShowResult 保证(本 PR 保留不动,§4.5),无需另设机制。docs/design/compact-mode/ 标记为"superseded by ctrl-o-detail-expand"(保留历史,加一行指引),避免两份相互矛盾的设计并存。一句话:#3100 是"如何把 compact 模式做好"的设计;本方案是"为什么不要 compact 模式、改用 transcript"的设计,二者是同一问题上的迭代决策。
PR 依赖关系:本 PR 叠加在 #5661(partition 基线)+ #5751(鼠标基础设施) 之上,二者均已合入 main,本分支已 rebase 其上(§9)。
ToolGroupMessage.tsx(forceExpandAll + 分区决策):forceExpandAll = hasConfirmingTool || hasSubagentPendingConfirmation || hasErrorTool || isEmbeddedShellFocused || isUserInitiated || hasTerminalSubagent(不含 compactMode/allComplete)。collapsibleTools = forceExpandAll ? [] : inlineToolCalls.filter(t => isCollapsibleTool(t.name) && t.status !== Canceled);nonCollapsibleTools = forceExpandAll ? inlineToolCalls : 其余。全 collapsible 组 → <CompactToolGroupDisplay>;混合组 → 摘要行 + 逐个 <ToolMessage>;对 ToolMessage 逐个传 forceShowResult={isUserInitiated || Confirming || Error || isAgentWithPendingConfirmation || isTerminalSubagentTool}。本 PR 叠加:forceExpandAll = fullDetail || ...、forceShowResult = fullDetail || ...、fullDetail 时 availableTerminalHeight=undefined。CompactToolGroupDisplay.tsx(分区摘要渲染器 + isCollapsibleTool):导出(export)getOverallStatus、isCollapsibleTool(name)(判定 read/search/list)、buildToolSummary、CompactToolGroupDisplay;内部符号(非导出,仅文件内使用)ToolCategory、TOOL_NAME_TO_CATEGORY、CATEGORY_ORDER(实际顺序 search/read/list/command/edit/write/agent/other)、getToolCategory。本 PR 保留。ToolMessage.tsx(shouldCollapseResult gate):shouldCollapseResult = !forceShowResult && status === ToolCallStatus.Success && isCollapsibleTool(name) && (effectiveDisplayRenderer.type === 'string' || 'ansi')(注意 isCollapsibleTool(name) 守卫:仅 read/search/list 的 string/ansi 结果折叠,Shell/Edit/Agent 结果始终显示;diff/plan/todo/task 走各自 renderer);高度约束符号:MaxSizedBox、sliceTextForMaxHeight、shellOutputMaxLines/shellStringCapHeight、isCappingShell(被 !forceShowResult 解除)。本 PR 保留 gate,叠加 fullDetail/点击展开联动(forceShowResult=true + availableTerminalHeight=undefined,无需改 ToolMessage 本体)。forceExpandAll / isCollapsibleTool"——事实相反,这两个符号是 #5661 合入实现的核心,确实存在。shouldForceFullDetail.ts / COLLAPSIBLE_CATEGORIES 不存在(force 语义内联在 forceExpandAll,分区类别内联在 TOOL_NAME_TO_CATEGORY/isCollapsibleTool),以真实代码为准。packages/cli/src/config/keyBindings.ts:225(TOGGLE_COMPACT_MODE→Ctrl+O)、:223(SHOW_MORE_LINES→Ctrl+S)packages/cli/src/ui/AppContainer.tsx:3257-3269packages/cli/src/ui/contexts/CompactModeContext.tsx(CompactModeProvider/useCompactMode)HistoryItemDisplay.tsx:203,215(expanded={!compactMode});渲染 ConversationMessages.tsx:373-454settingsSchema.ts:940-958(compactMode/compactInline);serve/routes/workspace-settings.ts:36components/shared/ScrollableList.tsx、VirtualizedList.tsx(MainContent 默认对其用恒定 estimatedItemHeight=3,transcript 须调大/自适应);覆盖层 DialogManager.tsx、layouts/DefaultAppLayout.tsx;Esc 统一关闭 hooks/useDialogClose.tspackages/cli/src/ui/components/AlternateScreen.tsx(PR #5627)——useEffect 写 ENTER_ALT_SCREEN+CLEAR+HIDE_CURSOR、卸载/process.on('exit') 写 SHOW_CURSOR+EXIT_ALT_SCREEN,用 useTerminalOutput()/useTerminalSize(),带 disabled?: boolean(注释:"Skip escape writes when the root Ink renderer already owns the alt screen (VP mode)")gemini.tsx:367(const useVP = settings.merged.ui?.useTerminalBuffer ?? false;)、:379(alternateScreen: useVP)ink ^7.0.3;gemini-cli 用 fork npm:@jrichman/[email protected](v6)——不同包不同大版本,alt-screen 能力基于 qwen 自己的 ink v7 + 复用上述组件ThoughtExpandedContext(Alt+T TOGGLE_THINKING_EXPANDED)、ThinkingViewer/ThinkingViewerContext、thoughtExpanded/thinkingFullText props、buildThinkingFullTextMap、ClickableThinkMessage(详见 §4.7)DialogManager.tsx 渲染 shellConfirmationRequest(ShellConfirmationDialog)、loopDetectionConfirmationRequest(LoopDetectionConfirmation)、confirmationRequest(ConsentPrompt)、confirmUpdateExtensionRequests(ConsentPrompt)、providerUpdateRequest(ProviderUpdatePrompt) 等(§4.6 #1)packages/web-shell/client/App.tsx(读 'ui.compactMode'、独立 CompactModeContext)——WEB_SHELL_SETTINGS 须保留该键透传(§6 / §7 #5)defaultBindings.ts(ctrl+o: app:toggleTranscript;escape/q/ctrl+c → transcript:exit,行 160-169)、useGlobalKeybindings.tsx(setScreen 切换 + tengu_toggle_transcript)、REPL.tsx(transcript = 虚拟滚动 + verbose;frozenTranscriptState 冻结 { messagesLength, streamingToolUsesLength } 两个长度、render 时 slice 而非克隆,行 1325/4184/4381)、ink/components/AlternateScreen.tsx + termio/dec.ts:16(ALT_SCREEN_CLEAR: 1049 → \x1b[?1049h/l)、CtrlOToExpand.tsx((ctrl+o to expand))ToolActionsContext(expandedTools / toggleExpansion,per-block 维度,与 main 的 Alt+T 同思路,详见 §2 / §4.7)docs/design/compact-mode/compact-mode-design.md(PR #3100 竞品分析,本方案 §10 互鉴并修正)