Skip to content

fix(cli): ignore expired live agents in focus navigation#5070

Merged
wenshao merged 1 commit into
QwenLM:mainfrom
he-yufeng:fix/live-agent-rendered-roster
Jun 13, 2026
Merged

fix(cli): ignore expired live agents in focus navigation#5070
wenshao merged 1 commit into
QwenLM:mainfrom
he-yufeng:fix/live-agent-rendered-roster

Conversation

@he-yufeng

Copy link
Copy Markdown
Contributor

Fixes #5067.

Summary

  • Share the live-agent panel visibility predicate between rendering and keyboard focus gates.
  • Stop composer and tab-bar navigation from focusing terminal agents after the panel visibility window.
  • Release stale live-panel focus once the rendered agent roster ages out.

To verify

  • npm test --workspace @qwen-code/qwen-code -- src/ui/components/InputPrompt.test.tsx src/ui/components/agent-view/AgentTabBar.test.tsx src/ui/components/background-view/LiveAgentPanel.test.tsx
  • npm run lint --workspace @qwen-code/qwen-code -- --max-warnings 0
  • npm run typecheck --workspace @qwen-code/qwen-code

) {
return false;
}
return 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] After the defensive auto-defocus fires (all visible agents have expired), non-printable keys are silently consumed via return true without any fallback navigation. If the user pressed Down arrow expecting to reach the tab bar, the keystroke is lost — focus is released back to the composer but doesn't descend further.

Consider re-routing the key instead of swallowing it:

Suggested change
return true;
return descendFromComposer();

Or at minimum return false for arrow keys so they fall through to the normal handler. The printable-character path (return false) is already correct.

— qwen3.7-max via Qwen Code /review

setBgSelectedIndex(agentIdx);
const entry = visibleBgAgents[agentIdx];
const entryIdx = entry
? bgEntries.findIndex(

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] This new findIndex remapping — mapping visibleBgAgents[agentIdx] back to the bgEntries index via agentId match — has no test coverage with a mixed roster. A regression here would silently open the wrong agent's detail view or fail to open any.

Suggested test: bgEntries = [shellTask, expiredAgent, runningAgent] with livePanelSelectedIndex: 2 (pointing at the running agent in the visible list). Press Enter and assert setSelectedIndex was called with the correct bgEntries index (2, not 0).

— qwen3.7-max via Qwen Code /review

// Gate panel focus on the panel's own render condition (`kind === 'agent'`),
// not `bgEntries.length` (which also counts shell/monitor/dream tasks).
const hasBgAgentRoster = bgEntries.some((e) => e.kind === 'agent');
const hasBgAgentRoster = () =>

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] hasBgAgentRoster was changed from a boolean to an arrow function, but the has* prefix conventionally implies a boolean value. A future maintainer writing if (hasBgAgentRoster) (truthy check on the function reference) would always get true, silently breaking the Up-arrow guard.

Consider renaming to signal it's callable:

Suggested change
const hasBgAgentRoster = () =>
const hasVisibleBgAgentRoster = () =>
bgEntries.some((e) => isLiveAgentPanelVisibleEntry(e, Date.now()));

Then update the call site at line 91 to hasVisibleBgAgentRoster().

— qwen3.7-max via Qwen Code /review

@wenshao

wenshao commented Jun 13, 2026

Copy link
Copy Markdown
Collaborator

✅ Local real-TUI verification (maintainer)

Follow-up to the real-TUI verification done for #4911 — same approach, applied to this fix for #5067. Verified locally in a git worktree off the PR head (d967ecf), built into the actual qwen binary and driven through tmux.

Verdict: the fix works end-to-end and the new tests genuinely guard it. No regressions found. LGTM to merge.

Environment

  • macOS (darwin), Node 22.22.2, npm 10.9.7
  • Worktree on PR head d967ecf; npm ci clean; model qwen3.7-max, YOLO mode
  • Background sub-agent spawned deterministically via the /fork slash command (no model-decision nondeterminism for the spawn)

1. PR "To verify" commands — all green

Command Result
npm test … InputPrompt / AgentTabBar / LiveAgentPanel 3 files passed · 188 passed, 1 skipped
npm run typecheck (tsc --noEmit) exit 0
npm run lint -- --max-warnings 0 exit 0

(The 15 curly lint warnings seen during a full npm run build are all in packages/vscode-ide-companion/* — pre-existing and unrelated to this PR; the @qwen-code/qwen-code workspace lints clean at --max-warnings 0.)

2. Mutation / guard test — the new tests really catch the bug

Reverted only the 3 source files to the pre-fix parent (d967ecf^) while keeping the PR's tests, then re-ran the suite:

❯ AgentTabBar.test.tsx     × Up ignores terminal bg agents after the live panel visibility window (#5067)
❯ LiveAgentPanel.test.tsx  × releases focus when the selected terminal row has aged out (#5067)
❯ InputPrompt.test.tsx     × arrow Down skips terminal bg agents after the live panel visibility window (#5067)
 Test Files  3 failed (3)
      Tests  3 failed | 185 passed | 1 skipped (189)

Exactly the 3 new #5067 tests fail on pre-fix code (and all 185 others still pass → the test edits are backward-compatible). Restoring the fix → 188 pass. This proves the tests are non-vacuous and the fix is what flips them.

3. Real-TUI A/B in tmux — reproduced symptom 1 on a pre-fix build, gone after the fix

I built both the fixed binary and a pre-fix binary (3 source files reverted, rebuilt), and ran the identical steps on each.

Precondition (identical on both builds). /fork → the agent appears in the LiveAgentPanel, runs, finishes, and after the 8 s TERMINAL_VISIBLE_MS window the row ages out — panel renders nothing, but the entry is still retained (1 task done). This is the exact #5067 setup: gate counts it, panel doesn't render it.

 During window:   main
                  ✔ fork: reply with exactly the word done … ▶ 12s · 33k tokens
 After  >8s:      (panel renders nothing)        ← status bar still shows "· 1 task done"

The symptom is silent (focus moves to a hidden panel with no visual change), so I made it observable using the composer's "press Esc twice to clear" prompt: with an empty/queued composer, type ABC, then , then Esc. Whoever owns focus reacts to that Esc.

Fixed build — is a no-op, focus stays on the composer; the first Esc reaches it:

* ABC                                              ← after typing
* ABC      … YOLO mode … · 1 task done             ← after ↓  (no-op, no hidden focus)
* ABC      … Press Esc again to clear. · 1 task    ← after Esc #1  ✅ composer got it

Pre-fix build — silently focuses the hidden panel; the first Esc is swallowed:

* ABC      … YOLO mode … · 1 task done             ← after ↓  (no visual change — phantom focus)
* ABC      … YOLO mode … · 1 task done             ← after Esc #1  ❌ swallowed, no clear-prompt
* ABC      … Press Esc again to clear. · 1 task    ← after Esc #2  (only now does the composer respond)

So a "dead key": pre-fix needs 2× Esc to reach the composer, fixed needs 1× — precisely symptom 1 from the issue ("the next ↑/↓/Esc/Enter is silently swallowed by the hidden panel's handler"), reproduced in the real binary and eliminated by the fix.

Notes (non-blocking)

  • Symptom 2 (phantom selection slot / maxIdx) has no dedicated regression test. The three new tests cover the entry gates ( from composer, from tab bar) and the panel-side focus auto-release. The maxIdx = visibleBgAgents.length change and the visible-index→bgEntries remap (findIndex by agentId) are correct and exercised indirectly, but no test sets up one visible + one expired agent to assert the extra- phantom slot is gone. A small follow-up test would lock in the second half of the issue.
  • Design matches the issue's proposed direction: one shared pure helper (liveAgentPanelVisibility.ts) consumed by all three call sites (so the gate can't drift from the render again), snapshot-at-keypress for the gates, and a panel-side useEffect backstop that releases focus when the roster ages out under the user.
🇨🇳 中文版(点击展开)

✅ 本地真实 TUI 验证(维护者)

延续 #4911 的真实 TUI 验证做法,这次应用到 #5067 的修复。在 PR head(d967ecf)的 git worktree 中本地构建出真实的 qwen 二进制,通过 tmux 驱动验证。

结论:修复端到端有效,新增测试确实能守住该 bug,未发现回归。建议合并。

环境

  • macOS,Node 22.22.2,npm 10.9.7;worktree 基于 d967ecf,npm ci 干净;模型 qwen3.7-max,YOLO 模式
  • 后台子 agent 通过 /fork slash 命令确定性触发(spawn 不依赖模型决策)

1. PR "To verify" 三条命令 —— 全绿

命令 结果
npm test(3 个测试文件) 3 文件通过 · 188 passed, 1 skipped
npm run typecheck exit 0
npm run lint -- --max-warnings 0 exit 0

(完整 npm run build 时出现的 15 个 curly warning 全在 packages/vscode-ide-companion/*,是既有的、与本 PR 无关;@qwen-code/qwen-code 包在 --max-warnings 0 下零 warning。)

2. 变异 / 守护测试 —— 证明新测试真能抓到 bug

仅 3 个源文件回退到修复前的父提交(d967ecf^),保留 PR 的测试后重跑:恰好 3 个 #5067 新用例失败,其余 185 个仍通过(说明对既有测试的改动向后兼容);恢复修复后 188 全过。这证明这些测试非空壳,且正是该修复让它们由挂变绿。

3. tmux 真实 TUI A/B —— 修复前复现症状 1,修复后消失

同时构建了 fixed 二进制和 pre-fix 二进制(回退 3 个源文件后重建),在两者上跑完全相同的步骤。

前置条件(两个构建一致): /fork → agent 出现在 LiveAgentPanel,运行、完成,8 秒 TERMINAL_VISIBLE_MS 窗口过后该行老化消失 —— 面板渲染为空,但条目仍被保留(1 task done)。这正是 #5067 的场景:门控仍计数它,面板已不再渲染它

该症状是静默的(焦点跑到隐藏面板,界面无变化),所以我借助 composer 的"连按两次 Esc 清空"提示来让它可观测:输入 ABCEsc,谁持有焦点谁就会响应这个 Esc

  • Fixed: 是 no-op,焦点留在 composer,第一个 Esc 就触发 "Press Esc again to clear."
  • Pre-fix: 静默聚焦隐藏面板,第一个 Esc 被吞(状态栏不变),需要第二个 Esc 才出现清空提示。

即"死键":修复前需按 2 次 Esc,修复后只需 1 次 —— 正是 issue 描述的症状 1("下一个 ↑/↓/Esc/Enter 被隐藏面板 handler 静默吞掉"),在真实二进制里复现并被修复消除。

备注(非阻断)

  • 症状 2(幽灵选择槽 / maxIdx)没有专门的回归测试。 三个新测试覆盖的是进入门控(composer 的 、tab bar 的 )和面板侧焦点自动释放。maxIdx = visibleBgAgents.length 以及"可见索引→bgEntries"的重映射(按 agentId findIndex)实现正确、被间接覆盖,但没有用例专门构造一个可见 + 一个过期的 agent 来断言"多出来的那一下 "幽灵槽已消失。补一个小测试可锁住 issue 的后半部分。
  • 设计与 issue 提议方向一致:一个共享纯函数(liveAgentPanelVisibility.ts)被三处调用方共用(门控不会再和渲染漂移)、门控采用按键时刻快照、面板侧用 useEffect 兜底——当 roster 在用户脚下老化时释放焦点。

wenshao
wenshao previously approved these changes Jun 13, 2026
@wenshao

wenshao commented Jun 13, 2026

Copy link
Copy Markdown
Collaborator

@qwen-code /triage

@qwen-code-ci-bot

qwen-code-ci-bot commented Jun 13, 2026

Copy link
Copy Markdown
Collaborator

Thanks for the update, @he-yufeng!

Template still partial (no What/Why/Test Plan/Risk/中文说明 sections) — same note as before, not blocking since the code and maintainer verification speak for themselves.

On direction: unchanged — clearly aligned. Fixes #5067, a real focus-navigation bug. No concerns.

On approach: the updated PR addresses all three review suggestions from the first pass:

  • Renamed hasBgAgentRosterhasVisibleBgAgentRoster in AgentTabBar — no longer looks like a boolean field, reads clearly as a callable.
  • Stale-focus fallback now calls descendFromComposer() for non-printable keys instead of swallowing them — navigation flows through to the tab bar rather than dead-ending.
  • New mixed-roster regression test covers the phantom maxIdx slot (shell + expired entries interleaved with visible agents), which was flagged as missing last time.

The scope is still proportional — +276/-45 across 7 files. Moving on to code review. 🔍

中文说明

感谢更新!

模板仍不完整(缺 What/Why/Test Plan/Risk/中文说明),与上次相同,不阻断。

方向:不变,明确对齐,修复 #5067 的焦点导航 bug。

方案:更新后的 PR 解决了首轮审查的三个建议:

  • hasBgAgentRoster 重命名为 hasVisibleBgAgentRoster,不再看起来像布尔字段。
  • 过期焦点回退对非可打印键调用 descendFromComposer() 而非吞掉按键,导航能流向 tab bar。
  • 新增混合 roster 回归测试覆盖了幽灵 maxIdx 槽(shell + 过期条目与可见 agent 交错)。

范围仍然成比例。进入代码审查 🔍

Qwen Code · qwen3.7-max

@qwen-code-ci-bot

qwen-code-ci-bot commented Jun 13, 2026

Copy link
Copy Markdown
Collaborator

Code Review (re-run — updated PR)

Independent proposal (before reading the diff): Same as before — extract the panel's visibility predicate into a shared pure function, use it in all gate call sites, add a panel-side effect to release stale focus. The updated PR should additionally address: callable naming, non-printable key fallthrough, and a mixed-roster regression test.

Assessment: The updated implementation matches the independent proposal and addresses all three review suggestions. Changes since the first pass:

  1. AgentTabBar.tsxhasBgAgentRosterhasVisibleBgAgentRoster. Clear improvement — the old name looked like a boolean property, the new name reads as a callable predicate. ✓

  2. InputPrompt.tsx — when livePanelFocused && visibleBgAgents.length === 0, non-printable keys now call descendFromComposer() instead of returning true (swallowing). This means navigation keys (↓, ↑, etc.) flow through to the tab bar rather than dead-ending on a phantom panel. Printable characters still fall through (return false) so typing reaches the composer. ✓

  3. InputPrompt.test.tsx — new test "Enter on the live panel maps visible agents back to their bg entry index" sets up a mixed roster [shell, live-agent-1, expired-agent, live-agent-2], selects index 2 (the second live agent), presses Enter, and asserts setBgSelectedIndex(3) — proving the visible-index→bgEntries remap via findIndex by agentId works correctly with interleaved expired entries. This locks in the phantom maxIdx slot concern from the first review. ✓

No correctness bugs. No regressions. No AGENTS.md violations.

Testing

Unit tests: 3 files passed · 190 passed | 1 skipped (191) — up from 188 with 2 new tests

 Test Files  3 passed (3)
      Tests  190 passed | 1 skipped (191)
   Duration  60.32s

Typecheck: tsc --noEmit exit 0 ✓
Lint: eslint --max-warnings 0 exit 0 ✓

Tmux dev build:

$ npm run dev -- -p 'say hello'
DEV is set to true, but the React DevTools server is not running.

Hello! How can I help you today?

Interactive focus-navigation test: this bug requires Arena mode + background sub-agent spawn + 8s wait for the visibility window to expire — a timing-dependent scenario that can't be meaningfully automated with -p. Maintainer @wenshao already performed thorough real-TUI A/B testing on the original PR (see earlier comment): pre-fix vs post-fix binaries, mutation testing, dead-key reproduction and elimination. The updated code is a strict superset of the original fix with only the three targeted improvements.

中文说明

代码审查(重新运行 — 更新后的 PR)

独立方案: 与之前相同。更新后的实现匹配独立方案并解决了三个审查建议:

  1. hasBgAgentRosterhasVisibleBgAgentRoster — 命名改善。✓
  2. 过期焦点时非可打印键调用 descendFromComposer() — 导航不再死路。✓
  3. 新增混合 roster 测试 — 锁住了幽灵 maxIdx 槽。✓

无正确性 bug,无回归,无 AGENTS.md 违规。

测试

  • 单元测试: 3 文件通过 · 190 passed | 1 skipped(比上轮多 2 个新测试)
  • 类型检查: exit 0 ✓
  • Lint: --max-warnings 0 exit 0 ✓
  • tmux dev 构建: 正常启动,响应正确

Qwen Code · qwen3.7-max

@qwen-code-ci-bot

qwen-code-ci-bot commented Jun 13, 2026

Copy link
Copy Markdown
Collaborator

The updated PR is a strict improvement over the original. All three review suggestions were addressed cleanly:

  • The callable rename eliminates the boolean-field ambiguity.
  • The descendFromComposer() fallthrough for non-printable keys is the right behavior — navigation shouldn't dead-end on a phantom panel.
  • The mixed-roster test locks in the second half of the bug that was only indirectly covered before.

190 tests pass (up from 188), typecheck and lint both clean. The maintainer's earlier A/B verification already proved the fix end-to-end, and the code hasn't changed in any way that would invalidate that evidence.

Approving. ✅

中文说明

更新后的 PR 是原始版本的严格改进。三个审查建议都已干净解决:

  • 可调用重命名消除了布尔字段歧义。
  • 非可打印键的 descendFromComposer() 回退是正确行为。
  • 混合 roster 测试锁住了 bug 的后半部分。

190 个测试通过(比上轮多 2 个),类型检查和 lint 均干净。批准合并 ✅

Qwen Code · qwen3.7-max

@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. ✅

@he-yufeng he-yufeng dismissed stale reviews from qwen-code-ci-bot and wenshao via 650dd25 June 13, 2026 18:55
@he-yufeng he-yufeng force-pushed the fix/live-agent-rendered-roster branch from d967ecf to 650dd25 Compare June 13, 2026 18:55
@he-yufeng

Copy link
Copy Markdown
Contributor Author

Addressed the three review suggestions in the latest force-push.

Updates:

  • Renamed the callable hasBgAgentRoster to hasVisibleBgAgentRoster so it no longer looks like a boolean value.
  • When the live-agent panel focus is stale and no visible agents remain, non-printable navigation now falls through through descendFromComposer() instead of being swallowed.
  • Added a mixed-roster regression test where shell and expired entries are interleaved with visible agents, and Enter maps the selected visible agent back to the correct bgEntries index.

Validated on Windows:

  • npm run test --workspace=packages/cli -- src/ui/components/InputPrompt.test.tsx
  • npm run test --workspace=packages/cli -- src/ui/components/agent-view/AgentTabBar.test.tsx
  • npm run typecheck --workspace=packages/cli
  • npx prettier --check packages/cli/src/ui/components/InputPrompt.tsx packages/cli/src/ui/components/InputPrompt.test.tsx packages/cli/src/ui/components/agent-view/AgentTabBar.tsx packages/cli/src/ui/components/agent-view/AgentTabBar.test.tsx
  • npx eslint packages/cli/src/ui/components/InputPrompt.tsx packages/cli/src/ui/components/InputPrompt.test.tsx packages/cli/src/ui/components/agent-view/AgentTabBar.tsx packages/cli/src/ui/components/agent-view/AgentTabBar.test.tsx --max-warnings 0
  • git diff --check

Notes: one earlier attempt ran two coverage-enabled Vitest targets in parallel and hit the known coverage temp-file race after tests had passed; rerunning the target by itself passed. After rebasing onto the latest upstream/main, CLI typecheck initially saw stale workspace dist types; rebuilding packages/core and rerunning packages/cli typecheck passed.

@wenshao

wenshao commented Jun 13, 2026

Copy link
Copy Markdown
Collaborator

@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, updated code addresses all review feedback. ✅

@wenshao

wenshao commented Jun 13, 2026

Copy link
Copy Markdown
Collaborator

Re-verification at the new head (merge reference)

This supersedes my earlier real-TUI report, which tested head d967ecf. That commit was force-pushed away; the branch was rebased onto 0db3273174 and the force-push addressed the three triage suggestions. I re-ran the verification at the current head 650dd25c02 on Linux (Node 22.22.2), built into the real qwen binary and driven through tmux.

Headline: the LGTM holds at the new head, the three force-push changes are correct, and the symptom-2 gap I flagged last time (phantom maxIdx slot with no dedicated test) is now closed by the new mixed-roster Enter test — which I confirmed is non-vacuous via mutation.

What changed since d967ecf (verified present at head)

  1. Rename hasBgAgentRosterhasVisibleBgAgentRoster() (now a function called at keypress time). No dangling references to the old name or to the removed bgAgentCount anywhere in packages/cli/src.
  2. Behavioral: when the live panel is focused but the visible roster has emptied, non-printable navigation now releases focus and falls through descendFromComposer() instead of being swallowed (printable single chars still type through via return false).
  3. New tests: a mixed-roster Enter-remap test (shell + expired interleaved with visible agents) and a "Down from an expired panel" test.

The shared predicate isLiveAgentPanelVisibleEntry is consumed by all three call sites (InputPrompt ↓-gate, AgentTabBar ↑-gate, LiveAgentPanel render + focus-release effect) — one source of truth, so the gate can't drift from the render again.

1. PR "To verify" — all green at 650dd25c02

Command Result
vitest run on the 3 test files 3 files passed · 190 passed, 1 skipped
npm run typecheck --workspace @qwen-code/qwen-code exit 0
npm run lint -- --max-warnings 0 exit 0
prettier --check (7 changed files) + git diff --check clean

(190 vs the 188 from my d967ecf run = the two extra force-push tests.)

2. Mutation test — the new tests are (mostly) non-vacuous

Reverted the source files to the pre-fix parent 0db3273174 (helper deleted) while keeping the PR's tests, then re-ran:

× AgentTabBar  › Up ignores terminal bg agents after the live panel visibility window (#5067)
× LiveAgentPanel › releases focus when the selected terminal row has aged out (#5067)
× InputPrompt  › arrow Down skips terminal bg agents after the live panel visibility window (#5067)
× InputPrompt  › Enter on the live panel maps visible agents back to their bg entry index   ← closes my symptom-2 gap
✓ InputPrompt  › Down from an expired live panel falls through to the tab bar               ← passes on pre-fix too
 Tests  4 failed | 186 passed | 1 skipped

4 of the 5 new tests fail on pre-fix → they are real regression guards. The important one for me: the mixed-roster Enter test (entries = [shell, running, expired, running], selectedIndex = 2, asserts setBgSelectedIndex(3) — the correct bgEntries index, not the visible index 1) fails on pre-fix and passes on the fix. That is exactly the symptom-2 / phantom-slot half of #5067 I noted was untested last time. Gap closed.

One honest caveat (non-blocking): the Down from an expired live panel falls through to the tab bar test passes on pre-fix too, so it documents intent but does not catch the regression. With its setup (livePanelSelectedIndex: 1, one expired agent), pre-fix computes maxIdx = bgAgentCount = 1, so 1 < 1 is false and the existing "bottom of panel → descend" branch already fires setLivePanelFocused(false) + setAgentTabBarFocused(true) — the same observable result as the new fall-through path. Using livePanelSelectedIndex: 0 (where pre-fix would instead navigate into the phantom slot) would make it discriminating. Minor test-strength nit, not a code issue.

3. Real-TUI A/B in tmux at the new head — symptom 1 reproduced on pre-fix, gone on the fix

Built both binaries from 650dd25c02 (fixed) and 0db3273174 (pre-fix — the 3 source files reverted + helper removed, re-bundled), and ran the identical steps on each: /fork a background agent → it completes → after the 8 s TERMINAL_VISIBLE_MS window the row ages out (panel renders nothing, status bar still shows · 1 task done) → then ABC, , Enter. Whoever owns focus decides whether Enter submits.

Stable observable: does Enter submit the ABC draft? (composer focused → yes; phantom panel focused → swallowed).

Fixed — is a no-op, the composer keeps focus, the first Enter submits:

* ABC                          ← after typing
* ABC          · 1 task done   ← after ↓  (no-op, no phantom focus)
> ABC … ✦ done                 ← after Enter #1  ✅ submitted

Pre-fix — silently focuses the hidden empty panel, the first Enter is swallowed:

* ABC          · 1 task done   ← after ↓  (no visual change — phantom focus on aged-out panel)
* ABC          · 1 task done   ← after Enter #1  ❌ swallowed by the panel, NOT submitted
> ABC … ✦ done                 ← after Enter #2  (only now does the composer submit)

So a dead key: pre-fix needs 2× Enter, the fix needs 1× — precisely symptom 1 ("the next ↑/↓/Esc/Enter is silently swallowed by the hidden panel's handler"), reproduced in the real binary at the new head and eliminated by the fix.

Verdict

Re-confirmed at 650dd25c02: the fix works end-to-end, the focus logic is green under unit + mutation testing, the design keeps one source of truth, and the previously-untested symptom-2 path now has a genuine (non-vacuous) regression test. The only nit is that one of the two added tests (the expired-panel Down fall-through) is non-discriminating as written. LGTM to merge.

🇨🇳 中文版(点击展开)

在新 head 上的复验(合并参考)

本评论取代我此前的真实 TUI 报告(当时测的是 head d967ecf)。该提交已被强推覆盖;分支已 rebase 到 0db3273174,并在强推中处理了三条 triage 建议。我在当前 head 650dd25c02 上、在 Linux(Node 22.22.2)重新构建出真实 qwen 二进制,通过 tmux 驱动验证。

要点:新 head 上 LGTM 依然成立,三处强推改动正确,且我上次指出的"症状 2 缺口"(幽灵 maxIdx 槽无专门测试)现已被新增的 mixed-roster Enter 测试补上——我用变异测试确认了它非空壳。

相对 d967ecf 的变化(已确认存在于 head)

  1. 重命名 hasBgAgentRosterhasVisibleBgAgentRoster()(现为按键时调用的函数)。packages/cli/src 中无任何对旧名或已删除的 bgAgentCount 的悬挂引用。
  2. 行为变化:当 live 面板被聚焦但可见 roster 已清空时,非可打印导航键现在会释放焦点并经 descendFromComposer() 下沉,而不再被吞掉(可打印单字符仍通过 return false 输入到 composer)。
  3. 新测试:一个 mixed-roster Enter 重映射测试(shell + 过期项与可见 agent 交错)和一个"从过期面板按 Down"测试。

共享谓词 isLiveAgentPanelVisibleEntry 被三处调用方共用(InputPrompt 的 ↓ 门控、AgentTabBar 的 ↑ 门控、LiveAgentPanel 渲染 + 焦点释放 effect)——单一真相源,门控不会再和渲染漂移。

1. PR "To verify" —— 新 head 上全绿

命令 结果
vitest run 3 个测试文件 3 文件通过 · 190 passed, 1 skipped
npm run typecheck exit 0
npm run lint -- --max-warnings 0 exit 0
prettier --check(7 个改动文件)+ git diff --check 干净

(190 vs 我在 d967ecf 上的 188 = 强推新增的两个测试。)

2. 变异测试 —— 新测试(大部分)非空壳

源文件回退到修复前父提交 0db3273174(删除 helper),保留 PR 测试后重跑:

× AgentTabBar  › Up ignores terminal bg agents … (#5067)
× LiveAgentPanel › releases focus when the selected terminal row has aged out (#5067)
× InputPrompt  › arrow Down skips terminal bg agents … (#5067)
× InputPrompt  › Enter on the live panel maps visible agents back to their bg entry index   ← 补上我指出的症状 2 缺口
✓ InputPrompt  › Down from an expired live panel falls through to the tab bar               ← 在 pre-fix 上也通过
 Tests  4 failed | 186 passed | 1 skipped

5 个新测试中 4 个在 pre-fix 上失败 → 它们是真正的回归守护。 对我最关键的是 mixed-roster Enter 测试entries = [shell, running, expired, running]selectedIndex = 2,断言 setBgSelectedIndex(3)——正确的 bgEntries 索引,而非可见索引 1在 pre-fix 上失败、在修复后通过。这正是上次我说没被测到的 #5067 症状 2 / 幽灵槽那一半。缺口已补。

一个诚实的提醒(不阻断)Down from an expired live panel falls through to the tab bar 这个测试在 pre-fix 上也通过,因此它只是记录了预期行为,并不能抓到回归。按其设置(livePanelSelectedIndex: 1、一个过期 agent),pre-fix 算出 maxIdx = bgAgentCount = 1,于是 1 < 1 为假,既有的"面板底部 → 下沉"分支已经触发 setLivePanelFocused(false) + setAgentTabBarFocused(true)——与新的 fall-through 路径同样的可观测结果。若改用 livePanelSelectedIndex: 0(此时 pre-fix 会改为进入幽灵槽)就能让它具备区分力。属于测试强度的小瑕疵,不是代码问题。

3. tmux 真实 TUI A/B(新 head)—— 修复前复现症状 1,修复后消失

650dd25c02(fixed)和 0db3273174(pre-fix——回退 3 个源文件 + 删除 helper 后重新 bundle)分别构建二进制,在两者上跑完全相同的步骤:/fork 一个后台 agent → 完成 → 8 秒 TERMINAL_VISIBLE_MS 窗口过后该行老化(面板不再渲染,状态栏仍显示 · 1 task done)→ 然后 ABCEnter。谁持有焦点,谁决定 Enter 是否提交。

稳定观测点:Enter 是否提交 ABC 草稿?(composer 持焦 → 提交;幽灵面板持焦 → 被吞)。

Fixed —— 是 no-op,composer 保持焦点,第一个 Enter 即提交:

* ABC                          ← 输入后
* ABC          · 1 task done   ← ↓ 之后(no-op,无幽灵焦点)
> ABC … ✦ done                 ← Enter #1 之后  ✅ 已提交

Pre-fix —— 静默聚焦隐藏的空面板,第一个 Enter 被吞:

* ABC          · 1 task done   ← ↓ 之后(界面无变化——焦点落在已老化的面板上)
* ABC          · 1 task done   ← Enter #1 之后  ❌ 被面板吞掉,未提交
> ABC … ✦ done                 ← Enter #2 之后(此时 composer 才提交)

即"死键":pre-fix 需按 2 次 Enter,修复后只需 1 次——正是症状 1("下一个 ↑/↓/Esc/Enter 被隐藏面板 handler 静默吞掉"),在新 head 的真实二进制里复现并被修复消除。

结论

650dd25c02 上再次确认:修复端到端有效,焦点逻辑在单元 + 变异测试下全绿,设计保持单一真相源,且此前未被测到的症状 2 路径现在有了真正(非空壳)的回归测试。唯一小瑕疵是新增两个测试中的一个(过期面板 Down fall-through)按当前写法不具区分力。建议合并。

@wenshao wenshao merged commit 533fafa into QwenLM:main Jun 13, 2026
21 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

3 participants