feat(cli): reference prior sessions via @ and add completion tabs - #7302
Conversation
…n under budget, guard stale session results
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.
…ting" This reverts commit 08bbbc2.
# Conflicts: # packages/cli/src/ui/components/SuggestionsDisplay.tsx
|
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 Direction: Aligned. Session management is on the project roadmap ( Size: Core paths touched ( 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:
Moving on to code review. 🔍 中文说明感谢贡献! 模板完整 ✓ 问题:这是一个功能新增,不是 bug 修复——动机很清晰:用户目前必须恢复或 fork 会话才能复用其上下文,而这个 PR 添加了一个轻量的 方向:对齐。会话管理在项目路线图上(存在 规模:触及核心路径( 方案:范围看起来是一个完整的垂直切片——服务、处理器、补全 hook、键盘绑定、渲染和鼠标连接都是这个功能端到端工作所必需的。两点观察:
进入代码审查 🔍 — Qwen Code · qwen3.8-max-preview Reviewed at |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
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
|
@qwen-code /takeover |
|
🤝 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 循环现在管理此 PR —— 将持续处理新的评审反馈与 base 冲突,直到移除标签或达到轮次上限。移除 |
|
🤖 Addressed the latest review feedback (round 1/100). What changed, and what I pushed back on: Review feedback addressedAll 3 suggestion-level findings were implemented. 1.
|
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
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
Review —
|
| 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.findSessionsByTitleis a case-insensitive exact match oncustomTitleonly. It never matches the first-prompt fallback — but the picker's label iss.customTitle?.trim() || s.prompt || s.sessionId. A user who reads "fix the auth bug" in the dropdown and types@session:fix the auth buggets "No session matches".parseAllAtCommands(atCommandProcessor.ts:132) terminates an@token at the first unescaped whitespace or,;!?()[]{}. So@session:Fix auth bugparses as titleFixregardless — 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: '' }intoscopedMentionEntries, which flows intoprocessedQueryParts; every other producer in this file pushes non-empty text. They also add the failed ref toscopedMentionLabels, sofilesRead/recording.filesReadrecord 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 !== nullearly return (useAtCompletion.ts:423) dispatches without acategory, 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 newSEARCH_SUCCESSarrives with no sessions,SuggestionsDisplayreturnsnullfor the render before the reset-to-'all'effect commits. Cosmetic. - Docs.
@server:uriis documented indocs/users/common-workflow.md:321anddocs/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|specsis an established location onmain, so those files themselves aren't scope creep.)
What checks out
- Cross-project scope holds.
loadSessionFromStategates onsessionBelongsToCurrentProjectbefore returning, so a UUID copied from another project resolves tonotFoundand produces an error card — no transcript leak across projects. Verified in source, not just assumed from the plan doc. - No path traversal.
loadSessiondoes not validateSESSION_FILE_PATTERN(unlikeloadArchivedSession/getSessionTitle), but only UUID-shaped ids fromparseSessionRefor real on-disk ids fromfindSessionsByTitleever reach it. The UUID regex is load-bearing here — worth a comment atparseSessionRefso a future relaxation doesn't silently open a traversal. - Tool result bodies never injected — summaries derive from
functionResponsenames +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 filteredsuggestionsarray thatSuggestionsDisplayrenders, 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:1402 在 availableCategories.length > 2 时拦截 ←/→,而裸 @ 下文件类别恒存在、只要项目有 ≥1 个历史会话就会有 session 类别 —— 于是 tab 栏与 ←/→ 拦截成为 @ 补全的默认状态。本 PR 之前,多类别需要配置 MCP 或扩展才会出现,所以这个行为变化比"只有存在多个补全分类时"的描述要广得多。后果是:下拉框打开时 ← 无法回退光标去修正前面的笔误,必须先按 Esc。已有的 tab 切换先例(PermissionsDialog、AskUserQuestionDialog)都是没有可编辑缓冲区的模态框,不构成从输入框夺取方向键的先例。建议:仅在光标位于 @ token 末尾时拦截,或改用修饰键(Ctrl+←/→)。修饰键守卫本身是正确的:command: false 在 matchKeyBinding 中映射到 key.meta,因此 Alt/Option+← 仍然是按词左移。测试缺口:should NOT switch category… 只断言 switchCategory 未被调用,没有断言光标确实移动了;也没有测试覆盖 >2 类别时 ←/→ 被阻止移动光标,以及 tab 栏出现时 Alt+← 仍然按词移动。
3 · 会话搜索只能看到最近 20 个会话。 getSessionSuggestions 先分页(size: MAX_SESSION_SUGGESTIONS)再过滤,因此输入 @auth 无法命中排在第 25 位的 "auth refactor"。应先取更大的分页(或把过滤下推到列举层)。同一函数还有:过滤只匹配 label 和 description,从不匹配 sessionId —— 一旦选择器插入了 @session:<uuid>,在该 token 上重新触发补全会匹配为空,Sessions 分类随之消失。
4 · @session:<title> 在实践中几乎不可用,而错误提示还在引导用户去用它。 两个约束叠加:findSessionsByTitle 只对 customTitle 做大小写不敏感的精确匹配,从不匹配"首条 prompt"回退,但选择器的 label 是 customTitle || prompt || sessionId;同时 parseAllAtCommands(atCommandProcessor.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.js、ja.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:uri 在 docs/users/common-workflow.md:321 和 docs/users/features/mcp.md 中有说明,而 @session: 只出现在内部 plan/spec 中,建议补一小节说明 8k 预算、只读语义和仅限当前项目的作用域。(docs/superpowers/plans|specs 在 main 上已是既有位置,这两个文件本身不算 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 正确区分;索引对齐一致 —— handleAutocomplete、navigateUp/Down 与鼠标 hover/select 都作用于 SuggestionsDisplay 渲染的同一个已过滤数组,分类过滤不会导致选中错位;错误处理确实周全 —— 每种会话失败模式都会给出卡片并保留字面 token,UUID 与标题两种形式解析到同一会话时的去重也已处理。
结论: 没有发现正确性阻塞问题。建议合并前处理 #1 与 #2 —— 两者都影响每个用户的每次按键,而 #1 的修复代码已经写好并带测试,就躺在本分支的历史里。#3–#6 可作为后续跟进。
Local build & real-run verification — 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 |
|
| 5 | ←/→ stop moving the caret whenever ≥2 categories are present | |
| 6 | Session search only sees the 20 most recent sessions | |
| 7 | New tab-bar strings untranslated |
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: 30 → truncated: 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.
→ → narrows to Sessions, each row badged Session:
Accepting inserts @session:<uuid> — the UUID, not the visible label:
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:
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:
| 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:
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):
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:
Control: nothing changed except that session's mtime (making it the most recent). Same project, same needle:
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):
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;loadArchivedSessionalready takes amaxBytesguard,loadSessiondoes not.@session:<title>remains close to unusable by hand —findSessionsByTitleis a case-insensitive exact match oncustomTitleonly, andparseAllAtCommandsterminates 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 上第一次「构建 + 真实运行」验证 —— 我此前的构建验证分别在 7d90d1fa6 与 c9be8c904,最近一次评审 则是源码阅读加微基准。自 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 目录 |
|
| 5 | 只要有 ≥2 个分类,←/→ 就不再移动光标 | |
| 6 | 会话搜索只能看到最近 20 个会话 | |
| 7 | 新增 tab 栏文案未翻译 |
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: 30 → truncated: 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:1403 在 availableCategories.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 个、再过滤,因此更早的会话无法被搜索触及。应改为取更大分页,或把过滤下推到列举层。
同一函数的相关问题:过滤只匹配 label 与 description,从不匹配 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)' 已存在且已翻译(zh、zh-TW、ca) |
t('Sessions') |
能解析 —— 但命中的是统计面板的 key:zh 会话数、zh-TW 會話數、ja セッション数,意为「会话数量」,作为分类标签是错的 |
t('Session')(徽标) |
仅 en、zh、zh-TW、ca |
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.1 与 6.2 —— 两者都影响每个用户的每次按键,而 6.1 的修复代码就躺在本分支自己的历史里、且已带测试。§7 各项可作为后续跟进。
|
🤖 Addressed the latest review feedback (round 7/100). What changed, and what I pushed back on: · 已处理最新评审反馈(第 7/100 轮)。改动内容与我反驳保留之处如下: Review feedback addressed on PR #7302Addressed the two findings the reviewer (@wenshao) flagged as wanted-before-merge #1 —
|
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
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
- 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
|
🤖 Addressed the latest review feedback (round 8/100). What changed, and what I pushed back on: · 已处理最新评审反馈(第 8/100 轮)。改动内容与我反驳保留之处如下: Review feedback addressedInline comments (automated reviewer — all Suggestion-level)
Issue-level comment (@wenshao — local build & real-run verification)
Verification
中文说明已处理的评审反馈行内评论(自动评审器 — 均为 Suggestion 级别)
Issue 级评论(@wenshao — 本地构建与真实运行验证)
验证
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/模型 |
|
@qwen-code /triage |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
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
|
@qwen-code /triage |
Code ReviewIndependent 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 Comparison with the diff: The PR's approach matches this closely and is well-executed. The No critical blockers found. The code is correct, follows project conventions, and integrates cleanly with the existing architecture. Observations (non-blocking):
Test ResultsUnit tests — all pass (398 tests across 8 files): Typecheck: Clean (both 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 Files changed (26 of 26 shown)
中文说明代码审查独立方案: 对于"通过 @ 引用历史会话",我会添加一个核心服务来加载会话并生成确定性压缩文本块(无 LLM),将其集成到现有的 与 diff 的比较: PR 的方案与此高度吻合且执行良好。 未发现关键阻塞问题。 代码正确,遵循项目惯例,与现有架构干净集成。 观察(非阻塞):
测试结果单元测试——全部通过(8 个文件 398 个测试)。 类型检查: 干净。 E2E 验证(测试脚本回退): Ink TUI 输入提示在此 CI 环境的 tmux 会话中不渲染(备用屏幕缓冲区问题),因此无法可视化捕获交互式 — Qwen Code · qwen3.8-max-preview Reviewed at |
|
Triage re-run completed without a new review. The stage comments above were updated with the latest result. View workflow run. |
|
🤖 Reviewed the latest feedback — no changes needed. Why, point by point: · 已审阅最新反馈——无需改动。逐点说明原因如下: No action required — Round 3All feedback in this round is informational or explicitly non-blocking:
No inline comments require a response. No checks are failing. 中文说明无需操作 — 第 3 轮本轮所有反馈均为信息性或明确标记为不阻塞:
无内联评论需要回复。无失败检查项。 Base-conflict check · 基分支冲突检查: no conflict with main. · 与 main 无冲突。 🧠 Handled by Qwen Code · model/模型 |
| @@ -608,6 +639,154 @@ export async function resolveAtCommandQuery({ | |||
| }); | |||
| } | |||
There was a problem hiding this comment.
[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>(); | ||
|
|
There was a problem hiding this comment.
[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
left a comment
There was a problem hiding this comment.
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

















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
@, and confirm that multiple result categories appear and left/right switches between them.@session:<title>reference and confirm that the literal token remains in the prompt while an error card explains the resolution failure.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
Environment (optional)
Node.js 22.23.1 in a local macOS worktree with sandboxing disabled.
Risk & Scope
@now starts an asynchronous current-project session listing, and each accepted reference can add up to roughly 8k tokens of context.@latency optimization, and manual Windows/Linux verification.Linked Issues
N/A
中文说明
本 PR 做了什么
在交互式
@提及补全中加入当前项目的历史会话。选择会话后会插入@session:<id>引用,并注入一份确定性生成的只读会话摘要:保留用户与助手的可见文本,将工具结果压缩为单行状态摘要,并在 8k token 预算内优先保留最新内容。补全下拉框按全部、文件、会话、MCP 和扩展分类。只有存在多个补全分类时,左右方向键才会切换分类,因此其他场景下的正常光标移动行为保持不变。
为什么需要
目前用户如果想复用早期会话的上下文,只能恢复或 fork 那个会话。这个改动在保持当前对话独立的同时,提供了一个轻量引用入口,并让相关历史能够从现有的
@工作流中直接发现。Reviewer 测试计划
如何验证
@,确认出现多个结果分类,并且左右方向键可以切换分类。@session:<title>引用,确认原始 token 仍保留在提示中,并出现解释解析失败原因的错误卡片。证据(Before & After)
Before:历史对话不会出现在
@补全中,复用其上下文必须恢复或 fork 会话。After:当前项目的会话会作为独立分类出现,并解析为有大小上限的只读上下文。服务、处理器、补全 hook、键盘、渲染和鼠标连接行为均有测试覆盖;本次没有录制截图或视频。测试平台
环境(可选)
Node.js 22.23.1,本地 macOS worktree,未启用 sandbox。
风险与范围
@现在会异步读取当前项目的会话列表,每个被接受的引用最多可增加约 8k token 上下文。@延迟优化,以及 Windows/Linux 手动验证。关联 Issue
无。