Skip to content
Closed
Show file tree
Hide file tree
Changes from 19 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
ac2945a
feat(cli): Add model toggle hotkey (Ctrl+F)
Jul 21, 2026
f7179d7
fix(cli): resolve target auth type for cross-provider model toggle
Jul 21, 2026
8bf4189
refactor(cli): extract toggle decision logic into testable pure funct…
Jul 21, 2026
23cf916
fix(cli): narrow agent PTY toggle guard + add PTY tracking tests
Jul 21, 2026
63d423a
test(cli): add Ctrl+F delegation tests for AgentChatContent
Jul 21, 2026
934ca93
fix(cli): suppress Ctrl+F cursor-right in non-vim mode + fix test name
Jul 22, 2026
4b59e75
fix(cli): add handleToggleKeypress to InputPrompt test mocks
Jul 22, 2026
e85e8f9
test(cli): cover Ctrl+F suppression branch + invalidation scenario
Jul 22, 2026
0e43f0f
fix(cli): isolate switchModel rejection from post-switch side effects
Jul 22, 2026
956e9bf
test(cli): extract setAgentShellFocused mock for future assertions
Jul 22, 2026
f2ddbb1
fix(cli): localize model toggle runtime messages + gate debug log
Jul 24, 2026
0a45b20
fix(cli): document cross-provider toggleModel disambiguation + add am…
Jul 24, 2026
21e82a4
fix(cli): resolve merge conflicts with upstream/main in InputPrompt t…
Jul 28, 2026
50e819a
fix(cli): restore deferUntilIdle for Ctrl+Q queue + remove provenance…
Jul 28, 2026
c5eeee8
fix(cli): resolve merge conflicts with main in InputPrompt submit + p…
qwen-code-dev-bot Jul 29, 2026
ec47b37
fix(cli): add handleToggleKeypress to test mocks in InputPrompt.test.tsx
Jul 30, 2026
69ae108
Merge upstream/main into feat/model-toggle-hotkey
Aug 1, 2026
ef729cc
fix(cli): address review feedback on model toggle hotkey
Aug 1, 2026
a7701b1
fix(cli): block model toggle only for the active agent's PTY
Aug 1, 2026
f74c6b7
test(cli): address review feedback on test files
Aug 1, 2026
e238a00
fix(cli): address CI review feedback on model toggle hotkey
Aug 1, 2026
2331c9b
test(cli): add mutant survivor tests for CI review suggestions
Aug 1, 2026
ecd7488
fix(cli): address CI review feedback on model toggle hotkey
Aug 2, 2026
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
3 changes: 2 additions & 1 deletion docs/users/reference/keyboard-shortcuts.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ This document lists the available keyboard shortcuts in Qwen Code.
| `Ctrl+B` | While a foreground shell command is running: promote it to a background task. The child keeps running, the agent's turn unblocks, and the shell appears in `/tasks` + the Background tasks dialog. No-op when no shell is executing — Ctrl+B then falls through to its prompt-area binding (cursor-left). |
| `Alt/Option+M` | Toggle Markdown output between rich rendered previews and raw/source mode. On macOS, the terminal must send Option as Meta. |
| `Shift+Tab` (`Tab` on Windows) | Cycle approval modes (`plan` → `default` → `auto-edit` → `auto` → `yolo`) |
| `Ctrl+F` | Toggle between the current model and the alternate model set in `model.toggleModel`. Requires `model.toggleModel` to be configured in settings. Only active when no shell is focused. |

## Input Prompt

Expand All @@ -35,7 +36,7 @@ This document lists the available keyboard shortcuts in Qwen Code.
| `Esc` (double press) | 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. |
| `Right Arrow` | Move the cursor one character to the right. |
| `Ctrl+H` / `Backspace` | Delete the character to the left of the cursor. |
| `Ctrl+K` | Delete from the cursor to the end of the line. |
| `Ctrl+Left Arrow` / `Meta+Left Arrow` / `Meta+B` | Move the cursor one word to the left. |
Expand Down
113 changes: 113 additions & 0 deletions integration-tests/interactive/model-toggle-hotkey.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
/**
* @license
* Copyright 2026 Qwen Team
* SPDX-License-Identifier: Apache-2.0
*/

/**
* E2E test for the Ctrl+F model toggle hotkey.
*
* Sends a raw Ctrl+F byte (0x06) through the PTY — the same byte
* a non-kitty PTY would send for Ctrl+F — and asserts the "Switched
* to" message appears in the TUI output.
*
* NOTE: PTY-based interactive tests don't work on Windows (node-pty
* limitation). Skipped via IS_WINDOWS guard.
*/

import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { TestRig } from '../test-helper.js';

const IS_WINDOWS = process.platform === 'win32';

(IS_WINDOWS ? describe.skip : describe)('model toggle hotkey', () => {
let rig: TestRig;
let savedLang: string | undefined;

beforeEach(async () => {
// Save original value so afterEach can restore it.
savedLang = process.env['QWEN_CODE_LANG'];
// Pre-seed locale so waitForText('Type your message') is stable
// regardless of the host's locale settings.
process.env['QWEN_CODE_LANG'] = 'en';

rig = new TestRig();
await rig.setup('model-toggle-hotkey-e2e', {
settings: {
security: {
auth: {
selectedType: 'openai',
},
},
model: {
name: 'model-a',
toggleModel: 'model-b',
},
modelProviders: {
openai: [{ id: 'model-a' }, { id: 'model-b' }],
},
},
});
});

afterEach(async () => {
// Restore original value instead of blindly deleting.
if (savedLang !== undefined) {
process.env['QWEN_CODE_LANG'] = savedLang;
} else {
delete process.env['QWEN_CODE_LANG'];
}
await rig.cleanup();
});

it('should toggle model on Ctrl+F and show Switched to message', async () => {
const { ptyProcess, promise } = rig.runInteractive();

let output = '';

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] E2E test never terminates the PTY process

runInteractive() returns { ptyProcess, promise } but only ptyProcess is destructured. The CLI process is never stopped — rig.cleanup() only deletes the test directory. Orphaned PTY processes accumulate during test runs.

Suggested change
let output = '';
const { ptyProcess, promise } = rig.runInteractive();

Then at the end of the test, gracefully exit:

ptyProcess.write('\x03'); // Ctrl+C
await new Promise(r => setTimeout(r, 500));
ptyProcess.write('\x03'); // Ctrl+C again
await promise;

— qwen3.7-max via Qwen Code /review

ptyProcess.onData((data) => {
output += data;
});

// If the CLI exits early (crash, OOM), surface the exit code in the
// failure message instead of a generic waitForText timeout.
let earlyExit: string | null = null;
void promise.then(({ exitCode, output: exitOutput }) => {
earlyExit = `CLI exited with code ${exitCode}. Last output:\n${exitOutput.slice(-500)}`;
});

// Wait for CLI to be ready
const isReady = await rig.waitForText('Type your message', 30000);
expect(isReady, earlyExit ?? 'CLI did not start in interactive mode').toBe(
true,
);

// Raw Ctrl+F byte (0x06). In a PTY without kitty-protocol
// negotiation this is what the terminal sends for Ctrl+F;
// Ink decodes it as Key({ctrl: true, name: 'f'}).
const CTRL_F = '\x06';

// Toggle to toggleModel (model-b) and wait for the info message
ptyProcess.write(CTRL_F);
const switchedToModelB = await rig.waitForText(
'Switched to model-b',
10000,
);
expect(
switchedToModelB,
earlyExit ??
`Expected 'Switched to model-b' after first toggle. Output:\n${output}`,
).toBe(true);

// Toggle back to original model (model-a)
ptyProcess.write(CTRL_F);
const switchedToModelA = await rig.waitForText(
'Switched to model-a',
10000,
);
expect(
switchedToModelA,
earlyExit ??
`Expected 'Switched to model-a' after second toggle. Output:\n${output}`,
).toBe(true);
});
});
Loading