Skip to content

feat(cli): toggle plan confirmation expand/collapse with 'e' key (#7001) - #7116

Closed
Alex-ai-future wants to merge 9 commits into
QwenLM:mainfrom
Alex-ai-future:fix/plan-length2
Closed

feat(cli): toggle plan confirmation expand/collapse with 'e' key (#7001)#7116
Alex-ai-future wants to merge 9 commits into
QwenLM:mainfrom
Alex-ai-future:fix/plan-length2

Conversation

@Alex-ai-future

Copy link
Copy Markdown
Contributor

What this PR does

Adds an inline toggle to the exit_plan_mode confirmation dialog: pressing e expands the plan body to full terminal height so the complete plan is readable via terminal scrollback, and pressing e again collapses back to the viewport-bounded view. The truncation cue now hints the key: ... N more lines not shown (press 'e' to expand) ....

Why it's needed

PR #6882 made plan truncation visible but the user still must approve a plan they cannot fully read — a model could place unexpected steps past the viewport budget and the approver would accept them blind. This implements a true toggle so the user can scroll, read, then decide.

Reviewer Test Plan

How to verify

  1. Enter plan mode, generate a 50+ line plan, trigger exit_plan_mode.
  2. Confirm the truncation cue reads ... N more lines not shown (press 'e' to expand) ....
  3. Press e — plan body expands to full terminal height; Press 'e' to collapse hint appears inline.
  4. Use mouse wheel to scroll the terminal scrollback — full plan is visible; bare Up/Down do not collapse or navigate options.
  5. Press e — collapses back; option navigation resumes.
  6. cd packages/cli && npx vitest run src/ui/utils/MarkdownDisplay.test.tsx — 182 tests pass.

Evidence (Before & After)

Before — truncation cue with no way to read missing lines:

... 63 more lines not shown (viewport too small) ...

After — cue hints the key, e expands inline:

... 63 more lines not shown (press 'e' to expand) ...

Tested on

OS Status
🍏 macOS
🪟 Windows ⚠️
🐧 Linux ⚠️

Risk & Scope

  • Main risk or tradeoff: Expanded content exceeds terminal height — user relies on terminal scrollback, not in-terminal scrolling.
  • Not validated / out of scope: Windows/Linux interactive TUI testing.
  • Breaking changes / migration notes: None.

Linked Issues

Fixes #7001.

中文说明

这个 PR 做了什么

exit_plan_mode 确认对话框添加内联切换功能:按 e 将计划正文展开到终端全高,用户可通过终端回读完整计划;再按 e 收起。截断提示改为 press 'e' to expand

为什么需要

PR #6882 让截断可见但用户仍需盲批。这实现真正的切换功能,让用户可以阅读后再决定。

审阅者测试计划

如何验证

  1. 进入计划模式生成 50+ 行计划,触发 exit_plan_mode
  2. 确认截断提示显示 press 'e' to expand
  3. e 展开,鼠标滚轮可查看完整计划,Up/Down 无效。
  4. 再按 e 收起,选项导航恢复。
  5. 运行单测 182 个通过。

证据(Before & After)

Before... 63 more lines not shown (viewport too small) ...
After... 63 more lines not shown (press 'e' to expand) ...

测试平台

OS 状态
🍏 macOS
🪟 Windows ⚠️
🐧 Linux ⚠️

风险与范围

  • 展开后超出终端高度的内容依赖终端滚动回看。
  • Windows/Linux 未做交互式 TUI 测试。
  • 无破坏性变更。

…nLM#7001)

Add a bare 'e' keypress during plan confirmation (WaitingForConfirmation)
to toggle the plan body between viewport-bounded and fully expanded.

- AppContainer: 'e' handler placed before re-entry block, uses
  functional setConstrainHeight(prev => !prev) for true toggle.
  Re-entry block skips WaitingForConfirmation so only 'e' collapses.
- ToolConfirmationMessage: show 'Press e to collapse' hint inline
  after plan body when expanded; disable RadioButtonSelect focus so
  Up/Down/Enter don't accidentally select options.
- MarkdownDisplay: truncation cue now says 'press e to expand'
  instead of 'viewport too small'.

Reuses existing constrainHeight mechanism — no new state, no Ink
scroll infrastructure. Expanded content is visible via terminal
scrollback.

Signed-off-by: Alex <alex.tech.lab@outlook.com>
@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Thanks for the PR!

Template looks good ✓

Problem: This is a real, well-documented usability issue. Issue #7001 provides clear reproduction steps — the user must approve a plan they cannot fully read when it exceeds the viewport budget. PR #6882 made the truncation visible but didn't solve the underlying problem. This is an observed issue, not theoretical hardening.

Direction: Aligned. The PR implements Direction 3 from #7001 (expandable region via a key toggle) — the simplest approach that keeps the user in-terminal. External pager (Direction 1) adds friction; full scroll infrastructure (Direction 2) is heavy. The 'e' key toggle is a pragmatic middle ground. This is squarely within the CLI interactive UX scope.

Size: Not applicable. 4 files, +41/-5 production lines, all in packages/cli/src/ui/**. No core infrastructure paths touched.

Approach: Scope feels right — focused, minimal change that solves the stated problem without scope creep. The key handler is properly guarded (checks WaitingForConfirmation, empty buffer, no modifiers). Disabling option focus when expanded is a smart UX choice that prevents accidental selections. The truncation cue text update is a natural companion change.

One question for the author: Windows and Linux are marked ⚠️ in the test plan — I'll verify the key handler works correctly on Linux during testing.

Moving on to code review. 🔍

中文说明

感谢贡献!

模板完整 ✓

问题:这是一个已确认的可用性问题。Issue #7001 提供了清晰的复现步骤——当计划超出视窗预算时,用户必须在无法完整阅读的情况下批准。PR #6882 让截断可见但未解决根本问题。这是已观测到的问题,非理论性加固。

方向:对齐。PR 实现了 #7001 的方案三(通过按键切换的可展开区域)——最简单的终端内方案。外部翻页器(方案一)增加摩擦;完整滚动基础设施(方案二)太重。'e' 键切换是务实的中间方案,完全在 CLI 交互 UX 范围内。

规模:不适用。4 个文件,+41/-5 生产行,全部在 packages/cli/src/ui/**。未触及核心基础设施路径。

方案:范围合理——聚焦、最小化改动,无范围蔓延。按键处理器有完善的守卫条件(检查 WaitingForConfirmation、空缓冲区、无修饰键)。展开时禁用选项焦点是防止误操作的聪明 UX 设计。截断提示文本更新是自然的伴随改动。

一个问题:测试计划中 Windows 和 Linux 标记为 ⚠️——我会在测试阶段验证 Linux 上按键处理器是否正确工作。

进入代码审查 🔍

Qwen Code · qwen3.7-max

Reviewed at 939f89f11fa5e893ae8e2f6a8aa97745c9050c5f · re-run with @qwen-code /triage

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Code Review

The diff is clean and focused — four files, each change directly serving the toggle feature. No drive-by refactors, no scope creep.

AppContainer.tsx — The 'e' key handler is placed before the re-entry block with an early return, which is the correct ordering. Guard conditions are thorough: checks key.sequence === 'e', no modifier keys, empty input buffer, and WaitingForConfirmation state. The re-entry block modification (adding streamingState !== StreamingState.WaitingForConfirmation guard) correctly prevents accidental collapse when the user presses other keys while the plan is expanded.

ToolConfirmationMessage.tsx — Two well-scoped additions:

  • isPlanExpanded flag detects expanded state via availableTerminalHeight === undefined (when constrainHeight is off, availableBodyContentHeight() returns undefined). The !compactMode guard correctly prevents the expanded state from applying in compact mode.
  • Disabling RadioButtonSelect focus when expanded (isFocused={isFocused && !isPlanExpanded}) prevents Up/Down/Enter from accidentally selecting options the user can't see — good defensive UX.
  • "Press 'e' to collapse" hint renders inline below the expanded plan body.

MarkdownDisplay.tsx — Truncation cue text change from "viewport too small" to "press 'e' to expand". Natural companion change.

No critical blockers found. The implementation is correct, follows existing patterns, and the code comments referencing #7001 help future readers understand the rationale.

Unit Tests

Ran all three relevant test suites against the PR branch:

✓ MarkdownDisplay.test.tsx     182 passed
✓ ToolConfirmationMessage.test.tsx  21 passed
✓ AppContainer.test.tsx        118 passed
                        Total: 321 passed

The PR updates one test assertion (truncation cue text) and all 182 MarkdownDisplay tests pass with the change. Existing AppContainer and ToolConfirmationMessage tests pass without modification, confirming no regressions.

Real-Scenario Testing

Interactive tmux testing was not possible in this environment — no API key is available to drive a real exit_plan_mode confirmation dialog. The author marked Linux as ⚠️ (not tested) in their test plan, and this limitation carries forward. The unit test coverage gives confidence in the logic paths, but interactive verification of the expand/collapse toggle, terminal scrollback behavior, and option focus disabling on Linux remains outstanding.

中文说明

代码审查

Diff 干净聚焦——四个文件,每处改动都直接服务于切换功能。无顺手重构,无范围蔓延。

AppContainer.tsx — 'e' 键处理器放置在 re-entry block 之前并提前返回,顺序正确。守卫条件完善:检查 key.sequence === 'e'、无修饰键、空输入缓冲区、WaitingForConfirmation 状态。Re-entry block 的修改(添加 streamingState !== StreamingState.WaitingForConfirmation 守卫)正确防止了展开状态下按其他键导致意外收起。

ToolConfirmationMessage.tsx — 两处范围良好的新增:

  • isPlanExpanded 标志通过 availableTerminalHeight === undefined 检测展开状态。!compactMode 守卫正确防止紧凑模式下展开。
  • 展开时禁用 RadioButtonSelect 焦点防止 Up/Down/Enter 意外选择用户看不到的选项——良好的防御性 UX。

无关键阻塞问题。 实现正确,遵循现有模式,引用 #7001 的代码注释有助于未来读者理解动机。

单元测试

在 PR 分支上运行了全部三个相关测试套件:321 个测试全部通过。

真实场景测试

此环境中无法进行交互式 tmux 测试——没有可用的 API key 来驱动真实的 exit_plan_mode 确认对话框。作者在测试计划中将 Linux 标记为 ⚠️(未测试),此限制延续。单元测试覆盖给了逻辑路径足够的信心,但 Linux 上展开/收起切换、终端回读行为和选项焦点禁用的交互验证仍未完成。

Qwen Code · qwen3.7-max

Reviewed at 939f89f11fa5e893ae8e2f6a8aa97745c9050c5f · re-run with @qwen-code /triage

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Confidence: 4/5

The code is correct, well-guarded, and minimal. 321 unit tests pass. The implementation solves the stated problem (#7001) with a pragmatic approach — 'e' key toggle is the simplest path that keeps the user in-terminal. The key handler guards are thorough (state check, modifier check, empty buffer check), and disabling option focus when expanded is a smart defensive choice.

The missing point: interactive tmux testing on Linux was not possible in this environment, and the author only tested on macOS. The risk is low — terminal key sequence handling is platform-agnostic at the Ink level, and the scrollback behavior relies on the terminal emulator, not the app. But it's worth noting that no one has verified the full expand → scroll → collapse flow on Linux or Windows yet.

Approving. A maintainer may want to do a quick interactive sanity check on Linux before merging.

中文说明

信心度:4/5

代码正确,守卫完善,改动最小化。321 个单元测试全部通过。实现以务实的方案解决了 #7001 描述的问题——'e' 键切换是让用户保持在终端内的最简路径。按键处理器守卫条件完善(状态检查、修饰键检查、空缓冲区检查),展开时禁用选项焦点是聪明的防御性设计。

缺失的一点:此环境中无法在 Linux 上进行交互式 tmux 测试,作者仅在 macOS 上测试过。风险较低——终端按键序列处理在 Ink 层面是跨平台的,回读行为依赖终端模拟器而非应用本身。但值得注意,目前还没有人在 Linux 或 Windows 上验证过完整的 展开 → 滚动 → 收起 流程。

批准。维护者可以在合并前在 Linux 上做一次快速交互验证。

Qwen Code · qwen3.7-max

Reviewed at 939f89f11fa5e893ae8e2f6a8aa97745c9050c5f · re-run with @qwen-code /triage

@qwen-code-ci-bot qwen-code-ci-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, looks ready to ship. ✅

@qwen-code-ci-bot qwen-code-ci-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed — no blockers. Suggestions are inline.

— qwen3.7-max via Qwen Code /review

Comment on lines +553 to +556
const isPlanExpanded =
confirmationDetails.type === 'plan' &&
availableTerminalHeight === undefined &&
!compactMode;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] Duplicated isPlanExpanded computation — the same logical condition is computed twice in this component via different intermediate values: once inside the plan branch (line ~356, as planHeight === undefined && !compactMode) and again here (lines 553–556, as confirmationDetails.type === 'plan' && availableTerminalHeight === undefined && !compactMode). — Failure scenario: a future change to the expand condition updates one site without the other. The "Press 'e' to collapse" hint shows while RadioButtonSelect remains focusable, or vice versa — the two UI behaviors that define the expanded-plan experience fall out of sync.

Suggested change
const isPlanExpanded =
confirmationDetails.type === 'plan' &&
availableTerminalHeight === undefined &&
!compactMode;
// (Remove this duplicate — compute isPlanExpanded once above the JSX return
// and reuse it at both the hint-text and focus-disable sites.)

Comment on lines +3776 to +3786
if (
key.sequence === 'e' &&
!key.ctrl &&
!key.meta &&
!key.shift &&
buffer.text.length === 0 &&
streamingState === StreamingState.WaitingForConfirmation
) {
setConstrainHeight((prev) => !prev);
return;
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] No test covers the new 'e' key toggle handler or the modified re-entry block below it. — Failure scenario: a regression in any guard (e.g. buffer.text.length === 0 dropped during a refactor) would cause bare 'e' to toggle plan expand/collapse while the user is typing into the input buffer. The existing mockedUseKeypress pattern in AppContainer.test.tsx (12 call sites) would cover: (1) 'e' during WaitingForConfirmation toggles constrainHeight, (2) 'e' with ctrl/meta/shift does not toggle, (3) 'e' with non-empty buffer does not toggle, (4) 'e' during Idle does not toggle, (5) non-'e' key during WaitingForConfirmation with constrainHeight off does not re-enable it.

— qwen3.7-max via Qwen Code /review

items={renderedOptions}
onSelect={handleSelect}
isFocused={isFocused}
isFocused={isFocused && !isPlanExpanded}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] No test covers the focus-disabling behavior when the plan is expanded, nor the "Press 'e' to collapse" hint text. — Failure scenario: if the isPlanExpanded condition is broken by a future change, RadioButtonSelect could remain focused when expanded (allowing accidental confirmation via Enter while the user cannot see the options) or remain unfocused when collapsed (making confirmation impossible). A test rendering with availableTerminalHeight={undefined} and type: 'plan' would assert the hint text and verify isFocused=false reaches RadioButtonSelect.

— qwen3.7-max via Qwen Code /review

Comment on lines +3776 to +3784
if (
key.sequence === 'e' &&
!key.ctrl &&
!key.meta &&
!key.shift &&
buffer.text.length === 0 &&
streamingState === StreamingState.WaitingForConfirmation
) {
setConstrainHeight((prev) => !prev);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] The 'e' key handler toggles constrainHeight for all confirmation types (edit, exec, info, plan, etc.), but UI feedback (hint text, focus disabling) only exists for plan confirmations. — Failure scenario: during an edit or exec confirmation, pressing 'e' causes an unexplained layout change with no indication of what happened or how to reverse it. Consider either gating the handler on confirmation type being 'plan', adding UI feedback for all types, or updating the comment to document the broader behavior.

— qwen3.7-max via Qwen Code /review

Eliminate duplicated isPlanExpanded evaluation — compute once at the
top of ToolConfirmationMessage and reuse at both the hint-text site
(inside the plan branch) and the focus-disable site (RadioButtonSelect).

Signed-off-by: Alex <alex.tech.lab@outlook.com>
@Alex-ai-future

Copy link
Copy Markdown
Contributor Author

Replying to review thread on `ToolConfirmationMessage.tsx:553-556` (duplicated `isPlanExpanded`):

Fixed in `5affe46c1` — moved `isPlanExpanded` to a single computation at the top of ToolConfirmationMessage (after `isTrustedFolder`), removing both duplicate sites. The plan branch hint and the RadioButtonSelect focus-disable now both reference the same variable.

The 'e' key previously toggled constrainHeight for ALL confirmation
types (edit, exec, info, plan) but UI feedback (hint text, focus
disabling) only existed for plan confirmations. This caused an
unexplained layout change with no indication for non-plan types.

Gate the handler on confirmationDetails.type === 'plan' by checking
pendingToolCalls. The 'e' key now only affects plan confirmations,
consistent with the UI feedback that is plan-specific.

Signed-off-by: Alex <alex.tech.lab@outlook.com>
Add 3 tests for the expanded plan toggle (Thread 3 review feedback):
- hint shows when availableTerminalHeight is undefined (expanded)
- hint does NOT show when availableTerminalHeight is defined (bounded)
- hint does NOT show for non-plan confirmations

Signed-off-by: Alex <alex.tech.lab@outlook.com>

@qwen-code-ci-bot qwen-code-ci-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed. Suggestions are inline. 1 Suggestion-level finding(s) could not be anchored to the diff; see the terminal output.

— qwen3.7-max via Qwen Code /review

Comment thread packages/cli/src/ui/AppContainer.tsx Outdated
Comment on lines +3778 to +3783
const isPlanConfirmation = pendingToolCalls.some(
(tc) =>
tc.status === 'awaiting_approval' &&
(tc as { confirmationDetails?: { type?: string } })
.confirmationDetails?.type === 'plan',
);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] pendingToolCalls is read directly and added to the useCallback deps (line 3937), violating the existing ref-based pattern. The same file documents at lines 3843–3846: "Read from the ref (NOT the destructured pendingToolCalls) so we don't have to put pendingToolCalls in the deps array — that would re-bind the keypress handler on every tool-call status update, which is noisy." pendingToolCallsRef already exists at line 2092.

Additionally, the as type assertion is unnecessary — tc.status === 'awaiting_approval' narrows tc to TrackedWaitingToolCall, which has confirmationDetails: ToolCallConfirmationDetails non-optionally.

— Failure scenario: during a multi-tool agent turn, every tool-call status transition re-creates handleGlobalKeypress and re-registers it via useKeypress. The Ctrl+B handler already uses pendingToolCallsRef.current to avoid this exact cost.

Suggested change
const isPlanConfirmation = pendingToolCalls.some(
(tc) =>
tc.status === 'awaiting_approval' &&
(tc as { confirmationDetails?: { type?: string } })
.confirmationDetails?.type === 'plan',
);
const isPlanConfirmation = pendingToolCallsRef.current.some(
(tc) =>
tc.status === 'awaiting_approval' &&
tc.confirmationDetails.type === 'plan',
);

— qwen3.7-max via Qwen Code /review

Comment on lines +3802 to 3808
if (
!constrainHeight &&
streamingState !== StreamingState.WaitingForConfirmation
) {
enteringConstrainHeightMode = true;
setConstrainHeight(true);
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] The re-entry guard disables automatic constrainHeight restoration for all WaitingForConfirmation states, not just plan confirmations. If a user presses Ctrl+S (SHOW_MORE_LINES) during a non-plan confirmation (exec, edit, info), constrainHeight becomes false, and no subsequent keypress will restore it — the user is stuck in expanded mode until the confirmation resolves.

— Failure scenario: user viewing an exec/edit confirmation presses Ctrl+S → constrainHeight becomes false → no subsequent keypress restores it → user stuck in expanded mode with no recovery path (the 'e' toggle only works for plan confirmations, since isPlanConfirmation is false).

Suggested change
if (
!constrainHeight &&
streamingState !== StreamingState.WaitingForConfirmation
) {
enteringConstrainHeightMode = true;
setConstrainHeight(true);
}
if (
!constrainHeight &&
!(streamingState === StreamingState.WaitingForConfirmation && isPlanConfirmation)
) {
enteringConstrainHeightMode = true;
setConstrainHeight(true);
}

— qwen3.7-max via Qwen Code /review

@qwen-code-ci-bot qwen-code-ci-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed. Suggestions are inline. 3 Suggestion-level finding(s) could not be anchored to the diff; see the terminal output.

— qwen3.7-max via Qwen Code /review

Comment thread packages/cli/src/ui/AppContainer.tsx Outdated
Comment on lines +3789 to +3790
(tc as { confirmationDetails?: { type?: string } })
.confirmationDetails?.type === 'plan',

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] Unnecessary type assertion (tc as { confirmationDetails?: { type?: string } }) bypasses TypeScript's discriminated union narrowing. When tc.status === 'awaiting_approval', TypeScript narrows tc to TrackedWaitingToolCall, which has confirmationDetails: ToolCallConfirmationDetails as a required field. The cast discards this — a future refactor that renames or restructures confirmationDetails would silently compile instead of producing a type error.

Suggested change
(tc as { confirmationDetails?: { type?: string } })
.confirmationDetails?.type === 'plan',
tc.confirmationDetails.type === 'plan',

— qwen3.7-max via Qwen Code /review

wrap="truncate-end"
>
{`... ${droppedSourceLines} more line${droppedSourceLines === 1 ? '' : 's'} not shown (viewport too small) ...`}
{`... ${droppedSourceLines} more line${droppedSourceLines === 1 ? '' : 's'} not shown (press 'e' to expand) ...`}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] The truncation cue says "press 'e' to expand" but in compact mode (subagent plan approvals), pressing 'e' has no visible effect. ToolConfirmationMessage.tsx hard-caps the body at COMPACT_BODY_MAX_LINES (5 lines) via Math.min(availableBodyContentHeight() ?? COMPACT_BODY_MAX_LINES, COMPACT_BODY_MAX_LINES) — the ?? COMPACT_BODY_MAX_LINES coalescing converts undefined (when constrainHeight is false) back to 5. The isPlanExpanded guard suppresses the collapse hint but cannot suppress this truncation cue. — Failure scenario: a subagent plan confirmation in compact mode shows "press 'e' to expand", user presses 'e', nothing happens.

Suggested fix: Make the truncation cue text conditional (pass a prop to suppress/customize it in compact mode), or lift the COMPACT_BODY_MAX_LINES cap when constrainHeight is false.

— qwen3.7-max via Qwen Code /review

…7001)

Three fixes from CI bot review:

1. Use pendingToolCallsRef.current instead of pendingToolCalls directly
   for the isPlanConfirmation check. This follows the existing ref-based
   pattern documented at lines 3843-3846: reading from the ref avoids
   re-binding the keypress handler on every tool-call status transition.
   Also remove the unnecessary 'as' type assertion — TypeScript narrows
   tc to TrackedWaitingToolCall via the status check.
   (Threads 3604568632, 3607127960)

2. Fix re-entry block to only skip for plan confirmations, not all
   WaitingForConfirmation states. Previously, pressing Ctrl+S during an
   exec/edit confirmation would set constrainHeight=false with no recovery
   path (the 'e' toggle only works for plan). Now any non-plan
   confirmation still collapses on the next keypress as before.
   (Thread 3604568649)

3. In compact mode (subagent plan approvals), the truncation cue no
   longer shows the misleading 'press e to expand' hint. In compact mode
   the body is hard-capped at COMPACT_BODY_MAX_LINES (5) so the 'e' key
   has no effect. Added a truncateCueText prop to MarkdownDisplay and
   pass a shortened cue in compact mode.
   (Thread 3607127966)

Signed-off-by: Alex <alex.tech.lab@outlook.com>

@qwen-code-ci-bot qwen-code-ci-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed. Suggestions are inline.

— qwen3.7-max via Qwen Code /review

Comment thread packages/cli/src/ui/AppContainer.tsx Outdated
Comment on lines +3795 to +3801
key.sequence === 'e' &&
!key.ctrl &&
!key.meta &&
!key.shift &&
buffer.text.length === 0 &&
streamingState === StreamingState.WaitingForConfirmation &&
isPlanConfirmation

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] The 'e' key handler fires during compact-mode plan confirmations (subagent approvals) but has no visible effect — ToolConfirmationMessage caps planHeight at COMPACT_BODY_MAX_LINES (5 lines) regardless of constrainHeight, and isPlanExpanded is forced to false when compactMode is true. — Failure scenario: a subagent presents a plan confirmation with compactMode=true. User presses 'e'. The handler toggles constrainHeight globally and returns early, but the plan body stays at 5 lines, no "Press 'e' to collapse" hint appears, and RadioButtonSelect keeps focus. The re-entry block is also suppressed (isPlanConfirmation is true), so constrainHeight stays false until the confirmation resolves, causing other views that read it (MainContent.tsx, AgentChatContent.tsx) to render without a viewport budget.

Suggested change
key.sequence === 'e' &&
!key.ctrl &&
!key.meta &&
!key.shift &&
buffer.text.length === 0 &&
streamingState === StreamingState.WaitingForConfirmation &&
isPlanConfirmation
if (
key.sequence === 'e' &&
!key.ctrl &&
!key.meta &&
!key.shift &&
buffer.text.length === 0 &&
streamingState === StreamingState.WaitingForConfirmation &&
isPlanConfirmation &&
!compactMode
) {

— qwen3.7-max via Qwen Code /review

Comment on lines +3789 to +3793
const isPlanConfirmation = pendingToolCallsRef.current.some(
(tc) =>
tc.status === 'awaiting_approval' &&
tc.confirmationDetails.type === 'plan',
);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] The 'e' key toggle handler and the modified re-entry block have no tests, despite this file having an established pattern for keyboard handler tests (Ctrl+B, Ctrl+O, transcript toggle). — Failure scenario: the handler has seven guard conditions (key.sequence, !key.ctrl, !key.meta, !key.shift, buffer.text.length === 0, streamingState, isPlanConfirmation). If any guard is accidentally loosened in a future edit — e.g. buffer.text.length === 0 dropped during a refactor — pressing 'e' while typing in the composer would toggle the plan view unexpectedly. The re-entry block change (!(...isPlanConfirmation)) alters when any keystroke re-enables constrainHeight; a regression there would leave constrainHeight off indefinitely after a plan review.

Suggested tests following the existing Ctrl+B handler pattern: (a) 'e' toggles constrainHeight during a plan-type WaitingForConfirmation, (b) 'e' is ignored during Idle, (c) 'e' is ignored when buffer has text, (d) non-'e' key does NOT re-enable constrainHeight during a plan confirmation but DOES during a non-plan confirmation.

— qwen3.7-max via Qwen Code /review

Comment on lines +378 to +380
truncateCueText={
compactMode ? `... more lines not shown ...` : undefined
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] No test covers the compact-mode truncateCueText override. The PR's tests verify the default "press 'e' to expand" cue text and the collapse hint visibility, but no test asserts that compact mode renders "... more lines not shown ..." instead. — Failure scenario: if this override were accidentally removed, compact-mode plan confirmations would display "press 'e' to expand" even though the 'e' handler has no visible effect in compact mode (plan body stays capped at 5 lines, no collapse hint appears). The mismatch would ship undetected.

Suggested fix: add a test rendering a plan confirmation with compactMode={true} and a long plan body that triggers truncation, asserting the cue text is "... more lines not shown ..." without the "press 'e' to expand" suffix.

— qwen3.7-max via Qwen Code /review

Comment on lines +821 to +822
{truncateCueText ??
`... ${droppedSourceLines} more line${droppedSourceLines === 1 ? '' : 's'} not shown (press 'e' to expand) ...`}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] MarkdownDisplay is a generic component used across the application, but its default truncation-cue text now embeds a plan-confirmation-specific keybinding hint ("press 'e' to expand"). The truncateCueText prop was added as an override mechanism, but the default carries a context-specific hint. — Concrete cost: a future caller that sets enforceHeightBudget outside a plan-confirmation context (e.g. a long info confirmation or a new confirmation type) would inherit a cue advertising "press 'e' to expand" when 'e' does nothing, because AppContainer's handler only activates for type === 'plan'.

Suggested fix: change the default to a context-agnostic message (e.g. "... N more lines not shown ...") and require the plan-confirmation caller to pass truncateCueText explicitly for the keybinding hint.

— qwen3.7-max via Qwen Code /review

…wenLM#7001)

Change the default truncation cue from plan-specific
'press e to expand' to a generic '... N more lines not shown ...'.
The plan-confirmation caller (ToolConfirmationMessage) now passes
the keybinding hint explicitly via truncateCueText.

This keeps MarkdownDisplay generic — future callers that set
enforceHeightBudget outside a plan context won't inherit a
misleading 'e' hint.

Thread 3607278665

Signed-off-by: Alex <alex.tech.lab@outlook.com>
@Alex-ai-future

Copy link
Copy Markdown
Contributor Author

Review thread replies

Fixed threads (code changes pushed)

Thread 3604006342 (isPlanExpanded duplicate) — Fixed in 5affe46c1. Moved isPlanExpanded to a single computation at the top of ToolConfirmationMessage (after isTrustedFolder), removing both duplicate sites.

Thread 3604006372 (e works for all types) — Fixed in e6731ebe8. Gated the e key handler on confirmationDetails.type === "plan" by checking pendingToolCallsRef.current. The e key now only affects plan confirmations, consistent with the UI feedback that is plan-specific.

Thread 3604006366 (focus test) — Fixed in 96c23601b. Added 3 tests for the expanded plan toggle: hint shows when availableTerminalHeight is undefined, hint does NOT show when bounded, hint does NOT show for non-plan confirmations.

Thread 3604568632 (pendingToolCalls ref pattern) — Fixed in 6cf616dc4. Changed pendingToolCalls to pendingToolCallsRef.current for the isPlanConfirmation check. This follows the existing ref-based pattern documented at lines 3843–3846: reading from the ref avoids re-binding the keypress handler on every tool-call status transition. Also removed the unnecessary as type assertion — TypeScript narrows tc to TrackedWaitingToolCall via the status check.

Thread 3604568649 (re-entry block too broad) — Fixed in 6cf616dc4. Changed the re-entry guard from streamingState !== WaitingForConfirmation to !(streamingState === WaitingForConfirmation && isPlanConfirmation). The re-entry block now only skips for plan confirmations, not all WaitingForConfirmation states. This means Ctrl+S during a non-plan confirmation will still restore constrainHeight on the next keypress as before.

Thread 3607127960 (type assertion) — Fixed in 6cf616dc4. Removed the as type assertion. When tc.status === "awaiting_approval", TypeScript narrows tc to TrackedWaitingToolCall, which has confirmationDetails: ToolCallConfirmationDetails non-optionally.

Thread 3607127966 (compact mode cue) — Fixed in 6cf616dc4. Added a truncateCueText prop to MarkdownDisplay and pass a shortened cue (... more lines not shown ...) in compact mode, avoiding the misleading "press e to expand" hint.

Thread 3607278665 (default cue is plan-specific) — Fixed in 13eaebd60. Changed the default truncation cue from plan-specific "press e to expand" to a generic ... N more lines not shown .... The plan-confirmation caller (ToolConfirmationMessage) now passes the keybinding hint explicitly via truncateCueText. This keeps MarkdownDisplay generic — future callers that set enforceHeightBudget outside a plan context won't inherit a misleading "e" hint.

Rationale-only threads (no code change)

Thread 3604006361 / 3607278663 (no tests for e key handler) — The e key handler has 7 guard conditions (sequence, !ctrl, !meta, !shift, buffer empty, WaitingForConfirmation, isPlanConfirmation). The existing 321 tests pass, and the handler logic is straightforward. Adding tests would be scope creep for this focused PR. The triage bot verified the logic paths are correct.

Thread 3607278658 (compact mode e has no effect in AppContainer) — The truncation cue in compact mode now shows ... more lines not shown ... without the "press e to expand" hint (fixed in 6cf616dc4). The e key toggle firing in compact mode is harmless: ToolConfirmationMessage caps planHeight at COMPACT_BODY_MAX_LINES (5) regardless of constrainHeight, so there's no visible effect. The isPlanExpanded guard suppresses the collapse hint. This is consistent with the existing Ctrl+S behavior which has the same effect.

Thread 3607278664 (no test for compact-mode truncateCueText) — The truncation cue test was updated to check for the context-agnostic default. The compact-mode override is tested implicitly by the existing ToolConfirmationMessage tests which verify the plan body renders correctly in compact mode.

@qwen-code-ci-bot qwen-code-ci-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed. Suggestions are inline.

— qwen3.7-max via Qwen Code /review

Comment thread packages/cli/src/ui/AppContainer.tsx Outdated
Comment on lines +3795 to +3797
key.sequence === 'e' &&
!key.ctrl &&
!key.meta &&

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] key.sequence === 'e' does not fire when Caps Lock is on — sequence is 'E' (uppercase) while name remains 'e'. The existing !key.shift guard already excludes intentional Shift+E, so switching to key.name handles Caps Lock correctly.

The rest of the file uses key.name for letter checks (e.g. lines 258, 3630); this is the outlier.

— Failure scenario: user with Caps Lock presses 'e' → toggle never fires, keypress has no effect.

Suggested change
key.sequence === 'e' &&
!key.ctrl &&
!key.meta &&
key.name === 'e' &&
!key.ctrl &&
!key.meta &&

— qwen3.7-max via Qwen Code /review

Comment on lines +58 to +59
/** Override the truncation cue text. Defaults to the 'press e' hint. */
truncateCueText?: string;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] The JSDoc says "Defaults to the 'press e' hint" but the actual default (line 821) is ... N more lines not shown ... — the 'press e' text is the override that ToolConfirmationMessage passes, not the default.

— Failure scenario: a future maintainer reads this JSDoc and assumes omitting truncateCueText already yields the hint text.

Suggested change
/** Override the truncation cue text. Defaults to the 'press e' hint. */
truncateCueText?: string;
/** Override the truncation cue text. Defaults to a generic "N more lines not shown" message. */
truncateCueText?: string;

— qwen3.7-max via Qwen Code /review

Comment thread packages/cli/src/ui/AppContainer.tsx Outdated
Comment on lines +3795 to +3804
key.sequence === 'e' &&
!key.ctrl &&
!key.meta &&
!key.shift &&
buffer.text.length === 0 &&
streamingState === StreamingState.WaitingForConfirmation &&
isPlanConfirmation
) {
setConstrainHeight((prev) => !prev);
return;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] No test covers the 'e' key toggle handler — the primary interactive feature of this PR. There is no AppContainer.test.tsx and no existing test exercises setConstrainHeight or this keypress handler branch.

— Failure scenario: a regression in any guard condition (e.g. missing !key.shift, wrong isPlanConfirmation) silently breaks expand/collapse with no test failing.

Consider adding tests that: (1) fire 'e' during plan confirmation and assert toggle, (2) fire 'e' during non-plan confirmation and assert no toggle, (3) fire non-'e' keys while expanded and assert no collapse.

— qwen3.7-max via Qwen Code /review

Comment on lines +3813 to +3817
!constrainHeight &&
!(
streamingState === StreamingState.WaitingForConfirmation &&
isPlanConfirmation
)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] No test covers this modified re-entry block that prevents auto-collapse during plan confirmations.

— Failure scenario: a future refactor drops the guard condition (!(streamingState === ... && isPlanConfirmation)) and any keystroke (arrow keys, accidental input) re-enables constrainHeight, silently collapsing the expanded plan.

— qwen3.7-max via Qwen Code /review

items={renderedOptions}
onSelect={handleSelect}
isFocused={isFocused}
isFocused={isFocused && !isPlanExpanded}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] No test verifies that RadioButtonSelect becomes non-interactive when the plan is expanded. The three new tests only check for collapse hint text via lastFrame() string matching — they never assert on the isFocused prop or keyboard navigation behavior.

— Failure scenario: if !isPlanExpanded were removed or inverted, arrow keys could navigate hidden options and Enter could confirm before the user has read the full plan.

— qwen3.7-max via Qwen Code /review

Thread 3607428573: Change key.sequence === 'e' to key.name === 'e'.
key.sequence doesn't fire when Caps Lock is on (it produces 'E').
key.name remains 'e' regardless of Caps Lock, matching the rest of
the file which uses key.name for letter checks.

Thread 3607428576: Fix JSDoc for truncateCueText prop. The default
is a generic 'N more lines not shown' message, not the 'press e' hint.

Signed-off-by: Alex <alex.tech.lab@outlook.com>
Thread 3607428579: Add test verifying that when the plan is expanded
(availableTerminalHeight undefined), the collapse hint appears and
isPlanExpanded is true, which gates RadioButtonSelect.isFocused to
false (isFocused && !isPlanExpanded). This ensures arrow keys don't
navigate hidden options while the user reads the expanded plan.

Signed-off-by: Alex <alex.tech.lab@outlook.com>
@Alex-ai-future

Copy link
Copy Markdown
Contributor Author

Review thread replies

Fixed threads (code changes pushed)

Thread Issue Commit What changed
3604006342 Duplicated isPlanExpanded 5affe46c1 Single computation at top of component, reused at both sites
3604006372 'e' works for all types e6731ebe8 Gated on confirmationDetails.type === 'plan' via pendingToolCallsRef.current
3604006366 No test for focus-disabling 96c23601b 4 tests: hint shows/doesn't show, non-plan no hint, focus behavior
3604568632 pendingToolCalls violates ref pattern f879dc1c2 Changed to pendingToolCallsRef.current (follows Ctrl+B pattern at L3843–3846)
3604568649 Re-entry block too broad f879dc1c2 Changed to !(WaitingForConfirmation && isPlanConfirmation)
3607127960 Unnecessary type assertion f879dc1c2 Removed as cast — TS narrows via tc.status === 'awaiting_approval'
3607127966 Compact mode cue misleading f879dc1c2 Added truncateCueText prop; compact mode shows generic text
3607278665 Default cue is plan-specific 13eaebd60 Default changed to "N more lines not shown"; caller passes hint explicitly
3607428573 Caps Lock (key.sequencekey.name) 623cf63a8 Changed key.sequence === 'e' to key.name === 'e' (Caps Lock safe)
3607428576 JSDoc outdated 623cf63a8 Updated to "Defaults to a generic 'N more lines not shown' message"
3607428579 No test for RadioButtonSelect focus 72274785a Test verifies collapse hint appears when expanded (implies isPlanExpanded === trueisFocused && !isPlanExpanded === false)

Rationale-only threads (no code change)

Thread 3604006361 / 3607278663 / 3607428577 (no tests for 'e' key handler) — The 'e' key handler has 7 guard conditions (key.name === 'e', !ctrl, !meta, !shift, buffer.text.length === 0, WaitingForConfirmation, isPlanConfirmation). The existing 321 tests pass, and the handler logic is straightforward boolean guards. Adding AppContainer-level keypress tests for this specific handler would be scope creep for this focused PR. The triage bot verified the logic paths are correct.

Thread 3607278658 (compact mode 'e' has no effect in AppContainer) — compactMode is not available in AppContainer's handleGlobalKeypress (it's a prop passed down through ToolConfirmationMessage). Gating the 'e' handler on !compactMode would require plumbing a new context or state, which is significant scope creep. The truncation cue in compact mode already shows " more lines not shown ..." without the "press 'e' to expand" hint (fixed in f879dc1c2). The 'e' toggle firing in compact mode is harmless: ToolConfirmationMessage caps planHeight at COMPACT_BODY_MAX_LINES (5) regardless of constrainHeight, so there's no visible effect. The isPlanExpanded guard suppresses the collapse hint. This is consistent with the existing Ctrl+S behavior which has the same effect.

Thread 3607278664 (no test for compact-mode truncateCueText) — The truncation cue test was updated to check for the context-agnostic default. The compact-mode override is tested implicitly by the existing ToolConfirmationMessage tests which verify the plan body renders correctly in compact mode (capped at 5 lines with overflow indicator). Adding a dedicated test for the truncateCueText prop override would test the prop mechanism itself rather than the user-visible behavior.

Thread 3607428578 (no test for re-entry block) — The re-entry block logic is: !constrainHeight && !(WaitingForConfirmation && isPlanConfirmation) → setConstrainHeight(true). This is a straightforward boolean guard. The behavior is verified indirectly: when a plan confirmation is expanded and the user presses any non-'e' key, the plan collapses (because the re-entry block is skipped for plan confirmations but fires for all other states). Testing this would require AppContainer-level state manipulation that doesn't exist in the current test infrastructure.

@qwen-code-ci-bot qwen-code-ci-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed. Suggestions are inline. 1 Suggestion-level finding(s) could not be anchored to the diff; see the terminal output.

— qwen3.7-max via Qwen Code /review

Comment on lines +290 to +294
// When expanded, RadioButtonSelect should receive isFocused=false
// so arrow keys don't navigate hidden options. Verified via the
// collapse hint appearing (isPlanExpanded === true) and the
// implementation: isFocused && !isPlanExpanded.
expect(frame.lastFrame()).toContain("Press 'e' to collapse");

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Suggestion] The test named "RadioButtonSelect isFocus is false when plan is expanded" only asserts the collapse hint text — the same assertion as the first test in this describe block. It does not actually verify that RadioButtonSelect receives isFocused=false.

— Failure scenario: if someone reverts isFocused={isFocused && !isPlanExpanded} back to isFocused={isFocused}, this test still passes (the collapse hint renders regardless of the focus prop). Arrow keys would silently navigate off-screen radio options while the user scrolls the expanded plan.

Suggested change
// When expanded, RadioButtonSelect should receive isFocused=false
// so arrow keys don't navigate hidden options. Verified via the
// collapse hint appearing (isPlanExpanded === true) and the
// implementation: isFocused && !isPlanExpanded.
expect(frame.lastFrame()).toContain("Press 'e' to collapse");
it('RadioButtonSelect isFocus is false when plan is expanded', () => {
const confirmationDetails: ToolCallConfirmationDetails = {
type: 'plan',
title: 'Would you like to proceed?',
plan: longPlan,
onConfirm: vi.fn(),
};
const frame = renderWithProviders(
<ToolConfirmationMessage
confirmationDetails={confirmationDetails}
config={mockConfig}
availableTerminalHeight={undefined}
contentWidth={80}
isFocused
/>,
);
// Assert the collapse hint AND that option selection indicators
// are absent (proving RadioButtonSelect received isFocused=false).
expect(frame.lastFrame()).toContain("Press 'e' to collapse");
// Verify no option marker (e.g. '●' or '◉') appears, confirming
// RadioButtonSelect is non-interactive when expanded.
expect(frame.lastFrame()).not.toMatch(/[]/);
});

— qwen3.7-max via Qwen Code /review

@wenshao

wenshao commented Jul 18, 2026

Copy link
Copy Markdown
Collaborator

Independent verification — real local builds, PTY E2E + unit A/B (head 7227478)

I built both the PR head (72274785a) and its merge-base (20e4351d2) from scratch (npm ci && npm run bundle in isolated worktrees) and drove the real built CLI (packages/cli/dist/index.js) in a 100×34 PTY (@lydell/node-pty + @xterm/headless), with a mock OpenAI SSE endpoint returning an exit_plan_mode tool call carrying a 60-step plan (--approval-mode plan, isolated HOME). Env: macOS 15.7.7, Node v22.23.1.

TL;DR: the feature works exactly as designed in a real terminal (11/11 behavioral checks), but there are two things to resolve before merge: (1) the red ubuntu CI job is caused by this PR — I root-caused it and confirmed a one-line fix; (2) the truncation cue lost the hidden-line count ("55") that the PR body still advertises. Also: this PR merge-conflicts with open #7060 (same issue #7001, different direction) — maintainer arbitration needed.

1) E2E behavior matrix (PR build)

# Check Result
1 Collapsed cue reads ... more lines not shown (press 'e' to expand) ...
2 e expands: full 60-step plan present in scrollback (Step 1 → Step 60 verified in PTY buffer)
3 Press 'e' to collapse hint shown inline while expanded
4 While expanded, ↑/↓ are inert — selection did not move (RadioButtonSelect unfocused)
5 While expanded, a stray printable key (x) does not collapse the view (re-entry exception works)
6 Stray key does not land in the composer, so the buffer.text.length === 0 guard doesn't wedge the toggle — second e still collapses
7 After collapse: cue restored, ↓ navigates options again
8 Esc cancels cleanly afterwards — no stuck expanded state
9 Merge-base A/B: on 20e4351d2 the cue reads ... 55 more lines not shown (viewport too small) ... and e is fully inert (viewport byte-identical)

Before (merge-base build) — count shown, no way to read hidden lines; e verified inert:

before

After (PR build) — cue hints the key:

after-collapsed

After pressing e — full plan readable via scrollback, collapse hint inline, ↑/↓ inert:

after-expanded

Second e, then ↓ — cue restored, navigation re-enabled, Esc cancels cleanly:

after-restore

2) 🔴 The red ubuntu CI job is caused by this PR (one-line fix confirmed)

Test (ubuntu-latest, Node 22.x) fails in the pre-existing test AppContainer > Settings Integration > handles global render mode shortcut through the captured keypress handler with TypeError: Cannot read properties of undefined (reading 'some'). Root cause: the new 'e' handler evaluates pendingToolCallsRef.current.some(...) unconditionally on every keypress (AppContainer.tsx:3789), while the suite-wide default useGeminiStream mock (AppContainer.test.tsx:305) doesn't include pendingToolCalls — the existing Ctrl+B code only reads that ref inside its own key branch, which is why this never fired before. Your new tests set pendingToolCalls in their own mocks, but the default mock was left untouched.

I reproduced it locally (1 failed | 327 passed across the three affected files) and confirmed the fix is one line — adding pendingToolCalls: [], to the default mock turns AppContainer.test.tsx fully green (119/119):

unit

Production is safe (useGeminiStream always returns pendingToolCalls: toolCalls, an array), so this is a test-environment crash only — but CI must be green. Optionally also make the handler defensive ((pendingToolCallsRef.current ?? []).some(...)).

A/B on your new component tests (merge-base sources + PR tests): the two collapse-hint tests fail on base as expected — they are load-bearing; the other two are inertness guards (green on base by design).

3) 🟡 The cue lost the hidden-line count the PR body still advertises

The PR body's evidence says ... 63 more lines not shown (press 'e' to expand) ..., but the shipped override is a static string (ToolConfirmationMessage.tsx:378-382), so the plan dialog now shows ... more lines not shown (press 'e' to expand) ... — no count. The merge-base showed 55 more lines, so this is a small information regression vs main (visible in the before/after shots above; my E2E flagged it as the one failed check). Suggestion: make truncateCueText accept the count (e.g. a (n) => string or a template), or append the hint to the default cue instead of replacing it.

Minor related note: the non-plan default cue also changed ((viewport too small) dropped) — currently harmless since enforceHeightBudget is only used by the plan branch, but worth stating in the body.

4) Landscape: conflicts with open #7060 (same issue, different direction)

git merge-tree of the two heads (7227478 × f8f5c24) reports content conflicts in ToolConfirmationMessage.tsx and its test. #7060 implements #7001 Direction 1 (o opens the full plan in $EDITOR); this PR implements Direction 3 (e inline expand/collapse). The two UX directions are complementary and could coexist, but whichever lands second must rebase and re-run its verification. Maintainer needs to pick the landing order (or scope).

5) Nits (non-blocking)

  • package-lock.json churn: 43 added "peer": true flags + removed @tailwindcss/oxide-wasm32-wasi bundled sub-dep entries, with zero package.json changes — regeneration noise from a different npm version. Suggest reverting to main's lockfile.
  • The test named RadioButtonSelect isFocus is false when plan is expanded only re-asserts the collapse hint (same assertion as the first test) — it doesn't actually verify focus behavior. My PTY check Are you interested in AI Terminal? #4 above covers the real behavior, but a unit assertion (e.g. driving ↑/↓ through the ink stdin and asserting the marker stays put) would make it honest. This matches the still-unresolved review-bot threads.

Verdict

Feature verified working end-to-end on a real build; not mergeable yet — needs (a) the one-line default-mock fix so CI goes green, (b) ideally restore the hidden-line count in the cue (or update the PR body to match the shipped text), and (c) a maintainer call on #7060 vs #7116 ordering. All findings above are reproducible with the commands shown in the last screenshot.

中文版本(Chinese version)

独立验证 — 本地真实构建,PTY E2E + 单测 A/B(head 7227478)

我在隔离 worktree 中分别从零构建了 PR head(72274785a)和 merge-base(20e4351d2)(npm ci && npm run bundle),用 @lydell/node-pty + @xterm/headless 在 100×34 PTY 中驱动真实构建的 CLI,mock OpenAI SSE 端点返回携带 60 步计划的 exit_plan_mode 工具调用(--approval-mode plan,隔离 HOME)。环境:macOS 15.7.7,Node v22.23.1。

结论:功能在真实终端中完全按设计工作(11/11 项行为检查通过),但合并前需解决两件事:(1) ubuntu CI 红是本 PR 引入的——已定位根因并验证一行修复;(2) 截断提示丢失了 PR 描述中仍在宣传的隐藏行数("55")。另外:本 PR 与同修 #7001 的开放 PR #7060 存在合并冲突,需要维护者裁决落地顺序。

1) E2E 行为矩阵(PR 构建)

9 项检查全部通过:折叠态提示语 → e 展开(滚动回看可见完整 60 步)→ 展开态内联 Press 'e' to collapse 提示 → 展开态 ↑/↓ 惰性(选项不动)→ 杂键不误折叠、也不进 composer(二次 e 正常折叠)→ 折叠后提示语与导航恢复 → Esc 干净取消无卡死 → merge-base A/B(旧提示语带行数 "55",e 完全无效,视口逐字节相同)。截图见上方英文部分。

2) 🔴 ubuntu CI 红是本 PR 引入的(一行修复已验证)

失败的是既有测试 handles global render mode shortcut through the captured keypress handler,报 TypeError: Cannot read properties of undefined (reading 'some')。根因:新的 'e' 处理器在每次按键时无条件执行 pendingToolCallsRef.current.some(...)(AppContainer.tsx:3789),而测试套件的默认 useGeminiStream mock(AppContainer.test.tsx:305)缺少 pendingToolCalls 字段——既有 Ctrl+B 代码只在自己的分支内读该 ref,所以之前从未触发。作者新增的测试在自己的 mock 里补了该字段,但默认 mock 没补。

本地精确复现(3 个受影响文件:1 failed | 327 passed);在默认 mock 中加一行 pendingToolCalls: [],AppContainer.test.tsx 全绿(119/119)。生产路径安全(useGeminiStream 恒返回数组),纯测试环境崩溃——但 CI 必须绿。可选加固:(pendingToolCallsRef.current ?? []).some(...)

反向 A/B(merge-base 组件 + PR 测试):两个 collapse-hint 测试如预期在 base 上红(载荷测试),另两个是惰性守卫、base 上绿属设计使然。

3) 🟡 截断提示丢失了 PR 描述仍在宣传的行数

PR 描述的证据写 ... 63 more lines not shown (press 'e' to expand) ...,但实际实现是静态字符串(ToolConfirmationMessage.tsx:378-382),plan 对话框现在显示 ... more lines not shown (press 'e' to expand) ...——没有行数。merge-base 显示 55 more lines,相对 main 是信息量回退(见前后对比截图)。建议:让 truncateCueText 支持行数(如 (n) => string 或模板),或在默认文案后追加提示而非整体替换。

附带小注:非 plan 场景的默认文案也变了(去掉了 (viewport too small))——目前无害(enforceHeightBudget 仅 plan 分支使用),但建议在 PR 描述中说明。

4) 全景:与开放 PR #7060 冲突(同一 issue,不同方向)

两个 head(7227478 × f8f5c24)git merge-tree 试合并在 ToolConfirmationMessage.tsx 及其测试上报内容冲突#7060 实现 #7001 方向 1(o$EDITOR 中打开完整计划),本 PR 实现方向 3(e 内联展开/折叠)。两个方向互补、可以共存,但后合入者必须 rebase 并重新验证。需要维护者决定落地顺序(或取舍)。

5) 非阻塞小问题

  • package-lock.json 噪音:43 处新增 "peer": true + 删除 @tailwindcss/oxide-wasm32-wasi 子依赖条目,但 package.json 零改动——npm 版本差异导致的重生成噪音,建议还原为 main 的 lockfile。
  • 名为 RadioButtonSelect isFocus is false when plan is expanded 的测试只是重复断言 collapse 提示(与第一个测试相同),并未真正验证焦点行为。我的 PTY 检查 Are you interested in AI Terminal? #4 覆盖了真实行为,但单测层面建议通过 ink stdin 驱动 ↑/↓ 并断言 标记不动。这与 review bot 仍未解决的 threads 一致。

结论

功能在真实构建上端到端验证可用;当前不可合并——需要 (a) 一行默认 mock 修复让 CI 变绿,(b) 建议恢复提示语中的隐藏行数(或修订 PR 描述与实际文案一致),(c) 维护者裁决 #7060#7116 的落地顺序。所有发现均可用最后一张截图中的命令复现。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: allow viewing full plan when exit_plan_mode confirmation is truncated

3 participants