feat: adds a Space-to-preview affordance to the /resume session picker#3605
Conversation
Press Space on a highlighted session to open a read-only transcript preview; Enter resumes, Esc returns. Works from both in-session `/resume` and standalone `qwen --resume`. The standalone path runs before `loadCliConfig`, so no real Config / LoadedSettings exist when its render tree mounts. `StandaloneSessionPicker` wraps the picker in stub Providers — every downstream access in the preview render path is either optional-chained or gated on states (Confirming / Executing) that never occur in resumed session data, so the stubs' methods are only read, never invoked for real work. Tool descriptions degrade to the raw function-call name in preview; users get full fidelity after pressing Enter to resume. Co-Authored-By: Qwen-Coder <noreply@qwen.ai>
`'─'.repeat(boxWidth - 2)` would throw RangeError when columns < 6 (tmux splits, small panes). Clamp boxWidth to a safe minimum and compute separatorWidth with Math.max(0, …). Co-Authored-By: Qwen-Coder <noreply@alibabacloud.com>
E2E 测试报告 —
|
| 项 | 值 |
|---|---|
| Terminal | tmux 3.6a,pane 140×45 |
| CLI | node dist/cli.js --resume(重新 bundle 过,含本分支 2 个 commit) |
| 数据源 | ~/.qwen/projects/-Users-qqqys-Desktop-qys-qwen-code/chats/,7 个真实会话 |
| Node | 项目标配 |
| 平台 | macOS (🍏) |
被测 Commit
47009e631 feat(cli): add Space-to-preview in resume session picker995febb79 fix(cli): guard SessionPreview separator width on narrow terminals
测试用例与结果
| # | 用例 | 操作 | 预期 | 结果 |
|---|---|---|---|---|
| 1 | Picker 初始渲染 | node dist/cli.js --resume |
列表 + footer 含 Space to preview · ↑↓ to navigate · Esc to cancel |
✅ |
| 2 | Space 打开预览 | 在首行按 Space | 渲染会话正文,footer 切换为 18 messages · 11 hours ago · feat/session-preview + Enter to resume · Esc to back |
✅ |
| 3 | tool_group 预览路径 |
首行会话含 todo_write / read_file / run_shell_command |
Stub Providers 生效,工具组以工具名渲染,无崩溃 | ✅(这是 StandaloneSessionPicker 的 context 桩真正接管的证据) |
| 4 | Esc 返回列表 | 预览中按 Esc | 回到列表,footer 恢复列表 hints | ✅ |
| 5 | 跨会话切换预览 | Esc → ↓ ↓ → Space |
新预览加载成功,footer meta 更新为新会话(6 messages · 1 day ago · feat/session-auto-title-trigger,分支不同),无残留内容 |
✅(cancelled 标志 + 新加载工作正常) |
| 6 | 双 Esc 退出 | 列表上 Esc | CLI 干净退出 | ✅ |
关键截帧
Frame A — Picker 列表 (用例 1)
╭────────────────────────────────────────────────────────────────╮
│ Resume Session │
│────────────────────────────────────────────────────────────────│
│ › # Create Issue │
│ 11 hours ago · 18 messages · feat/session-preview │
│ │
│ 查看最近五次提交 │
│ 12 hours ago · 5 messages · feat/session-preview │
│ ... │
│────────────────────────────────────────────────────────────────│
│ B to toggle branch · Space to preview · ↑↓ to navigate · Esc │
╰────────────────────────────────────────────────────────────────╯
Frame B — 预览 (用例 2, 3)
> 帮我提交issue
✦ Determining title for submission
...
╭──────────────────────────────────────╮
│ ✓ todo_write │
│ ✓ read_file │
│ ✓ run_shell_command │
│ Creating issue in QwenLM/... │
╰──────────────────────────────────────╯
✦ Submitted: https://github.com/QwenLM/qwen-code/issues/3571
────────────────────────────────────────
18 messages · 11 hours ago · feat/session-preview
Enter to resume · Esc to back
Frame C — 跨会话切换 (用例 5)
6 messages · 1 day ago · feat/session-auto-title-trigger
Enter to resume · Esc to back
覆盖到的关键路径
useSessionPicker的viewMode切换:list ↔ preview 在真实键盘输入下往返正常。SessionPreview的useEffect重新加载:切换会话触发新的loadSession,meta 行反映新数据(没看到旧会话内容残留)。PREVIEW_CONFIG_STUB/PREVIEW_SETTINGS_STUB:真实含工具调用的 JSONL 渲染通过,证明ToolGroupMessage/ToolMessage的useConfig()/useSettings()调用被桩 Provider 接住。- 新翻译 key:
Space to preview ·和↑↓ to navigate · Esc to cancel在英文 locale 下正常展示,没有孤儿 fallback 异常。 - 今天刚加的窄终端 guard:本轮在 140 宽下没有触发(分隔线
─正常渲染 138 个字符),但 clamp 逻辑已通过单测覆盖(23 个 vitest 单测全绿)。
单测补充结果
npx vitest run \
packages/cli/src/ui/components/SessionPreview.test.tsx \
packages/cli/src/ui/components/StandaloneSessionPicker.test.tsx
SessionPreview.test.tsx:5 / 5 通过StandaloneSessionPicker.test.tsx:18 / 18 通过(含 Preview Mode 新增 3 个用例)- 合计:23 / 23 通过,耗时 5.85s
| if (name === 'space') { | ||
| const session = filteredSessions[selectedIndex]; | ||
| if (session) { | ||
| setPreviewSessionId(session.sessionId); |
There was a problem hiding this comment.
[Critical] Preview mode is enabled unconditionally in the shared session picker, including the delete-session dialog. In the delete flow, Space opens this preview, but Enter still calls the picker’s onSelect; for the delete dialog that is the delete handler, while the preview UI says Enter will resume. That creates a misleading path to a destructive action.
Consider gating preview behind an explicit prop such as enablePreview or adding a picker mode/action label, then enabling resume preview only for resume flows. Alternatively, make delete preview text/action accurately reflect deletion and keep the intended confirmation path.
— gpt-5.5 via Qwen Code /review
There was a problem hiding this comment.
Good catch — fixed in 870e940.
SessionPicker is shared by both the resume and the delete dialog (DialogManager.tsx:376 / :388). Preview's Enter forwarded to onSelect unconditionally, so in the delete flow Space → preview → Enter would call handleDelete while the footer still read "Enter to resume". Confirmed critical.
| <Text color={theme.text.secondary}> | ||
| {t('Loading session preview...')} | ||
| </Text> | ||
| </Box> |
There was a problem hiding this comment.
[Suggestion] The preview renders every converted history item for the selected session. Opening preview for a large session can perform a large conversion/render in one pass and potentially freeze or severely slow the terminal UI.
Consider limiting the preview to a bounded subset of history items, or implementing lazy/windowed rendering with navigation. For example, render only the most recent N items and clearly indicate truncation.
— gpt-5.5 via Qwen Code /review
`SessionPicker` is shared by the resume dialog and the delete-session dialog. Preview's Enter shortcut forwards to `onSelect`, which for delete is `handleDelete` — so Space → preview → Enter would silently delete the session while the preview UI still says "Enter to resume". Add `enablePreview?: boolean` (default false). Resume callers (the in-app resume dialog and `--resume` standalone) opt in; the delete dialog stays opt-out and behaves exactly as before. Footer hint and preview render branch are both gated on the prop. Add a regression test that emulates the delete dialog and asserts Space is a no-op, the hint is absent, and Enter still flows straight to onSelect. Co-Authored-By: Qwen-Coder <noreply@alibabacloud.com>
wenshao
left a comment
There was a problem hiding this comment.
No issues found. LGTM! ✅ — gpt-5.5 via Qwen Code /review
tmux end-to-end verificationSpawned the merged-branch bundle inside tmux and exercised the picker both at standard width and at a narrow 30-column pane. 11/11 assertions passed. Test set A — standard width (120 cols)Verifies:
Test set B — narrow terminal (30 cols)Verifies the narrow-terminal guard at const boxWidth = Math.max(10, columns - 4);
const separatorWidth = Math.max(0, boxWidth - 2);Without these clamps, What this confirms about the designThe trickiest part of this PR is that
…together demonstrate the pre-config render path actually works end-to-end in a real terminal, not just in unit tests. Unit testsAll adjacent suites pass locally on this branch:
LGTM |
… (#182) * feat: adds a Space-to-preview affordance to the /resume session picker (QwenLM#3605) * feat(cli): add Space-to-preview in resume session picker Press Space on a highlighted session to open a read-only transcript preview; Enter resumes, Esc returns. Works from both in-session `/resume` and standalone `qwen --resume`. The standalone path runs before `loadCliConfig`, so no real Config / LoadedSettings exist when its render tree mounts. `StandaloneSessionPicker` wraps the picker in stub Providers — every downstream access in the preview render path is either optional-chained or gated on states (Confirming / Executing) that never occur in resumed session data, so the stubs' methods are only read, never invoked for real work. Tool descriptions degrade to the raw function-call name in preview; users get full fidelity after pressing Enter to resume. Co-Authored-By: Qwen-Coder <noreply@qwen.ai> * fix(cli): guard SessionPreview separator width on narrow terminals `'─'.repeat(boxWidth - 2)` would throw RangeError when columns < 6 (tmux splits, small panes). Clamp boxWidth to a safe minimum and compute separatorWidth with Math.max(0, …). Co-Authored-By: Qwen-Coder <noreply@alibabacloud.com> * fix(cli): gate Space-to-preview behind enablePreview prop `SessionPicker` is shared by the resume dialog and the delete-session dialog. Preview's Enter shortcut forwards to `onSelect`, which for delete is `handleDelete` — so Space → preview → Enter would silently delete the session while the preview UI still says "Enter to resume". Add `enablePreview?: boolean` (default false). Resume callers (the in-app resume dialog and `--resume` standalone) opt in; the delete dialog stays opt-out and behaves exactly as before. Footer hint and preview render branch are both gated on the prop. Add a regression test that emulates the delete dialog and asserts Space is a no-op, the hint is absent, and Enter still flows straight to onSelect. Co-Authored-By: Qwen-Coder <noreply@alibabacloud.com> --------- Co-authored-by: Qwen-Coder <noreply@qwen.ai> Co-authored-by: Qwen-Coder <noreply@alibabacloud.com> * ci: force CI re-trigger --------- Co-authored-by: qqqys <qys177@gmail.com> Co-authored-by: Qwen-Coder <noreply@qwen.ai> Co-authored-by: Qwen-Coder <noreply@alibabacloud.com> Co-authored-by: Automaker <automaker@localhost>
QwenLM#3605) * feat(cli): add Space-to-preview in resume session picker Press Space on a highlighted session to open a read-only transcript preview; Enter resumes, Esc returns. Works from both in-session `/resume` and standalone `qwen --resume`. The standalone path runs before `loadCliConfig`, so no real Config / LoadedSettings exist when its render tree mounts. `StandaloneSessionPicker` wraps the picker in stub Providers — every downstream access in the preview render path is either optional-chained or gated on states (Confirming / Executing) that never occur in resumed session data, so the stubs' methods are only read, never invoked for real work. Tool descriptions degrade to the raw function-call name in preview; users get full fidelity after pressing Enter to resume. Co-Authored-By: Qwen-Coder <noreply@qwen.ai> * fix(cli): guard SessionPreview separator width on narrow terminals `'─'.repeat(boxWidth - 2)` would throw RangeError when columns < 6 (tmux splits, small panes). Clamp boxWidth to a safe minimum and compute separatorWidth with Math.max(0, …). Co-Authored-By: Qwen-Coder <noreply@alibabacloud.com> * fix(cli): gate Space-to-preview behind enablePreview prop `SessionPicker` is shared by the resume dialog and the delete-session dialog. Preview's Enter shortcut forwards to `onSelect`, which for delete is `handleDelete` — so Space → preview → Enter would silently delete the session while the preview UI still says "Enter to resume". Add `enablePreview?: boolean` (default false). Resume callers (the in-app resume dialog and `--resume` standalone) opt in; the delete dialog stays opt-out and behaves exactly as before. Footer hint and preview render branch are both gated on the prop. Add a regression test that emulates the delete dialog and asserts Space is a no-op, the hint is absent, and Enter still flows straight to onSelect. Co-Authored-By: Qwen-Coder <noreply@alibabacloud.com> --------- Co-authored-by: Qwen-Coder <noreply@qwen.ai> Co-authored-by: Qwen-Coder <noreply@alibabacloud.com>
TLDR
Adds a Space-to-preview affordance to the
/resumesession picker. Hit Space on any row to inline-render that session's conversation (reusing the realHistoryItemDisplaypipeline), Enter to resume, Esc to go back. Also hardens the preview against narrow terminals.Screenshots / Video Demo
Dive Deeper
Why reuse
HistoryItemDisplay? The preview renders through the same components that drive the live chat (HistoryItemDisplay→ToolGroupMessage→ToolMessage). That's the only way to keep preview fidelity in sync with the real UI for free — text, thoughts, tool groups, compact mode, etc.The Config/Settings problem.
--resumeruns the picker beforeloadCliConfig, so no realConfigorLoadedSettingsexist yet. But the render tree callsuseConfig()/useSettings(), which throw without a Provider. Two pieces resolve this:StandaloneSessionPickermountsConfigContext.Provider/SettingsContext.Providerwith minimal stubs (StandaloneSessionPicker.tsx:29-48). Every downstream access in the preview path is either optional-chained or gated onConfirming/Executingstates that never appear in resumed data — so stub methods are read, never exercised.buildResumedHistoryItemsnow acceptsConfig | null. Whennull, tool-group items degrade to name-only (no registry lookup) and thoughts render verbatim. Full fidelity returns the moment the user hits Enter and the real config loads.Keypress routing. The session-picker hook gates its keymap with
isActive: isActive && viewMode === 'list'plus an early return at the top of the handler. Preview owns its own keymap while active; this prevents double-handling of Enter/Esc.Narrow-terminal guard.
'─'.repeat(boxWidth - 2)would throwRangeErroron tmux splits / small panes whencolumns < 6.boxWidthis now clamped to a safe minimum and the separator usesMath.max(0, boxWidth - 2).Reviewer Test Plan
N messages · <relative time> · <branch>.RangeError, header/footer separators render (possibly zero width) without throwing.Bin the list, then Space into preview, then Esc — branch filter state should survive the round trip.Automated tests:
npx vitest run packages/cli/src/ui/components/SessionPreview.test.tsx \ packages/cli/src/ui/components/StandaloneSessionPicker.test.tsxCovers: loading state, full-render, footer metadata, Esc/Enter wiring, tool_group with stub Providers, Space→preview→Enter flow.
Testing Matrix
Linked issues / bugs
Resolves #3510