Skip to content
Merged
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
2 changes: 2 additions & 0 deletions packages/cli/src/i18n/locales/ca.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ export default {
'Shell mode': 'Mode shell',
'YOLO mode': 'Mode YOLO',
'Auto mode': 'Mode auto',
'auto_mode.entry_notice':
"Mode auto activat.\n Un classificador LLM avalua cada crida d'eina — les accions segures s'aproven automàticament,\n les arriscades es bloquegen. Sortiu: Shift+Tab o /approval-mode default.",
'plan mode': 'mode de planificació',
'auto-accept edits': 'acceptació automàtica de canvis',
'Accepting edits': 'Acceptant canvis',
Expand Down
2 changes: 2 additions & 0 deletions packages/cli/src/i18n/locales/de.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ export default {
'Shell mode': 'Shell-Modus',
'YOLO mode': 'YOLO-Modus',
'Auto mode': 'Auto-Modus',
'auto_mode.entry_notice':
'Auto-Modus aktiviert.\n Ein LLM-Klassifikator bewertet jeden Tool-Aufruf — sichere Aktionen werden automatisch genehmigt,\n riskante werden blockiert. Beenden: Shift+Tab oder /approval-mode default.',
'plan mode': 'Planungsmodus',
'auto-accept edits': 'Änderungen automatisch akzeptieren',
'Accepting edits': 'Änderungen werden akzeptiert',
Expand Down
2 changes: 2 additions & 0 deletions packages/cli/src/i18n/locales/en.js
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,8 @@ export default {
'Shell mode': 'Shell mode',
'YOLO mode': 'YOLO mode',
'Auto mode': 'Auto mode',
'auto_mode.entry_notice':
'Auto mode enabled.\n An LLM classifier evaluates each tool call — safe actions auto-approve,\n risky ones are blocked. Exit: Shift+Tab or /approval-mode default.',
'plan mode': 'plan mode',
'auto-accept edits': 'auto-accept edits',
'Accepting edits': 'Accepting edits',
Expand Down
2 changes: 2 additions & 0 deletions packages/cli/src/i18n/locales/fr.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ export default {
'Shell mode': 'Mode shell',
'YOLO mode': 'Mode YOLO',
'Auto mode': 'Mode auto',
'auto_mode.entry_notice':
"Mode auto activé.\n Un classificateur LLM évalue chaque appel d'outil — les actions sûres sont approuvées automatiquement,\n les actions risquées sont bloquées. Quitter : Shift+Tab ou /approval-mode default.",
'plan mode': 'mode plan',
'auto-accept edits': 'acceptation automatique des modifications',
'Accepting edits': 'Acceptation des modifications',
Expand Down
4 changes: 3 additions & 1 deletion packages/cli/src/i18n/locales/ja.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@ export default {
'@src/myFile.ts': '@src/myFile.ts',
'Shell mode': 'シェルモード',
'YOLO mode': 'YOLOモード',
'Auto mode': 'Autoモード',
'Auto mode': '自動モード',
'auto_mode.entry_notice':
'自動モードが有効です。\n LLM 分類器が各ツール呼び出しを評価します — 安全な操作は自動承認され、\n 危険な操作はブロックされます。終了: Shift+Tab または /approval-mode default。',
'plan mode': 'プランモード',
'auto-accept edits': '編集を自動承認',
'Accepting edits': '編集を承認中',
Expand Down
2 changes: 2 additions & 0 deletions packages/cli/src/i18n/locales/pt.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ export default {
'Shell mode': 'Modo shell',
'YOLO mode': 'Modo YOLO',
'Auto mode': 'Modo auto',
'auto_mode.entry_notice':
'Modo auto ativado.\n Um classificador LLM avalia cada chamada de ferramenta — ações seguras são aprovadas automaticamente,\n ações arriscadas são bloqueadas. Sair: Shift+Tab ou /approval-mode default.',
'plan mode': 'modo planejamento',
'auto-accept edits': 'aceitar edições automaticamente',
'Accepting edits': 'Aceitando edições',
Expand Down
2 changes: 2 additions & 0 deletions packages/cli/src/i18n/locales/ru.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ export default {
'Shell mode': 'Режим терминала',
'YOLO mode': 'Режим YOLO',
'Auto mode': 'Автоматический режим',
'auto_mode.entry_notice':
'Автоматический режим включен.\n LLM-классификатор оценивает каждый вызов инструмента — безопасные действия утверждаются автоматически,\n рискованные блокируются. Выход: Shift+Tab или /approval-mode default.',
'plan mode': 'Режим планирования',
'auto-accept edits': 'Режим принятия правок',
'Accepting edits': 'Принятие правок',
Expand Down
4 changes: 3 additions & 1 deletion packages/cli/src/i18n/locales/zh-TW.js
Original file line number Diff line number Diff line change
Expand Up @@ -219,7 +219,9 @@ export default {
'@src/myFile.ts': '@src/myFile.ts',
'Shell mode': 'Shell 模式',
'YOLO mode': 'YOLO 模式',
'Auto mode': 'Auto 模式',
'Auto mode': '自動模式',
'auto_mode.entry_notice':
'已啟用自動模式。\n LLM 分類器會評估每次工具呼叫 — 安全操作將自動批准,\n 有風險的操作將被阻止。退出:Shift+Tab 或 /approval-mode default。',
'plan mode': '規劃模式',
'auto-accept edits': '自動接受編輯',
'Accepting edits': '接受編輯',
Expand Down
4 changes: 3 additions & 1 deletion packages/cli/src/i18n/locales/zh.js
Original file line number Diff line number Diff line change
Expand Up @@ -223,7 +223,9 @@ export default {
'@src/myFile.ts': '@src/myFile.ts',
'Shell mode': 'Shell 模式',
'YOLO mode': 'YOLO 模式',
'Auto mode': 'Auto 模式',
'Auto mode': '自动模式',
'auto_mode.entry_notice':
'已启用自动模式。\n LLM 分类器会评估每次工具调用 — 安全操作将自动批准,\n 有风险的操作将被阻止。退出:Shift+Tab 或 /approval-mode default。',
'plan mode': '规划模式',
'auto-accept edits': '自动接受编辑',
'Accepting edits': '接受编辑',
Expand Down
1 change: 1 addition & 0 deletions packages/cli/src/i18n/mustTranslateKeys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ export const MUST_TRANSLATE_KEYS = [
'? for shortcuts',
'Invalid approval mode "{{arg}}". Valid modes: {{modes}}',
'Approval mode set to "{{mode}}"',
'auto_mode.entry_notice',
"Set up Qwen Code's status line UI",
'Cached (included in Input): {{tokens}}',
'By source:',
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/src/ui/commands/approvalModeCommand.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ describe('approvalModeCommand', () => {

expect(result.type).toBe('message');
expect(result.messageType).toBe('info');
expect(result.content).toContain('auto-edit');
expect(result.content).toContain('auto-accept edits');

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.

[Critical] The PR adds emitAutoModeEntryNotices() in approvalModeCommand.ts when switching to AUTO mode via the slash command, but this test file has no test case for the auto argument. Tests exist for plan, yolo, auto-edit, and default, but the new auto code path (the only place where the slash command triggers notice emission) is untested.

Add a test case similar to the existing mode tests:

it('should set approval mode to "auto" and emit entry notices', async () => {
  const result = (await approvalModeCommand.action?.(mockContext, 'auto')) as MessageActionReturn;
  expect(result.type).toBe('message');
  expect(result.messageType).toBe('info');
  expect(result.content).toContain('Auto mode');
  expect(mockSetApprovalMode).toHaveBeenCalledWith('auto');
  // Verify addItem was called with the auto-mode entry notice
});

— qwen3.7-max via Qwen Code /review

expect(mockSetApprovalMode).toHaveBeenCalledWith('auto-edit');
});

Expand Down
19 changes: 17 additions & 2 deletions packages/cli/src/ui/components/AutoAcceptIndicator.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { render } from 'ink-testing-library';
import { describe, it, expect } from 'vitest';
import { AutoAcceptIndicator } from './AutoAcceptIndicator.js';
import { ApprovalMode } from '@qwen-code/qwen-code-core';
import { setLanguageAsync } from '../../i18n/index.js';

describe('<AutoAcceptIndicator />', () => {
it('renders DEFAULT mode with pause badge and Ask permissions text', () => {
Expand All @@ -33,11 +34,25 @@ describe('<AutoAcceptIndicator />', () => {
expect(lastFrame()).toContain('auto-accept edits');
});

it('renders AUTO mode indicator', () => {
it('renders AUTO mode indicator with the localized label', () => {
const { lastFrame } = render(
<AutoAcceptIndicator approvalMode={ApprovalMode.AUTO} />,
);
expect(lastFrame()).toContain('auto mode (classifier-evaluated)');
const frame = lastFrame()!;
expect(frame).toContain('Auto mode');
expect(frame).not.toContain('auto mode (classifier-evaluated)');
});

it('renders AUTO mode indicator with the active Chinese locale', async () => {
await setLanguageAsync('zh');
try {
const { lastFrame } = render(
<AutoAcceptIndicator approvalMode={ApprovalMode.AUTO} />,
);
expect(lastFrame()).toContain('自动模式');
} finally {
await setLanguageAsync('en');
}
});

it('renders YOLO mode indicator', () => {
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/src/ui/components/AutoAcceptIndicator.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ export const AutoAcceptIndicator: React.FC<AutoAcceptIndicatorProps> = ({
subText = cycleText;
break;
case ApprovalMode.AUTO:
textContent = t('auto mode (classifier-evaluated)');
textContent = t('Auto mode');
subText = cycleText;
break;
case ApprovalMode.YOLO:
Expand Down
134 changes: 133 additions & 1 deletion packages/cli/src/ui/hooks/useAutoAcceptIndicator.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,18 @@ import {
type Mock,
} from 'vitest';
import { renderHook, act } from '@testing-library/react';
import { useAutoAcceptIndicator } from './useAutoAcceptIndicator.js';
import {
emitAutoModeEntryNotices,
useAutoAcceptIndicator,
} from './useAutoAcceptIndicator.js';

import { Config, ApprovalMode } from '@qwen-code/qwen-code-core';
import type { Config as ActualConfigType } from '@qwen-code/qwen-code-core';
import type { Key } from './useKeypress.js';
import { useKeypress } from './useKeypress.js';
import { MessageType } from '../types.js';
import { setLanguage, setLanguageAsync } from '../../i18n/index.js';
import { SettingScope, type LoadedSettings } from '../../config/settings.js';

vi.mock('./useKeypress.js');

Expand Down Expand Up @@ -55,6 +60,13 @@ interface MockConfigInstanceShape {

type UseKeypressHandler = (key: Key) => void;

function createMockSettings(autoModeAcknowledged: boolean): LoadedSettings {
return {
merged: { ui: { autoModeAcknowledged } },
setValue: vi.fn(),
} as unknown as LoadedSettings;
}

describe('useAutoAcceptIndicator', () => {
let mockConfigInstance: MockConfigInstanceShape;
let capturedUseKeypressHandler: UseKeypressHandler;
Expand Down Expand Up @@ -494,6 +506,126 @@ describe('useAutoAcceptIndicator', () => {
);
});

it('should emit the localizable AUTO mode entry notice', async () => {
await setLanguageAsync('en');
const mockAddItem = vi.fn();

emitAutoModeEntryNotices({
config: mockConfigInstance as unknown as ActualConfigType,
addItem: mockAddItem,
});

expect(mockAddItem).toHaveBeenCalledWith(
{
type: MessageType.INFO,
text:
'Auto mode enabled.\n' +
' An LLM classifier evaluates each tool call — safe actions auto-approve,\n' +
' risky ones are blocked. Exit: Shift+Tab or /approval-mode default.',
},
expect.any(Number),
);
});

it('should fall back to readable English when the entry notice key is not loaded', async () => {
const stderrWrite = vi
.spyOn(process.stderr, 'write')
.mockImplementation(() => true);
setLanguage('ca');
try {
const mockAddItem = vi.fn();

emitAutoModeEntryNotices({
config: mockConfigInstance as unknown as ActualConfigType,
addItem: mockAddItem,
});

expect(mockAddItem).toHaveBeenCalledWith(
{
type: MessageType.INFO,
text:
'Auto mode enabled.\n' +
' An LLM classifier evaluates each tool call — safe actions auto-approve,\n' +
' risky ones are blocked. Exit: Shift+Tab or /approval-mode default.',
},
expect.any(Number),
);
} finally {
stderrWrite.mockRestore();
await setLanguageAsync('en');
}
});

it('should persist acknowledgement after emitting the AUTO mode entry notice', async () => {
await setLanguageAsync('en');
const mockAddItem = vi.fn();
const mockSettings = createMockSettings(false);

emitAutoModeEntryNotices({
config: mockConfigInstance as unknown as ActualConfigType,
settings: mockSettings,
addItem: mockAddItem,
});

expect(mockAddItem).toHaveBeenCalledWith(
expect.objectContaining({
type: MessageType.INFO,
text: expect.stringContaining('Auto mode enabled.'),
}),
expect.any(Number),
);
expect(mockSettings.setValue).toHaveBeenCalledWith(
SettingScope.User,
'ui.autoModeAcknowledged',
true,
);
});

it('should skip the AUTO mode entry notice after acknowledgement', async () => {
await setLanguageAsync('en');
const mockAddItem = vi.fn();
const mockSettings = createMockSettings(true);

emitAutoModeEntryNotices({
config: mockConfigInstance as unknown as ActualConfigType,
settings: mockSettings,
addItem: mockAddItem,
});

expect(mockAddItem).not.toHaveBeenCalledWith(
expect.objectContaining({
text: expect.stringContaining('Auto mode enabled.'),
}),
expect.any(Number),
);
expect(mockSettings.setValue).not.toHaveBeenCalled();
});

it('should emit the AUTO mode entry notice with the active locale', async () => {
await setLanguageAsync('zh');
try {
const mockAddItem = vi.fn();

emitAutoModeEntryNotices({
config: mockConfigInstance as unknown as ActualConfigType,
addItem: mockAddItem,
});

expect(mockAddItem).toHaveBeenCalledWith(
{
type: MessageType.INFO,
text:
'已启用自动模式。\n' +
' LLM 分类器会评估每次工具调用 — 安全操作将自动批准,\n' +
' 有风险的操作将被阻止。退出:Shift+Tab 或 /approval-mode default。',
},
expect.any(Number),
);
} finally {
await setLanguageAsync('en');
}
});

it('should not cycle approval mode on Windows when shouldBlockTab returns true', () => {
const originalPlatform = process.platform;
Object.defineProperty(process, 'platform', {
Expand Down
15 changes: 13 additions & 2 deletions packages/cli/src/ui/hooks/useAutoAcceptIndicator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,21 @@ import { useKeypress } from './useKeypress.js';
import type { HistoryItemWithoutId } from '../types.js';
import { MessageType } from '../types.js';
import { type LoadedSettings, SettingScope } from '../../config/settings.js';
import { t } from '../../i18n/index.js';

const AUTO_MODE_FIRST_TIME_MESSAGE =
const AUTO_MODE_FIRST_TIME_MESSAGE_KEY = 'auto_mode.entry_notice';
const AUTO_MODE_FIRST_TIME_MESSAGE_FALLBACK =
'Auto mode enabled.\n' +
' An LLM classifier evaluates each tool call — safe actions auto-approve,\n' +
' risky ones are blocked. Exit: Shift+Tab or /approval-mode default.';

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] The fallback branch of getAutoModeFirstTimeMessage() (when t() returns the raw key because the locale is missing) is never exercised by tests. All four emitAutoModeEntryNotices test cases call setLanguageAsync('en') or setLanguageAsync('zh') first, so they always hit the translated path. A single test asserting the fallback text when i18n is uninitialized (or using a locale without the key) would close this coverage gap.

— qwen3.7-max via Qwen Code /review

const getAutoModeFirstTimeMessage = () => {
const message = t(AUTO_MODE_FIRST_TIME_MESSAGE_KEY);
return message === AUTO_MODE_FIRST_TIME_MESSAGE_KEY
? AUTO_MODE_FIRST_TIME_MESSAGE_FALLBACK
: message;
};

export interface UseAutoAcceptIndicatorArgs {
config: Config;
/** Settings handle — used to read/write `ui.autoModeAcknowledged`. */
Expand Down Expand Up @@ -145,7 +154,7 @@ export function emitAutoModeEntryNotices(opts: {
const acknowledged = settings?.merged.ui?.autoModeAcknowledged === true;
if (!acknowledged) {
addItem(
{ type: MessageType.INFO, text: AUTO_MODE_FIRST_TIME_MESSAGE },
{ type: MessageType.INFO, text: getAutoModeFirstTimeMessage() },
now,
);
if (settings) {
Expand All @@ -165,6 +174,8 @@ export function emitAutoModeEntryNotices(opts: {
stripped &&
(stripped.persistent.length > 0 || stripped.session.length > 0)
) {
// Intentionally untranslated operational notice: rule text is copied from
// user/session allow-rule configuration and may contain command syntax.
const lines = [
'ℹ Auto mode temporarily disabled these allow rules',
' (they would bypass the classifier):',
Expand Down
Loading
Loading