diff --git a/docs/users/features/approval-mode.md b/docs/users/features/approval-mode.md index e072f237c..c46067093 100644 --- a/docs/users/features/approval-mode.md +++ b/docs/users/features/approval-mode.md @@ -20,7 +20,7 @@ Qwen Code offers three distinct permission modes that allow you to flexibly cont > [!tip] > -> You can quickly cycle through modes during a session using **Shift+Tab**. The terminal status bar shows your current mode, so you always know what permissions Qwen Code has. +> You can quickly cycle through modes during a session using **Shift+Tab** (or **Tab** on Windows). The terminal status bar shows your current mode, so you always know what permissions Qwen Code has. ## 1. Use Plan Mode for safe code analysis @@ -36,9 +36,9 @@ Plan Mode instructs Qwen Code to create a plan by analyzing the codebase with ** **Turn on Plan Mode during a session** -You can switch into Plan Mode during a session using **Shift+Tab** to cycle through permission modes. +You can switch into Plan Mode during a session using **Shift+Tab** (or **Tab** on Windows) to cycle through permission modes. -If you are in Normal Mode, **Shift+Tab** first switches into `auto-edits` Mode, indicated by `⏵⏵ accept edits on` at the bottom of the terminal. A subsequent **Shift+Tab** will switch into Plan Mode, indicated by `⏸ plan mode`. +If you are in Normal Mode, **Shift+Tab** (or **Tab** on Windows) first switches into `auto-edits` Mode, indicated by `⏵⏵ accept edits on` at the bottom of the terminal. A subsequent **Shift+Tab** (or **Tab** on Windows) will switch into Plan Mode, indicated by `⏸ plan mode`. **Start a new session in Plan Mode** @@ -100,7 +100,7 @@ Default Mode is the standard way to work with Qwen Code. In this mode, you maint **Turn on Default Mode during a session** -You can switch into Default Mode during a session using **Shift+Tab**​ to cycle through permission modes. If you're in any other mode, pressing **Shift+Tab**​ will eventually cycle back to Default Mode, indicated by the absence of any mode indicator at the bottom of the terminal. +You can switch into Default Mode during a session using **Shift+Tab**​ (or **Tab** on Windows) to cycle through permission modes. If you're in any other mode, pressing **Shift+Tab** (or **Tab** on Windows) will eventually cycle back to Default Mode, indicated by the absence of any mode indicator at the bottom of the terminal. **Start a new session in Default Mode** @@ -164,7 +164,7 @@ Auto-Edit Mode instructs Qwen Code to automatically approve file edits while req /approval-mode auto-edit # Or use keyboard shortcut -Shift+Tab # Switch from other modes +Shift+Tab (or Tab on Windows) # Switch from other modes ``` ### Workflow Example @@ -235,7 +235,7 @@ qwen --prompt "Run the test suite, fix all failing tests, then commit changes" ### Keyboard Shortcut Switching -During a Qwen Code session, use **Shift+Tab**​ to quickly cycle through the three modes: +During a Qwen Code session, use **Shift+Tab**​ (or **Tab** on Windows) to quickly cycle through the three modes: ``` Default Mode → Auto-Edit Mode → YOLO Mode → Plan Mode → Default Mode diff --git a/docs/users/reference/keyboard-shortcuts.md b/docs/users/reference/keyboard-shortcuts.md index 46f3c8c42..fc2f86286 100644 --- a/docs/users/reference/keyboard-shortcuts.md +++ b/docs/users/reference/keyboard-shortcuts.md @@ -4,16 +4,16 @@ This document lists the available keyboard shortcuts in Qwen Code. ## General -| Shortcut | Description | -| ----------- | --------------------------------------------------------------------------------------------------------------------- | -| `Esc` | Close dialogs and suggestions. | -| `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. | -| `Ctrl+O` | Toggle the display of the debug console. | -| `Ctrl+S` | Allows long responses to print fully, disabling truncation. Use your terminal's scrollback to view the entire output. | -| `Ctrl+T` | Toggle the display of tool descriptions. | -| `Shift+Tab` | Cycle approval modes (`plan` → `default` → `auto-edit` → `yolo`). | +| Shortcut | Description | +| ------------------------------ | --------------------------------------------------------------------------------------------------------------------- | +| `Esc` | Close dialogs and suggestions. | +| `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. | +| `Ctrl+O` | Toggle the display of the debug console. | +| `Ctrl+S` | Allows long responses to print fully, disabling truncation. Use your terminal's scrollback to view the entire output. | +| `Ctrl+T` | Toggle the display of tool descriptions. | +| `Shift+Tab` (`Tab` on Windows) | Cycle approval modes (`plan` → `default` → `auto-edit` → `yolo`) | ## Input Prompt diff --git a/package-lock.json b/package-lock.json index b4f0301d3..c198aeb82 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3879,6 +3879,7 @@ "version": "2.4.9", "resolved": "https://registry.npmjs.org/@types/prompts/-/prompts-2.4.9.tgz", "integrity": "sha512-qTxFi6Buiu8+50/+3DGIWLHM6QuWsEKugJnnP6iv2Mc4ncxE4A/OJkjuVOA+5X0X1S/nq5VJRa8Lu+nwcvbrKA==", + "dev": true, "license": "MIT", "dependencies": { "@types/node": "*", @@ -17348,7 +17349,6 @@ "@iarna/toml": "^2.2.5", "@modelcontextprotocol/sdk": "^1.25.1", "@qwen-code/qwen-code-core": "file:../core", - "@types/prompts": "^2.4.9", "@types/update-notifier": "^6.0.8", "ansi-regex": "^6.2.2", "command-exists": "^1.2.9", @@ -17392,6 +17392,7 @@ "@types/diff": "^7.0.2", "@types/dotenv": "^6.1.1", "@types/node": "^20.11.24", + "@types/prompts": "^2.4.9", "@types/react": "^19.1.8", "@types/react-dom": "^19.1.6", "@types/semver": "^7.7.0", diff --git a/packages/cli/src/i18n/locales/de.js b/packages/cli/src/i18n/locales/de.js index 95496b4be..10082a350 100644 --- a/packages/cli/src/i18n/locales/de.js +++ b/packages/cli/src/i18n/locales/de.js @@ -23,6 +23,7 @@ export default { 'auto-accept edits': 'Änderungen automatisch akzeptieren', 'Accepting edits': 'Änderungen werden akzeptiert', '(shift + tab to cycle)': '(Umschalt + Tab zum Wechseln)', + '(tab to cycle)': '(Tab zum Wechseln)', 'Execute shell commands via {{symbol}} (e.g., {{example1}}) or use natural language (e.g., {{example2}}).': 'Shell-Befehle über {{symbol}} ausführen (z.B. {{example1}}) oder natürliche Sprache verwenden (z.B. {{example2}}).', '!': '!', @@ -1358,4 +1359,8 @@ export default { 'Erweiterungsseite wird im Browser geöffnet: {{url}}', 'Failed to open browser. Check out the extensions gallery at {{url}}': 'Browser konnte nicht geöffnet werden. Besuchen Sie die Erweiterungsgalerie unter {{url}}', + 'You can switch permission mode quickly with Shift+Tab or /approval-mode.': + 'Sie können den Berechtigungsmodus schnell mit Shift+Tab oder /approval-mode wechseln.', + 'You can switch permission mode quickly with Tab or /approval-mode.': + 'Sie können den Berechtigungsmodus schnell mit Tab oder /approval-mode wechseln.', }; diff --git a/packages/cli/src/i18n/locales/en.js b/packages/cli/src/i18n/locales/en.js index cef757a0b..063c586d4 100644 --- a/packages/cli/src/i18n/locales/en.js +++ b/packages/cli/src/i18n/locales/en.js @@ -23,6 +23,7 @@ export default { 'auto-accept edits': 'auto-accept edits', 'Accepting edits': 'Accepting edits', '(shift + tab to cycle)': '(shift + tab to cycle)', + '(tab to cycle)': '(tab to cycle)', 'Execute shell commands via {{symbol}} (e.g., {{example1}}) or use natural language (e.g., {{example2}}).': 'Execute shell commands via {{symbol}} (e.g., {{example1}}) or use natural language (e.g., {{example2}}).', '!': '!', @@ -1091,6 +1092,8 @@ export default { 'You can resume a previous conversation by running qwen --continue or qwen --resume.', 'You can switch permission mode quickly with Shift+Tab or /approval-mode.': 'You can switch permission mode quickly with Shift+Tab or /approval-mode.', + 'You can switch permission mode quickly with Tab or /approval-mode.': + 'You can switch permission mode quickly with Tab or /approval-mode.', // ============================================================================ // Exit Screen / Stats diff --git a/packages/cli/src/i18n/locales/ru.js b/packages/cli/src/i18n/locales/ru.js index 24fd24b9d..5aa3ef4c2 100644 --- a/packages/cli/src/i18n/locales/ru.js +++ b/packages/cli/src/i18n/locales/ru.js @@ -23,6 +23,7 @@ export default { 'auto-accept edits': 'Режим принятия правок', 'Accepting edits': 'Принятие правок', '(shift + tab to cycle)': '(shift + tab для переключения)', + '(tab to cycle)': '(Tab для переключения)', 'Execute shell commands via {{symbol}} (e.g., {{example1}}) or use natural language (e.g., {{example2}}).': 'Выполняйте команды терминала через {{symbol}} (например, {{example1}}) или используйте естественный язык (например, {{example2}}).', '!': '!', @@ -1363,4 +1364,8 @@ export default { 'Открываем страницу расширений в браузере: {{url}}', 'Failed to open browser. Check out the extensions gallery at {{url}}': 'Не удалось открыть браузер. Посетите галерею расширений по адресу {{url}}', + 'You can switch permission mode quickly with Shift+Tab or /approval-mode.': + 'Вы можете быстро переключать режим разрешений с помощью Shift+Tab или /approval-mode.', + 'You can switch permission mode quickly with Tab or /approval-mode.': + 'Вы можете быстро переключать режим разрешений с помощью Tab или /approval-mode.', }; diff --git a/packages/cli/src/i18n/locales/zh.js b/packages/cli/src/i18n/locales/zh.js index 4f8c95d88..0fa64a46c 100644 --- a/packages/cli/src/i18n/locales/zh.js +++ b/packages/cli/src/i18n/locales/zh.js @@ -22,6 +22,7 @@ export default { 'auto-accept edits': '自动接受编辑', 'Accepting edits': '接受编辑', '(shift + tab to cycle)': '(shift + tab 切换)', + '(tab to cycle)': '(按 tab 切换)', 'Execute shell commands via {{symbol}} (e.g., {{example1}}) or use natural language (e.g., {{example2}}).': '通过 {{symbol}} 执行 shell 命令(例如,{{example1}})或使用自然语言(例如,{{example2}})', '!': '!', @@ -1031,6 +1032,8 @@ export default { '运行 qwen --continue 或 qwen --resume 可继续之前的会话。', 'You can switch permission mode quickly with Shift+Tab or /approval-mode.': '按 Shift+Tab 或输入 /approval-mode 可快速切换权限模式。', + 'You can switch permission mode quickly with Tab or /approval-mode.': + '按 Tab 或输入 /approval-mode 可快速切换权限模式。', // ============================================================================ // Exit Screen / Stats diff --git a/packages/cli/src/ui/components/AutoAcceptIndicator.tsx b/packages/cli/src/ui/components/AutoAcceptIndicator.tsx index 550c77dc7..d22b39a19 100644 --- a/packages/cli/src/ui/components/AutoAcceptIndicator.tsx +++ b/packages/cli/src/ui/components/AutoAcceptIndicator.tsx @@ -21,21 +21,26 @@ export const AutoAcceptIndicator: React.FC = ({ let textContent = ''; let subText = ''; + const cycleText = + process.platform === 'win32' + ? ` ${t('(tab to cycle)')}` + : ` ${t('(shift + tab to cycle)')}`; + switch (approvalMode) { case ApprovalMode.PLAN: textColor = theme.status.success; textContent = t('plan mode'); - subText = ` ${t('(shift + tab to cycle)')}`; + subText = cycleText; break; case ApprovalMode.AUTO_EDIT: textColor = theme.status.warning; textContent = t('auto-accept edits'); - subText = ` ${t('(shift + tab to cycle)')}`; + subText = cycleText; break; case ApprovalMode.YOLO: textColor = theme.status.error; textContent = t('YOLO mode'); - subText = ` ${t('(shift + tab to cycle)')}`; + subText = cycleText; break; case ApprovalMode.DEFAULT: default: diff --git a/packages/cli/src/ui/components/Help.test.tsx b/packages/cli/src/ui/components/Help.test.tsx index 0540247db..23b379eaf 100644 --- a/packages/cli/src/ui/components/Help.test.tsx +++ b/packages/cli/src/ui/components/Help.test.tsx @@ -46,6 +46,18 @@ const mockCommands: readonly SlashCommand[] = [ ]; describe('Help Component', () => { + it('should render platform-specific keyboard shortcuts', () => { + const { lastFrame } = render(); + const output = lastFrame(); + + if (process.platform === 'win32') { + expect(output).toContain('Tab'); + expect(output).not.toContain('Shift+Tab'); + } else { + expect(output).toContain('Shift+Tab'); + } + }); + it('should not render hidden commands', () => { const { lastFrame } = render(); const output = lastFrame(); diff --git a/packages/cli/src/ui/components/Help.tsx b/packages/cli/src/ui/components/Help.tsx index 6b51a6a8c..64c2f7688 100644 --- a/packages/cli/src/ui/components/Help.tsx +++ b/packages/cli/src/ui/components/Help.tsx @@ -154,7 +154,7 @@ export const Help: React.FC = ({ commands, width }) => ( - Shift+Tab + {process.platform === 'win32' ? 'Tab' : 'Shift+Tab'} {' '} - {t('Cycle approval modes')} diff --git a/packages/cli/src/ui/components/KeyboardShortcuts.tsx b/packages/cli/src/ui/components/KeyboardShortcuts.tsx index 75ca5eca9..9ce49b415 100644 --- a/packages/cli/src/ui/components/KeyboardShortcuts.tsx +++ b/packages/cli/src/ui/components/KeyboardShortcuts.tsx @@ -28,7 +28,10 @@ const getShortcuts = (): Shortcut[] => [ { key: '/', description: t('for commands') }, { key: '@', description: t('for file paths') }, { key: 'esc esc', description: t('to clear input') }, - { key: 'shift+tab', description: t('to cycle approvals') }, + { + key: process.platform === 'win32' ? 'tab' : 'shift+tab', + description: t('to cycle approvals'), + }, { key: 'ctrl+c', description: t('to quit') }, { key: getNewlineKey(), description: t('for newline') + ' ⏎' }, { key: 'ctrl+l', description: t('to clear screen') }, diff --git a/packages/cli/src/ui/components/Tips.tsx b/packages/cli/src/ui/components/Tips.tsx index 62d82ba4c..d1b6a71bf 100644 --- a/packages/cli/src/ui/components/Tips.tsx +++ b/packages/cli/src/ui/components/Tips.tsx @@ -17,7 +17,9 @@ const startupTips = [ 'You can run any shell commands from Qwen Code using ! (e.g. !ls).', 'Type / to open the command popup; Tab autocompletes slash commands and saved prompts.', 'You can resume a previous conversation by running qwen --continue or qwen --resume.', - 'You can switch permission mode quickly with Shift+Tab or /approval-mode.', + process.platform === 'win32' + ? 'You can switch permission mode quickly with Tab or /approval-mode.' + : 'You can switch permission mode quickly with Shift+Tab or /approval-mode.', ] as const; export const Tips: React.FC = () => { diff --git a/packages/cli/src/ui/contexts/KeypressContext.test.tsx b/packages/cli/src/ui/contexts/KeypressContext.test.tsx index 1130f8352..93e7742f8 100644 --- a/packages/cli/src/ui/contexts/KeypressContext.test.tsx +++ b/packages/cli/src/ui/contexts/KeypressContext.test.tsx @@ -229,33 +229,6 @@ describe('KeypressContext - Kitty Protocol', () => { }), ); }); - - it('should not process kitty sequences when kitty protocol is disabled', async () => { - const keyHandler = vi.fn(); - - const { result } = renderHook(() => useKeypressContext(), { - wrapper: ({ children }) => - wrapper({ children, kittyProtocolEnabled: false }), - }); - - act(() => { - result.current.subscribe(keyHandler); - }); - - // Send kitty protocol sequence for numpad enter - act(() => { - stdin.sendKittySequence(`\x1b[57414u`); - }); - - // When kitty protocol is disabled, the sequence should be passed through - // as individual keypresses, not recognized as a single enter key - expect(keyHandler).not.toHaveBeenCalledWith( - expect.objectContaining({ - name: 'return', - kittyProtocol: true, - }), - ); - }); }); describe('Escape key handling', () => { @@ -1256,13 +1229,13 @@ describe('KeypressContext - Kitty Protocol', () => { }); expect(consoleLogSpy).toHaveBeenCalledWith( - '[DEBUG] Kitty buffer accumulating:', + '[DEBUG] CSI buffer accumulating:', expect.stringContaining('\x1b[27u'), ); const parsedCall = consoleLogSpy.mock.calls.find( (args) => typeof args[0] === 'string' && - args[0].includes('[DEBUG] Kitty sequence parsed successfully'), + args[0].includes('[DEBUG] CSI sequence parsed successfully'), ); expect(parsedCall).toBeTruthy(); expect(parsedCall?.[1]).toEqual(expect.stringContaining('\x1b[27u')); @@ -1293,7 +1266,7 @@ describe('KeypressContext - Kitty Protocol', () => { }); expect(consoleLogSpy).toHaveBeenCalledWith( - '[DEBUG] Kitty buffer overflow, clearing:', + '[DEBUG] CSI buffer overflow, clearing:', expect.any(String), ); }); @@ -1384,13 +1357,13 @@ describe('KeypressContext - Kitty Protocol', () => { // Verify debug logging for accumulation expect(consoleLogSpy).toHaveBeenCalledWith( - '[DEBUG] Kitty buffer accumulating:', + '[DEBUG] CSI buffer accumulating:', sequence, ); // Verify warning for char codes expect(consoleWarnSpy).toHaveBeenCalledWith( - 'Kitty sequence buffer has char codes:', + 'CSI sequence buffer has char codes:', [27, 91, 49, 50], ); }); diff --git a/packages/cli/src/ui/contexts/KeypressContext.tsx b/packages/cli/src/ui/contexts/KeypressContext.tsx index 0f01712cc..8df81a9d8 100644 --- a/packages/cli/src/ui/contexts/KeypressContext.tsx +++ b/packages/cli/src/ui/contexts/KeypressContext.tsx @@ -508,95 +508,97 @@ export function KeypressProvider({ return; } - if (kittyProtocolEnabled) { - if ( - kittySequenceBuffer || - (key.sequence.startsWith(`${ESC}[`) && - !key.sequence.startsWith(PASTE_MODE_PREFIX) && - !key.sequence.startsWith(PASTE_MODE_SUFFIX) && - !key.sequence.startsWith(FOCUS_IN) && - !key.sequence.startsWith(FOCUS_OUT)) - ) { - kittySequenceBuffer += key.sequence; + // Parse CSI sequences for both Kitty protocol and legacy terminals + // This ensures Shift+Tab and other special keys work correctly even when + // Kitty protocol is not available (e.g., Windows PowerShell) + if ( + kittySequenceBuffer || + (key.sequence && + key.sequence.startsWith(`${ESC}[`) && + !key.sequence.startsWith(PASTE_MODE_PREFIX) && + !key.sequence.startsWith(PASTE_MODE_SUFFIX) && + !key.sequence.startsWith(FOCUS_IN) && + !key.sequence.startsWith(FOCUS_OUT)) + ) { + kittySequenceBuffer += key.sequence; + if (debugKeystrokeLogging) { + console.log('[DEBUG] CSI buffer accumulating:', kittySequenceBuffer); + } + + // Try to peel off as many complete sequences as are available at the + // start of the buffer. This handles batched inputs cleanly. If the + // prefix is incomplete or invalid, skip to the next CSI introducer + // (ESC[) so that a following valid sequence can still be parsed. + let parsedAny = false; + while (kittySequenceBuffer) { + const parsed = parseKittyPrefix(kittySequenceBuffer); + if (!parsed) { + // Look for the next potential CSI start beyond index 0 + const nextStart = kittySequenceBuffer.indexOf(`${ESC}[`, 1); + if (nextStart > 0) { + if (debugKeystrokeLogging) { + console.log( + '[DEBUG] Skipping incomplete/invalid CSI prefix:', + kittySequenceBuffer.slice(0, nextStart), + ); + } + kittySequenceBuffer = kittySequenceBuffer.slice(nextStart); + continue; + } + break; + } + if (debugKeystrokeLogging) { + const parsedSequence = kittySequenceBuffer.slice(0, parsed.length); + if (kittySequenceBuffer.length > parsed.length) { + console.log( + '[DEBUG] CSI sequence parsed successfully (prefix):', + parsedSequence, + ); + } else { + console.log( + '[DEBUG] CSI sequence parsed successfully:', + parsedSequence, + ); + } + } + // Consume the parsed prefix and broadcast it. + kittySequenceBuffer = kittySequenceBuffer.slice(parsed.length); + broadcast(parsed.key); + parsedAny = true; + } + if (parsedAny) return; + + if (config?.getDebugMode() || debugKeystrokeLogging) { + const codes = Array.from(kittySequenceBuffer).map((ch) => + ch.charCodeAt(0), + ); + console.warn('CSI sequence buffer has char codes:', codes); + } + + if ( + kittyProtocolEnabled && + kittySequenceBuffer.length > MAX_KITTY_SEQUENCE_LENGTH + ) { if (debugKeystrokeLogging) { console.log( - '[DEBUG] Kitty buffer accumulating:', + '[DEBUG] CSI buffer overflow, clearing:', kittySequenceBuffer, ); } - - // Try to peel off as many complete sequences as are available at the - // start of the buffer. This handles batched inputs cleanly. If the - // prefix is incomplete or invalid, skip to the next CSI introducer - // (ESC[) so that a following valid sequence can still be parsed. - let parsedAny = false; - while (kittySequenceBuffer) { - const parsed = parseKittyPrefix(kittySequenceBuffer); - if (!parsed) { - // Look for the next potential CSI start beyond index 0 - const nextStart = kittySequenceBuffer.indexOf(`${ESC}[`, 1); - if (nextStart > 0) { - if (debugKeystrokeLogging) { - console.log( - '[DEBUG] Skipping incomplete/invalid CSI prefix:', - kittySequenceBuffer.slice(0, nextStart), - ); - } - kittySequenceBuffer = kittySequenceBuffer.slice(nextStart); - continue; - } - break; - } - if (debugKeystrokeLogging) { - const parsedSequence = kittySequenceBuffer.slice( - 0, - parsed.length, - ); - if (kittySequenceBuffer.length > parsed.length) { - console.log( - '[DEBUG] Kitty sequence parsed successfully (prefix):', - parsedSequence, - ); - } else { - console.log( - '[DEBUG] Kitty sequence parsed successfully:', - parsedSequence, - ); - } - } - // Consume the parsed prefix and broadcast it. - kittySequenceBuffer = kittySequenceBuffer.slice(parsed.length); - broadcast(parsed.key); - parsedAny = true; - } - if (parsedAny) return; - - if (config?.getDebugMode() || debugKeystrokeLogging) { - const codes = Array.from(kittySequenceBuffer).map((ch) => - ch.charCodeAt(0), + if (config) { + const event = new KittySequenceOverflowEvent( + kittySequenceBuffer.length, + kittySequenceBuffer, ); - console.warn('Kitty sequence buffer has char codes:', codes); - } - - if (kittySequenceBuffer.length > MAX_KITTY_SEQUENCE_LENGTH) { - if (debugKeystrokeLogging) { - console.log( - '[DEBUG] Kitty buffer overflow, clearing:', - kittySequenceBuffer, - ); - } - if (config) { - const event = new KittySequenceOverflowEvent( - kittySequenceBuffer.length, - kittySequenceBuffer, - ); - logKittySequenceOverflow(config, event); - } - kittySequenceBuffer = ''; - } else { - return; + logKittySequenceOverflow(config, event); } + kittySequenceBuffer = ''; + } else if (!kittyProtocolEnabled) { + // For non-Kitty terminals, clear the buffer to avoid accumulation + kittySequenceBuffer = ''; + } else { + return; } } diff --git a/packages/cli/src/ui/hooks/useAutoAcceptIndicator.test.ts b/packages/cli/src/ui/hooks/useAutoAcceptIndicator.test.ts index 35e7d7430..430fc4c3c 100644 --- a/packages/cli/src/ui/hooks/useAutoAcceptIndicator.test.ts +++ b/packages/cli/src/ui/hooks/useAutoAcceptIndicator.test.ts @@ -240,7 +240,13 @@ describe('useAutoAcceptIndicator', () => { shift: false, } as Key); }); - expect(mockConfigInstance.setApprovalMode).not.toHaveBeenCalled(); + if (process.platform === 'win32') { + // On Windows, Tab alone toggles approval mode + expect(mockConfigInstance.setApprovalMode).toHaveBeenCalled(); + mockConfigInstance.setApprovalMode.mockClear(); + } else { + expect(mockConfigInstance.setApprovalMode).not.toHaveBeenCalled(); + } act(() => { capturedUseKeypressHandler({ diff --git a/packages/cli/src/ui/hooks/useAutoAcceptIndicator.ts b/packages/cli/src/ui/hooks/useAutoAcceptIndicator.ts index e09c5d0eb..e3908608c 100644 --- a/packages/cli/src/ui/hooks/useAutoAcceptIndicator.ts +++ b/packages/cli/src/ui/hooks/useAutoAcceptIndicator.ts @@ -36,7 +36,16 @@ export function useAutoAcceptIndicator({ useKeypress( (key) => { // Handle Shift+Tab to cycle through all modes - if (key.shift && key.name === 'tab') { + // On Windows, Shift+Tab is indistinguishable from Tab (\t) in some terminals, + // so we allow Tab to switch modes as well to support the shortcut. + const isShiftTab = key.shift && key.name === 'tab'; + const isWindowsTab = + process.platform === 'win32' && + key.name === 'tab' && + !key.ctrl && + !key.meta; + + if (isShiftTab || isWindowsTab) { const currentMode = config.getApprovalMode(); const currentIndex = APPROVAL_MODES.indexOf(currentMode); const nextIndex =