Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions docs/users/common-workflow.md
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,35 @@ run tests for the refactored code
> - Request that changes maintain backward compatibility when needed
> - Do refactoring in small, testable increments

## Rewind a conversation

Suppose you realize a previous prompt sent the session down the wrong path, but you want to keep the old branch for reference.

**1. Open conversation history**

- Press `Esc` twice with an empty composer, or run:

```text
/rewind
```

**2. Select the earlier prompt**

Use `↑` and `↓` to highlight the prompt you want to return to, then press `Enter`.

**3. Choose how to rewind**

Depending on whether files changed after that prompt, Qwen Code can offer:

- `Restore conversation` to fork the chat while keeping the current files
- `Restore code` to restore the file snapshot while keeping the current chat
- `Restore code and conversation` to do both
- `Summarize from here` to compact the later messages into a summary before you continue

**4. Continue from that point**

Qwen Code keeps the later branch available for reference, restores the selected state you asked for, and puts that prompt back into the composer so you can continue from there.

## Use specialized subagents

Suppose you want to use specialized AI subagents to handle specific tasks more effectively.
Expand Down
17 changes: 9 additions & 8 deletions docs/users/features/commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,15 @@ Slash commands are used to manage Qwen Code sessions, interface, and basic behav

These commands help you save, restore, and summarize work progress.

| Command | Description | Usage Examples |
| ----------- | --------------------------------------------------------- | ------------------------------------ |
| `/init` | Analyze current directory and create initial context file | `/init` |
| `/summary` | Generate project summary based on conversation history | `/summary` |
| `/compress` | Replace chat history with summary to save Tokens | `/compress` |
| `/resume` | Resume a previous conversation session | `/resume` |
| `/recap` | Show a 1-3 sentence "where you left off" summary | `/recap` |
| `/restore` | Restore files to state before tool execution | `/restore` (list) or `/restore <ID>` |
| Command | Description | Usage Examples |
| ----------- | ------------------------------------------------------------------------------------------------------- | ------------------------------------ |
| `/init` | Analyze current directory and create initial context file | `/init` |
| `/summary` | Generate project summary based on conversation history | `/summary` |
| `/compress` | Replace chat history with summary to save Tokens | `/compress` |
| `/resume` | Resume a previous conversation session | `/resume` |
| `/recap` | Show a 1-3 sentence "where you left off" summary | `/recap` |
| `/rewind` | Browse prompts in the current conversation and restore conversation, code, or a summary from that point | `/rewind` |
| `/restore` | Restore files to state before tool execution | `/restore` (list) or `/restore <ID>` |

### 1.2 Interface and Workspace Control

Expand Down
5 changes: 3 additions & 2 deletions docs/users/reference/keyboard-shortcuts.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ This document lists the available keyboard shortcuts in Qwen Code.

| Shortcut | Description |
| ------------------------------ | --------------------------------------------------------------------------------------------------------------------- |
| `Esc` | Close dialogs and suggestions. |
| `Esc` | Close dialogs and suggestions. When the composer is empty, press twice to open Rewind for the current conversation. |
| `Ctrl+C` | Cancel the ongoing request and clear the input. Press twice to exit the application. |
| `Ctrl+D` | Exit the application if the input is empty. Press twice to confirm. |
| `Ctrl+L` | Clear the screen. |
Expand All @@ -30,7 +30,8 @@ This document lists the available keyboard shortcuts in Qwen Code.
| `Ctrl+A` / `Home` | Move the cursor to the beginning of the line. |
| `Ctrl+B` / `Left Arrow` | Move the cursor one character to the left. |
| `Ctrl+C` | Clear the input prompt |
| `Esc` (double press) | Clear the input prompt. |
| `Esc` (double press, empty prompt) | Open Rewind so you can restore conversation, code, or summarize from an earlier prompt. |
| `Esc` (double press, with input) | Clear the input prompt. |
| `Ctrl+D` / `Delete` | Delete the character to the right of the cursor. |
| `Ctrl+E` / `End` | Move the cursor to the end of the line. |
| `Ctrl+F` / `Right Arrow` | Move the cursor one character to the right. |
Expand Down
2 changes: 2 additions & 0 deletions packages/cli/src/services/BuiltinCommandLoader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ import { trustCommand } from '../ui/commands/trustCommand.js';
import { quitCommand } from '../ui/commands/quitCommand.js';
import { recapCommand } from '../ui/commands/recapCommand.js';
import { restoreCommand } from '../ui/commands/restoreCommand.js';
import { rewindCommand } from '../ui/commands/rewindCommand.js';
import { resumeCommand } from '../ui/commands/resumeCommand.js';
import { settingsCommand } from '../ui/commands/settingsCommand.js';
import { skillsCommand } from '../ui/commands/skillsCommand.js';
Expand Down Expand Up @@ -121,6 +122,7 @@ export class BuiltinCommandLoader implements ICommandLoader {
quitCommand,
recapCommand,
restoreCommand(this.config),
rewindCommand,
resumeCommand,
skillsCommand,
statsCommand,
Expand Down
74 changes: 69 additions & 5 deletions packages/cli/src/ui/AppContainer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ import { useModelCommand } from './hooks/useModelCommand.js';
import { useArenaCommand } from './hooks/useArenaCommand.js';
import { useApprovalModeCommand } from './hooks/useApprovalModeCommand.js';
import { useResumeCommand } from './hooks/useResumeCommand.js';
import { useRewindCommand } from './hooks/useRewindCommand.js';
import { useSlashCommandProcessor } from './hooks/slashCommandProcessor.js';
import { useVimMode } from './contexts/VimModeContext.js';
import { CompactModeProvider } from './contexts/CompactModeContext.js';
Expand Down Expand Up @@ -576,6 +577,21 @@ export const AppContainer = (props: AppContainerProps) => {
startNewSession,
remount: refreshStatic,
});
const {
isRewindDialogOpen,
rewindTarget,
openRewindDialog,
closeRewindDialog,
closeRewindConfirmation,
handleRewind,
handleRewindAction,
} = useRewindCommand({
config,
historyManager,
startNewSession,
setInputText: buffer.setText,
remount: refreshStatic,
});

const { toggleVimEnabled } = useVimMode();

Expand Down Expand Up @@ -626,6 +642,7 @@ export const AppContainer = (props: AppContainerProps) => {
openMcpDialog,
openHooksDialog,
openResumeDialog,
openRewindDialog,
}),
[
openAuthDialog,
Expand All @@ -647,6 +664,7 @@ export const AppContainer = (props: AppContainerProps) => {
openMcpDialog,
openHooksDialog,
openResumeDialog,
openRewindDialog,
],
);

Expand Down Expand Up @@ -1484,6 +1502,7 @@ export const AppContainer = (props: AppContainerProps) => {
IdeContext | undefined
>();
const [showEscapePrompt, setShowEscapePrompt] = useState(false);
const [showHistoryPrompt, setShowHistoryPrompt] = useState(false);
const [showIdeRestartPrompt, setShowIdeRestartPrompt] = useState(false);

const { isFolderTrustDialogOpen, handleFolderTrustSelect, isRestarting } =
Expand Down Expand Up @@ -1664,6 +1683,10 @@ export const AppContainer = (props: AppContainerProps) => {
closeSettingsDialog,
isMemoryDialogOpen,
closeMemoryDialog,
isRewindDialogOpen,
isRewindConfirmationOpen: rewindTarget !== null,
closeRewindDialog,
closeRewindConfirmation,
activeArenaDialog,
closeArenaDialog,
isFolderTrustDialogOpen,
Expand Down Expand Up @@ -1792,6 +1815,10 @@ export const AppContainer = (props: AppContainerProps) => {
return;
}

if (dialogsVisibleRef.current) {
return;
}

// If input has content, use double-press to clear
if (buffer.text.length > 0) {
if (escapePressedOnce) {
Expand All @@ -1801,8 +1828,10 @@ export const AppContainer = (props: AppContainerProps) => {
}
// First press: set flag and show prompt
setEscapePressedOnce(true);
setShowHistoryPrompt(false);
escapeTimerRef.current = setTimeout(() => {
setEscapePressedOnce(false);
setShowHistoryPrompt(false);
escapeTimerRef.current = null;
}, CTRL_EXIT_PROMPT_DURATION_MS);
return;
Expand All @@ -1816,15 +1845,29 @@ export const AppContainer = (props: AppContainerProps) => {
}
cancelOngoingRequest?.();
setEscapePressedOnce(false);
setShowHistoryPrompt(false);
return;
}

// No action available, reset the flag
if (escapeTimerRef.current) {
clearTimeout(escapeTimerRef.current);
escapeTimerRef.current = null;
if (escapePressedOnce) {
if (escapeTimerRef.current) {
clearTimeout(escapeTimerRef.current);
escapeTimerRef.current = null;
}
setEscapePressedOnce(false);
setShowHistoryPrompt(false);
openRewindDialog();
return;
}
setEscapePressedOnce(false);

// First press on empty input: arm rewind/history prompt
setEscapePressedOnce(true);
setShowHistoryPrompt(true);
escapeTimerRef.current = setTimeout(() => {
setEscapePressedOnce(false);
setShowHistoryPrompt(false);
escapeTimerRef.current = null;
}, CTRL_EXIT_PROMPT_DURATION_MS);
return;
}

Expand Down Expand Up @@ -1915,6 +1958,7 @@ export const AppContainer = (props: AppContainerProps) => {
compactMode,
setCompactMode,
refreshStatic,
openRewindDialog,
],
);

Expand Down Expand Up @@ -1987,6 +2031,8 @@ export const AppContainer = (props: AppContainerProps) => {
isHooksDialogOpen ||
isApprovalModeDialogOpen ||
isResumeDialogOpen ||
isRewindDialogOpen ||
rewindTarget !== null ||
isExtensionsManagerDialogOpen;
dialogsVisibleRef.current = dialogsVisible;

Expand Down Expand Up @@ -2030,6 +2076,8 @@ export const AppContainer = (props: AppContainerProps) => {
isPermissionsDialogOpen,
isApprovalModeDialogOpen,
isResumeDialogOpen,
isRewindDialogOpen,
rewindTarget,
slashCommands,
pendingSlashCommandHistoryItems,
commandContext,
Expand Down Expand Up @@ -2062,6 +2110,7 @@ export const AppContainer = (props: AppContainerProps) => {
ctrlCPressedOnce,
ctrlDPressedOnce,
showEscapePrompt,
showHistoryPrompt,
isFocused,
elapsedTime,
currentLoadingPhrase,
Expand Down Expand Up @@ -2137,6 +2186,8 @@ export const AppContainer = (props: AppContainerProps) => {
isPermissionsDialogOpen,
isApprovalModeDialogOpen,
isResumeDialogOpen,
isRewindDialogOpen,
rewindTarget,
slashCommands,
pendingSlashCommandHistoryItems,
commandContext,
Expand Down Expand Up @@ -2169,6 +2220,7 @@ export const AppContainer = (props: AppContainerProps) => {
ctrlCPressedOnce,
ctrlDPressedOnce,
showEscapePrompt,
showHistoryPrompt,
isFocused,
elapsedTime,
currentLoadingPhrase,
Expand Down Expand Up @@ -2281,6 +2333,12 @@ export const AppContainer = (props: AppContainerProps) => {
openResumeDialog,
closeResumeDialog,
handleResume,
// Rewind current conversation dialog
openRewindDialog,
closeRewindDialog,
closeRewindConfirmation,
handleRewind,
handleRewindAction,
// Feedback dialog
openFeedbackDialog,
closeFeedbackDialog,
Expand Down Expand Up @@ -2341,6 +2399,12 @@ export const AppContainer = (props: AppContainerProps) => {
openResumeDialog,
closeResumeDialog,
handleResume,
// Rewind current conversation dialog
openRewindDialog,
closeRewindDialog,
closeRewindConfirmation,
handleRewind,
handleRewindAction,
// Feedback dialog
openFeedbackDialog,
closeFeedbackDialog,
Expand Down
5 changes: 4 additions & 1 deletion packages/cli/src/ui/commands/restoreCommand.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -173,11 +173,14 @@ describe('restoreCommand', () => {
expect(mockSetHistory).toHaveBeenCalledWith(toolCallData.clientHistory);
expect(mockGitService.restoreProjectFromSnapshot).toHaveBeenCalledWith(
toolCallData.commitHash,
{
untrackedFiles: { mode: 'preserve' },
},
);
expect(mockContext.ui.addItem).toHaveBeenCalledWith(
{
type: 'info',
text: 'Restored project to the state before the tool call.',
text: 'Restored tracked project files to the state before the tool call. New untracked files were preserved.',
},
expect.any(Number),
);
Expand Down
26 changes: 24 additions & 2 deletions packages/cli/src/ui/commands/restoreCommand.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,19 @@ import {
import type { Config } from '@qwen-code/qwen-code-core';
import { t } from '../../i18n/index.js';

type RestoreProjectOptions = {
untrackedFiles:
| { mode: 'preserve' }
| { mode: 'deleteListed'; paths: string[] };
};

interface RestoreGitService {
restoreProjectFromSnapshot(
commitHash: string,
options: RestoreProjectOptions,
): Promise<void>;
}

async function restoreAction(
context: CommandContext,
args: string,
Expand Down Expand Up @@ -94,11 +107,20 @@ async function restoreAction(
}

if (toolCallData.commitHash) {
await gitService?.restoreProjectFromSnapshot(toolCallData.commitHash);
const restoreGitService = gitService as
| RestoreGitService
| null
| undefined;
await restoreGitService?.restoreProjectFromSnapshot(
toolCallData.commitHash,
{
untrackedFiles: { mode: 'preserve' },
},
);
addItem(
{
type: 'info',
text: 'Restored project to the state before the tool call.',
text: 'Restored tracked project files to the state before the tool call. New untracked files were preserved.',
},
Date.now(),
);
Expand Down
38 changes: 38 additions & 0 deletions packages/cli/src/ui/commands/rewindCommand.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/**
* @license
* Copyright 2025 Qwen Code
* SPDX-License-Identifier: Apache-2.0
*/

import { describe, it, expect, beforeEach } from 'vitest';
import { rewindCommand } from './rewindCommand.js';
import { type CommandContext } from './types.js';
import { createMockCommandContext } from '../../test-utils/mockCommandContext.js';

describe('rewindCommand', () => {
let mockContext: CommandContext;

beforeEach(() => {
mockContext = createMockCommandContext();
});

it('should return a dialog action to open the rewind dialog', async () => {
if (!rewindCommand.action) {
throw new Error('The rewind command must have an action.');
}

const result = await rewindCommand.action(mockContext, '');

expect(result).toEqual({
type: 'dialog',
dialog: 'rewind',
});
});

it('should have the correct name and description', () => {
expect(rewindCommand.name).toBe('rewind');
expect(rewindCommand.description).toBe(
'Browse prompts in the current conversation and fork from one',
);
});
});
Loading
Loading