-
Notifications
You must be signed in to change notification settings - Fork 3k
feat(cli): Add model toggle hotkey (Ctrl+F) #6486
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
ac2945a
f7179d7
8bf4189
23cf916
63d423a
934ca93
4b59e75
e85e8f9
0e43f0f
956e9bf
f2ddbb1
0a45b20
21e82a4
50e819a
c5eeee8
ec47b37
69ae108
ef729cc
a7701b1
f74c6b7
e238a00
2331c9b
ecd7488
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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 | ||
|
|
||
|
|
@@ -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. | | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [Suggestion] Ctrl+F cursor-right is removed from docs unconditionally, but the actual behavior is only suppressed when Concrete cost: users reading the shortcuts reference may stop using a working readline binding, or file bug reports about "missing" cursor-right functionality. Consider adding a parenthetical: — qwen3.7-max via Qwen Code /review |
||
| | `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. | | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,119 @@ | ||||||||||||||||
| /** | ||||||||||||||||
| * @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'; | ||||||||||||||||
|
Comment on lines
+1
to
+19
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [Suggestion] Integration test is outside every npm workspace — This test file lives in Concrete cost: a regression in Consider adding unit-level tests within — qwen3.7-max via Qwen Code /review |
||||||||||||||||
|
|
||||||||||||||||
| const IS_WINDOWS = process.platform === 'win32'; | ||||||||||||||||
|
|
||||||||||||||||
| (IS_WINDOWS ? describe.skip : describe)('model toggle hotkey', () => { | ||||||||||||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [Suggestion] This integration test lives outside every npm workspace, so the per-workspace 中文说明该集成测试位于所有 npm workspace 之外,因此按 workspace 运行的 — qwen3.8-max-preview via Qwen Code /review |
||||||||||||||||
| 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', | ||||||||||||||||
| }, | ||||||||||||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [Critical] E2E test uses synthetic model IDs ( The
Suggested change
Use real model IDs from the registry, or set up a — qwen3.7-max via Qwen Code /review |
||||||||||||||||
| 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 () => { | ||||||||||||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [Suggestion] The test only covers the idle/prompt state. No test verifies that Ctrl+F is a no-op when a shell command is executing. — Concrete cost: a future refactor could break the shell-focus guard, causing Ctrl+F to toggle the model while a shell is running, with no test to catch it. — qwen3.7-max via Qwen Code /review |
||||||||||||||||
| const { ptyProcess, promise } = rig.runInteractive(); | ||||||||||||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [Suggestion] The test never kills the PTY it spawns. 中文说明该测试从不杀死它启动的 PTY。 — qwen3.8-max-preview via Qwen Code /review |
||||||||||||||||
|
|
||||||||||||||||
| try { | ||||||||||||||||
| let output = ''; | ||||||||||||||||
| 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); | ||||||||||||||||
| } finally { | ||||||||||||||||
| ptyProcess.kill(); | ||||||||||||||||
| await promise; | ||||||||||||||||
| } | ||||||||||||||||
| }); | ||||||||||||||||
| }); | ||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[Critical] Ctrl+F is documented with two conflicting meanings in this file. The General section (this line) says "Ctrl+F → toggle model", but the Input Prompt section (line 39) still says "Ctrl+F / Right Arrow → move cursor right". Users reading both sections will be confused.
Consider following the Ctrl+B pattern, which documents both behaviors: "promote to background task ... No-op when no shell is executing — Ctrl+B then falls through to its prompt-area binding (cursor-left)." E.g.:
— qwen3.7-max via Qwen Code /review