feat(cli): add conversation rewind feature with double-ESC and /rewind command#3441
Conversation
…d command (#3186) Add the ability to rewind conversation to a previous user turn, similar to Claude Code's message selector. Users can trigger rewind via: - Double-ESC on empty prompt while idle - /rewind (or /rollback) slash command The RewindSelector component provides a two-phase UI: a scrollable pick-list of user turns followed by a confirmation dialog. On confirm, both UI history and API history are truncated consistently, the terminal is re-rendered, and the original prompt text is pre-populated in the input for editing. Key implementation details: - historyMapping.ts correctly handles tool-call loops (functionResponse entries) and the startup context pair when mapping UI turns to API Content[] indices - useDoublePress hook provides generic double-press detection with 800ms timeout and proper cleanup on unmount - ESC handler guards against WaitingForConfirmation state to prevent accidental rewind during tool approval - Chat recording service records rewind events with tree-branching via parentUuid for session replay support Closes #3186 Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Code Coverage Summary
CLI Package - Full Text ReportCore Package - Full Text ReportFor detailed HTML reports, please see the 'coverage-reports-22.x-ubuntu-latest' artifact from the main CI run. |
- Actually invoke chatRecordingService.recordRewind() after rewind - Remove tree-branching from recordRewind (no UI-to-recording UUID mapping exists yet) to avoid corrupting the parentUuid chain - Simplify RewindRecordPayload to just truncatedCount Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Keep both rewind (PR branch) and custom_title/finalize (main) additions in chatRecordingService.ts and AppContainer.tsx. Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Automated verification of all 5 manual test items from PR description: 1. /rewind command flow (pick turn, confirm, verify truncation) 2. Double-ESC opens selector (with btw dismiss handling) 3. ESC during streaming cancels (no rewind) 4. /rewind with no history (guard blocks) 5. After rewind, model ignores removed turns Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
wenshao
left a comment
There was a problem hiding this comment.
No blocking issues found. LGTM! ✅ — gpt-5.4 via Qwen Code /review
|
@doudouOUC 合入前复审,发现 4-20 回复 "Fixed" 的两处 [Critical] 代码层面并没有落地(是不是修复分支漏 push?),列证据请 check: 1) chatRecordingService → resume你回复说:payload 扩成 实际(HEAD =
→ /rewind 后 /resume 同一 session,被 rewound 的消息会全部恢复。 2) openRewindSelector 的 IDE guard你回复说: 实际 const openRewindSelector = useCallback(() => {
if (streamingState !== StreamingState.Idle) return;
if (dialogsVisibleRef.current) return;
const hasUserTurns = historyManager.history.some((h) => h.type === 'user');
if (!hasUserTurns) return;
setIsRewindSelectorOpen(true);
}, [streamingState, historyManager.history]);
顺手建议(非 blocker)
修复分支 push 上来我再过一遍就可以合入。两处 Critical 真正落地 + (可选)单测即可。 |
- chatRecordingService: add turnParentUuids tracking and rewindRecording() which re-roots the parentUuid chain so rewound messages land on a dead branch; reconstructHistory() then skips them automatically on resume. Add rebuildTurnBoundaries() for re-populating the index after /resume. - AppContainer: fix truncatedCount bug (was always 0 after loadHistory), wire handleRewindConfirm to rewindRecording() with correct targetTurnIndex, add config.getIdeMode() guard to openRewindSelector so rewind is disabled in IDE sessions where extra user Content entries break the API boundary mapping. - useResumeCommand: call rebuildTurnBoundaries() after startNewSession so rewind works correctly within resumed sessions. - resumeHistoryUtils: surface "Conversation rewound." info item when a rewind record is encountered during history reconstruction. - historyMapping.test.ts: add 9 unit tests for computeApiTruncationIndex covering normal flow, startup context pair, tool responses, and compression fallback. - Copyright headers: standardize new files to "Copyright 2025 Qwen Code". 🤖 Generated with [Qwen Code](https://github.com/QwenLM/qwen-code)
|
感谢 @wenshao 的详细复审,两处 Critical 均已在 Critical 1 — Resume 后 rewind 失效根本原因是 修复思路:在录制层创建正确的 tree 分支,而不是依赖 resume 路径去"感知"并截断。
Critical 2 — IDE 模式下 API history 截断位置错误在 非 blocker
如有其他问题欢迎继续指出。 |
…tor) Keep both our turnParentUuids field and main's async appendRecord write-queue fields (chatsDirEnsured, cachedConversationFile, writeChain). For useHistoryManager, retain truncateToItem while adopting the useMemo return-value wrapping introduced on main. 🤖 Generated with [Qwen Code](https://github.com/QwenLM/qwen-code)
Three bugs found by Codex review: 1. P1: `/rewind` slash command bypassed the IDE-mode guard because `slashCommandActions.openRewindSelector` called `setIsRewindSelectorOpen` directly. Fixed by introducing a ref bridge (`openRewindSelectorRef`) that delegates to the guarded callback. 2. P1: Slash-command invocations (`/help`, `/stats`, etc.) are stored as `type: 'user'` in UI history but never reach the API or recording service. The turn-index counter in `handleRewindConfirm` and `computeApiTruncationIndex` counted them, producing off-by-N errors. Added `isRealUserTurn()` helper that excludes items starting with `/` or `?`, applied in all three counting sites (AppContainer, historyMapping, RewindSelector). 3. P2: After chat compression, `computeApiTruncationIndex` returned `apiHistory.length` when the target turn was unreachable, silently keeping the full API history while the UI was truncated. Changed to return `-1`; `handleRewindConfirm` now aborts with an error message when the target turn was absorbed by compression. Tests: 14 unit tests for historyMapping (including slash-command and compression cases), full suite 616/616 passed. 🤖 Generated with [Qwen Code](https://github.com/QwenLM/qwen-code)
wenshao
left a comment
There was a problem hiding this comment.
No issues found. LGTM! ✅ — gpt-5.5 via Qwen Code /review
Post-merge note:
|
#3622) Test 1 asserted `say exactly GAMMA3` after pressing Up once in the rewind selector, but that only passed because `/rewind` was incorrectly counted as a user turn. After `isRealUserTurn()` excluded slash commands, the turn list is [ALPHA1, BETA2, GAMMA3] and Up from the initial selection (GAMMA3) lands on BETA2. Update the assertion to match. Ref: #3441 (comment) Co-authored-by: jinye.djy <jinye.djy@alibaba-inc.com> Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Adopts the LLM-history mapping logic from upstream QwenLM/qwen-code PR QwenLM#3441 to fix our rewind feature. The bug: our `applyConversationRewind` walked the API history counting `role === 'user'` entries as user turns, which conflated three distinct classes of entries: 1. Real user prompts (what we wanted to count) 2. Tool result entries (`role: 'user'` with `functionResponse` parts) 3. The startup context pair (env preamble + "Got it" model ack) It also miscounted the UI side: slash-command items (`/help`, `/stats`) have `type: 'user'` in our UI history but never reach the API, so they threw off the alignment between UI turn count and API user-content count. The combination meant the LLM history was sliced at the wrong index, which left orphan tool-result entries or stale context attached to the wrong turn — and on the next prompt the model produced output that no longer corresponded to the visible UI scrollback. Fix: - New `historyMapping.ts` utility with `computeApiTruncationIndex` and `isRealUserTurn` (adapted from upstream). - `applyConversationRewind` now uses `computeApiTruncationIndex` for the LLM slice and aborts cleanly when the target turn was absorbed by compression (returns -1 instead of truncating to a wrong index). - After truncating, calls `stripThoughtsFromHistory()` so stale thinking blocks don't leak into the next request. - Compression-abort path surfaces an error instead of silently rewinding to the wrong turn. Co-authored-by: Automaker <automaker@localhost> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…d command (QwenLM#3441) * feat(cli): add conversation rewind feature with double-ESC and /rewind command (QwenLM#3186) Add the ability to rewind conversation to a previous user turn, similar to Claude Code's message selector. Users can trigger rewind via: - Double-ESC on empty prompt while idle - /rewind (or /rollback) slash command The RewindSelector component provides a two-phase UI: a scrollable pick-list of user turns followed by a confirmation dialog. On confirm, both UI history and API history are truncated consistently, the terminal is re-rendered, and the original prompt text is pre-populated in the input for editing. Key implementation details: - historyMapping.ts correctly handles tool-call loops (functionResponse entries) and the startup context pair when mapping UI turns to API Content[] indices - useDoublePress hook provides generic double-press detection with 800ms timeout and proper cleanup on unmount - ESC handler guards against WaitingForConfirmation state to prevent accidental rewind during tool approval - Chat recording service records rewind events with tree-branching via parentUuid for session replay support Closes QwenLM#3186 Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix: call recordRewind() in handleRewindConfirm and simplify payload - Actually invoke chatRecordingService.recordRewind() after rewind - Remove tree-branching from recordRewind (no UI-to-recording UUID mapping exists yet) to avoid corrupting the parentUuid chain - Simplify RewindRecordPayload to just truncatedCount Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * test: add tmux-based E2E script for rewind feature Automated verification of all 5 manual test items from PR description: 1. /rewind command flow (pick turn, confirm, verify truncation) 2. Double-ESC opens selector (with btw dismiss handling) 3. ESC during streaming cancels (no rewind) 4. /rewind with no history (guard blocks) 5. After rewind, model ignores removed turns Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(rewind): resolve resume persistence and IDE mode issues - chatRecordingService: add turnParentUuids tracking and rewindRecording() which re-roots the parentUuid chain so rewound messages land on a dead branch; reconstructHistory() then skips them automatically on resume. Add rebuildTurnBoundaries() for re-populating the index after /resume. - AppContainer: fix truncatedCount bug (was always 0 after loadHistory), wire handleRewindConfirm to rewindRecording() with correct targetTurnIndex, add config.getIdeMode() guard to openRewindSelector so rewind is disabled in IDE sessions where extra user Content entries break the API boundary mapping. - useResumeCommand: call rebuildTurnBoundaries() after startNewSession so rewind works correctly within resumed sessions. - resumeHistoryUtils: surface "Conversation rewound." info item when a rewind record is encountered during history reconstruction. - historyMapping.test.ts: add 9 unit tests for computeApiTruncationIndex covering normal flow, startup context pair, tool responses, and compression fallback. - Copyright headers: standardize new files to "Copyright 2025 Qwen Code". 🤖 Generated with [Qwen Code](https://github.com/QwenLM/qwen-code) * fix(rewind): close slash-command, compression, and IDE bypass holes Three bugs found by Codex review: 1. P1: `/rewind` slash command bypassed the IDE-mode guard because `slashCommandActions.openRewindSelector` called `setIsRewindSelectorOpen` directly. Fixed by introducing a ref bridge (`openRewindSelectorRef`) that delegates to the guarded callback. 2. P1: Slash-command invocations (`/help`, `/stats`, etc.) are stored as `type: 'user'` in UI history but never reach the API or recording service. The turn-index counter in `handleRewindConfirm` and `computeApiTruncationIndex` counted them, producing off-by-N errors. Added `isRealUserTurn()` helper that excludes items starting with `/` or `?`, applied in all three counting sites (AppContainer, historyMapping, RewindSelector). 3. P2: After chat compression, `computeApiTruncationIndex` returned `apiHistory.length` when the target turn was unreachable, silently keeping the full API history while the UI was truncated. Changed to return `-1`; `handleRewindConfirm` now aborts with an error message when the target turn was absorbed by compression. Tests: 14 unit tests for historyMapping (including slash-command and compression cases), full suite 616/616 passed. 🤖 Generated with [Qwen Code](https://github.com/QwenLM/qwen-code) --------- Co-authored-by: jinye.djy <jinye.djy@alibaba-inc.com> Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
QwenLM#3622) Test 1 asserted `say exactly GAMMA3` after pressing Up once in the rewind selector, but that only passed because `/rewind` was incorrectly counted as a user turn. After `isRealUserTurn()` excluded slash commands, the turn list is [ALPHA1, BETA2, GAMMA3] and Up from the initial selection (GAMMA3) lands on BETA2. Update the assertion to match. Ref: QwenLM#3441 (comment) Co-authored-by: jinye.djy <jinye.djy@alibaba-inc.com> Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Summary
Closes #3186
Add conversation history rollback/rewind feature that allows users to return to a previous conversation turn and restart from that point:
/rewind(alias/rollback) slash command opens the rewind selectorKey design decisions
historyMapping.tscorrectly handles tool-call loops (functionResponseentries) and the startup context pair when mapping UI turns to APIContent[]indicesWaitingForConfirmationstate to prevent accidental rewind during tool approvalparentUuidfor session replay supportuseDoublePresshook includes cleanup on unmount to prevent state updates after unmountNew files (5)
packages/cli/src/ui/hooks/useDoublePress.tspackages/cli/src/ui/components/RewindSelector.tsxpackages/cli/src/ui/utils/historyMapping.tspackages/cli/src/ui/commands/rewindCommand.ts/rewind(alias/rollback) slash commandscripts/test-rewind-e2e.shModified files (12)
useHistoryManager.tstruncateToItemmethodgeminiChat.ts/client.tstruncateHistorymethodAppContainer.tsxhandleRewindConfirmorchestrationDialogManager.tsxRewindSelectorUIStateContext.tsx/UIActionsContext.tsxslashCommandProcessor.ts/types.ts'rewind'dialog typeBuiltinCommandLoader.ts/rewindcommandFooter.tsxchatRecordingService.tsrecordRewindwith tree branchingTest plan
/rewind→ pick turn → verify UI truncated, input pre-populated/rewindwith no history → selector shows empty state message人工验证
场景 1: multi-turn → /rewind → pick turn → verify
- 被选中 turn 之后的对话消息全部消失
- 输入框已预填充被选中 turn 的原始 prompt 文本
- 出现提示 "Conversation rewound. Edit your prompt and press Enter to continue."
DingTalk.Recording.Screen_2026-04-24.150137.mp4
场景 2: double-ESC → selector opens
- 第一次 ESC 后,底部 footer 应显示 "Press Esc again to rewind conversation."
- 第二次 ESC 后,Rewind Conversation 选择器打开
▎ 注意:如果有 btw 侧栏弹出,第一次 ESC 会先关闭 btw,需要再额外按两次 ESC。
DingTalk.Recording.Screen_2026-04-24.171319.mp4
场景 3: ESC during streaming → cancels, NOT rewind
- 生成被取消,回到 idle 状态
- 不应该出现 "Rewind Conversation" 选择器
- 不应该出现 "Press Esc again to rewind" 提示
DingTalk.Recording.Screen_2026-04-24.171450.mp4
场景 4: /rewind with no history
- 如果因为某种原因打开了,组件会显示 "No user turns to rewind to."
▎ 代码逻辑在 AppContainer.tsx:1573-1578:没有 user turn 时 openRewindSelector 直接返回。
场景 5: rewind 后 model 不引用被移除的内容
DingTalk.Recording.Screen_2026-04-24.171650.mp4
tmux E2E test results
All 5 manual test items were automated via
scripts/test-rewind-e2e.shusing tmuxsend-keys/capture-pane. The script launches the CLI in a tmux session, sends keystrokes, and asserts on visible screen content. Two consecutive runs passed:Test details & methodology
/rewindflow/rewind→ Up → Enter → Y/rewindimmediately/rewinditself), no crashKey findings during test development:
tmux escape-timemust be set to0(server-level) for ESC to be delivered immediatelywait_idlerequires triple-check: prompt visible + no btw "esc to cancel" indicator + screen stable for 3scancelBtw()before the rewind handler can see it — real users need ESC×3 if btw is active/rewindcommand text itself gets recorded as a user turn, so the "no history" guard passes — the selector opens with 1 (useless) entryRun the tests:
npm run build && npm run bundle bash scripts/test-rewind-e2e.sh🤖 Generated with Qwen Code