From 90436b96d6074e2ca6b5c0eca7c91831703509a1 Mon Sep 17 00:00:00 2001 From: SLP-DEV1 Date: Tue, 1 Sep 2026 23:55:28 +0200 Subject: [PATCH 01/10] fix(cli): hide unavailable external editor option --- .../src/ui/components/messages/ToolConfirmationMessage.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/cli/src/ui/components/messages/ToolConfirmationMessage.tsx b/packages/cli/src/ui/components/messages/ToolConfirmationMessage.tsx index a3915caad37..8eaf42b2df9 100644 --- a/packages/cli/src/ui/components/messages/ToolConfirmationMessage.tsx +++ b/packages/cli/src/ui/components/messages/ToolConfirmationMessage.tsx @@ -25,6 +25,7 @@ import { IdeClient, ToolConfirmationOutcome, buildHumanReadableRuleLabel, + isEditorAvailable, } from '@qwen-code/qwen-code-core'; import type { RadioSelectItem } from '../shared/RadioButtonSelect.js'; import { RadioButtonSelect } from '../shared/RadioButtonSelect.js'; @@ -259,7 +260,7 @@ export const ToolConfirmationMessage: React.FC< if ( !confirmationDetails.hideModify && (!config.getIdeMode() || !isDiffingEnabled) && - preferredEditor + isEditorAvailable(preferredEditor) ) { options.push({ label: t('Modify with external editor'), @@ -778,4 +779,4 @@ export const ToolConfirmationMessage: React.FC< ); -}; +}; \ No newline at end of file From 9eca27ab9db8b6e886f316f00b2c4701d28c7250 Mon Sep 17 00:00:00 2001 From: SLP-DEV1 Date: Tue, 1 Sep 2026 23:55:42 +0200 Subject: [PATCH 02/10] test(cli): cover unavailable preferred editor --- ...onfirmationMessage.externalEditor.test.tsx | 89 +++++++++++++++++++ 1 file changed, 89 insertions(+) create mode 100644 packages/cli/src/ui/components/messages/ToolConfirmationMessage.externalEditor.test.tsx diff --git a/packages/cli/src/ui/components/messages/ToolConfirmationMessage.externalEditor.test.tsx b/packages/cli/src/ui/components/messages/ToolConfirmationMessage.externalEditor.test.tsx new file mode 100644 index 00000000000..8291dd23206 --- /dev/null +++ b/packages/cli/src/ui/components/messages/ToolConfirmationMessage.externalEditor.test.tsx @@ -0,0 +1,89 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import { beforeEach, describe, expect, it, vi } from 'vitest'; + +const { isEditorAvailableMock } = vi.hoisted(() => ({ + isEditorAvailableMock: vi.fn(), +})); + +vi.mock('@qwen-code/qwen-code-core', async (importOriginal) => { + const actual = await importOriginal< + typeof import('@qwen-code/qwen-code-core') + >(); + return { + ...actual, + isEditorAvailable: isEditorAvailableMock, + }; +}); + +import { ToolConfirmationMessage } from './ToolConfirmationMessage.js'; +import type { + Config, + ToolCallConfirmationDetails, +} from '@qwen-code/qwen-code-core'; +import { renderWithProviders } from '../../../test-utils/render.js'; +import type { LoadedSettings } from '../../../config/settings.js'; + +describe('ToolConfirmationMessage external editor availability', () => { + const config = { + isTrustedFolder: () => true, + getIdeMode: () => false, + } as unknown as Config; + + const confirmationDetails: ToolCallConfirmationDetails = { + type: 'edit', + title: 'Confirm Edit', + fileName: 'test.txt', + filePath: '/test.txt', + fileDiff: '...diff...', + originalContent: 'a', + newContent: 'b', + onConfirm: vi.fn(), + }; + + const settings = { + merged: { general: { preferredEditor: 'vscode' } }, + } as unknown as LoadedSettings; + + beforeEach(() => { + isEditorAvailableMock.mockReset(); + }); + + it('shows the modify option when the preferred editor is available', () => { + isEditorAvailableMock.mockReturnValue(true); + + const { lastFrame } = renderWithProviders( + , + { settings }, + ); + + expect(isEditorAvailableMock).toHaveBeenCalledWith('vscode'); + expect(lastFrame()).toContain('Modify with external editor'); + }); + + it('hides the modify option when the configured editor is unavailable', () => { + isEditorAvailableMock.mockReturnValue(false); + + const { lastFrame } = renderWithProviders( + , + { settings }, + ); + + expect(isEditorAvailableMock).toHaveBeenCalledWith('vscode'); + expect(lastFrame()).not.toContain('Modify with external editor'); + }); +}); From 62b17884d98cf4fd7714f5a13fdefc7ec8faab3c Mon Sep 17 00:00:00 2001 From: SLP-DEV1 Date: Tue, 1 Sep 2026 23:57:01 +0200 Subject: [PATCH 03/10] chore(cli): preserve final newline --- .../cli/src/ui/components/messages/ToolConfirmationMessage.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/cli/src/ui/components/messages/ToolConfirmationMessage.tsx b/packages/cli/src/ui/components/messages/ToolConfirmationMessage.tsx index 8eaf42b2df9..3aee97b582d 100644 --- a/packages/cli/src/ui/components/messages/ToolConfirmationMessage.tsx +++ b/packages/cli/src/ui/components/messages/ToolConfirmationMessage.tsx @@ -779,4 +779,4 @@ export const ToolConfirmationMessage: React.FC< ); -}; \ No newline at end of file +}; From 794a5f80b869361c04eb1822455d180957762298 Mon Sep 17 00:00:00 2001 From: SLP-DEV1 Date: Wed, 2 Sep 2026 00:52:39 +0200 Subject: [PATCH 04/10] test(cli): mock external editor availability --- .../messages/ToolConfirmationMessage.test.tsx | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/packages/cli/src/ui/components/messages/ToolConfirmationMessage.test.tsx b/packages/cli/src/ui/components/messages/ToolConfirmationMessage.test.tsx index afc7975c342..ddc3ca37c9c 100644 --- a/packages/cli/src/ui/components/messages/ToolConfirmationMessage.test.tsx +++ b/packages/cli/src/ui/components/messages/ToolConfirmationMessage.test.tsx @@ -11,12 +11,22 @@ import { Box } from 'ink'; // Capture launches of the external editor so the full-plan viewer (#7001) // can be asserted without spawning a real editor process. -const { launchEditorMock } = vi.hoisted(() => ({ +const { launchEditorMock, isEditorAvailableMock } = vi.hoisted(() => ({ launchEditorMock: vi.fn((_filePath: string) => Promise.resolve()), + isEditorAvailableMock: vi.fn((editor: string | undefined) => Boolean(editor)), })); vi.mock('../../hooks/useLaunchEditor.js', () => ({ useLaunchEditor: () => launchEditorMock, })); +vi.mock('@qwen-code/qwen-code-core', async (importOriginal) => { + const actual = await importOriginal< + typeof import('@qwen-code/qwen-code-core') + >(); + return { + ...actual, + isEditorAvailable: isEditorAvailableMock, + }; +}); import { ToolConfirmationMessage } from './ToolConfirmationMessage.js'; import type { From 1d1a71a6b8a02ccc9277b4ef3200bde621f47098 Mon Sep 17 00:00:00 2001 From: SLP-DEV1 Date: Wed, 2 Sep 2026 09:41:49 +0200 Subject: [PATCH 05/10] fix(cli): memoize external editor availability --- .../components/messages/ToolConfirmationMessage.tsx | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/packages/cli/src/ui/components/messages/ToolConfirmationMessage.tsx b/packages/cli/src/ui/components/messages/ToolConfirmationMessage.tsx index 3aee97b582d..87c3b0b30d6 100644 --- a/packages/cli/src/ui/components/messages/ToolConfirmationMessage.tsx +++ b/packages/cli/src/ui/components/messages/ToolConfirmationMessage.tsx @@ -5,7 +5,7 @@ */ import type React from 'react'; -import { useEffect, useState } from 'react'; +import { useEffect, useMemo, useState } from 'react'; import { promises as fs } from 'node:fs'; import * as os from 'node:os'; import * as path from 'node:path'; @@ -68,6 +68,13 @@ export const ToolConfirmationMessage: React.FC< const preferredEditor = settings.merged.general?.preferredEditor as | EditorType | undefined; + const editorAvailable = useMemo( + () => + confirmationDetails.type === 'edit' && + preferredEditor !== undefined && + isEditorAvailable(preferredEditor), + [confirmationDetails.type, preferredEditor], + ); const [ideClient, setIdeClient] = useState(null); const [isDiffingEnabled, setIsDiffingEnabled] = useState(false); @@ -260,7 +267,7 @@ export const ToolConfirmationMessage: React.FC< if ( !confirmationDetails.hideModify && (!config.getIdeMode() || !isDiffingEnabled) && - isEditorAvailable(preferredEditor) + editorAvailable ) { options.push({ label: t('Modify with external editor'), From cb2aaa75192163decdb284e3c678dbc5a898f184 Mon Sep 17 00:00:00 2001 From: SLP-DEV1 Date: Wed, 2 Sep 2026 09:44:28 +0200 Subject: [PATCH 06/10] test(cli): consolidate external editor coverage --- .../messages/ToolConfirmationMessage.test.tsx | 37 ++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/packages/cli/src/ui/components/messages/ToolConfirmationMessage.test.tsx b/packages/cli/src/ui/components/messages/ToolConfirmationMessage.test.tsx index ddc3ca37c9c..00d15a9a2f3 100644 --- a/packages/cli/src/ui/components/messages/ToolConfirmationMessage.test.tsx +++ b/packages/cli/src/ui/components/messages/ToolConfirmationMessage.test.tsx @@ -607,6 +607,40 @@ describe('ToolConfirmationMessage', () => { isTrustedFolder: () => true, getIdeMode: () => false, } as unknown as Config; + isEditorAvailableMock.mockClear(); + + const props = { + confirmationDetails: editConfirmationDetails, + config: mockConfig, + availableTerminalHeight: 30, + contentWidth: 80, + }; + const { lastFrame, rerender } = renderWithProviders( + , + { + settings: { + merged: { general: { preferredEditor: 'vscode' } }, + } as unknown as LoadedSettings, + }, + ); + + expect(lastFrame()).toContain('Modify with external editor'); + expect(isEditorAvailableMock).toHaveBeenCalledWith('vscode'); + expect(isEditorAvailableMock).toHaveBeenCalledTimes(1); + + rerender( + , + ); + expect(isEditorAvailableMock).toHaveBeenCalledTimes(1); + }); + + it('should NOT show "Modify with external editor" when the configured editor is unavailable', () => { + const mockConfig = { + isTrustedFolder: () => true, + getIdeMode: () => false, + } as unknown as Config; + isEditorAvailableMock.mockClear(); + isEditorAvailableMock.mockReturnValueOnce(false); const { lastFrame } = renderWithProviders( { }, ); - expect(lastFrame()).toContain('Modify with external editor'); + expect(isEditorAvailableMock).toHaveBeenCalledWith('vscode'); + expect(lastFrame()).not.toContain('Modify with external editor'); }); it('should NOT show "Modify with external editor" when preferredEditor is not set', () => { From 168ec7ecb7f65fa0534928a021f21fd70ecadaa4 Mon Sep 17 00:00:00 2001 From: SLP-DEV1 Date: Wed, 2 Sep 2026 09:44:40 +0200 Subject: [PATCH 07/10] test(cli): remove duplicate external editor test file --- ...onfirmationMessage.externalEditor.test.tsx | 89 ------------------- 1 file changed, 89 deletions(-) delete mode 100644 packages/cli/src/ui/components/messages/ToolConfirmationMessage.externalEditor.test.tsx diff --git a/packages/cli/src/ui/components/messages/ToolConfirmationMessage.externalEditor.test.tsx b/packages/cli/src/ui/components/messages/ToolConfirmationMessage.externalEditor.test.tsx deleted file mode 100644 index 8291dd23206..00000000000 --- a/packages/cli/src/ui/components/messages/ToolConfirmationMessage.externalEditor.test.tsx +++ /dev/null @@ -1,89 +0,0 @@ -/** - * @license - * Copyright 2025 Google LLC - * SPDX-License-Identifier: Apache-2.0 - */ - -import { beforeEach, describe, expect, it, vi } from 'vitest'; - -const { isEditorAvailableMock } = vi.hoisted(() => ({ - isEditorAvailableMock: vi.fn(), -})); - -vi.mock('@qwen-code/qwen-code-core', async (importOriginal) => { - const actual = await importOriginal< - typeof import('@qwen-code/qwen-code-core') - >(); - return { - ...actual, - isEditorAvailable: isEditorAvailableMock, - }; -}); - -import { ToolConfirmationMessage } from './ToolConfirmationMessage.js'; -import type { - Config, - ToolCallConfirmationDetails, -} from '@qwen-code/qwen-code-core'; -import { renderWithProviders } from '../../../test-utils/render.js'; -import type { LoadedSettings } from '../../../config/settings.js'; - -describe('ToolConfirmationMessage external editor availability', () => { - const config = { - isTrustedFolder: () => true, - getIdeMode: () => false, - } as unknown as Config; - - const confirmationDetails: ToolCallConfirmationDetails = { - type: 'edit', - title: 'Confirm Edit', - fileName: 'test.txt', - filePath: '/test.txt', - fileDiff: '...diff...', - originalContent: 'a', - newContent: 'b', - onConfirm: vi.fn(), - }; - - const settings = { - merged: { general: { preferredEditor: 'vscode' } }, - } as unknown as LoadedSettings; - - beforeEach(() => { - isEditorAvailableMock.mockReset(); - }); - - it('shows the modify option when the preferred editor is available', () => { - isEditorAvailableMock.mockReturnValue(true); - - const { lastFrame } = renderWithProviders( - , - { settings }, - ); - - expect(isEditorAvailableMock).toHaveBeenCalledWith('vscode'); - expect(lastFrame()).toContain('Modify with external editor'); - }); - - it('hides the modify option when the configured editor is unavailable', () => { - isEditorAvailableMock.mockReturnValue(false); - - const { lastFrame } = renderWithProviders( - , - { settings }, - ); - - expect(isEditorAvailableMock).toHaveBeenCalledWith('vscode'); - expect(lastFrame()).not.toContain('Modify with external editor'); - }); -}); From c777c192bc315cad88bec27ba50c7965392cf595 Mon Sep 17 00:00:00 2001 From: SLP-DEV1 Date: Wed, 2 Sep 2026 13:52:07 +0200 Subject: [PATCH 08/10] fix(cli): skip unnecessary editor availability probes --- .../ui/components/messages/ToolConfirmationMessage.tsx | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/cli/src/ui/components/messages/ToolConfirmationMessage.tsx b/packages/cli/src/ui/components/messages/ToolConfirmationMessage.tsx index 87c3b0b30d6..f37177361f7 100644 --- a/packages/cli/src/ui/components/messages/ToolConfirmationMessage.tsx +++ b/packages/cli/src/ui/components/messages/ToolConfirmationMessage.tsx @@ -68,12 +68,18 @@ export const ToolConfirmationMessage: React.FC< const preferredEditor = settings.merged.general?.preferredEditor as | EditorType | undefined; + const hideModify = + confirmationDetails.type === 'edit' + ? confirmationDetails.hideModify + : false; const editorAvailable = useMemo( () => + !compactMode && confirmationDetails.type === 'edit' && + !hideModify && preferredEditor !== undefined && isEditorAvailable(preferredEditor), - [confirmationDetails.type, preferredEditor], + [compactMode, confirmationDetails.type, hideModify, preferredEditor], ); const [ideClient, setIdeClient] = useState(null); From 5136040edbc125edc418aa263779acec4c980cbe Mon Sep 17 00:00:00 2001 From: SLP-DEV1 Date: Wed, 2 Sep 2026 13:54:00 +0200 Subject: [PATCH 09/10] test(cli): pin external editor availability guards --- .../messages/ToolConfirmationMessage.test.tsx | 78 +++++++++++++++---- 1 file changed, 61 insertions(+), 17 deletions(-) diff --git a/packages/cli/src/ui/components/messages/ToolConfirmationMessage.test.tsx b/packages/cli/src/ui/components/messages/ToolConfirmationMessage.test.tsx index 00d15a9a2f3..abe7f12cafd 100644 --- a/packages/cli/src/ui/components/messages/ToolConfirmationMessage.test.tsx +++ b/packages/cli/src/ui/components/messages/ToolConfirmationMessage.test.tsx @@ -36,6 +36,7 @@ import type { import { IdeClient, ToolConfirmationOutcome } from '@qwen-code/qwen-code-core'; import { renderWithProviders } from '../../../test-utils/render.js'; import type { LoadedSettings } from '../../../config/settings.js'; +import { SettingsContext } from '../../contexts/SettingsContext.js'; describe('ToolConfirmationMessage', () => { const mockConfig = { @@ -601,6 +602,16 @@ describe('ToolConfirmationMessage', () => { newContent: 'b', onConfirm: vi.fn(), }; + const execConfirmationDetails: ToolCallConfirmationDetails = { + type: 'exec', + title: 'Confirm Execution', + command: 'echo hello', + rootCommand: 'echo', + onConfirm: vi.fn(), + }; + const preferredEditorSettings = { + merged: { general: { preferredEditor: 'vscode' } }, + } as unknown as LoadedSettings; it('should show "Modify with external editor" when preferredEditor is set', () => { const mockConfig = { @@ -616,12 +627,9 @@ describe('ToolConfirmationMessage', () => { contentWidth: 80, }; const { lastFrame, rerender } = renderWithProviders( - , - { - settings: { - merged: { general: { preferredEditor: 'vscode' } }, - } as unknown as LoadedSettings, - }, + + + , ); expect(lastFrame()).toContain('Modify with external editor'); @@ -629,8 +637,11 @@ describe('ToolConfirmationMessage', () => { expect(isEditorAvailableMock).toHaveBeenCalledTimes(1); rerender( - , + + + , ); + expect(lastFrame()).toContain('Modify with external editor'); expect(isEditorAvailableMock).toHaveBeenCalledTimes(1); }); @@ -649,13 +660,10 @@ describe('ToolConfirmationMessage', () => { availableTerminalHeight={30} contentWidth={80} />, - { - settings: { - merged: { general: { preferredEditor: 'vscode' } }, - } as unknown as LoadedSettings, - }, + { settings: preferredEditorSettings }, ); + expect(lastFrame()).toContain('Yes, allow once'); expect(isEditorAvailableMock).toHaveBeenCalledWith('vscode'); expect(lastFrame()).not.toContain('Modify with external editor'); }); @@ -665,6 +673,7 @@ describe('ToolConfirmationMessage', () => { isTrustedFolder: () => true, getIdeMode: () => false, } as unknown as Config; + isEditorAvailableMock.mockClear(); const { lastFrame } = renderWithProviders( { }, ); + expect(isEditorAvailableMock).not.toHaveBeenCalled(); expect(lastFrame()).not.toContain('Modify with external editor'); }); @@ -688,6 +698,7 @@ describe('ToolConfirmationMessage', () => { isTrustedFolder: () => true, getIdeMode: () => false, } as unknown as Config; + isEditorAvailableMock.mockClear(); const { lastFrame } = renderWithProviders( { availableTerminalHeight={30} contentWidth={80} />, - { - settings: { - merged: { general: { preferredEditor: 'vscode' } }, - } as unknown as LoadedSettings, - }, + { settings: preferredEditorSettings }, ); + expect(isEditorAvailableMock).not.toHaveBeenCalled(); expect(lastFrame()).not.toContain('Modify with external editor'); }); + it('should NOT probe editor availability in compactMode', () => { + isEditorAvailableMock.mockClear(); + + const { lastFrame } = renderWithProviders( + , + { settings: preferredEditorSettings }, + ); + + expect(lastFrame()).toContain('Yes, allow once'); + expect(isEditorAvailableMock).not.toHaveBeenCalled(); + expect(lastFrame()).not.toContain('Modify with external editor'); + }); + + it('should NOT probe editor availability for a non-edit confirmation', () => { + isEditorAvailableMock.mockClear(); + + const { lastFrame } = renderWithProviders( + , + { settings: preferredEditorSettings }, + ); + + expect(lastFrame()).toContain('Yes, allow once'); + expect(isEditorAvailableMock).not.toHaveBeenCalled(); + }); + it('renders edit warnings and honors hideAlwaysAllow on small terminals', () => { const mockConfig = { isTrustedFolder: () => true, From 8e97cad30d7333b10ea2987f04228e9b8f9d568e Mon Sep 17 00:00:00 2001 From: SLP-DEV1 Date: Fri, 4 Sep 2026 12:54:08 +0200 Subject: [PATCH 10/10] test(cli): preserve providers across editor memo rerender --- .../messages/ToolConfirmationMessage.test.tsx | 21 +++++++++++-------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/packages/cli/src/ui/components/messages/ToolConfirmationMessage.test.tsx b/packages/cli/src/ui/components/messages/ToolConfirmationMessage.test.tsx index abe7f12cafd..40e53f8224a 100644 --- a/packages/cli/src/ui/components/messages/ToolConfirmationMessage.test.tsx +++ b/packages/cli/src/ui/components/messages/ToolConfirmationMessage.test.tsx @@ -8,6 +8,7 @@ import { describe, it, expect, vi } from 'vitest'; import { EOL } from 'node:os'; import { promises as fsp } from 'node:fs'; import { Box } from 'ink'; +import { render } from 'ink-testing-library'; // Capture launches of the external editor so the full-plan viewer (#7001) // can be asserted without spawning a real editor process. @@ -36,6 +37,7 @@ import type { import { IdeClient, ToolConfirmationOutcome } from '@qwen-code/qwen-code-core'; import { renderWithProviders } from '../../../test-utils/render.js'; import type { LoadedSettings } from '../../../config/settings.js'; +import { KeypressProvider } from '../../contexts/KeypressContext.js'; import { SettingsContext } from '../../contexts/SettingsContext.js'; describe('ToolConfirmationMessage', () => { @@ -623,24 +625,25 @@ describe('ToolConfirmationMessage', () => { const props = { confirmationDetails: editConfirmationDetails, config: mockConfig, - availableTerminalHeight: 30, contentWidth: 80, }; - const { lastFrame, rerender } = renderWithProviders( + const tree = (height: number) => ( - - , + + + + ); + const { lastFrame, rerender } = render(tree(30)); expect(lastFrame()).toContain('Modify with external editor'); expect(isEditorAvailableMock).toHaveBeenCalledWith('vscode'); expect(isEditorAvailableMock).toHaveBeenCalledTimes(1); - rerender( - - - , - ); + rerender(tree(31)); expect(lastFrame()).toContain('Modify with external editor'); expect(isEditorAvailableMock).toHaveBeenCalledTimes(1); });