Skip to content

feat(cli): reference prior sessions via @ and add completion tabs - #7302

Merged
wenshao merged 39 commits into
QwenLM:mainfrom
LaZzyMan:lazzy/at-session-ref
Jul 24, 2026
Merged

feat(cli): reference prior sessions via @ and add completion tabs#7302
wenshao merged 39 commits into
QwenLM:mainfrom
LaZzyMan:lazzy/at-session-ref

Conversation

@LaZzyMan

Copy link
Copy Markdown
Collaborator

What this PR does

Adds project-scoped prior sessions to interactive @ mention completion. Selecting a session inserts an @session:<id> reference and injects a deterministic, read-only transcript summary that keeps visible user and assistant text, replaces tool results with one-line status summaries, and retains the newest content within an 8k-token budget.

The completion dropdown is grouped into All, Files, Sessions, MCP, and Extensions categories. Left and right arrow keys switch categories only while multiple completion categories are available, so normal cursor movement remains unchanged elsewhere.

Why it's needed

Users currently have to resume or fork an earlier session to reuse its context. This adds a lightweight reference path that keeps the current conversation independent while making relevant history discoverable from the existing @ workflow.

Reviewer Test Plan

How to verify

  1. Start an interactive session in a project that already has at least two saved sessions, type @, and confirm that multiple result categories appear and left/right switches between them.
  2. Select a prior session, submit a prompt that refers to it, and confirm that a successful Referenced Session card appears without resuming or forking the old session.
  3. Enter a missing or ambiguous @session:<title> reference and confirm that the literal token remains in the prompt while an error card explains the resolution failure.
  4. Confirm that completion with only one category does not show the category bar and left/right continues to move the input cursor.

Evidence (Before & After)

Before: prior conversations were not available from @ completion and reusing their context required resume or fork. After: current-project sessions are discoverable as a dedicated category and resolve to bounded read-only context. The behavior was covered by service, processor, completion-hook, keyboard, rendering, and mouse-wiring tests; no screenshot or recording was captured.

Tested on

OS Status
macOS ✅ automated tests, build, typecheck, and prior manual smoke test
Windows ⚠️ not tested locally
Linux ⚠️ not tested locally

Environment (optional)

Node.js 22.23.1 in a local macOS worktree with sandboxing disabled.

Risk & Scope

  • Main risk or tradeoff: opening @ now starts an asynchronous current-project session listing, and each accepted reference can add up to roughly 8k tokens of context.
  • Not validated / out of scope: archived-session references, additional @ latency optimization, and manual Windows/Linux verification.
  • Breaking changes / migration notes: none.

Linked Issues

N/A

中文说明

本 PR 做了什么

在交互式 @ 提及补全中加入当前项目的历史会话。选择会话后会插入 @session:<id> 引用,并注入一份确定性生成的只读会话摘要:保留用户与助手的可见文本,将工具结果压缩为单行状态摘要,并在 8k token 预算内优先保留最新内容。

补全下拉框按全部、文件、会话、MCP 和扩展分类。只有存在多个补全分类时,左右方向键才会切换分类,因此其他场景下的正常光标移动行为保持不变。

为什么需要

目前用户如果想复用早期会话的上下文,只能恢复或 fork 那个会话。这个改动在保持当前对话独立的同时,提供了一个轻量引用入口,并让相关历史能够从现有的 @ 工作流中直接发现。

Reviewer 测试计划

如何验证

  1. 在已有至少两个保存会话的项目中启动交互式会话,输入 @,确认出现多个结果分类,并且左右方向键可以切换分类。
  2. 选择一个历史会话,提交引用它的提示,确认出现成功的 Referenced Session 卡片,同时旧会话没有被恢复或 fork。
  3. 输入不存在或有歧义的 @session:<title> 引用,确认原始 token 仍保留在提示中,并出现解释解析失败原因的错误卡片。
  4. 确认只有一个分类时不显示分类栏,并且左右方向键仍用于移动输入光标。

证据(Before & After)

Before:历史对话不会出现在 @ 补全中,复用其上下文必须恢复或 fork 会话。After:当前项目的会话会作为独立分类出现,并解析为有大小上限的只读上下文。服务、处理器、补全 hook、键盘、渲染和鼠标连接行为均有测试覆盖;本次没有录制截图或视频。

测试平台

OS 状态
macOS ✅ 自动化测试、构建、类型检查及此前的手动冒烟测试
Windows ⚠️ 未在本地测试
Linux ⚠️ 未在本地测试

环境(可选)

Node.js 22.23.1,本地 macOS worktree,未启用 sandbox。

风险与范围

  • 主要风险或取舍:打开 @ 现在会异步读取当前项目的会话列表,每个被接受的引用最多可增加约 8k token 上下文。
  • 未验证或不在范围内:归档会话引用、进一步的 @ 延迟优化,以及 Windows/Linux 手动验证。
  • 破坏性改动或迁移说明:无。

关联 Issue

无。

LaZzyMan added 15 commits July 17, 2026 10:50
Dispatch file/MCP suggestions immediately and append prior-session
suggestions in a second render once the disk listing resolves, instead
of blocking the first render on session I/O. Cache the per-cwd session
listing for a short TTL so rapid keystrokes don't re-walk the chats dir.
# Conflicts:
#	packages/cli/src/ui/components/SuggestionsDisplay.tsx
@LaZzyMan
LaZzyMan marked this pull request as ready for review July 20, 2026 07:34
@qwen-code-ci-bot

qwen-code-ci-bot commented Jul 20, 2026

Copy link
Copy Markdown
Collaborator

Thanks for the PR!

Template looks good ✓

Problem: This is a feature addition, not a bug fix — the motivation is clear: users currently have to resume or fork a session to reuse its context, and this adds a lightweight @session:<id> reference path. The use case is real and well-described.

Direction: Aligned. Session management is on the project roadmap (roadmap/session-management label exists), and extending the existing @ mention workflow is a natural fit. The approach of deterministic slimming (no LLM call) with a fixed token budget is sensible.

Size: Core paths touched (packages/core/src/services/session-reference-service.ts, packages/core/src/index.ts). Breakdown: ~829 production logic lines, ~1410 test lines, ~1329 docs lines. Since production lines exceed 500, flagging for maintainer awareness per the two-tier gate. Not blocking on size — this is a feat PR.

Approach: The scope feels like a complete vertical slice — service, processor, completion hook, keyboard bindings, rendering, and mouse wiring are all needed for this feature to work end-to-end. Two observations:

  1. The 1329 lines of docs in docs/superpowers/ (implementation plan + design spec) are unusual — the project convention is docs/design/ for design docs and docs/plans/ for plans. These read like AI-generated planning artifacts. Consider whether they belong in the repo at all, or at minimum should move to the conventional location.
  2. The category tab UI (Ctrl+←/→ to switch between All/Files/Sessions/MCP/Extensions) is a nice touch, but it adds non-trivial complexity to useCompletion and SuggestionsDisplay. Worth confirming the maintainer wants this tabbed UX vs. a simpler flat list with session items mixed in.

Moving on to code review. 🔍

中文说明

感谢贡献!

模板完整 ✓

问题:这是一个功能新增,不是 bug 修复——动机很清晰:用户目前必须恢复或 fork 会话才能复用其上下文,而这个 PR 添加了一个轻量的 @session:<id> 引用路径。使用场景真实且描述充分。

方向:对齐。会话管理在项目路线图上(存在 roadmap/session-management 标签),扩展现有的 @ 提及工作流是自然的选择。确定性压缩(无 LLM 调用)加固定 token 预算的方案合理。

规模:触及核心路径(packages/core/src/services/session-reference-service.tspackages/core/src/index.ts)。分解:约 829 行生产逻辑、约 1410 行测试、约 1329 行文档。生产行超过 500,按两级门控标记维护者关注。不因规模阻塞——这是 feat 类型 PR。

方案:范围看起来是一个完整的垂直切片——服务、处理器、补全 hook、键盘绑定、渲染和鼠标连接都是这个功能端到端工作所必需的。两点观察:

  1. docs/superpowers/ 中的 1329 行文档(实现计划 + 设计规格)不太寻常——项目惯例是 docs/design/ 放设计文档、docs/plans/ 放计划。这些读起来像 AI 生成的规划产物。考虑它们是否应该放在仓库中,或者至少应该移到常规位置。
  2. 分类标签 UI(Ctrl+←/→ 在 All/Files/Sessions/MCP/Extensions 之间切换)是个不错的 touches,但给 useCompletionSuggestionsDisplay 增加了非平凡的复杂度。值得确认维护者是否想要这个标签式 UX,还是更简单的平铺列表。

进入代码审查 🔍

Qwen Code · qwen3.8-max-preview

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

@LaZzyMan
LaZzyMan requested review from qwen-code-ci-bot and wenshao and removed request for wenshao July 20, 2026 08:46

@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 a changed line and were dropped; nothing further to act on here. Not reviewed: build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally.

— qwen3.7-max via Qwen Code /review

Comment thread packages/cli/src/ui/hooks/atCommandProcessor.ts Outdated
Comment thread packages/cli/src/ui/components/SuggestionsDisplay.tsx Outdated
Comment thread packages/cli/src/ui/hooks/atCommandProcessor.ts Outdated
@wenshao

wenshao commented Jul 21, 2026

Copy link
Copy Markdown
Collaborator

@qwen-code /takeover

@qwen-code-dev-bot qwen-code-dev-bot added the autofix/takeover Summon the autofix loop to manage this PR (remove to release; needs triage+) label Jul 21, 2026
@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

🤝 Takeover engaged: the autofix loop now manages this PR — it will address new review feedback and resolve base conflicts until the label is removed or the round cap is reached. Remove the autofix/takeover label (or comment @qwen-code /takeover stop) to release.

中文说明

🤝 已接管:autofix 循环现在管理此 PR —— 将持续处理新的评审反馈与 base 冲突,直到移除标签或达到轮次上限。移除 autofix/takeover 标签(或评论 @qwen-code /takeover stop)即可释放。

@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

🤖 Addressed the latest review feedback (round 1/100). What changed, and what I pushed back on:

Review feedback addressed

All 3 suggestion-level findings were implemented.

1. findSessionsByTitle not wrapped in try/catch (rc:3613110199)

Decision: Implemented.

Change: Wrapped the findSessionsByTitle call in atCommandProcessor.ts with try/catch. On a filesystem error (EACCES, EIO, etc.), matches stays empty and the code falls through to the existing "No session matches" error-card path, preserving the documented "never aborting the turn" contract. This mirrors the pattern already used in getSessionSuggestions (session-completion.ts).

Test added: survives a filesystem error during title lookup — mocks findSessionsByTitle to reject with EACCES and asserts shouldProceed remains true with an error card emitted.

2. Tab-bar labels bypass t() i18n (rc:3613110212)

Decision: Implemented (adapted from the suggestion).

Change: Replaced the module-level CATEGORY_LABEL constant with a categoryLabel() function that calls t() at render time. The reviewer's suggested code placed t() calls at module scope, but translations are loaded asynchronously after module evaluation, so module-level calls would always return the English fallback. The function approach ensures translations are resolved when the component renders. 'MCP' remains untranslated (acronym).

3. ! assertion on sessionId (rc:3613110218)

Decision: Implemented.

Change: Added an explicit if (!sessionId) guard before the resolve call. This eliminates all three sessionId! non-null assertions — TypeScript now narrows sessionId to string through the guard. The guard emits an error card and continues, consistent with the other failure paths in the loop.

Verification

  • npm run build
  • npm run typecheck
  • npm run lint
  • npx vitest run on all touched + related test files: 41 tests passed ✅
  • Prettier formatting check ✅
中文说明

已处理的审查反馈

全部 3 条建议级别的发现均已实现。

1. findSessionsByTitle 未包裹 try/catch(rc:3613110199)

决定: 已实现。

变更:atCommandProcessor.ts 中为 findSessionsByTitle 调用添加了 try/catch。当发生文件系统错误(EACCES、EIO 等)时,matches 保持为空数组,代码会走到已有的"No session matches"错误卡片路径,从而保持文档中"绝不中断用户回合"的契约。这与 getSessionSuggestionssession-completion.ts)中已有的模式一致。

新增测试: survives a filesystem error during title lookup —— 模拟 findSessionsByTitle 以 EACCES 拒绝,并断言 shouldProceed 仍为 true 且输出错误卡片。

2. 标签栏标签绕过了 t() 国际化(rc:3613110212)

决定: 已实现(在建议基础上做了调整)。

变更: 将模块级 CATEGORY_LABEL 常量替换为 categoryLabel() 函数,在渲染时调用 t()。审查者建议的代码将 t() 放在模块顶层,但翻译是在模块求值之后异步加载的,因此模块级调用始终返回英文回退值。函数方式确保翻译在组件渲染时正确解析。'MCP' 保持不翻译(缩写词)。

3. sessionId 上的 ! 断言(rc:3613110218)

决定: 已实现。

变更:resolve 调用前添加了显式的 if (!sessionId) 守卫。这消除了所有三处 sessionId! 非空断言 —— TypeScript 现在通过守卫将 sessionId 收窄为 string。守卫会发出错误卡片并继续循环,与循环中其他失败路径保持一致。

验证

  • npm run build
  • npm run typecheck
  • npm run lint
  • npx vitest run 覆盖所有修改及相关测试文件:41 个测试全部通过 ✅
  • Prettier 格式检查 ✅

Base-conflict check: no conflict with main.

Re-review when you have a moment. After round 100 this bot stops and leaves the PR for a human.


🧠 Handled by Qwen Code · model/模型 qwen3.8-max-preview

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

Not reviewed: chunk 5, chunk 1, chunk 2, chunk 8, chunk 6, chunk 3, chunk 4, chunk 7 — launched with a prompt that is not the one the CLI built.

Not reviewed: Agent 0: Issue fidelity & root-cause ownership, Test coverage matrix (whole-diff), Agent 1b: Removed-behavior audit, Agent 1c: Cross-file tracer, Agent 7: Build & test verification — its prompt was built, but no agent on record was launched with it.

Not reviewed: reverse audit — an auditor ran and opened its brief, but no agent was launched with the prompt the CLI built — the launch was written by hand, and what the agent was actually asked is not what this skill certifies.

— qwen3.7-max via Qwen Code /review

Comment thread packages/cli/src/ui/hooks/atCommandProcessor.ts Outdated
Comment thread packages/cli/src/ui/hooks/atCommandProcessor.ts Outdated
@wenshao

wenshao commented Jul 24, 2026

Copy link
Copy Markdown
Collaborator

Review — feat(cli): reference prior sessions via @ and add completion tabs

Reviewed at head 37b9d99b9 in an isolated worktree.

What this PR does

Three layers, cleanly separated:

  • coreSessionReferenceService.resolve(id) loads a prior session via SessionService.loadSession, deterministically slims it (user/assistant visible text, thoughts dropped, tool calls collapsed to [tool: <name> — <status>] from the response side only, never result bodies), and tail-retains to an 8k-token budget with an [earlier turns omitted] marker. No model call.
  • cli/parse+injectsession-mention-ref.ts parses @session:<uuid|title>; atCommandProcessor collects those refs before MCP/filesystem resolution and resolves them after the loop, injecting the slimmed block as a scoped-mention part. Every failure path (not-found, ambiguous title, I/O error, corrupted transcript) emits an error card and leaves the token as literal text — the turn never aborts.
  • cli/UISuggestion.category, a session producer in useAtCompletion, category filtering in useCompletion, and a tab bar in SuggestionsDisplay driven by new COMPLETION_TAB_LEFT/RIGHT bindings.

The layering is right and mirrors the existing extension-mention-ref / MCP-resource analogs. My findings below are about behavior at the edges, not architecture.

Verification I ran on this branch

Check Result
vitest packages/core src/services/session-reference-service.test.ts 22/22 pass
vitest packages/cli — session-mention-ref, session-completion, atCommandProcessor.session, useCompletion, useAtCompletion, SuggestionsDisplay 87/87 pass
Benchmarked listSessions (the per-keystroke cost) and SessionReferenceService.resolve (the per-turn cost) numbers inline below

Findings

1. @ completion re-walks the chats dir on every keystroke — and the fix for this was written, then reverted

getSessionSuggestions calls new SessionService(cwd).listSessions({ size: 20 }) from the at-completion effect (useAtCompletion.ts:453), whose deps include state.pattern. There is no debounce upstream (slowSearchTimer is only a "show loading" timer), so this is one full listing per character typed.

listSessions is not cheap, and its expensive prefix is synchronous: fs.readdirSync over the whole chats dir plus fs.statSync per file, then a synchronous tail read (readLastJsonStringFieldsSync) per page entry. That portion blocks the Ink event loop — i.e. it is input latency, not background I/O, and the "Kicked off CONCURRENTLY so the disk listing never delays…" comment at useAtCompletion.ts:449 is misleading about the sync part.

Measured on this branch (macOS, warm APFS SSD, sessions of ~40 turns, median of 5 runs):

sessions in project listSessions wall of which event-loop-blocked
10 3.0 ms 0.4 ms
50 4.9 ms 0.8 ms
200 6.8 ms 1.6 ms
500 8.2 ms 2.9 ms

Cold cache, a network filesystem, or Windows will all be materially worse.

What makes this stand out: commit 08bbbc213 ("perf(cli): remove @ completion input latency from session listing") already implemented the right fix — a per-cwd TTL cache plus a deferred second dispatch, with 3 accompanying tests — and it was reverted wholesale in 9083aabcb with no explanation anywhere in the PR record. Please restore at least the TTL cache, or say why it had to go.

2. ←/→ stop moving the caret for essentially every user, not just multi-source ones

InputPrompt.tsx:1402 consumes ←/→ whenever availableCategories.length > 2, i.e. whenever ≥2 categories are present. On a bare @, files are always present and sessions are present for any project with ≥1 prior session — so the tab bar, and the ←/→ hijack, become the default state of @ completion for nearly everyone. Before this PR the multi-category case required a configured MCP server or extension, so this is a much broader behavior change than "only while multiple completion categories are available" suggests.

Concretely: with the dropdown open, ← no longer moves the cursor back to fix an earlier typo — the user has to press Esc first. The existing tab-switching precedents (PermissionsDialog, AskUserQuestionDialog) are modal dialogs with no live text buffer, so they aren't a precedent for stealing arrows from an editable input.

Suggestion: gate the interception on the caret being at the end of the @ token, or move it to a modifier (Ctrl+←/→).

The modifier guard itself is correct, and worth noting explicitly since it's non-obvious: command: false maps to key.meta in matchKeyBinding, so Alt/Option+← still does word-left. Good.

Test gap: should NOT switch category on left/right when availableCategories <= 2 asserts switchCategory wasn't called but never asserts the caret actually moved; there's no test that ←/→ are blocked from moving the caret in the >2 case, and none that Alt+← still word-moves while the tab bar is up.

3. Session search only ever sees the 20 most recent sessions

getSessionSuggestions pages first (listSessions({ size: MAX_SESSION_SUGGESTIONS })) and filters second. So typing @auth cannot surface a session titled "auth refactor" if it is the 25th most recent — the needle never reaches it. Fetch a larger page (or push the filter into the listing) before filtering.

Related, same function: the filter matches label and description only — never sessionId. Once the picker has inserted @session:<uuid>, re-triggering completion on that token matches nothing, so the Sessions tab disappears from the very reference the picker just wrote.

4. @session:<title> is close to unusable in practice, and the error copy points users at it

Two independent constraints stack:

  • SessionService.findSessionsByTitle is a case-insensitive exact match on customTitle only. It never matches the first-prompt fallback — but the picker's label is s.customTitle?.trim() || s.prompt || s.sessionId. A user who reads "fix the auth bug" in the dropdown and types @session:fix the auth bug gets "No session matches".
  • parseAllAtCommands (atCommandProcessor.ts:132) terminates an @ token at the first unescaped whitespace or ,;!?()[]{}. So @session:Fix auth bug parses as title Fix regardless — multi-word titles require @session:Fix\ auth\ bug.

The picker path (which inserts UUIDs) is unaffected, so this isn't a blocker — but the ambiguity/not-found error strings actively invite hand-typed titles. Either match what the picker displays (custom title or first prompt), or have the error copy mention the escaping requirement.

5. resolve() reads the entire transcript into memory to keep 8k tokens

loadSession (active state) has no size cap — unlike loadArchivedSession, which takes maxBytes and throws SessionTranscriptTooLargeError, showing the codebase already treats unbounded transcript reads as a hazard. resolve() reads and reconstructs every record, then discards all but the tail. Measured on this branch:

transcript records resolve() wall heap delta tokens kept
5 MB 2,770 27 ms 20 MB 7,823
25 MB 13,837 93 ms 44 MB 7,826
100 MB 55,294 367 ms 140 MB 7,827

This runs inline on prompt submission. A maxBytes guard that surfaces the existing error card would be cheaper than a multi-second stall plus heap spike on a pathological transcript. (Tail-reading isn't a drop-in alternative — the parentUuid walk needs the whole file — so a cap is the pragmatic fix.)

6. Every new UI string is untranslated in all 9 locales

t('All'), t('Files'), t('Sessions'), t('Extensions'), t('Session') (source badge) and t('(←/→ to switch)') have zero entries in packages/cli/src/i18n/locales/*.js (verified against zh.js and ja.js). t() falls back to the English key, so the whole tab bar renders in English on every non-English locale.

Note '(←/→ or tab to cycle)' already exists and is translated in all locales (PermissionsDialog.tsx:988). Reusing it — or adding the six keys — is a small fix.

7. Nits

  • Empty parts on error paths. The three session error branches push part: { text: '' } into scopedMentionEntries, which flows into processedQueryParts; every other producer in this file pushes non-empty text. They also add the failed ref to scopedMentionLabels, so filesRead / recording.filesRead record a session that was never injected. Cheaper and cleaner to emit only the display card for error entries.
  • Category not stamped on the MCP drill-in path. The resourceSuggestions !== null early return (useAtCompletion.ts:423) dispatches without a category, so those suggestions default to 'file'. Harmless today (single category ⇒ no tab bar), but it falsifies the "every @ suggestion carries its category" invariant the rest of the change relies on.
  • One-frame panel flicker. If the active tab is 'session' and a new SEARCH_SUCCESS arrives with no sessions, SuggestionsDisplay returns null for the render before the reset-to-'all' effect commits. Cosmetic.
  • Docs. @server:uri is documented in docs/users/common-workflow.md:321 and docs/users/features/mcp.md; @session: is documented only in the internal plan/spec. A short user-facing section covering the 8k budget, read-only semantics, and current-project-only scope would be worth adding. (docs/superpowers/plans|specs is an established location on main, so those files themselves aren't scope creep.)

What checks out

  • Cross-project scope holds. loadSessionFromState gates on sessionBelongsToCurrentProject before returning, so a UUID copied from another project resolves to notFound and produces an error card — no transcript leak across projects. Verified in source, not just assumed from the plan doc.
  • No path traversal. loadSession does not validate SESSION_FILE_PATTERN (unlike loadArchivedSession / getSessionTitle), but only UUID-shaped ids from parseSessionRef or real on-disk ids from findSessionsByTitle ever reach it. The UUID regex is load-bearing here — worth a comment at parseSessionRef so a future relaxation doesn't silently open a traversal.
  • Tool result bodies never injected — summaries derive from functionResponse names + toolCallResult.status, with 'success'→'ok' and cancelled correctly distinguished from ok.
  • Index alignment is consistent. handleAutocomplete, navigateUp/Down, and the mouse hover/select handlers all operate on the same filtered suggestions array that SuggestionsDisplay renders, so the category filter can't desync selection.
  • Error handling is genuinely thorough — every session failure mode surfaces a card and preserves the literal token; cross-form (UUID vs title) dedup is handled.

Verdict

Nothing here is a correctness blocker, and the error-path coverage is better than most features of this size get. I'd want #1 and #2 resolved before merge — both affect every user on every keystroke, and #1 already has a written, tested fix sitting in the branch history. #3#6 are reasonable follow-ups.

中文说明

在隔离 worktree 中基于 head 37b9d99b9 完成评审。分层(core 服务 → ref 解析 → 补全生产者 → processor 分支 → tab UI)是合理的,错误路径覆盖也比同等规模的功能更完整。以下问题都出在边界行为上,而非架构。

已运行的验证: core 22/22 通过;CLI 6 个文件 87/87 通过;另外实测了 listSessions(每次按键成本)与 SessionReferenceService.resolve(每轮成本)。

1(建议合并前处理)· 每次按键都重新遍历 chats 目录,而修复它的提交被 revert 了。 getSessionSuggestions 在 at-completion effect 中被调用,依赖包含 state.pattern,上游没有 debounce,因此每输入一个字符就完整列一次会话。listSessions 昂贵的前半段是同步的(readdirSync + 每文件 statSync,再加每个分页项一次同步 tail 读),直接阻塞 Ink 事件循环 —— 也就是说这是输入延迟,而不是后台 I/O,useAtCompletion.ts:449 处"CONCURRENTLY"的注释对这部分是有误导的。实测(macOS、热缓存 SSD、每会话约 40 轮、5 次取中位数):10 个会话 3.0 ms(同步 0.4 ms)/50 个 4.9 ms(0.8 ms)/200 个 6.8 ms(1.6 ms)/500 个 8.2 ms(2.9 ms);冷缓存、网络文件系统或 Windows 只会更差。值得注意的是:提交 08bbbc213 已经实现了正确的修复(按 cwd 的 TTL 缓存 + 延迟二次 dispatch,并带 3 个测试),却在 9083aabcb 被整体 revert,PR 记录中没有任何说明。请至少恢复 TTL 缓存,或说明为何必须移除。

2(建议合并前处理)· ←/→ 对几乎所有用户都不再移动光标。 InputPrompt.tsx:1402availableCategories.length > 2 时拦截 ←/→,而裸 @ 下文件类别恒存在、只要项目有 ≥1 个历史会话就会有 session 类别 —— 于是 tab 栏与 ←/→ 拦截成为 @ 补全的默认状态。本 PR 之前,多类别需要配置 MCP 或扩展才会出现,所以这个行为变化比"只有存在多个补全分类时"的描述要广得多。后果是:下拉框打开时 ← 无法回退光标去修正前面的笔误,必须先按 Esc。已有的 tab 切换先例(PermissionsDialogAskUserQuestionDialog)都是没有可编辑缓冲区的模态框,不构成从输入框夺取方向键的先例。建议:仅在光标位于 @ token 末尾时拦截,或改用修饰键(Ctrl+←/→)。修饰键守卫本身是正确的:command: falsematchKeyBinding 中映射到 key.meta,因此 Alt/Option+← 仍然是按词左移。测试缺口should NOT switch category… 只断言 switchCategory 未被调用,没有断言光标确实移动了;也没有测试覆盖 >2 类别时 ←/→ 被阻止移动光标,以及 tab 栏出现时 Alt+← 仍然按词移动。

3 · 会话搜索只能看到最近 20 个会话。 getSessionSuggestions 先分页(size: MAX_SESSION_SUGGESTIONS)再过滤,因此输入 @auth 无法命中排在第 25 位的 "auth refactor"。应先取更大的分页(或把过滤下推到列举层)。同一函数还有:过滤只匹配 labeldescription,从不匹配 sessionId —— 一旦选择器插入了 @session:<uuid>,在该 token 上重新触发补全会匹配为空,Sessions 分类随之消失。

4 · @session:<title> 在实践中几乎不可用,而错误提示还在引导用户去用它。 两个约束叠加:findSessionsByTitle 只对 customTitle 做大小写不敏感的精确匹配,从不匹配"首条 prompt"回退,但选择器的 label 是 customTitle || prompt || sessionId;同时 parseAllAtCommandsatCommandProcessor.ts:132)在第一个未转义的空白或 ,;!?()[]{} 处截断 token,所以 @session:Fix auth bug 无论如何都会被解析成标题 Fix,多词标题必须写成 @session:Fix\ auth\ bug。选择器路径不受影响,因此不是阻塞项,但歧义/未找到的错误文案实际在鼓励用户手输标题。建议匹配选择器展示的内容(自定义标题首条 prompt),或在错误文案中说明转义要求。

5 · resolve() 为了保留 8k token 而把整个 transcript 读入内存。 loadSession(active)没有大小上限 —— 而 loadArchivedSession 接受 maxBytes 并抛 SessionTranscriptTooLargeError,说明代码库已把无界 transcript 读取视为风险。实测:5 MB/2,770 条记录 → 27 ms、堆增长 20 MB;25 MB/13,837 条 → 93 ms、44 MB;100 MB/55,294 条 → 367 ms、140 MB(三者保留 token 均约 7.8k)。这发生在提交 prompt 的同步路径上。加一个 maxBytes 守卫并复用现有错误卡片,比在极端 transcript 上卡顿数秒外加堆尖峰要划算。(尾部读取不是可直接替换的方案 —— parentUuid 遍历需要整个文件 —— 所以上限是务实的做法。)

6 · 所有新增 UI 字符串在 9 个语言包中均无翻译。 t('All')t('Files')t('Sessions')t('Extensions')t('Session')(来源徽章)和 t('(←/→ to switch)')packages/cli/src/i18n/locales/*.js 中条目数为 0(已对 zh.jsja.js 核实)。t() 回退到英文 key,因此整个 tab 栏在所有非英文 locale 下都显示英文。注意 '(←/→ or tab to cycle)' 已存在且在所有语言包中都有翻译(PermissionsDialog.tsx:988),复用它或补上这 6 个 key 都是小改动。

7 · 细节问题。 ① 三个 session 错误分支向 scopedMentionEntries 推入 part: { text: '' },最终进入 processedQueryParts,而该文件中其他所有生产者推入的都是非空文本;它们同时把失败的 ref 加入 scopedMentionLabels,导致 filesRead / recording.filesRead 记录了从未注入的会话 —— 错误项只发 display 卡片更干净。② useAtCompletion.ts:423 的 MCP 下钻早返回路径未标记 category,这些建议会默认成 'file';今天无害(单类别不显示 tab 栏),但破坏了本次改动依赖的"每个 @ 建议都携带类别"这一不变式。③ 当活动分类为 'session' 而新的 SEARCH_SUCCESS 不含会话时,重置为 'all' 的 effect 提交之前会有一帧 SuggestionsDisplay 返回 null,属于观感问题。④ 文档:@server:uridocs/users/common-workflow.md:321docs/users/features/mcp.md 中有说明,而 @session: 只出现在内部 plan/spec 中,建议补一小节说明 8k 预算、只读语义和仅限当前项目的作用域。(docs/superpowers/plans|specsmain 上已是既有位置,这两个文件本身不算 scope creep。)

已核实无问题的部分: 跨项目作用域成立 —— loadSessionFromState 在返回前检查 sessionBelongsToCurrentProject,从其他项目复制来的 UUID 会解析为 notFound 并给出错误卡片,不存在跨项目 transcript 泄漏(这是在源码中核实的,而非照搬 plan 文档的说法);无路径穿越 —— loadSession 确实没有校验 SESSION_FILE_PATTERN(不同于 loadArchivedSession / getSessionTitle),但只有来自 parseSessionRef 的 UUID 形态 id 或来自 findSessionsByTitle 的真实磁盘 id 能到达它,这里 UUID 正则是承重的,建议在 parseSessionRef 处加注释以免将来放宽时无声打开穿越面;工具结果正文从不注入 —— 摘要来自 functionResponse 名称与 toolCallResult.status'success'→'ok' 映射正确,cancelled 也与 ok 正确区分;索引对齐一致 —— handleAutocompletenavigateUp/Down 与鼠标 hover/select 都作用于 SuggestionsDisplay 渲染的同一个已过滤数组,分类过滤不会导致选中错位;错误处理确实周全 —— 每种会话失败模式都会给出卡片并保留字面 token,UUID 与标题两种形式解析到同一会话时的去重也已处理。

结论: 没有发现正确性阻塞问题。建议合并前处理 #1#2 —— 两者都影响每个用户的每次按键,而 #1 的修复代码已经写好并带测试,就躺在本分支的历史里。#3#6 可作为后续跟进。

@wenshao

wenshao commented Jul 24, 2026

Copy link
Copy Markdown
Collaborator

Local build & real-run verification — head 37b9d99b9 (macOS)

Built this PR from source in an isolated worktree and drove the real interactive CLI end to end. This is the first build + run pass at this head — my earlier build-verifies were at 7d90d1fa6 and c9be8c904, and my most recent review was source-reading plus micro-benchmarks. Since c9be8c904 the branch merged main once and took six review-fix commits, so everything below was re-derived from scratch.

Verdict: the feature works exactly as specified — all four Reviewer Test Plan items reproduce, and read-only semantics hold on disk. Two of my earlier findings are now fixed in-tree. The two items I flagged as pre-merge are still open, and I can now show both happening in the running CLI rather than argue them from source.

Environment: macOS 15.7.7 (Darwin 24.6.0), Node v22.23.1, npm ci + full build in a detached worktree at 37b9d99b9; CLI reports v0.20.1. Isolated HOME, a deterministic mock OpenAI server so model turns are reproducible, and prior sessions created by actually running the CLI — so chats/*.jsonl is genuinely written by the recorder and then read back by this feature. TUI driven with node-pty + @xterm/headless; every screenshot is a faithful render of the actual terminal cell grid.


Prior findings → status at this head

# Finding (earlier round) Status at 37b9d99b9
1 Budget trimming is O(N²) — measured 6.8 s to resolve() an 8 000-record session Fixed in 7a54bfcbc — replaced with single-pass tail retention (estimate per line once, accumulate backwards). Re-verified below.
2 Tool status derived from .error rather than the persisted .status Fixed in 37b9d99b9 — now maps 'success' → 'ok' off toolCallResult.status.
3 Empty text: '' part pushed on the miss/error branches Harmless in practice — captured the error turn on the wire: the empty part is dropped before the request is built (see §5). Still worth removing, but it is not reaching providers.
4 @ completion re-lists the chats dir on every keystroke ⚠️ Open — now measured in the running TUI (§6.1).
5 ←/→ stop moving the caret whenever ≥2 categories are present ⚠️ Open — reproduced with screenshots (§6.2).
6 Session search only sees the 20 most recent sessions ⚠️ Open — isolated with a controlled A/B (§7.1).
7 New tab-bar strings untranslated ⚠️ Open, and slightly worse than I reported (§7.2).

1 · Build, static checks, automated tests

Check Result
npm ci + full package build (core + cli) ✅ clean
tsc --build packages/core packages/cli ✅ clean
eslint — 24 changed .ts/.tsx files ✅ clean
npm run check-i18n All checks passed (see §7.2 — it does not gate missing keys)
Vitest — packages/core session-reference-service.test.ts 22 / 22 pass
Vitest — 10 changed/added packages/cli test files 371 / 371 pass
CLI test breakdown
✓ src/ui/components/InputPrompt.test.tsx        (191)
✓ src/ui/keyMatchers.test.ts                     (49)
✓ src/ui/hooks/useAtCompletion.test.ts           (36)
✓ src/ui/hooks/extension-mention-ref.test.ts     (24)
✓ src/ui/hooks/useCompletion.test.ts             (23)
✓ src/ui/hooks/useExportCompletion.test.ts       (20)
✓ src/ui/components/SuggestionsDisplay.test.tsx   (9)
✓ src/ui/hooks/atCommandProcessor.session.test.ts (8)
✓ src/ui/hooks/session-mention-ref.test.ts        (6)
✓ src/ui/hooks/session-completion.test.ts         (5)
  Test Files  10 passed   Tests  371 passed

2 · Real SessionReferenceService against real on-disk sessions

Driving the built service (no stubs) against session files the CLI itself wrote, including one turn that really executed run_shell_command:

--- Referenced session "Fix the authentication bug in login.ts where == is used instead of ===. TOOLS..." (slimmed, read-only) ---
User: Fix the authentication bug in login.ts where == is used instead of ===. TOOLSEED run the build to verify.
[tool: run_shell_command — ok]
Assistant: Acknowledged (mock reply).
Assertion Result
listSessions() returns exactly this project's sessions
User / assistant visible text retained, header derived from the first prompt
Tool call collapsed to one status line
Tool result body (build-ok stdout, present in the raw transcript) never appears in the block
resolve(<missing uuid>){ notFound: true }
Session id from another project{ notFound: true } — no cross-project transcript leak
budgetTokens: 30truncated: true, [earlier turns omitted], newest line kept

The O(N²) trim fix is live: the loop is now a single backward pass with per-line estimates, and the truncation semantics are unchanged (same [earlier turns omitted] + newest-line-always-kept behaviour I verified in the previous round).

3 · Real interactive TUI — Reviewer Test Plan

Typing @ opens the tabbed completion. All / Files / Sessions with the (←/→ to switch) hint; files and the real prior sessions appear together under All.

All tab

→ → narrows to Sessions, each row badged Session:

Sessions tab

Accepting inserts @session:<uuid> — the UUID, not the visible label:

Reference inserted

Submitting produces a ✓ Referenced Session card, and the model quotes the referenced session's title back — information it can only have obtained from the injected block:

Success card

Confirmed on the wire from the mock — this is exactly what was sent:

--- Referenced session "Plan a database migration that adds a created_at timestamp column to the user..." (slimmed, read-only) ---
User: Plan a database migration that adds a created_at timestamp column to the users table.
Assistant: Acknowledged (mock reply).

An unresolvable reference yields an error card, the literal token survives in the prompt, and the turn still proceeds:

Error card

Reviewer Test Plan step Result
1 · multiple categories appear, ←/→ switches between them
2 · select a session → success card, no resume/fork ✅ (+ on-wire and on-disk proof)
3 · missing/ambiguous ref → literal token kept + error card
4 · single category → no tab bar, ←/→ moves the cursor

4 · Read-only semantics, proven on disk

After a turn that referenced a prior session and contained a unique marker string:

Assertion Result
Referenced transcript byte-identical (sha256) after the turn
Referenced transcript contains no text from the new conversation
Every record inside it still carries only its own sessionId
Pre-existing session files modified none
New session file created for the current conversation 1 (as expected)

5 · Error-path parts on the wire

The three session error branches push part: { text: '' }. Captured the actual request for @session:release-notes-that-do-not-exist summarise it: the user message arrives verbatim and the empty part is not present — it is dropped during request assembly. Cosmetic cleanup, not a provider risk.


6 · What I would still fix before merge

6.1 · One full chats-directory listing per keystroke, synchronously, on the Ink event loop

getSessionSuggestions builds a fresh SessionService and calls listSessions({ size: 20 }) from the at-completion effect, whose deps include state.pattern, with no debounce upstream. I instrumented the running CLI with a --require shim that only counts fs calls against the chats directory (no code under test was modified) and typed a 5-character pattern after @:

sessions in project synchronous fs calls per keystroke main-thread blocking per keystroke (median of 6)
10 31 0.55 ms
60 101 1.25 ms
200 241 2.17 ms
500 541 4.37 ms

Per keystroke that is exactly one readdirSync of the chats directory, one statSync per session file, then a second statSync plus a synchronous tail read (openSync) per page entry — 1 + N + 2·min(N, 20), linear in the user's session count. Typing @auth in the 60-session project issued 505 synchronous chats-directory calls in total.

On a warm APFS SSD the absolute cost is small, so this is not a correctness blocker. But it is main-thread blocking (input latency, not background I/O), it grows without bound with a user's session history, and a cold cache, a network filesystem, or Windows all make it materially worse. The comment at useAtCompletion.ts:449"Kicked off CONCURRENTLY so the disk listing never delays…" — is misleading about this synchronous prefix.

Commit 08bbbc213 ("perf(cli): remove @ completion input latency from session listing") already implemented the right fix — a per-cwd TTL cache plus a deferred second dispatch, with three tests — and 9083aabcb reverted it wholesale with no explanation in the PR record. Please restore at least the TTL cache, or say why it had to go.

6.2 · ←/→ stop moving the caret for essentially every user

InputPrompt.tsx:1403 consumes ←/→ whenever availableCategories.length > 2, i.e. whenever ≥2 categories exist. On a bare @, files are always present and sessions are present for any project with ≥1 prior session — so the tab bar and the arrow-key interception are the default state of @ completion, not an edge case. Before this PR, reaching >1 category required a configured MCP server or extension.

Same buffer, one apart. Note the caret does not move; the active tab wraps All → Sessions:

Before ←
After ←

Typing a character afterwards confirms it: on refactor the auth module @, pressing then X yields refactor the auth module @X — the X lands at the end, so the caret never moved. In practice: with the dropdown open, ← can no longer walk back to fix an earlier typo without pressing Esc first.

The gating itself is correct — with a single category the tab bar is hidden and arrows behave normally (@READ← ←X gives @REXAD):

Single category

Suggestion unchanged: gate the interception on the caret being at the end of the @ token, or move it to Ctrl+←/→. (The modifier guard is right — command: false maps to key.meta, so Alt/Option+← still does word-left.)


7 · Follow-ups (non-blocking)

7.1 · Session search only reaches the 20 most recent sessions — isolated with an A/B

Project with 60 prior sessions, one of them titled "auth refactor plan for the login service" sitting 30th by recency. Typing @auth finds nothing:

60 sessions, @auth misses

Control: nothing changed except that session's mtime (making it the most recent). Same project, same needle:

Same needle after touch

That isolates the cause precisely: getSessionSuggestions pages to the 20 newest before filtering, so any older session is unreachable by search. Fetch a larger page, or push the filter into the listing.

Related, same function: the filter matches label and description only, never sessionId. I re-triggered completion on the token the picker had just written (@session:<uuid>) — zero suggestions, and the Sessions tab disappears from its own reference.

7.2 · The tab bar is partly untranslated, and the one key that resolves is the wrong string

Real run with QWEN_CODE_LANG=zh. Everything around the dropdown is Chinese; the tab bar reads All Files 会话数 (←/→ to switch):

Chinese locale

Exact state of the six new keys across packages/cli/src/i18n/locales/*.js:

key status
t('All') absent in every locale, including en.js — always renders English
t('Files') absent in every locale, including en.js — always renders English
t('(←/→ to switch)') absent in every locale. A near-duplicate '(Tab / ←→ to switch)' already exists and is translated (zh, zh-TW, ca)
t('Sessions') resolves — but to the stats-panel key: zh 会话数, zh-TW 會話數, ja セッション数 = "session count", wrong as a category tab label
t('Session') (badge) en, zh, zh-TW, ca only
t('Extensions') ✅ all locales

Note npm run check-i18n passes — it reports missing keys as non-required warnings and gates only on unused/extra keys, so CI will not catch this. Reusing '(Tab / ←→ to switch)' and adding four keys to en.js is a small fix.

7.3 · Smaller notes (unchanged)

  • resolve() reads the whole transcript to keep 8 k tokens; loadArchivedSession already takes a maxBytes guard, loadSession does not.
  • @session:<title> remains close to unusable by hand — findSessionsByTitle is a case-insensitive exact match on customTitle only, and parseAllAtCommands terminates the token at the first unescaped space — while the error copy invites users to type titles.
  • MCP drill-in early return dispatches without a category, breaking the "every @ suggestion carries its category" invariant (harmless today).

Verdict

Nothing here is a correctness blocker, and the error-path coverage is better than most features this size get: every failure mode surfaces a card, the literal token always survives, cross-project scope holds, tool result bodies never leak, and the referenced transcript is provably untouched on disk. The two earlier perf/robustness findings were properly fixed.

I would still like 6.1 and 6.2 resolved before merge — both affect every user on every keystroke, and 6.1's fix is already written and tested in this branch's own history. §7 items are reasonable follow-ups.

Method: npm ci + build from PR head in an isolated detached worktree; isolated HOME; deterministic mock OpenAI server; prior sessions created by really running the CLI; TUI driven via node-pty + @xterm/headless; screenshots are faithful renders of the captured terminal cell grid; the fs counter is a --require shim that wraps fs and changes no code under test.

中文版报告(合并参考)

本地构建与真实运行验证 —— head 37b9d99b9(macOS)

在隔离 worktree 中从源码构建本 PR,并驱动真实的交互式 CLI 做端到端验证。这是当前 head 上第一次「构建 + 真实运行」验证 —— 我此前的构建验证分别在 7d90d1fa6c9be8c904最近一次评审 则是源码阅读加微基准。自 c9be8c904 起分支合并了一次 main 并新增六个 review 修复提交,因此以下结论全部重新推导。

结论:功能完全符合描述 —— Reviewer 测试计划四项全部复现,只读语义在磁盘层面成立。 我此前的两个问题已在代码中修复;两个「建议合并前处理」的问题仍然存在,而这次我可以在真实运行的 CLI 中把它们演示出来,而不只是从源码论证。

环境:macOS 15.7.7(Darwin 24.6.0),Node v22.23.1,在 detached worktree 中对 37b9d99b9 执行 npm ci 与完整构建,CLI 显示 v0.20.1。使用隔离 HOME、确定性 mock OpenAI server 保证模型回合可复现;历史会话是通过真实运行 CLI 产生的,因此 chats/*.jsonl 确由 recorder 写入、再被本功能读回。TUI 通过 node-pty + @xterm/headless 驱动,所有截图都是终端单元格网格的忠实渲染。

此前问题 → 当前 head 状态

# 此前发现 37b9d99b9 状态
1 预算裁剪是 O(N²)(实测 8 000 条记录 resolve() 需 6.8 秒) 已修复7a54bfcbc)—— 改为单趟尾部保留(每行只估算一次,从最新行往回累加)。本轮已复验。
2 工具状态由 .error 推导,而非持久化的 .status 已修复37b9d99b9)—— 现基于 toolCallResult.status 并将 'success' 映射为 'ok'
3 miss/error 分支推入空 text: '' part 实际无害 —— 抓取错误回合的请求体确认:空 part 在组装请求前已被丢弃(见 §5)。仍建议清理,但没有真的发给 provider。
4 @ 补全每次按键都重新遍历 chats 目录 ⚠️ 仍存在 —— 本轮在运行中的 TUI 里做了实测(§6.1)。
5 只要有 ≥2 个分类,←/→ 就不再移动光标 ⚠️ 仍存在 —— 已用截图复现(§6.2)。
6 会话搜索只能看到最近 20 个会话 ⚠️ 仍存在 —— 用受控 A/B 做了归因(§7.1)。
7 新增 tab 栏文案未翻译 ⚠️ 仍存在,且比我此前描述的更糟(§7.2)。

1 · 构建、静态检查与自动化测试

检查项 结果
npm ci + 完整包构建(core + cli) ✅ 通过
tsc --build packages/core packages/cli ✅ 通过
eslint —— 24 个改动的 .ts/.tsx ✅ 通过
npm run check-i18n All checks passed(见 §7.2 —— 它并不拦截缺失 key)
Vitest —— core session-reference-service.test.ts 22 / 22 通过
Vitest —— 10 个改动/新增的 cli 测试文件 371 / 371 通过

2 · 用真实 SessionReferenceService 读真实磁盘会话

构建产物(非 stub)读取 CLI 自己写入的会话文件,其中一次回合真实执行了 run_shell_command

--- Referenced session "Fix the authentication bug in login.ts where == is used instead of ===. TOOLS..." (slimmed, read-only) ---
User: Fix the authentication bug in login.ts where == is used instead of ===. TOOLSEED run the build to verify.
[tool: run_shell_command — ok]
Assistant: Acknowledged (mock reply).
断言 结果
listSessions() 恰好返回本项目的会话
保留用户/助手可见文本,标题从首条 prompt 派生
工具调用压缩为单行状态
工具结果正文(原始 transcript 中确实存在的 build-ok stdout)不出现在摘要块中
resolve(<不存在的 uuid>){ notFound: true }
其他项目的会话 id → { notFound: true },无跨项目 transcript 泄漏
budgetTokens: 30truncated: true[earlier turns omitted]、保留最新行

O(N²) 修复已经生效:裁剪循环改成了带逐行估算的单趟反向扫描,截断语义保持不变。

3 · 真实交互式 TUI —— Reviewer 测试计划

截图见英文版:

  • 输入 @ 打开分类补全,All / Files / Sessions + (←/→ to switch)All 下文件与历史会话混合展示。
  • 连按两次 收敛到 Sessions,每行带 Session 徽标。
  • 选中后插入 @session:<uuid>(插入的是 UUID,而非可见标题)。
  • 提交后出现 ✓ Referenced Session 卡片,且模型复述出被引用会话的标题 —— 该信息只可能来自注入的摘要块;mock 侧抓到的请求体证实了实际发送内容。
  • 无法解析的引用给出错误卡片,原始 token 保留在提示中,本轮对话继续进行。
Reviewer 测试计划 结果
1 · 出现多个分类,←/→ 可切换
2 · 选择会话 → 成功卡片,未 resume/fork ✅(含 wire 与磁盘双重证据)
3 · 缺失/歧义引用 → 保留字面 token + 错误卡片
4 · 单一分类 → 无分类栏,←/→ 移动光标

4 · 只读语义(磁盘层面证明)

在一次「引用历史会话 + 携带唯一标记串」的回合之后:

断言 结果
被引用 transcript 的 sha256 前后完全一致
被引用 transcript 不含新对话的任何文本
其中每条记录仍然只带它自己的 sessionId
被修改的既有会话文件
为当前对话新建的会话文件 1 个(符合预期)

5 · 错误路径的 part

三个 session 错误分支会推入 part: { text: '' }。抓取 @session:release-notes-that-do-not-exist summarise it 的真实请求:用户消息原样送达,空 part 并不存在 —— 在请求组装阶段已被丢弃。属于可清理的冗余,而非 provider 风险。

6 · 建议合并前处理

6.1 · 每次按键都在 Ink 事件循环上同步完整列举 chats 目录

getSessionSuggestions 在 at-completion effect 中新建 SessionService 并调用 listSessions({ size: 20 }),其依赖包含 state.pattern,上游没有 debounce。我用一个 --require shim 对运行中的 CLI 做了插桩(只统计针对 chats 目录的 fs 调用,不修改被测代码),在 @ 后输入 5 个字符:

项目内会话数 每次按键的同步 fs 调用 每次按键的主线程阻塞(6 次中位数)
10 31 0.55 ms
60 101 1.25 ms
200 241 2.17 ms
500 541 4.37 ms

每次按键精确对应:chats 目录一次 readdirSync、每个会话文件一次 statSync,再加上每个分页项一次 statSync 与一次同步尾部读(openSync)—— 即 1 + N + 2·min(N, 20),与用户的会话数呈线性。在 60 会话的项目里输入 @auth 共产生 505 次同步调用。

在热缓存 SSD 上绝对开销不大,因此不是正确性阻塞项;但它阻塞主线程(属于输入延迟而非后台 I/O),随用户历史无上限增长,冷缓存、网络文件系统或 Windows 只会更差。useAtCompletion.ts:449 处「CONCURRENTLY 所以磁盘列举不会延迟…」的注释对这段同步前缀是有误导的。

提交 08bbbc213("perf(cli): remove @ completion input latency from session listing")已经实现了正确的修复 —— 按 cwd 的 TTL 缓存 + 延迟二次 dispatch,并带三个测试 —— 却被 9083aabcb 整体 revert,PR 记录中没有任何说明。建议至少恢复 TTL 缓存,或说明为何必须移除。

6.2 · ←/→ 对几乎所有用户都不再移动光标

InputPrompt.tsx:1403availableCategories.length > 2(即存在 ≥2 个分类)时拦截 ←/→。裸 @ 下文件分类恒存在,只要项目有 ≥1 个历史会话就会有 session 分类 —— 于是 tab 栏与方向键拦截成为 @ 补全的默认状态,而非边界情况。本 PR 之前,要出现多个分类需要配置 MCP 或扩展。

英文版中给出了「按 ← 之前 / 之后」的对照截图:光标未动,活动分类从 All 回绕到 Sessions。再输入一个字符可以确认:在 refactor the auth module @ 上按 再输入 X,得到 refactor the auth module @X —— X 落在末尾,说明光标从未移动。实际影响是:下拉框打开时,← 无法回退去修正前面的笔误,必须先按 Esc。

门控本身是正确的 —— 只有一个分类时 tab 栏隐藏、方向键行为正常(@READ← ←X 得到 @REXAD)。

建议维持此前的方案:仅在光标位于 @ token 末尾时拦截,或改用 Ctrl+←/→。(修饰键守卫写得没问题:command: false 映射到 key.meta,因此 Alt/Option+← 仍是按词左移。)

7 · 后续跟进(不阻塞)

7.1 · 会话搜索只能触及最近 20 个会话 —— 用 A/B 完成归因

构造一个有 60 个历史会话的项目,其中标题为 "auth refactor plan for the login service" 的会话按时间排在第 30 位。输入 @auth 什么也匹配不到。

对照组:除了把该会话的 mtime 改成最新之外什么都没变,同一项目、同一关键词 —— 立刻匹配成功。

这精确定位了原因:getSessionSuggestions 先分页取最近 20 个、再过滤,因此更早的会话无法被搜索触及。应改为取更大分页,或把过滤下推到列举层。

同一函数的相关问题:过滤只匹配 labeldescription,从不匹配 sessionId。我在选择器刚写入的 @session:<uuid> token 上重新触发补全 —— 零建议,Sessions 分类从它自己产生的引用上消失了。

7.2 · tab 栏部分未翻译,而唯一能解析的 key 还指向了错误的字符串

QWEN_CODE_LANG=zh 真实运行:下拉框周围全部是中文,而 tab 栏显示 All Files 会话数 (←/→ to switch)

六个新 key 在 packages/cli/src/i18n/locales/*.js 中的确切状态:

key 状态
t('All') 所有语言包(含 en.js)均无 —— 永远显示英文
t('Files') 所有语言包(含 en.js)均无 —— 永远显示英文
t('(←/→ to switch)') 所有语言包均无。而近似重复的 '(Tab / ←→ to switch)' 已存在且已翻译zhzh-TWca
t('Sessions') 能解析 —— 但命中的是统计面板的 key:zh 会话数zh-TW 會話數ja セッション数,意为「会话数量」,作为分类标签是错的
t('Session')(徽标) enzhzh-TWca
t('Extensions') ✅ 全部语言包

注意 npm run check-i18n通过的 —— 它把缺失 key 记为「非必需」警告,只对多余/未使用 key 做拦截,所以 CI 不会发现这个问题。复用 '(Tab / ←→ to switch)' 并往 en.js 补四个 key 就是很小的改动。

7.3 · 其余小问题(与此前一致)

  • resolve() 为保留 8k token 而读入整个 transcript;loadArchivedSession 已有 maxBytes 守卫,loadSession 没有。
  • 手写 @session:<title> 仍然基本不可用 —— findSessionsByTitle 只对 customTitle 做大小写不敏感精确匹配,且 parseAllAtCommands 在第一个未转义空格处截断 token —— 而错误文案还在引导用户去手输标题。
  • MCP 下钻的早返回路径未标记 category,破坏了「每个 @ 建议都携带分类」的不变式(目前无害)。

结论

没有发现正确性阻塞问题,错误路径的覆盖比同等规模的功能更完整:每种失败模式都会给出卡片、字面 token 始终保留、跨项目作用域成立、工具结果正文从不泄漏、被引用 transcript 在磁盘上可证明未被改动。此前的两个性能/健壮性问题也已正确修复。

仍建议在合并前处理 6.16.2 —— 两者都影响每个用户的每次按键,而 6.1 的修复代码就躺在本分支自己的历史里、且已带测试。§7 各项可作为后续跟进。

@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

🤖 Addressed the latest review feedback (round 7/100). What changed, and what I pushed back on: · 已处理最新评审反馈(第 7/100 轮)。改动内容与我反驳保留之处如下:

Review feedback addressed on PR #7302

Addressed the two findings the reviewer (@wenshao) flagged as wanted-before-merge
(#1 and #2). The remaining findings (#3#7) are deferred as follow-ups, consistent
with both the reviewer's verdict ("#3#6 are reasonable follow-ups", #7 are nits)
and the repository's review policy: this branch has accumulated ~14 review rounds,
and after ~5 rounds only Critical/regression fixes are landed while remaining
Suggestions are deferred with a recorded reason.

#1@ completion re-walks the chats dir on every keystroke — Fixed

  • Decision: Implemented (requested before merge).
  • Change: Restored the per-cwd TTL cache in session-completion.ts, adapted
    from the reverted commit 08bbbc213 to the current code (which now strips the
    session: prefix and stamps a source badge). The unfiltered listing is cached
    keyed by cwd for a 3s TTL; pattern filtering still runs fresh on the cached
    items, and a listing failure caches nothing and yields [] as before. Added the
    three accompanying tests (cache hit within TTL, re-list after TTL expiry,
    per-cwd isolation) plus a beforeEach cache reset so the existing /proj cases
    don't collide within the TTL window.
  • Scope note: Restored the TTL cache only — not the two-dispatch render split
    from the same reverted commit — per the reviewer's "restore at least the TTL
    cache". The TTL cache is the substantive fix for the synchronous per-keystroke
    chats-dir walk; within the window, only the first keystroke pays the listing
    cost.

#2 — ←/→ stop moving the caret for essentially every user — Fixed

  • Decision: Implemented (requested before merge).
  • Change: Moved the COMPLETION_TAB_LEFT/COMPLETION_TAB_RIGHT bindings from
    plain ←/→ to Ctrl+←/→ (keyBindings.ts), so the bare arrow keys are never
    consumed by category tab switching and always move the caret in the editable
    buffer. Updated the tab-bar hint to (Ctrl+←/→ to switch) so the displayed key
    stays accurate, and refreshed the surrounding comments.
  • Why the modifier rather than the caret-at-end gate: a caret-at-end gate does
    not fix the common case — when @ is typed at the end of the buffer (the usual
    case), the caret is already at the end, so the first ← would still switch tabs
    and the caret would never move back. The modifier approach robustly restores
    plain ←/→ caret movement everywhere. Ctrl+←/→ word-movement is shadowed only
    while the dropdown is open; Alt/Option+←/→ still performs word movement
    (unchanged), as the reviewer noted.
  • Test gap closed: updated the ">2 categories" test to drive Ctrl+arrows, and
    added a test asserting plain ←/→ do not switch category when >2 categories
    are present (i.e. the caret stays free). Updated the keyMatchers comparison
    cases and hard-coded reference matchers to the Ctrl-based binding.

#3#7 — Deferred (follow-ups / nits)

Deferred per the ~5-round policy and the reviewer's own classification. None is a
correctness blocker or a regression introduced by this PR.

Conflict notes

--conflict false; the base branch was not merged and no conflicts were touched.

Verification

  • npm run build — passed
  • npm run typecheck — passed
  • npm run lint (full: eslint . --ext .ts,.tsx && eslint integration-tests) — passed
  • vitest src/ui/hooks/session-completion.test.ts (cli) — 8 passed
  • vitest src/ui/keyMatchers.test.ts (cli) — 49 passed
  • vitest src/ui/components/SuggestionsDisplay.test.tsx (cli) — 9 passed
  • vitest src/ui/hooks/useCompletion.test.ts (cli) — 23 passed
  • vitest src/config/keyBindings.test.ts (cli) — 4 passed
  • vitest src/ui/components/InputPrompt.test.tsx src/ui/hooks/useAtCompletion.test.ts (cli) — 228 passed
中文说明

PR #7302 上处理的评审反馈

处理了评审者(@wenshao)标记为合并前需解决的两项发现(#1#2)。其余发现(#3#7)作为后续跟进延后处理,这既符合评审者的结论("#3#6 可作为后续跟进"、#7 为细节问题),也符合仓库的评审策略:本分支已累计约 14 轮评审,而超过约 5 轮后只落地 Critical/回归类修复,其余 Suggestions 延后并记录原因。

#1@ 补全每次按键都重新遍历 chats 目录 — 已修复

  • 决定: 已实现(合并前要求)。
  • 改动:session-completion.ts 中恢复了按 cwd 的 TTL 缓存,从被 revert 的提交 08bbbc213 适配到当前代码(当前代码已会剥离 session: 前缀并打上来源徽章)。未过滤的列表按 cwd 缓存,TTL 为 3 秒;模式过滤仍在缓存项上每次新鲜执行,列表失败时不缓存并像之前一样返回 []。补充了配套的三个测试(TTL 内命中缓存、TTL 过期后重新列举、按 cwd 隔离),并增加了一个 beforeEach 缓存重置,避免已有的 /proj 用例在 TTL 窗口内相互串扰。
  • 范围说明: 仅恢复 TTL 缓存——未恢复同一被 revert 提交中的"两次 dispatch 渲染拆分"——依据评审者"请至少恢复 TTL 缓存"的表述。TTL 缓存是修复同步的每次按键遍历 chats 目录这一问题的实质性手段;在窗口内只有第一次按键承担列举成本。

#2 — ←/→ 对几乎所有用户都不再移动光标 — 已修复

  • 决定: 已实现(合并前要求)。
  • 改动:COMPLETION_TAB_LEFT/COMPLETION_TAB_RIGHT 绑定从纯 ←/→ 移到 Ctrl+←/→keyBindings.ts),使纯方向键永远不会被分类 tab 切换消费,始终用于在可编辑缓冲区中移动光标。将 tab 栏提示更新为 (Ctrl+←/→ to switch) 以保持所展示的按键准确,并刷新了相关注释。
  • 为何选修饰键而非"光标位于 @ token 末尾"的门控: 光标末尾门控无法修复常见情形——当 @ 输入在缓冲区末尾(通常情形)时,光标本就在末尾,第一次按 ← 仍会切换 tab,光标永远无法回退。修饰键方案能在所有位置稳健地恢复纯 ←/→ 的光标移动。Ctrl+←/→ 的按词移动仅在下拉框打开时被遮蔽;Alt/Option+←/→ 仍执行按词移动(不变),正如评审者所指出的。
  • 补齐测试缺口: 将 ">2 分类" 测试改为驱动 Ctrl+方向键,并新增一个测试断言当存在 >2 分类时纯 ←/→ 切换分类(即光标保持可用)。同时把 keyMatchers 的对比用例与硬编码参考匹配器更新为基于 Ctrl 的绑定。

#3#7 — 延后(后续跟进 / 细节)

依据约 5 轮策略以及评审者自身的分类延后处理。这些都不是正确性阻塞问题,也不是本 PR 引入的回归。

冲突说明

--conflict false;未合并 base 分支,也未触及任何冲突。

验证

  • npm run build — 通过
  • npm run typecheck — 通过
  • npm run lint(完整:eslint . --ext .ts,.tsx && eslint integration-tests)— 通过
  • vitest src/ui/hooks/session-completion.test.ts(cli)— 8 通过
  • vitest src/ui/keyMatchers.test.ts(cli)— 49 通过
  • vitest src/ui/components/SuggestionsDisplay.test.tsx(cli)— 9 通过
  • vitest src/ui/hooks/useCompletion.test.ts(cli)— 23 通过
  • vitest src/config/keyBindings.test.ts(cli)— 4 通过
  • vitest src/ui/components/InputPrompt.test.tsx src/ui/hooks/useAtCompletion.test.ts(cli)— 228 通过

Base-conflict check · 基分支冲突检查: no conflict with main. · 与 main 无冲突。

Re-review when you have a moment. After round 100 this bot stops and leaves the PR for a human. · 有空请复审;第 100 轮后本 bot 停止并将 PR 交给人工。


🧠 Handled by Qwen Code · model/模型 qwen3.8-max-preview

@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 a changed line and were dropped; nothing further to act on here. Not reviewed: chunk agents — launched with summarized prompts instead of the CLI-built verbatim prompts. Not reviewed: chunk 6, chunk 12, chunk 4, chunk 5, chunk 7, chunk 3, chunk 8, chunk 11, chunk 10, chunk 9, chunk 2, chunk 1 — launched with a prompt that is not the one the CLI built. Not reviewed: Test coverage matrix (whole-diff), Agent 1b: Removed-behavior audit, Agent 1c: Cross-file tracer, Agent 7: Build & test verification — its prompt was built, but no agent on record was launched with it. Not reviewed: reverse audit — an auditor ran and opened its brief, but no agent was launched with the prompt the CLI built — the launch was written by hand, and what the agent was actually asked is not what this skill certifies. Not reviewed: verification — the review posts findings, but no verifier was launched with a prompt this skill builds — they were ruled on, if at all, without the verdict bar its brief carries.

— qwen3.7-max via Qwen Code /review

Comment thread packages/cli/src/ui/components/SuggestionsDisplay.tsx Outdated
Comment thread packages/cli/src/ui/components/InputPrompt.tsx Outdated
Comment thread packages/cli/src/ui/components/InputPrompt.test.tsx
Comment thread packages/core/src/services/session-reference-service.ts Outdated
- Wrap MCP category label in t() for i18n consistency
- Fix comment to match > 2 tab guard semantics
- Add test for Ctrl+arrow with exactly 2 categories (guard boundary)
- Fix budget loop to not reserve marker tokens when session fits
@wenshao

wenshao commented Jul 24, 2026

Copy link
Copy Markdown
Collaborator

✅ Round-2 local build & real-run verification — head 74881a1e8

Following up on my earlier verifications (7d90d1fa6, c9be8c904). Since then the branch pushed exactly the two follow-ups I had left open — the per-keystroke chats listing and the ←/→ caret hijack — so this run is focused on those two changes at the current head, built from source. Both are fixed and verified in a real interactive TUI. Recommend merge.

Environment: macOS 15 (darwin arm64), Node v22.23.1. Fresh npm ci in an isolated git worktree at 74881a1e8 (no symlinked deps — a clean install so the build result is trustworthy). The TUI is driven in a real pty (@lydell/node-pty) and rendered with @xterm/headless, so every screenshot is the exact styled screen a user sees. Sessions were seeded as real chats/*.jsonl under an isolated QWEN_HOME and read back through the shipping storage layer.


1 · Build, typecheck & tests

Check Result
npm ci (clean)
npm run build (full monorepo)
npm run bundledist/cli.js
npm run typecheck ✅ 0 errors
Vitest — 11 changed/added test files 397 passed / 0 failed (CLI 375 · core 22)

Source delta since my last verify is small and surgical — 4 files, all in the completion path:
keyBindings.ts · InputPrompt.tsx · SuggestionsDisplay.tsx · session-completion.ts.


2 · Ctrl+←/→ switches tabs — plain ←/→ are free again ✅

The tab-switch binding moved from bare arrows to Ctrl+arrows (COMPLETION_TAB_LEFT/RIGHT), and InputPrompt now only consumes the Ctrl chord. I drove this in the real TUI with 5 seeded sessions. The highlighted pill is the active tab; the filtered list is the oracle for which tab is active.

Plain moves the caret and does not switch the tab — note the caret block moving @re▊@r▊e while the All pill stays put:

Type @re → All tab Press plain
all plain-left

Ctrl+→ does switchAll → Files → Sessions (list filters accordingly, pill moves):

Ctrl+→ → Files Ctrl+→ → Sessions
files sessions

Ctrl+← switches back symmetrically. No binding collision: Ctrl+←/→ was previously unbound (word-movement is Alt+arrows, Home/End are Ctrl+A/Ctrl+E), so this is additive. The two new unit tests (should switch category on Ctrl+left/right…, should NOT switch category on plain left/right…) both pass and match the observed behavior.


3 · Per-cwd TTL cache removes the per-keystroke disk walk ✅

getSessionSuggestions now caches the unfiltered listing per cwd for 3 s (SESSION_LIST_CACHE_TTL_MS); pattern filtering still runs fresh on every keystroke. I measured this against the real built getSessionSuggestions by wrapping node:fs (the tsc build keeps the import fs live binding), simulating a 6-keystroke @session:auth burst — inside the TTL (the fix) vs. spread past it (the pre-PR behavior, where every keystroke re-walked):

cache benchmark

Burst of 6 keystrokes disk walks total fs calls event-loop blocking
5 sessions — before → after 6 → 1 156 → 26 1.19 → 0.48 ms
200 sessions — before → after 6 → 1 1686 → 281 8.16 → 1.80 ms

statSync scales with the total session count (the mtime sort stats every file), so the win grows with history size. The three new cache tests (hit within TTL / re-list after expiry / per-cwd keying) pass; the injected nowMs clock makes them deterministic.


4 · Standing items (non-blocking)

  • i18n gap (pre-existing, now one key larger). With QWEN_CODE_LANG=zh the tab bar reads All Files 会话数 (Ctrl+←/→ to switch)All / Files / (Ctrl+←/→ to switch) have no entry in any of the 9 locales (including en.js), and t('Sessions') collides with the stats-panel key 会话数 (= session count, wrong sense here). The per-row badge 会话 is correct. npm run check-i18n passes because it gates unused/extra keys, not missing ones. Low severity, easy follow-up.

    zh i18n

  • macOS caveat for the Ctrl+←/→ chord. On stock macOS these are the Mission Control "Move left/right a space" shortcuts (enabled by default on this machine). In a real terminal the OS can intercept them before the CLI sees them, so tab-switching may appear inert for users on multi-Space setups. My pty harness injects the raw ESC[1;5D/C bytes, which tests the CLI's handling directly and correctly. Worth a docs line; not a code blocker.

  • Nit: listingCache (Map<cwd, …>) has no eviction. cwd cardinality per session is tiny, so this is negligible — noting for completeness.


Verdict

The two items I flagged last round are both resolved and behave correctly in a real interactive session, tests are green, and the build/bundle/typecheck are clean from a fresh install. Merge-ready. The i18n and macOS-chord notes are non-blocking follow-ups.

🔍 Methodology (reproducibility)
  • Isolated git worktree at 74881a1e8; fresh npm ci (no symlinked node_modules) so build/typecheck/test results are not contaminated by the donor tree. (Earlier symlinked attempts produced spurious cross-package TS2307/TS2353 errors — environmental, from node_modules/@qwen-code/* resolving to the donor packages, not this PR.)
  • Real CLI: node --expose-gc dist/cli.js driven directly in a pty (bypassing the cli-entry.js re-spawn so the preload + signals target one PID); @xterm/headless renders the styled cell buffer, serialized to HTML → Playwright PNG at 2× so screenshots preserve the active-tab highlight and caret.
  • Sessions seeded as genuine chats/<uuid>.jsonl (user + assistant + custom_title system record) under a temp QWEN_HOME, mtimes set explicitly; the project dir is keyed off the CLI's resolved cwd (/private/tmp… on macOS, not /tmp…).
  • Cache A/B: node:fs wrapped in-process around the built getSessionSuggestions; withCache = 6 calls with nowMs 40 ms apart (inside the 3 s TTL), noCache = 6 calls 5 s apart (each past the TTL = pre-PR behavior). Counters isolate /chats paths only.
  • i18n oracle: QWEN_CODE_LANG=zh (beats the OS-locale path); locale keys resolved by importing each locales/*.js module.
中文版本(简体)

✅ 第二轮本地构建与真机验证 — head 74881a1e8

延续我之前在 7d90d1fa6c9be8c904 的验证。此后分支恰好推送了我上一轮遗留的两个待办项 —— 每次按键都重新遍历会话目录、以及 ←/→ 抢占光标移动。本轮在当前 head 从源码构建,聚焦这两处改动。两项均已修复并在真实交互式 TUI 中验证通过,建议合并。

环境: macOS 15(darwin arm64),Node v22.23.1。在隔离的 git worktree 中对 74881a1e8 执行了干净的 npm ci(无软链依赖,确保构建结果可信)。TUI 在真实 pty 中驱动,用 @xterm/headless 渲染,因此每张截图都是用户实际看到的带样式屏幕。会话以真实的 chats/*.jsonl 预置在隔离的 QWEN_HOME 下,并经由线上存储层读回。

1 · 构建 / 类型检查 / 测试npm cinpm run build(整仓)、npm run bundlenpm run typecheck(0 错误) 全部通过;11 个改动/新增测试文件 397 通过 / 0 失败(CLI 375 · core 22)。改动很小:仅 keyBindings.tsInputPrompt.tsxSuggestionsDisplay.tsxsession-completion.ts 四个文件。

2 · Ctrl+←/→ 切换分类页,普通 ←/→ 恢复为光标移动 ✅:切换绑定从裸方向键改为 Ctrl+方向键。真机验证:按普通 时光标块从 @re▊ 移到 @r▊e,而 All 高亮页不变(证明普通方向键只移动光标、不切页);按 Ctrl+→ 则依次切换 All → Files → Sessions(列表随之过滤)。Ctrl+←/→ 此前未被占用(词移动是 Alt+方向键,Home/End 是 Ctrl+A/Ctrl+E),无冲突。两个新单测均通过。

3 · 按 cwd 的 TTL 缓存消除逐键磁盘遍历 ✅:getSessionSuggestions 现按 cwd 缓存未过滤列表 3 秒,过滤仍每次按键重新执行。针对真实构建产物测量一次 6 次按键的 @session:auth 输入:在 TTL 内(修复后)对比拉长到 TTL 外(PR 前行为,每键都遍历)—— 磁盘遍历 6 → 1 次;200 个会话时 fs 调用 1686 → 281、事件循环阻塞 8.16 → 1.80 msstatSync 随会话总数增长,历史越多收益越大。三个新缓存测试通过。

4 · 遗留项(不阻塞)

  • i18n 缺口(既有问题,本次新增一个键):QWEN_CODE_LANG=zh 下分类栏为 All Files 会话数 (Ctrl+←/→ to switch) —— All/Files/(Ctrl+←/→ to switch) 在全部 9 个语言包(含 en.js)中都无对应键;t('Sessions') 与统计面板的 会话数(意为会话数量)撞键。逐行徽标 会话 是对的。check-i18n 只校验多余/未用键、不校验缺失键,故仍通过。严重度低。
  • macOS 提示Ctrl+←/→ 在 macOS 默认是"移动到左/右一个空间"的 Mission Control 快捷键(本机默认开启),真实终端下系统可能先截获。我的 pty 直接注入 ESC[1;5D/C 原始字节,验证的是 CLI 本身的处理逻辑。建议文档提一句,非代码阻塞项。
  • 小提示listingCache 无淘汰机制;单会话 cwd 数量极少,可忽略。

结论:上一轮的两个待办项均已解决且真机行为正确,测试全绿,干净安装下构建/打包/类型检查通过。可以合并。 i18n 与 macOS 快捷键为非阻塞后续项。

wenshao
wenshao previously approved these changes Jul 24, 2026
@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

🤖 Addressed the latest review feedback (round 8/100). What changed, and what I pushed back on: · 已处理最新评审反馈(第 8/100 轮)。改动内容与我反驳保留之处如下:

Review feedback addressed

Inline comments (automated reviewer — all Suggestion-level)

rc id File Finding Decision Change
rc:3642667343 SuggestionsDisplay.tsx:59 'MCP' is the only categoryLabel case not wrapped in t() Implemented Wrapped in t('MCP') for i18n consistency with the other four labels
rc:3642667357 InputPrompt.tsx:1404 Comment says "more than one tab" but the guard checks > 2 (at least 3 entries) Implemented Updated comment to "more than two tabs (at least 3 entries including 'all')"
rc:3642667359 InputPrompt.test.tsx:2560 Test name claims to verify the > 2 threshold but sends plain arrows which never match COMPLETION_TAB_* (Ctrl+arrows) — the guard is never exercised Implemented Added a new test that sends Ctrl+arrow sequences (\x1b[1;5C / \x1b[1;5D) with availableCategories: ['all', 'file'] (length 2) and asserts switchCategory is NOT called — properly exercises the > 2 guard boundary
rc:3642667362 session-reference-service.ts:79 Budget loop reserves tokens for the [earlier turns omitted] marker even when the session fits entirely within budget, causing unnecessary truncation in a ~5-token window Implemented Split overhead into headerCost (header only) for the budget loop; compute full overhead (header + marker) only after the truncation decision. Simplified approxTokens to use the pre-computed overhead

Issue-level comment (@wenshao — local build & real-run verification)

Finding Status Notes
6.1 · Per-keystroke chats-dir listing Already fixed in current head session-completion.ts already has a per-cwd TTL cache (SESSION_LIST_CACHE_TTL_MS = 3000) with tests for cache hit, TTL expiry, and per-cwd isolation. The reviewer's report was at head 37b9d99b9; the cache was restored in a later commit on this branch
6.2 · ←/→ stop moving the caret Already fixed in current head Tab switching uses COMPLETION_TAB_LEFT/RIGHT bound to Ctrl+arrows (ctrl: true in keyBindings.ts). Plain arrows are never consumed. Tests verify both: Ctrl+arrows switch tabs when >2 categories, and plain arrows do NOT switch tabs
7.1 · Session search limited to 20 most recent Deferred (non-blocking follow-up) Requires a design decision on page size vs. server-side filtering
7.2 · Tab-bar strings partly untranslated Deferred (non-blocking follow-up) Requires adding i18n keys across all locale files; the t('MCP') fix in this commit is a step toward consistency
7.3 · Smaller notes Deferred (non-blocking follow-up) maxBytes guard on loadSession, title-based @session: UX, MCP drill-in category — all reasonable follow-ups

Verification

  • npm run build — passed
  • npm run typecheck — passed
  • npm run lint — passed
  • vitest packages/core session-reference-service.test.ts — 22 passed
  • vitest packages/cli SuggestionsDisplay.test.tsx — 9 passed
  • vitest packages/cli InputPrompt.test.tsx — 193 passed (includes the new guard-boundary test)
  • vitest packages/cli session-completion.test.ts — 8 passed
中文说明

已处理的评审反馈

行内评论(自动评审器 — 均为 Suggestion 级别)

rc id 文件 发现 决定 变更
rc:3642667343 SuggestionsDisplay.tsx:59 'MCP'categoryLabel 中唯一未用 t() 包裹的分支 已实现 包裹为 t('MCP'),与其余四个标签保持 i18n 一致性
rc:3642667357 InputPrompt.tsx:1404 注释写的是"more than one tab",但守卫检查的是 > 2(至少 3 个条目) 已实现 将注释更新为"more than two tabs (at least 3 entries including 'all')"
rc:3642667359 InputPrompt.test.tsx:2560 测试名称声称验证 > 2 阈值守卫,但发送的是普通方向键,永远不会匹配 COMPLETION_TAB_*(Ctrl+方向键)——守卫从未被执行 已实现 新增测试:发送 Ctrl+方向键序列(\x1b[1;5C / \x1b[1;5D),availableCategories: ['all', 'file'](长度为 2),断言 switchCategory 未被调用——正确覆盖了 > 2 守卫的边界
rc:3642667362 session-reference-service.ts:79 预算循环即使会话完全在预算内也会为 [earlier turns omitted] 标记预留 token,在约 5 token 的窗口内导致不必要的截断 已实现 将开销拆分为 headerCost(仅头部)用于预算循环;在截断决定之后才计算完整 overhead(头部 + 标记)。简化 approxTokens 直接使用预计算的 overhead

Issue 级评论(@wenshao — 本地构建与真实运行验证)

发现 状态 说明
6.1 · 每次按键都同步列举 chats 目录 当前 head 已修复 session-completion.ts 已包含按 cwd 的 TTL 缓存(SESSION_LIST_CACHE_TTL_MS = 3000),并带有缓存命中、TTL 过期和按 cwd 隔离的测试。评审者的报告基于 head 37b9d99b9;缓存在本分支的后续提交中已恢复
6.2 · ←/→ 不再移动光标 当前 head 已修复 Tab 切换使用绑定到 Ctrl+方向键的 COMPLETION_TAB_LEFT/RIGHTkeyBindings.tsctrl: true)。普通方向键从不被拦截。测试验证了两点:Ctrl+方向键在 >2 个分类时切换 tab,普通方向键不切换 tab
7.1 · 会话搜索仅限最近 20 个 延后(非阻塞后续) 需要关于分页大小与服务端过滤的设计决策
7.2 · Tab 栏文案部分未翻译 延后(非阻塞后续) 需要在所有语言文件中添加 i18n key;本次提交中的 t('MCP') 修复是朝一致性迈出的一步
7.3 · 其余小问题 延后(非阻塞后续) loadSessionmaxBytes 守卫、基于标题的 @session: 体验、MCP 下钻分类——均为合理的后续跟进

验证

  • npm run build — 通过
  • npm run typecheck — 通过
  • npm run lint — 通过
  • vitest packages/core session-reference-service.test.ts — 22 通过
  • vitest packages/cli SuggestionsDisplay.test.tsx — 9 通过
  • vitest packages/cli InputPrompt.test.tsx — 193 通过(包含新增的守卫边界测试)
  • vitest packages/cli session-completion.test.ts — 8 通过

Base-conflict check · 基分支冲突检查: no conflict with main. · 与 main 无冲突。

Re-review when you have a moment. After round 100 this bot stops and leaves the PR for a human. · 有空请复审;第 100 轮后本 bot 停止并将 PR 交给人工。


🧠 Handled by Qwen Code · model/模型 qwen3.8-max-preview

@wenshao

wenshao commented Jul 24, 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.

Reviewed. Not reviewed: chunk 8, chunk 6, chunk 7, chunk 10, chunk 12, chunk 4, chunk 9, chunk 5, chunk 1, chunk 11, chunk 2, chunk 3 — launched with a prompt that is not the one the CLI built. Not reviewed: Agent 0: Issue fidelity & root-cause ownership, Test coverage matrix (whole-diff), Agent 1b: Removed-behavior audit, Agent 1c: Cross-file tracer, Agent 7: Build & test verification — its prompt was built, but no agent on record was launched with it. Not reviewed: reverse audit — no auditor was launched with a prompt this skill builds — the pass that hunts what the rest of the review missed ran, if at all, without the method its brief carries.

— qwen3.7-max via Qwen Code /review

@wenshao

wenshao commented Jul 24, 2026

Copy link
Copy Markdown
Collaborator

@qwen-code /triage

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Code Review

Independent proposal: For "reference prior sessions via @", I would add a core service that loads a session and produces a deterministic slimmed text block (no LLM), integrate it into the existing atCommandProcessor resolution pipeline (alongside file/MCP/extension refs), and add session suggestions to the @ completion flow. The category tabs are an additional UX layer I'd probably defer to a follow-up.

Comparison with the diff: The PR's approach matches this closely and is well-executed. The SessionReferenceService is clean — single-pass O(N) tail-retention, proper handling of tool summaries (response-side only, avoiding duplicates), and title derivation that respects custom titles. The integration into atCommandProcessor follows the established pattern for MCP/extension refs exactly. The concurrent session listing in useAtCompletion (kicked off alongside file search, awaited only at assembly time) is a good pattern that avoids blocking.

No critical blockers found. The code is correct, follows project conventions, and integrates cleanly with the existing architecture.

Observations (non-blocking):

  1. The docs/superpowers/ directory (1329 lines) is not a conventional location — the project uses docs/design/ and docs/plans/. These read like AI-generated planning artifacts and probably shouldn't ship in the repo.
  2. The module-level listingCache in session-completion.ts is never explicitly cleaned up. Fine for a CLI (one process per session), but worth noting.
  3. The SessionReferenceService creates a new SessionService per instantiation, and atCommandProcessor creates a new SessionReferenceService per resolution. Consistent with how other services are used in that file, but slightly wasteful for multiple @session: refs in one prompt.

Test Results

Unit tests — all pass (398 tests across 8 files):

packages/core:
 ✓ src/services/session-reference-service.test.ts (22 tests) 10ms

packages/cli:
 ✓ src/ui/hooks/session-mention-ref.test.ts (6 tests) 4ms
 ✓ src/ui/hooks/session-completion.test.ts (8 tests) 6ms
 ✓ src/ui/hooks/atCommandProcessor.session.test.ts (8 tests) 14ms
 ✓ src/ui/hooks/useCompletion.test.ts (23 tests) 52ms
 ✓ src/ui/hooks/useAtCompletion.test.ts (36 tests) 2706ms
 ✓ src/ui/components/InputPrompt.test.tsx + SuggestionsDisplay.test.tsx + keyMatchers.test.ts + others (295 tests)

 Test Files  8 passed (8)
      Tests  398 passed (398)

Typecheck: Clean (both packages/core and packages/cli).

E2E verification (test-script fallback): The Ink TUI input prompt does not render in this CI environment's tmux sessions (alternate screen buffer issue), so interactive @ completion could not be captured visually. As a fallback, I exercised the SessionReferenceService programmatically against real session files:

=== PR #7302 Session Reference E2E Verification ===

--- Step 1: List sessions ---
Found 1 sessions:
  - 6662dbf1-83b1-41f4-abd8-5835bdba3701 | title: TERM=xterm-256color node packages/cli/dist/index.js --no-update-check

--- Step 2: Resolve session reference ---
✅ Resolved successfully:
  Title: TERM=xterm-256color node packages/cli/dist/index.js --no-update-check
  Message count: 13
  Approx tokens: 2413
  Truncated: false
  Text preview: --- Referenced session "..." (slimmed, read-only) ---

--- Step 3: Resolve with small budget (100 tokens) ---
  Truncated: true
  ✅ Truncation works correctly with small budget

--- Step 4: Resolve non-existent session ---
  ✅ Correctly returns notFound for non-existent session

=== Verification complete ===
Files changed (26 of 26 shown)
File What changed
docs/superpowers/plans/2026-07-17-at-session-reference.md Implementation plan (1136 lines, AI-generated)
docs/superpowers/specs/2026-07-17-at-session-reference-design.md Design spec (193 lines, AI-generated)
packages/core/src/services/session-reference-service.ts New service: loads session, deterministic slimming with token budget
packages/core/src/services/session-reference-service.test.ts 22 tests covering resolve, truncation, title derivation, tool summaries
packages/core/src/index.ts Export the new service
packages/cli/src/ui/hooks/session-completion.ts Session listing with TTL cache for @ completion suggestions
packages/cli/src/ui/hooks/session-completion.test.ts 8 tests for session suggestion filtering and caching
packages/cli/src/ui/hooks/session-mention-ref.ts Parse/build session: refs, UUID detection
packages/cli/src/ui/hooks/session-mention-ref.test.ts 6 tests for ref parsing
packages/cli/src/ui/hooks/atCommandProcessor.ts Session ref resolution in the @ pipeline (title lookup, dedup, error cards)
packages/cli/src/ui/hooks/atCommandProcessor.session.test.ts 8 tests for session resolution in the processor
packages/cli/src/ui/hooks/useCompletion.ts Category tab state: availableCategories, activeCategory, switchCategory
packages/cli/src/ui/hooks/useCompletion.test.ts 23 tests including category tab cycling and edge cases
packages/cli/src/ui/hooks/useAtCompletion.ts Concurrent session suggestion fetch alongside file search
packages/cli/src/ui/hooks/useAtCompletion.test.ts 36 tests covering MCP, file, and session suggestion merging
packages/cli/src/ui/hooks/useCommandCompletion.tsx Pass through category state from useCompletion
packages/cli/src/ui/components/SuggestionsDisplay.tsx Tab bar rendering, defensive category filter
packages/cli/src/ui/components/SuggestionsDisplay.test.tsx Tests for tab bar visibility
packages/cli/src/ui/components/InputPrompt.tsx Ctrl+arrow key handling for tab switching (gated behind 2+ categories)
packages/cli/src/ui/components/InputPrompt.test.tsx Tests for category switching keybindings
packages/cli/src/config/keyBindings.ts COMPLETION_TAB_LEFT/RIGHT bound to Ctrl+arrows
packages/cli/src/ui/keyMatchers.test.ts Tests for new key matchers
packages/cli/src/ui/utils/suggestions.ts SuggestionCategory type + category field on Suggestion
packages/cli/src/ui/hooks/useExportCompletion.test.ts Minor: assert activeCategory in existing test
packages/cli/src/ui/hooks/extension-mention-ref.test.ts Minor: assert activeCategory in existing test
packages/cli/src/ui/hooks/extension-mention-ref.ts Minor: add category field to extension suggestions
中文说明

代码审查

独立方案: 对于"通过 @ 引用历史会话",我会添加一个核心服务来加载会话并生成确定性压缩文本块(无 LLM),将其集成到现有的 atCommandProcessor 解析管道中(与文件/MCP/扩展引用并列),并在 @ 补全流程中添加会话建议。分类标签是我可能会推迟到后续 PR 的额外 UX 层。

与 diff 的比较: PR 的方案与此高度吻合且执行良好。SessionReferenceService 很干净——单遍 O(N) 尾部保留、正确处理工具摘要(仅响应侧,避免重复)、以及尊重自定义标题的标题派生。集成到 atCommandProcessor 完全遵循了 MCP/扩展引用的既有模式。useAtCompletion 中的并发会话列表(与文件搜索同时启动,仅在组装时 await)是一个避免阻塞的好模式。

未发现关键阻塞问题。 代码正确,遵循项目惯例,与现有架构干净集成。

观察(非阻塞):

  1. docs/superpowers/ 目录(1329 行)不是常规位置——项目使用 docs/design/docs/plans/。这些读起来像 AI 生成的规划产物,可能不应该提交到仓库。
  2. session-completion.ts 中的模块级 listingCache 没有显式清理。对 CLI 来说没问题(每个会话一个进程),但值得注意。
  3. SessionReferenceService 每次实例化都创建新的 SessionServiceatCommandProcessor 每次解析都创建新的 SessionReferenceService。与该文件中其他服务的使用方式一致,但对一个提示中多个 @session: 引用略显浪费。

测试结果

单元测试——全部通过(8 个文件 398 个测试)。

类型检查: 干净。

E2E 验证(测试脚本回退): Ink TUI 输入提示在此 CI 环境的 tmux 会话中不渲染(备用屏幕缓冲区问题),因此无法可视化捕获交互式 @ 补全。作为回退,通过编程方式对真实会话文件执行了 SessionReferenceService 验证,确认解析、截断和 notFound 行为正确。

Qwen Code · qwen3.8-max-preview

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

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Triage re-run completed without a new review.

The stage comments above were updated with the latest result. View workflow run.

@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

🤖 Reviewed the latest feedback — no changes needed. Why, point by point: · 已审阅最新反馈——无需改动。逐点说明原因如下:

No action required — Round 3

All feedback in this round is informational or explicitly non-blocking:

  1. @qwen-code-ci-bot review (COMMENTED): The automated reviewer noted infrastructure issues (chunks/agents not launched with the correct prompt). No code findings were produced — nothing to address.

  2. @wenshao verification (issue-level comment): A thorough round-2 real-TUI verification at head 74881a1e8 confirms both previously flagged items (per-keystroke disk walk, bare ←/→ caret hijack) are resolved. Build, typecheck, bundle, and 397 tests all pass from a clean install. Verdict: merge-ready.

  3. Non-blocking standing items (explicitly deferred to follow-ups by the reviewer):

    • i18n gap for tab-bar labels (All, Files, hint text) — pre-existing, low severity
    • macOS Ctrl+←/→ Mission Control conflict — docs note, not a code change
    • listingCache has no eviction — negligible cardinality, noted for completeness

No inline comments require a response. No checks are failing.

中文说明

无需操作 — 第 3 轮

本轮所有反馈均为信息性或明确标记为不阻塞:

  1. @qwen-code-ci-bot 评审(COMMENTED): 自动评审器报告了基础设施问题(代码块/代理未使用正确的提示词启动)。未产生任何代码层面的发现——无需处理。

  2. @wenshao 验证(issue 级评论): 在 head 74881a1e8 上进行了完整的第二轮真实 TUI 验证,确认此前标记的两项问题(逐键磁盘遍历、裸 ←/→ 抢占光标)均已修复。干净安装下构建、类型检查、打包及 397 个测试全部通过。结论:可以合并。

  3. 非阻塞遗留项(评审者明确归为后续跟进):

    • 分类栏标签(AllFiles、提示文字)的 i18n 缺口——既有问题,严重度低
    • macOS Ctrl+←/→ 与 Mission Control 冲突——建议文档补充,非代码变更
    • listingCache 无淘汰机制——基数极小,仅作完整性记录

无内联评论需要回复。无失败检查项。

Base-conflict check · 基分支冲突检查: no conflict with main. · 与 main 无冲突。


🧠 Handled by Qwen Code · model/模型 qwen3.8-max-preview

@tanzhenxin tanzhenxin 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!

@wenshao
wenshao added this pull request to the merge queue Jul 24, 2026
Merged via the queue into QwenLM:main with commit 7ede46c Jul 24, 2026
60 checks passed
@@ -608,6 +639,154 @@ export async function resolveAtCommandQuery({
});
}

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 4 session error paths (~640-780) each repeat ~20 lines of scopedMentionEntries.push({ ... status: Error }). Consider extracting pushSessionErrorCard(originalAtPath, ref, callId, reason) to reduce ~60 lines of duplication.

— qwen3.8-max-preview via Qwen Code /review

// Cache the UNFILTERED listing keyed by cwd; pattern filtering is cheap and
// always applied fresh below.
const listingCache = new Map<string, CacheEntry>();

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] listingCache is an unbounded Map keyed by cwd. In a long-running CLI process where the user works across multiple projects, this grows without limit. Consider an LRU cap or periodic cleanup. Low practical impact but worth noting.

— qwen3.8-max-preview via Qwen Code /review

@yiliang114 yiliang114 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. Clean architecture with SessionReferenceService well separated from UI. Token budget trimming uses efficient single-pass tail-retention. Security handled by sessionBelongsToCurrentProject. Two inline suggestions on error card duplication and unbounded listing cache.

— qwen3.8-max-preview via Qwen Code /review

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

Labels

autofix/takeover Summon the autofix loop to manage this PR (remove to release; needs triage+)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants