Skip to content

feat: adds a Space-to-preview affordance to the /resume session picker#3605

Merged
wenshao merged 3 commits into
QwenLM:mainfrom
qqqys:feat/session-preview
Apr 25, 2026
Merged

feat: adds a Space-to-preview affordance to the /resume session picker#3605
wenshao merged 3 commits into
QwenLM:mainfrom
qqqys:feat/session-preview

Conversation

@qqqys

@qqqys qqqys commented Apr 25, 2026

Copy link
Copy Markdown
Collaborator

TLDR

Adds a Space-to-preview affordance to the /resume session picker. Hit Space on any row to inline-render that session's conversation (reusing the real HistoryItemDisplay pipeline), Enter to resume, Esc to go back. Also hardens the preview against narrow terminals.

Screenshots / Video Demo

preview

Dive Deeper

Why reuse HistoryItemDisplay? The preview renders through the same components that drive the live chat (HistoryItemDisplayToolGroupMessageToolMessage). 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. --resume runs the picker before loadCliConfig, so no real Config or LoadedSettings exist yet. But the render tree calls useConfig() / useSettings(), which throw without a Provider. Two pieces resolve this:

  1. StandaloneSessionPicker mounts ConfigContext.Provider / SettingsContext.Provider with minimal stubs (StandaloneSessionPicker.tsx:29-48). Every downstream access in the preview path is either optional-chained or gated on Confirming / Executing states that never appear in resumed data — so stub methods are read, never exercised.
  2. buildResumedHistoryItems now accepts Config | null. When null, 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 throw RangeError on tmux splits / small panes when columns < 6. boxWidth is now clamped to a safe minimum and the separator uses Math.max(0, boxWidth - 2).

Reviewer Test Plan

npm run build
npm start -- --resume
  1. Open picker → Space on a row → preview renders with conversation content. Footer shows N messages · <relative time> · <branch>.
  2. Esc inside preview → returns to the list with selection preserved.
  3. Enter inside preview → resumes that session (equivalent to Enter on the list row).
  4. Preview a session containing tool calls (any session with Bash/Read/Edit history). Tool-group items should render by name without crashing — this is the path that would fail without the stub Providers.
  5. Shrink terminal to a very narrow width (e.g. 30 cols, or a tmux split) and repeat (1). No RangeError, header/footer separators render (possibly zero width) without throwing.
  6. Branch filter interaction. Toggle B in 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.tsx

Covers: loading state, full-render, footer metadata, Esc/Enter wiring, tool_group with stub Providers, Space→preview→Enter flow.

Testing Matrix

🍏 🪟 🐧
npm run
npx
Docker - - -
Podman - - -
Seatbelt - - -

Linked issues / bugs

Resolves #3510

qqqys and others added 2 commits April 24, 2026 10:24
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>
@qqqys

qqqys commented Apr 25, 2026

Copy link
Copy Markdown
Collaborator Author

E2E 测试报告 — feat/session-preview (tmux 驱动)

测试环境

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 picker
  • 995febb79 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

覆盖到的关键路径

  • useSessionPickerviewMode 切换:list ↔ preview 在真实键盘输入下往返正常。
  • SessionPreviewuseEffect 重新加载:切换会话触发新的 loadSession,meta 行反映新数据(没看到旧会话内容残留)。
  • PREVIEW_CONFIG_STUB / PREVIEW_SETTINGS_STUB:真实含工具调用的 JSONL 渲染通过,证明 ToolGroupMessage/ToolMessageuseConfig() / useSettings() 调用被桩 Provider 接住。
  • 新翻译 keySpace 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

@qqqys qqqys requested a review from tanzhenxin April 25, 2026 01:41
if (name === 'space') {
const session = filteredSessions[selectedIndex];
if (session) {
setPreviewSessionId(session.sessionId);

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.

[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

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

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>

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

No issues found. LGTM! ✅ — gpt-5.5 via Qwen Code /review

@wenshao

wenshao commented Apr 25, 2026

Copy link
Copy Markdown
Collaborator

tmux end-to-end verification

Spawned 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)

T1: Picker opened with preview-enabled hint
  [PASS] T1a — found "Space to preview"
  [PASS] T1b — found "Resume Session"

T2: Press Space, expect preview
  [PASS] T2a — found "Enter to resume"
  [PASS] T2b — found "Esc to back"
  [PASS] T2c-no-RangeError — "RangeError" absent

T3: Press Esc, back to list
  [PASS] T3a — found "Space to preview"
  [PASS] T3b — "Enter to resume · Esc to back" absent

Verifies:

  • The enablePreview prop wires through to StandaloneSessionPicker correctly — the footer hint appears on the resume path.
  • Space transitions to the preview view (footer changes to Enter to resume · Esc to back).
  • Esc restores the list view and its hint.
  • The preview render path does not throw despite running before loadCliConfig (the stub Config / Settings providers and the Config | null branch in buildResumedHistoryItems hold).

Test set B — narrow terminal (30 cols)

  [PASS] T4a-no-RangeError-on-list      — "RangeError" absent
  [PASS] T4b-no-RangeError-on-preview   — "RangeError" absent
  [PASS] T4c-no-uncaught                — "Uncaught" absent
  [PASS] T4d-no-crash-on-back           — "RangeError" absent

Verifies the narrow-terminal guard at SessionPreview.tsx:97-98:

const boxWidth = Math.max(10, columns - 4);
const separatorWidth = Math.max(0, boxWidth - 2);

Without these clamps, '─'.repeat(boxWidth - 2) would throw RangeError: Invalid count value when columns < 6 (tmux splits, narrow panes). At 30 cols the picker, the Space → preview transition, and the Esc → list transition all stayed clean.

What this confirms about the design

The trickiest part of this PR is that --resume runs before loadCliConfig, so useConfig() / useSettings() calls inside the render tree would normally throw. The fact that:

  1. preview opens (T2a/b) — the stubbed Providers from StandaloneSessionPicker.tsx:29-48 keep the render tree alive
  2. no RangeError / Uncaught errors appear (T2c, T4a-d) — the Config | null branch of buildResumedHistoryItems correctly degrades tool-group items to name-only and renders thoughts verbatim

…together demonstrate the pre-config render path actually works end-to-end in a real terminal, not just in unit tests.

Unit tests

All adjacent suites pass locally on this branch:

  • StandaloneSessionPicker.test.tsx — 19/19 (incl. 3 preview-mode cases and 1 regression test for the delete-dialog gating)
  • SessionPreview.test.tsx — 5/5
  • DialogManager.test.tsx, resumeHistoryUtils.test.ts, useResumeCommand.test.ts — 10/10

LGTM

@wenshao wenshao merged commit 83d1e6d into QwenLM:main Apr 25, 2026
13 checks passed
mabry1985 added a commit to protoLabsAI/protoCLI that referenced this pull request May 2, 2026
… (#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>
xaelistic pushed a commit to xaelistic/qwen-code that referenced this pull request Jun 7, 2026
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: Add session preview capability to /resume picker

2 participants