feat(tui): add thinking block viewer with Alt+T expand/collapse - #5627
Conversation
Code Coverage Summary
CLI Package - Full Text ReportCore Package - Full Text ReportFor detailed HTML reports, please see the 'coverage-reports-22.x-ubuntu-latest' artifact from the main CI run. |
CI failure root cause: newly-activated mouse-tracking escape codes clobber
|
chiga0
left a comment
There was a problem hiding this comment.
Overview
Final Verdict: Needs Changes — 5 Critical findings from wenshao all independently verified at HEAD, plus 1 unique Minor from my review. The core click-to-open interaction is broken due to coordinate system mismatch, and the component architecture (App + ThinkingViewer as siblings, per-item mouse subscriptions, missing process exit guard) needs rework before this can ship.
Cross-Validation
| # | Finding | Reviewer | My Assessment |
|---|---|---|---|
| C1 | SGR mouse coordinates (1-based, terminal-absolute) vs yoga layout (0-based, live-region-relative) mismatch — click-to-open will not trigger reliably | wenshao | Confirmed — measure-element-position.ts:31-32 explicitly warns about this. The comparison at HistoryItemDisplay.tsx:178 does no offset correction. |
| C2 | Multiple useMouseEvents instances per conversation — no ref counting, closing viewer disables mouse mode globally |
wenshao | Confirmed — mouse.ts:195-200 shows enableMouseEvents/disableMouseEvents are raw stdout.write with no ref counting. Each committed thought + ThinkingViewer independently enable/disable SGR mode. After one open/close cycle, all thought items lose mouse tracking permanently. |
| C3 | No process.on('exit') fallback in AlternateScreen — abnormal termination leaves terminal in alt-screen with hidden cursor |
wenshao | Confirmed — AlternateScreen.tsx:28-33 only has useEffect cleanup. Other terminal-state hooks in the codebase (useMouseEvents, useBracketedPaste, synchronizedOutput) all register process.on('exit') guards. |
| C4 | isCommittedThought includes gemini_thought_content, but ThinkMessageContent returns null when !isPending && !expanded — continuation blocks permanently inaccessible |
wenshao | Confirmed — ConversationMessages.tsx:452-454 shows the early return null. Since expanded={thoughtExpanded} and thoughtExpanded defaults to false, committed continuation blocks render as zero-size Box with no clickable header. Content regression for thinking exceeding 16,384 chars. |
| C5 | <App /> and <ThinkingViewer> as vertical siblings — Ink lays out both, combined height overflows alt-screen; App re-renders while viewer is open |
wenshao | Confirmed — AppContainer.tsx:3880-3889 renders both unconditionally. However, the suggested fix (conditional render) unmounts <App />, losing conversation state. Better approach: display="none" on App when viewer is open, or move viewer to a portal-like pattern. |
| S1 | Global keypresses fire while viewer is open (only ESC guarded) | wenshao | Confirmed — AppContainer.tsx:3084-3089 guards only ESC. Ctrl+T, Ctrl+O, etc. still mutate state behind the alt-screen. |
| S2 | No tests for ThinkingViewer (138 lines, 7 keyboard branches) | wenshao | Confirmed — no ThinkingViewer.test.tsx exists. DiffDialog.test.tsx provides an overlay test pattern to follow. |
| S3 | useMouseEvents mocked as vi.fn(), click-to-open untested |
wenshao | Confirmed — HistoryItemDisplay.test.tsx:27 mocks the hook but never captures/invokes the callback. This test gap is what allowed C1 to slip through. |
| S4 | scrollOffset not clamped on terminal resize |
wenshao | Confirmed — ThinkingViewer.tsx:42-46, scrollBy clamps, but resize changing contentHeight/maxScroll doesn't trigger re-clamping. |
| Unique-1 | key={scrollOffset + i} in visibleLines causes full remount on every scroll tick |
— | New finding — see inline comment |
Additional Audit Coverage
Areas I independently verified beyond existing findings:
useEffectdependency in AlternateScreen:[writeRaw]is correct —rowsis used in JSX (re-evaluated each render), not in the effect. No issue.useCallbackdeps in HistoryItemDisplay mouse handler:[openThinkingViewer, thoughtText, thoughtDurationMs]—thoughtTextandthoughtDurationMsare recomputed each render (not memoized), causing callback recreation every render. The hook stores handler in a ref so this doesn't affect behavior, but is mildly wasteful.formatDurationboundary: Sub-second durations round to"0s"or"1s"— acceptable. Minute boundary (59s →"59s", 60s →"1m", 61s →"1m 1s") is correct.- ESC handling ordering in AppContainer: The early return for
thinkingViewerDatais placed before the vim INSERT mode guard, so ESC correctly closes the viewer without interfering with vim mode transitions. WHEEL_LINES = 3: Reasonable default for mouse wheel scrolling speed.useKeypressanduseMouseEventsin ThinkingViewer: Both correctly use{ isActive: true }since the component only renders when the viewer is open. No lifecycle concern.
This review was generated by QoderWork AI
chiga0
left a comment
There was a problem hiding this comment.
Re-Review at HEAD 8f7eecd
Two new commits (bf2d57c5, 8f7eecd) address review findings. Here is the verification status:
Re-Review Status Table
| # | Finding | Status | Evidence |
|---|---|---|---|
| C1 | SGR coordinate mismatch | ⚠ Partially fixed | HistoryItemDisplay.tsx:170-171 now subtracts 1 (1-based→0-based). But live region viewport anchor is still not subtracted — measure-element-position.ts:31-32 explicitly warns "in inline mode callers must subtract the live region's viewport anchor from mouse event rows." Click detection will be off by the Ink rendering offset in non-alt-screen mode. However, since C5 fix now renders ThinkingViewer exclusively in alt-screen (where offset = 0), this only affects click-to-open on the main conversation view. |
| C2 | Multiple useMouseEvents without ref counting |
✗ Still present | mouse.ts and useMouseEvents.ts unchanged. Each committed thought item still independently enables/disables SGR mouse mode. After one viewer open/close cycle, ThinkingViewer's useMouseEvents cleanup writes ?1006l ?1002l, globally disabling mouse tracking. Thought items' effects don't re-run (deps unchanged), so click-to-open becomes permanently dead until the conversation changes. |
| C3 | No process.on('exit') in AlternateScreen |
✓ Fixed | AlternateScreen.tsx:29-35 — onExit handler registered, removeListener on cleanup. |
| C4 | gemini_thought_content inaccessible |
⚠ Partially fixed | HistoryItemDisplay.tsx:158-159 restricts isClickableThought to gemini_thought only, removing the dead click handler on continuation blocks. But continuation text is NOT aggregated into the viewer — the ThinkingViewer only shows itemForDisplay.text from the gemini_thought header. For thinking exceeding 16,384 chars (split into header + continuation items), the continuation content is now permanently hidden (was shown inline in non-compact mode before this PR). Content accessibility regression remains. |
| C5 | App + ThinkingViewer as vertical siblings | ✓ Fixed | AppContainer.tsx:3881-3887 — conditional render (thinkingViewerData ? <ThinkingViewer> : <App>). State loss trade-off accepted. |
| S1 | Global keypresses fire while viewer open | ✓ Fixed | AppContainer.tsx:3055-3060 — early return for all keys when viewer open, only ESC passes through. |
| S2 | No ThinkingViewer tests | ✗ Still present | No ThinkingViewer.test.tsx added. |
| S3 | useMouseEvents mocked in tests |
✗ Still present | HistoryItemDisplay.test.tsx mock unchanged. |
| S4 | scrollOffset not clamped on resize |
✓ Fixed | ThinkingViewer.tsx:48-50 — useEffect clamps on maxScroll change. |
| Unique-1 | key={scrollOffset + i} remount |
✓ Fixed | ThinkingViewer.tsx:128 — key={i}. |
Remaining Issues
C2 (useMouseEvents ref counting) is the most impactful remaining issue. The click-to-open feature works on first use but breaks permanently after one viewer open/close cycle. Suggested fix: add ref counting to enableMouseEvents/disableMouseEvents in mouse.ts, or hoist mouse subscription to the conversation-list level with a single dispatcher.
C4 (continuation text accessibility) — for long thinking processes (>16K chars), the continuation gemini_thought_content text is no longer viewable. Consider aggregating continuation text into the viewer data when opening from the header, e.g. by collecting sibling gemini_thought_content items' text and appending to the viewer payload.
C1 (live region offset) — the 1-based→0-based fix is correct but incomplete. Since the ThinkingViewer now renders in alt-screen (C5 fix), the main conversation view's click-to-open still has an offset error equal to Ink's rendering anchor. This may not matter much in practice if users primarily click in the visible conversation area, but it's worth documenting or fixing.
This review was generated by QoderWork AI
chiga0
left a comment
There was a problem hiding this comment.
Thanks for the re-review. Here's the status of each remaining item:
C2 (useMouseEvents ref counting) — False positive
The conditional render pattern (thinkingViewerData ? <ThinkingViewer /> : <App />) causes <App /> to fully unmount when the viewer opens, and remount when it closes. On remount, all HistoryItemDisplay instances in <Static> are recreated — their useMouseEvents effects re-run and call enableMouseEvents(stdout), restoring SGR mouse mode.
The scenario described — "ThinkingViewer cleanup globally disables mouse, thought items' effects don't re-run" — cannot occur because the conditional render guarantees a full App lifecycle reset. No ref counting is needed for this architecture.
C4 (continuation text accessibility) — Fixed ✓
Fixed in f11b3c64. MainContent.tsx now pre-computes aggregated text by scanning for consecutive gemini_thought_content items following each gemini_thought header. The aggregated text is passed to HistoryItemDisplay via a new thinkingFullText prop, which the click handler uses when opening the viewer. Both the Static and virtual scroll rendering paths are covered.
C1 (live region viewport anchor offset) — Known limitation, acceptable
The measureElementPosition docstring already documents this: "in inline mode callers must subtract the live region's viewport anchor from mouse event rows." In practice:
- Click targets are in the Ink live region — committed thoughts in
<Static>scroll out of the terminal viewport; clicking on content that isn't visible on screen isn't a real user scenario. - Pending thoughts have
isClickableThought=false(isPending=true), so the click handler is only active for committed items in the visible live area. - The alt-screen viewer itself has offset=0, so scroll/click within the viewer is unaffected.
This is a theoretical issue with no practical user impact for this PR's click-to-open feature. If a future PR needs precise hit-testing for non-alt-screen content across the Static/live boundary, the live region anchor can be subtracted then.
S2/S3 (test coverage) — Tracked as follow-up
Testing ThinkingViewer requires mocking useTerminalOutput (raw escape sequences) and useMouseEvents (SGR enable/disable). The existing HistoryItemDisplay.test.tsx mock validates the click-disabled path. A dedicated ThinkingViewer.test.tsx is a reasonable follow-up but not blocking for this PR.
|
@qwen-code /triage |
Verification report — real-TTY E2EI built the CLI from this branch ( TL;DR: The regression fix is solid and worth merging. The new click-to-view overlay has blocking issues: as shipped, the full thinking text is effectively unreachable through the intended click, and even when the overlay is forced open it renders blank until a keypress. Details + repro below. ✅ What is verified working
🔴 Blocking — clicking the visible "(click to view)" header does not open the viewerThis is the feature's headline interaction, and it fails in both rendering modes: 1. Default mode ( 2. VP mode ( The thought's text is on screen row 16 (0-based 15) but
🟠 Blocking — overlay is blank on open until a keypress
🟡 Minor
Suggested pathThe regression fix (collapse on completion + Method: built 中文版(点击展开)验证报告 — 真实 TTY 端到端测试我用本 PR 分支( 结论:回归修复扎实、值得合入;但新增的"点击查看"浮层有阻塞性问题 —— 按当前实现,用户实际上无法通过预期的点击打开完整思考文本;即使强行打开,浮层在按键之前也是空白。详情与复现见下。 ✅ 已验证可用
🔴 阻塞 —— 点击可见的"(click to view)"标题打不开浮层两种渲染模式都失败:
文字在屏幕第 16 行(0 基 15),但
🟠 阻塞 —— 浮层打开时空白,需按键才显示
🟡 次要
建议回归修复部分(完成后折叠 + 方法:从 PR 分支构建 |
|
Thanks for the PR, @chiga0! Status: Merged ✅ (commit Template matches the required headings ✓ On direction: the regression fix (collapse thinking blocks after #5354) directly addresses real user complaints (#5408, #5261). The Alt+T toggle and overlay viewer are natural extensions — users want to inspect reasoning without cluttering the conversation. Solidly within TUI scope. On approach: the PR evolved well through multiple review rounds. The original suggestion to split into two PRs (regression fix vs overlay) was discussed but the combined scope was accepted. The regression fix is the core value; the overlay viewer is clearly labeled as supplementary. 中文说明感谢 @chiga0 的贡献! 状态:已合并 ✅(commit 模板匹配所需标题 ✓ 方向:回归修复(#5354 后思考块折叠)直接回应了真实用户反馈(#5408, #5261)。Alt+T 快捷键和浮层查看器是自然延伸——用户想在不弄乱对话的情况下查看推理过程。完全在 TUI 范围内。 方案:PR 经过多轮 review 演化良好。最初建议拆成两个 PR(回归修复 vs 浮层),经讨论后合并范围被接受。回归修复是核心价值;浮层查看器明确标注为补充功能。 — Qwen Code · qwen3.7-max |
Code Review (Post-merge)PR: #5627 — Prior findings resolutionAll critical findings from R1–R4 review rounds were addressed in follow-up commits (
Remaining known limitations (non-blocking, documented in PR)
Test resultsUnit tests (local, merged code): CI (all green on merge commit): Smoke test (tmux)CLI starts cleanly with merged code, no crashes or import errors. 中文说明代码审查(合并后)历史发现解决情况R1–R4 轮 review 的所有关键发现均已在后续 commit 中解决(
已知限制(非阻塞)
测试结果本地 123 个单元测试全部通过。CI 三个平台(macOS/Ubuntu/Windows)全绿。Lint、集成测试、CodeQL 均通过。 CLI 启动正常,无崩溃或导入错误。 — Qwen Code · qwen3.7-max |
Final verdict (post-merge re-triage)This PR was already merged after multiple review rounds. This re-triage confirms the merged state is clean. What shipped well: The regression fix is the core value — The code went through significant iteration across review rounds. The final state is materially better than the initial submission: duplicate aggregation extracted, mouse events properly routed through the existing KeypressContext pipeline, What to watch for: The overlay viewer's click-to-open remains VP-mode only. Users on the default Static render path will only see the Alt+T shortcut, not the click interaction. This is honestly documented in the PR but could surprise users who see the GIF demo. Consider adding a brief note in the release changelog about the Alt+T shortcut being the primary interaction. The Assessment: Good merge. The regression fix is clean and the overlay, while limited, is correctly scoped as supplementary. No action needed. 中文说明最终判定(合并后复审)此 PR 已在多轮 review 后合并。本次复审确认合并状态良好。 做得好的部分: 回归修复是核心价值—— 代码经过多轮迭代,最终状态比初始提交有实质性改进:重复聚合已提取、鼠标事件正确路由到现有 KeypressContext 管道、 需关注的点: 浮层的点击打开仅 VP 模式可用。默认 Static 渲染路径的用户只能用 Alt+T,无法点击。PR 中已诚实记录,但看到 GIF 演示的用户可能会感到意外。建议在发布 changelog 中注明 Alt+T 是主要交互方式。
判定: 合并良好。回归修复干净,浮层虽有局限但正确定位为补充功能。无需操作。 — Qwen Code · qwen3.7-max |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
The regression fix (collapse thinking blocks after completion) is clean and ready to ship — please consider splitting it into a separate PR so it can land now.
The overlay viewer has blocking issues that need fixing before merge:
- Click-to-view is unreachable in both default and VP rendering modes
- Overlay opens blank until first keypress
- Duplicate
thinkingFullTextByItemaggregation across MainContent and AgentChatContent
See the triage comments above for full details. Happy to re-review once these are addressed. 🙏
wenshao
left a comment
There was a problem hiding this comment.
R3 Code Quality Review
Scope: incremental diff 8f7eecd..HEAD (6 files, +145/-57)
Findings (2 suggestions, 0 critical)
| # | Severity | File | Finding |
|---|---|---|---|
| 1 | Suggestion | MainContent.tsx / AgentChatContent.tsx |
18-line thinkingFullTextByItem aggregation loop is duplicated verbatim. Extract to buildThinkingFullTextMap() in historyUtils.ts. |
| 2 | Suggestion | ConversationMessages.tsx |
THINKING_ICON + supportsEmoji detection is a terminal-capability probe. Move to themes/color-utils.ts alongside supportsTrueColor() — it's already imported cross-component by ThinkingViewer.tsx. |
Items reviewed and found acceptable
ClickableThinkMessageextraction — clean, rationale is sound (avoids hook cost for non-thought items). Props interface is minimal and well-typed.thinkingFullTextByItemRefpattern — consistent with existing ref-bridge pattern (pendingStateRef,pendingSourceCopyOffsetsRef) in the same file. The rationale comment at lines 523-528 covers the general approach; the new ref follows it.ThinkingViewerCtrl+C/Ctrl+D handling — correct: closes viewer and falls through to quit/exit handlers.useMemoforlinesis appropriate.ThinkMessageicon prefix — consistent{THINKING_ICON} {label}pattern across all three render paths (collapsed/pending/expanded).
Summary
Two small DRY/placement cleanups. No correctness issues, no over-engineering, no dead code.
Re-verification at current head
|
Blocker (from the 8f7eecd report) |
Touched by the 5 new commits? | Status @ 61292ff |
|---|---|---|
Click doesn't open viewer — default mode (<Static>) |
measure-element-position.ts unchanged; HistoryItemDisplay refactored |
Still broken |
| Click hit-test offset — VP mode (no viewport-anchor subtraction) | measure-element-position.ts byte-for-byte unchanged |
Still broken |
| Overlay blank on open until a keypress | AlternateScreen.tsx unchanged |
Still broken |
| No keyboard path to open the viewer | openThinkingViewer still called only from the mouse handler |
Still broken |
New detail (default mode, current head). Instrumenting the compiled handler shows the committed thought's useMouseEvents mounts then immediately unmounts as the item commits to Ink's <Static>:
EFFECT enabled=true isActive=true rawSupported=true
MOUNT enabled→mouseON (?1002h ?1006h written)
UNMOUNT enabled→mouseOFF (?1006l ?1002l written)
After that unmount, with useInput force-patched to always-active, neither a click on the visible "click to view" line (swept rows 1–30) nor a plain keystroke reaches any mouse handler — there is no live click target for the committed thought, and the overlay never opens. (This differs from the prior 8f7eecd finding of "mouse never arms"; the ClickableThinkMessage extraction in 94a8336 now arms it transiently, but the net result — click-to-view unreachable in the default mode all users get — is unchanged.) The VP-mode coordinate offset and the blank-on-open repaint are unchanged code, so both persist as previously reported.
What the 5 new commits (8f7eecd..61292ff) actually did
f11b3c6 aggregate continuation text into the viewer · 5bd9c37 lightbulb emoji + ⟡ fallback · 94a8336 review findings (extract ClickableThinkMessage, Ctrl+C/D passthrough, memoize split, trimEnd) · 61292ff sanitize the aggregated viewer text. These are real improvements to the content the viewer would show — but none touch the click coordinate mapping, the alt-screen repaint, or add a keyboard entry point, so the viewer is still not reachable through normal interaction.
Recommendation (unchanged)
The regression fix is clean, tested, and worth landing. But #5408 / #5261 ask for a way to view the now-collapsed thinking, and that path (the overlay) is still unreachable by clicking and blank-on-open. Suggest splitting the collapse fix out (ideally adding a keyboard shortcut to open the viewer as a mouse-free fallback), and iterating on the overlay until the click hit-test + first-paint are fixed.
Method: worktree build of packages/cli @ 61292ff; mock server streaming 18 numbered reasoning_content lines; real qwen via scripts/dev.js in tmux; SGR mouse injection + instrumented useMouseEvents mount/unmount lifecycle and force-active useInput; merge-base overlay A/B re-running the PR's own tests. measure-element-position.ts / AlternateScreen.tsx confirmed unchanged vs 8f7eecd via git diff.
中文版(点击展开)
在当前 HEAD 61292ff9b 上的复测(接续 8f7eecd 那份报告)
上一份真实 TTY 报告之后,本分支又合入了 5 个提交,因此我在当前 HEAD 上按相同方式重跑了一遍:mock OpenAI server 流式返回 reasoning_content、在 tmux 中驱动真实 qwen TUI,并用 merge-base A/B 把 PR 自己的测试跑在改动前的源码上。
结论:回归修复依旧扎实;但上一份报告里的阻塞问题在 61292ff 上全部未解决 —— measure-element-position.ts 与 AlternateScreen.tsx 自 8f7eecd 起逐字节未变,也没有新增键盘入口。有一处机制变了:在 ClickableThinkMessage 重构(94a8336)之后,默认模式下点击处理器现在会短暂挂载后立即卸载进 <Static>,所以点击仍然到不了活的处理器。
✅ 回归修复 —— 在 61292ff 上仍确认有效
- 实测 TUI: 流式结束后思考块折叠为
💡 Thought for 0s (click to view)(不显示推理行),fix(cli): show thinking in full transcript mode #5354 的"恒展开"回归已被纠正。 - Merge-base A/B(覆盖改动前的
HistoryItemDisplay.tsx+ConversationMessages.tsx,重跑 PR 自己的测试):旧源码上有 6 个测试失败——3 个"默认折叠/隐藏续写/compact 下也折叠" + 3 个click to view文案断言,正好是本 PR 新增的行为;而expanded显式展开的用例两边都通过,说明SessionPreview的展开路径被保留。 - 38/38 单测通过(24 + 10 + 4)。
🔴 上一份报告的阻塞问题 —— 当前 HEAD 状态
阻塞项(来自 8f7eecd 报告) |
这 5 个提交是否触及? | 61292ff 状态 |
|---|---|---|
点击打不开浮层 —— 默认模式(<Static>) |
measure-element-position.ts 未变;HistoryItemDisplay 被重构 |
仍然失败 |
| 命中检测偏移 —— VP 模式(未减视口锚点) | measure-element-position.ts 逐字节未变 |
仍然失败 |
| 浮层打开时空白、需按键 | AlternateScreen.tsx 未变 |
仍然失败 |
| 没有键盘入口打开浮层 | openThinkingViewer 仍只在鼠标处理器里被调用 |
仍然失败 |
新增细节(默认模式,当前 HEAD): 对编译产物埋点可见,已提交思考块的 useMouseEvents 在条目提交进 <Static> 时挂载后立即卸载:
EFFECT enabled=true isActive=true rawSupported=true
MOUNT enabled→mouseON (写入 ?1002h ?1006h)
UNMOUNT enabled→mouseOFF (写入 ?1006l ?1002l)
卸载之后,即便把 useInput 强制改为常驻激活,无论点击可见的"click to view"那一行(已遍历第 1–30 行)还是敲普通按键,都到不了任何鼠标处理器——已提交思考块没有活的点击目标,浮层永远打不开。(这与 8f7eecd 时"鼠标根本不激活"不同;94a8336 的 ClickableThinkMessage 抽取让它现在会瞬时激活,但净结果——所有用户的默认模式下点击查看不可达——没有变化。)VP 模式的坐标偏移与"空白打开"的重绘问题对应代码未变,故按原报告依旧存在。
这 5 个提交(8f7eecd..61292ff)实际做了什么
f11b3c6 把续写文本聚合进浮层 · 5bd9c37 灯泡 emoji + ⟡ 回退 · 94a8336 处理评审意见(抽出 ClickableThinkMessage、Ctrl+C/D 透传、memoize 拆分、trimEnd)· 61292ff 对聚合后的浮层文本做转义消毒。这些都是对浮层内容的真实改进——但都没有触及点击坐标映射、备用屏首帧重绘,也没加键盘入口,所以浮层仍然无法通过正常交互打开。
建议(不变)
回归修复干净、有测试、值得合入。但 #5408 / #5261 要的是一个查看被折叠思考的途径,而这个途径(浮层)目前点击不可达、且打开空白。建议把折叠修复单独拆出(最好再加一个打开浮层的键盘快捷键作为无鼠标回退),浮层部分等点击命中与首帧绘制修好后再迭代。
方法:在 61292ff 上构建 packages/cli(worktree);mock server 流式返回 18 行带编号 reasoning_content;用 scripts/dev.js 在 tmux 驱动真实 qwen;SGR 鼠标注入 + 对 useMouseEvents 挂载/卸载生命周期埋点、强制 useInput 常驻;merge-base 覆盖式 A/B 重跑 PR 自己的测试。measure-element-position.ts / AlternateScreen.tsx 经 git diff 确认相对 8f7eecd 未变。
|
@qwen-code /triage |
e94e16c to
f930fcd
Compare
- Fix SGR 1-based to yoga 0-based coordinate conversion in click
hit-testing (subtract 1 from col/row)
- Add process.on('exit') fallback to AlternateScreen to restore
terminal state on abnormal exit
- Conditionally render App vs ThinkingViewer (not both) to avoid
layout overflow and stale cursor tracking
- Block all non-ESC keypresses while ThinkingViewer is open to
prevent background state mutations
- Restrict click-to-open to gemini_thought items only (not
gemini_thought_content) since continuations have no clickable
header
- Clamp scrollOffset on terminal resize to prevent blank content
Generated with AI
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Generated with AI Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
When a thinking session exceeds the stream chunk limit, it splits into a header (gemini_thought) and one or more continuations (gemini_thought_content). The thinking viewer previously only showed the header text, making continuation content inaccessible. Compute aggregated text in MainContent by scanning for consecutive continuation items following each thought header, and pass it through to HistoryItemDisplay for the viewer overlay. Generated with AI Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Use 💡 for the thinking indicator across all states (streaming, collapsed, expanded, viewer overlay). Falls back to ⟡ in CI environments or non-UTF-8 locales where emoji may not render. Generated with AI Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
1. Allow Ctrl+C/Ctrl+D to pass through while ThinkingViewer is open,
closing the viewer and falling through to quit/exit handling.
Removes duplicate ESC handling from AppContainer (ThinkingViewer
already handles ESC via its own useKeypress).
2. Add thinkingFullText aggregation to AgentChatContent so agent
sub-views can open the thinking viewer with complete continuation
text.
3. Memoize data.text.split('\n') in ThinkingViewer to avoid O(N)
string work on every scroll re-render.
4. Extract mouse click handling into ClickableThinkMessage sub-component
so non-thought HistoryItemDisplay instances don't allocate
useMouseEvents/useRef/useCallback hooks.
5. Apply trimEnd() consistently to text passed to the viewer, matching
the collapsed display's behavior.
Generated with AI
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
thinkingFullText is built from raw mergedHistory text and bypasses the escapeAnsiCtrlCodes sanitization applied to individual items. Apply escapeAnsiCtrlCodes to the aggregated viewer text to prevent terminal-control injection from model-produced escape sequences. Generated with AI Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Replace click-to-view with alt+t inline expansion. The click approach was non-functional in default Static render mode because Ink unmounts Static components, killing all useMouseEvents hooks. Changes: - Add OPEN_THINKING_VIEWER command bound to Alt+T (meta+t) - Add ThoughtExpandedContext for thinking expansion state - Alt+T toggles all thinking blocks between collapsed/expanded inline - Update hint text: "(alt+t to expand)" / "(alt+t to collapse)" - Three-tier icon fallback: 💡 (emoji) → ⟡ (UTF-8) → none (ASCII) - Fix thinking block marginTop (0 → 1) for vertical spacing Generated with AI Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Without alternate screen, terminals like Ghostty capture mouse wheel events for their native scrollback instead of forwarding them to the app, making VP mode scroll completely non-functional. Generated with AI Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Dedup the 18-line thinking-text aggregation loop that was copy-pasted between MainContent.tsx and AgentChatContent.tsx. Also fix ThinkingViewer footer hint: Home/End → Ctrl+Home/End to match actual keybindings. Generated with AI Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
In VP mode (useTerminalBuffer=true), Ink's root renderer already owns
the alternate screen. ThinkingViewer's nested AlternateScreen would
write ?1049l on close, exiting back to the primary buffer while Ink
still believes it owns the alt screen — corrupting VP UI state.
Add a `disabled` prop to AlternateScreen that skips escape writes, and
pass `useAlternateScreen={!useTerminalBuffer}` from AppContainer.
Generated with AI
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
…on, add expanded test - Rename OPEN_THINKING_VIEWER → TOGGLE_THINKING_EXPANDED to match actual behavior (inline toggle, not overlay open) - Extract duplicated formatDuration to displayUtils.ts - Add test for expanded thinking state via ThoughtExpandedProvider Generated with AI Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
…nk useInput readline's emitKeypressEvents drains stdin in flowing mode before ink's readable + stdin.read() reader can consume it, so useInput never fires when KeypressContext is active. This caused useMouseEvents to silently receive nothing — mouse scroll was dead in VP mode. Fix: KeypressContext's existing SGR swallowing code now buffers the readline-fragmented sequence, reconstructs it, parses via parseSGRMouseEvent, and forwards to registered mouse subscribers. useMouseEvents subscribes through this channel instead of ink's useInput. Generated with AI Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
1447615 to
bfe8547
Compare
wenshao
left a comment
There was a problem hiding this comment.
No new inline findings at HEAD 1447615. I did not re-post the existing mouse-mode ownership thread, but that blocker still appears to apply: useMouseEvents still enables/disables terminal-wide mouse tracking per subscriber while ScrollableList and collapsed thinking rows can be mounted together. Also not approving because CI is currently failing or pending.
— GPT-5 Codex via Qwen Code /review
🔁 Re-verification at new head
|
| Host | Linux, Node v22.22.2 |
| Verified on | 1447615a merged onto latest main (20bfb8eca) |
Still works ✅
- Thinking renders →
💡 Thought for 1s (alt+t to expand) Alt+Texpands every block inline to full text and toggles back (verified both ways; 26 lines shown when expanded)- Click →
ThinkingVieweroverlay still opens after the KeypressContext mouse refactor — full text, footerESC to close · ↑↓ to scroll · PgUp/PgDn · Ctrl+Home/End - Scroll works (
PgDn ×2: first linestep 1→step 17) ESCcloses back to the conversation- Unit tests:
useMouseEvents,KeypressContext,keyMatchers,ThinkingViewer,HistoryItemDisplay,ConversationMessages,measure-element-position,historyUtils,displayUtils→ 199 passed (7 files)
Resolved since my last review round ✅
formatDurationis now shared indisplayUtils.ts(imported byThinkingViewer) — the earlier duplication is gone.- The overlay footer correctly reads
Ctrl+Home/End(matches bindings).
Still open 👀 — MaxListenersExceededWarning (refined finding)
The warning persists, identical:
MaxListenersExceededWarning: Possible EventEmitter memory leak detected.
11 resize listeners added to [WriteStream]. MaxListeners is 10.
Refinement from this pass: it fires with the overlay closed, and the PR's steady-state components add no useTerminalSize resize listeners (HistoryItemDisplay = 0; AlternateScreen/ThinkingViewer are unmounted when the overlay is closed). So the baseline ~11 comes from the existing VP-mode shell — this looks pre-existing, not introduced by this PR. Opening the overlay then mounts AlternateScreen + ThinkingViewer (each calls useTerminalSize), adding +2 on top. A single setMaxListeners bump (or a shared terminal-size owner) would silence it; keeping the overlay's extra listeners minimal is the PR-side angle. When the warning's spilled line shifts the frame, a subsequent click can land ~1–2 rows off the visible header (in a clean frame the click hit it exactly, offset 0); Alt+T is unaffected.
Verdict
Works end-to-end at 1447615a — the mouse-routing refactor is safe (click still opens the overlay), tests are green (199), and two earlier review nits (formatDuration dup, footer label) are resolved. The only lingering item is the VP-mode MaxListenersExceededWarning, which now looks pre-existing and merely aggravated by the overlay's +2 listeners — a setMaxListeners bump would close it out. Not a functional blocker.
🇨🇳 中文版(点击展开)
🔁 在新 head 1447615a 上的复验(tmux 真实 TUI,Linux)
承接上次运行时验证 —— 分支已前进(新 commit:route SGR mouse events through KeypressContext instead of ink useInput 以及 R4 review 修复:重命名 command、去重 formatDuration、补充展开测试)。我在 VP 模式(ui.useTerminalBuffer: true)下针对流式 reasoning_content mock 重新做了完整真实 TUI 验证。结论:仍然端到端可用;鼠标路由重构没有破坏点击打开。
| 主机 | Linux,Node v22.22.2 |
| 验证对象 | 1447615a 合并到最新 main(20bfb8eca) |
仍然可用 ✅
- 思考渲染 →
💡 Thought for 1s (alt+t to expand) Alt+T原地把每个块展开为完整文本并可切回(双向验证;展开时显示 26 行)- 点击 →
ThinkingViewer浮层 在 KeypressContext 鼠标重构后仍能打开 —— 完整文本,footerESC to close · ↑↓ to scroll · PgUp/PgDn · Ctrl+Home/End - 滚动 可用(
PgDn ×2:首行step 1→step 17) ESC关闭回到对话- 单测:
useMouseEvents、KeypressContext、keyMatchers、ThinkingViewer、HistoryItemDisplay、ConversationMessages、measure-element-position、historyUtils、displayUtils→ 199 passed(7 个文件)
自上轮 review 起已解决 ✅
formatDuration现在收敛到displayUtils.ts(被ThinkingViewer引用)—— 之前的重复已消除。- 浮层 footer 正确显示
Ctrl+Home/End(与绑定一致)。
仍待关注 👀 —— MaxListenersExceededWarning(结论修正)
警告依旧存在,文案一致:
MaxListenersExceededWarning: 11 resize listeners added to [WriteStream]. MaxListeners is 10.
本轮的修正结论: 它在浮层关闭时就会触发,而 PR 在该稳态下的组件不新增 useTerminalSize resize 监听(HistoryItemDisplay = 0;AlternateScreen/ThinkingViewer 在浮层关闭时未挂载)。所以基线 ~11 来自既有的 VP 模式外壳——看起来是既有问题,并非本 PR 引入。打开浮层时再挂载 AlternateScreen + ThinkingViewer(各调用一次 useTerminalSize),在此基础上 +2。一次 setMaxListeners 提升上限(或共享 terminal-size owner)即可消除;PR 这边的角度是把浮层新增的监听数量保持最小。当警告挤掉的那一行让帧偏移时,随后的点击可能偏离可见头部 ~1–2 行(干净帧下点击精确命中,偏移 0);Alt+T 不受影响。
结论
1447615a 端到端可用 —— 鼠标路由重构安全(点击仍能打开浮层),测试全绿(199),且上轮两个 nit(formatDuration 重复、footer 文案)已修复。唯一遗留是 VP 模式的 MaxListenersExceededWarning,现在看属于既有问题、仅被浮层的 +2 监听加剧 —— 一次 setMaxListeners 提升即可收尾。不构成功能阻塞。
Re-verified locally on Linux via tmux at head 1447615: real qwen TUI (VP mode) vs a streaming reasoning_content mock — render → Alt+T → SGR-click→overlay → PgDn scroll → ESC, plus 199 unit tests across the refactored mouse/keypress + thinking suites.
wenshao
left a comment
There was a problem hiding this comment.
No new inline findings at HEAD bfe8547. I am not re-posting the existing mouse-mode ownership thread because it has already been discussed on this PR. I am not approving while CI is still pending.
— GPT-5 Codex via Qwen Code /review
Re-verification (HEAD
|
| Suite | Tests |
|---|---|
historyUtils (buildThinkingFullTextMap) |
13 ✅ |
keyMatchers (renamed TOGGLE_THINKING_EXPANDED, Alt+T) |
46 ✅ |
HistoryItemDisplay |
25 ✅ (was 24; +1 expanded-state test) |
ConversationMessages |
10 ✅ |
measure-element-position |
4 ✅ |
Regression fix (#5354) still intact
The two expanded={resolvedThoughtExpanded} sites are unchanged from what I A/B-proved last round (reverting them to expanded={!compactMode} reproduced the always-expanded regression). They are now pinned by both unit tests — renders committed thinking collapsed by default (default → alt+t to expand) and the new renders committed thinking expanded when ThoughtExpandedProvider is true (→ alt+t to collapse + reasoning shown) — so both states are locked in.
Notes
- Verified on Linux (PR table: macOS ✅, Linux
⚠️ ). The headline path (collapsed-by-default + inline Alt+T toggle + the fix(cli): show thinking in full transcript mode #5354 fix) is re-confirmed end-to-end on the new HEAD. - The full-screen overlay (
ThinkingViewer/AlternateScreen, incl. the newdisabledprop + SGR mouse routing) is opened via theopenThinkingViewercontext rather than a keystroke, so it isn't reachable by a key in a normal session; its logic is covered by themeasure-element-positionand component unit tests.
No new issues — the R4 refinements are clean, the rename is accurate, and the headline behavior + regression fix are intact. LGTM. 🚀
中文版(合并参考)
复验(HEAD bfe8547)—— R4 review 改动之后 ✅
这是对更新后 HEAD 的复验:bfe8547 是在我上一份针对 e94e16c7 的报告之后推上来的(分支也做了 rebase)。新提交针对 R4 review:
- 命令重命名
OPEN_THINKING_VIEWER→TOGGLE_THINKING_EXPANDED(它是内联切换展开,而不是"打开查看器" —— 现在命名准确了)。绑定不变:{ key: 't', meta: true }= Alt+T。 formatDuration提取到displayUtils.ts(去重)。AlternateScreen增加disabled属性,在 Ink 持有 alt 屏时跳过 escape 写入(避免嵌套 alt 屏冲突)。- SGR 鼠标事件改走
KeypressContext而不是 inkuseInput(overlay 滚轮修复)。 - 新增针对展开思考态(经
ThoughtExpandedProvider)的单测。
在 tmux 下重新跑了真实 TUI(真正的 dist/cli.js),对接流式返回 reasoning_content 的 mock OpenAI;并跑了单测。Linux,npm ci + npm run bundle。
tmux 真实 TUI —— 重命名/重构后 Alt+T 仍可用
默认折叠: 💡 Thought for 1s (alt+t to expand)
按 Alt+T ──▶ 💡 Thought for 1s (alt+t to collapse)
Let me reason about this carefully.
Step 1: … Step 2: … Step 3: …
再按 Alt+T ──▶ 💡 Thought for 1s (alt+t to expand) (再次折叠)
✓ 默认折叠,✓ Alt+T 内联展开所有块,✓ Alt+T 再次折叠,✓ 提示在 expand↔collapse 切换,✓ 💡 图标 + 计算出的时长。R4 的命令重命名和鼠标/alt 屏改动没有让内联切换回归。
单测(Linux 全绿)
| 套件 | 用例数 |
|---|---|
historyUtils(buildThinkingFullTextMap) |
13 ✅ |
keyMatchers(重命名后的 TOGGLE_THINKING_EXPANDED,Alt+T) |
46 ✅ |
HistoryItemDisplay |
25 ✅(原来 24;+1 展开态测试) |
ConversationMessages |
10 ✅ |
measure-element-position |
4 ✅ |
#5354 回归修复仍然完好
两处 expanded={resolvedThoughtExpanded} 与我上一轮 A/B 验证过的完全一致(把它们改回 expanded={!compactMode} 会复现"始终展开"的回归)。现在它们被两个单测共同锁定 —— renders committed thinking collapsed by default(默认 → alt+t to expand)和新增的 renders committed thinking expanded when ThoughtExpandedProvider is true(→ alt+t to collapse + 显示推理)—— 两个状态都被钉死。
说明
- 在 Linux 上验证(PR 表格:macOS ✅、Linux
⚠️ )。核心路径(默认折叠 + 内联 Alt+T 切换 + fix(cli): show thinking in full transcript mode #5354 修复)在新 HEAD 上端到端再次确认。 - 全屏 overlay(
ThinkingViewer/AlternateScreen,含新的disabled属性 + SGR 鼠标路由)是通过openThinkingViewer上下文打开而非某个按键,所以正常会话里无法用按键触达;其逻辑由measure-element-position和组件单测覆盖。
未发现新问题 —— R4 的改动干净、重命名准确,核心行为与回归修复都完好。LGTM 🚀
CI failure investigation — one real unit-test failure (stale assertion), not an environment issueOnly Failing testRoot causeThis PR adds a new option to the Ink const useVP = settings.merged.ui?.useTerminalBuffer ?? false;
const instance = render(
...
exitOnCtrlC: false,
isScreenReaderEnabled: config.getScreenReader(),
+ alternateScreen: useVP,
},But the assertion in // packages/cli/src/gemini.test.tsx:1247
expect(options).toEqual({
exitOnCtrlC: false,
isScreenReaderEnabled: false,
});
FixAdd the new key to the expected object. In the test environment expect(options).toEqual({
exitOnCtrlC: false,
isScreenReaderEnabled: false,
alternateScreen: false,
});NoteThis failure is deterministic and platform-independent — the still-pending macOS / Windows 中文版CI 失败调查 —— 一个真实的单元测试失败(断言过期),非环境问题只有 失败的测试根因本 PR 给 const useVP = settings.merged.ui?.useTerminalBuffer ?? false;
const instance = render(
...
exitOnCtrlC: false,
isScreenReaderEnabled: config.getScreenReader(),
+ alternateScreen: useVP,
},但 // packages/cli/src/gemini.test.tsx:1247
expect(options).toEqual({
exitOnCtrlC: false,
isScreenReaderEnabled: false,
});
修复在期望对象里补上新 key。测试环境下 expect(options).toEqual({
exitOnCtrlC: false,
isScreenReaderEnabled: false,
alternateScreen: false,
});说明这个失败是确定性的、与平台无关——仍在 pending 的 macOS / Windows |
Generated with AI Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
wenshao
left a comment
There was a problem hiding this comment.
R5 — 0 Critical, 3 Suggestions + test coverage gaps
Deterministic analysis clean (tsc=0, eslint=0). Build passed, 98/98 tests green.
All R1–R4 Criticals resolved. Three new optional-polish items below, plus test coverage gaps (SGR mouse buffer in KeypressContext has zero tests, formatDuration in displayUtils.ts has no unit test).
— qwen3.7-max via Qwen Code /review
|
|
||
| const { compactMode } = useCompactMode(); | ||
| const contextThoughtExpanded = useThoughtExpanded(); | ||
| const resolvedThoughtExpanded = thoughtExpanded ?? contextThoughtExpanded; |
There was a problem hiding this comment.
[Suggestion] useThoughtExpanded() is called here in the main HistoryItemDisplayComponent, so every visible history item — user messages, tool groups, gemini content, etc. — subscribes to ThoughtExpandedContext. When Alt+T toggles the boolean, all items re-render, not just thinking blocks. React context changes bypass React.memo, so the memo() on VirtualHistoryItem in the VP path doesn't help.
For a session with 200 items where only 5 are thinking blocks, 195 items re-render unnecessarily on each Alt+T press.
Fix: Move useThoughtExpanded() into the children that actually use it — ClickableThinkMessage and ThinkMessageContent — so only thinking-specific components subscribe to the context.
— qwen3.7-max via Qwen Code /review
|
|
||
| // Alt+T: toggle inline expansion of thinking blocks. | ||
| if (keyMatchers[Command.TOGGLE_THINKING_EXPANDED](key)) { | ||
| setThoughtExpanded((prev) => !prev); |
There was a problem hiding this comment.
[Suggestion] refreshStatic() clears the terminal and bumps historyRemountKey, forcing <Static> to fully remount all items. In VP mode (useTerminalBuffer=true), the render path uses ScrollableList (a regular React component, not Ink's append-only <Static>), so the context change propagates naturally without any terminal clear or remount. The refreshStatic() call here is pure overhead in VP mode.
| setThoughtExpanded((prev) => !prev); | |
| if (keyMatchers[Command.TOGGLE_THINKING_EXPANDED](key)) { | |
| setThoughtExpanded((prev) => !prev); | |
| if (!useTerminalBuffer) { | |
| refreshStatic(); | |
| } | |
| return; | |
| } |
— qwen3.7-max via Qwen Code /review
| // Ctrl+C / Ctrl+D close the viewer and fall through to quit/exit. | ||
| if (thinkingViewerData) { | ||
| if (keyMatchers[Command.QUIT](key) || keyMatchers[Command.EXIT](key)) { | ||
| closeThinkingViewer(); |
There was a problem hiding this comment.
[Suggestion] After closeThinkingViewer(), execution falls through (no return) into the normal Ctrl+C quit handler below, which arms the "press Ctrl+C again to quit" timer. The result: first Ctrl+C closes the overlay AND silently starts the quit countdown. A reflexive second Ctrl+C — natural when dismissing an overlay — exits the app.
Consider adding return after closeThinkingViewer() so Ctrl+C only closes the viewer. Users who want to quit can press Ctrl+C again after the overlay is gone.
| closeThinkingViewer(); | |
| if (thinkingViewerData) { | |
| if (keyMatchers[Command.QUIT](key) || keyMatchers[Command.EXIT](key)) { | |
| closeThinkingViewer(); | |
| return; | |
| } else { | |
| return; | |
| } | |
| } |
— qwen3.7-max via Qwen Code /review
|
@qwen-code /triage |
Re-verification (HEAD
|
Design revisions per PR review: - correct ink version (qwen uses upstream ink ^7.0.3; gemini-cli uses a fork @jrichman/ink@6.6.9 — not the same package) - reuse the existing AlternateScreen component (PR QwenLM#5627) with its disabled prop for VP mode instead of hand-rolling escapes - handle VP mode (useTerminalBuffer) already owning the alt screen - extend transcript anti-deadlock auto-close to ALL blocking confirmation dialogs, not just WaitingForConfirmation - guard refreshStatic and the message-queue drain while transcript is open - transcript close keys must be the very first handleGlobalKeypress branch (before QUIT/Ctrl+C and the vim INSERT guard) - single source of truth for isTranscriptOpen (AppContainer useState, surfaced via UIStateContext) - larger/adaptive estimatedItemHeight for the fullDetail transcript list - new section on coexistence with main's per-block thinking (Alt+T / ThinkingViewer) — they are complementary, not conflicting Code: keep `ui.compactMode` in WEB_SHELL_SETTINGS so the web shell's own independent compact feature is not silently broken by the CLI removal. Generated with AI Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
What this PR does
Adds thinking block display improvements in two layers:
Layer 1 — Regression fix + Alt+T expand/collapse (core)
expanded={!compactMode}evaluates totruewhencompactModedefaults tofalse. Changed toexpanded={false}so thinking blocks are always collapsed after completion.💡(emoji-capable UTF-8, non-CI) →⟡(UTF-8, CI or non-emoji) → empty string (non-UTF-8).thinkingFullTextByItemaggregation loop intobuildThinkingFullTextMap()inhistoryUtils.tswith unit tests.Layer 2 — Overlay viewer via mouse click + AlternateScreen (supplementary)
AlternateScreen.tsx: DEC 1049 alternate screen buffer wrapper withprocess.on('exit')safety guard.ThinkingViewer.tsx: full-screen scrollable viewer with keyboard navigation (↑↓, PgUp/PgDn, Ctrl+Home/End) and mouse wheel.ThinkingViewerContext.tsx:openThinkingViewerReact context.ClickableThinkMessageinHistoryItemDisplay.tsx: per-itemuseMouseEvents+measureElementPositionhit-test.55ef6518): enables alternate screen mouse scroll support in virtual-scroll mode.Why it's needed
After PR #5354 landed thinking block support, the blocks were always shown expanded — cluttering the conversation history with potentially long reasoning text. Users need a way to collapse completed thinking blocks by default and expand them on demand to inspect the model's reasoning process.
GIF
Reviewer Test Plan
How to verify
reasoning_contentin the streaming response).(alt+t to expand)↔(alt+t to collapse).Evidence (Before & After)
Before: thinking blocks always expanded after completion, no way to collapse.
After: thinking blocks collapsed by default, Alt+T toggles inline expansion.
Tested on
Environment (optional)
Local runtime:
npm run devRisk & Scope
<Static>render path (Ink architectural limitation).ThinkingViewerwrap="truncate-end"silently cuts wide content (follow-up).THINKING_ICONplacement could move to a terminal-capabilities utility (follow-up).Linked Issues
Fixes the thinking block display regression introduced by #5354.
中文说明
思考块显示改进分两层:
第一层 — 回归修复 + Alt+T 展开/折叠(核心)
expanded={!compactMode}在compactMode默认为false时始终展开。改为expanded={false}使完成后的思考块始终折叠。💡(支持 emoji 的 UTF-8 非 CI)→⟡(UTF-8 CI 或不支持 emoji)→ 空字符串(非 UTF-8)。historyUtils.ts中的buildThinkingFullTextMap(),并添加单元测试。第二层 — 鼠标点击 + AlternateScreen Overlay 查看器(补充功能)
AlternateScreen.tsx:DEC 1049 alternate screen buffer 封装,含process.on('exit')安全守卫。ThinkingViewer.tsx:全屏滚动查看器,支持键盘导航和鼠标滚轮。ClickableThinkMessage:基于useMouseEvents+measureElementPosition的点击交互。55ef6518):在虚拟滚动模式下启用 alternate screen 鼠标滚轮支持。风险:Alt+T 全局展开/折叠所有思考块,逐块切换需额外状态跟踪(后续跟进)。第二层的点击功能在 Static 模式下不可用(Ink 架构限制)。