Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 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
12 changes: 12 additions & 0 deletions packages/cli/src/i18n/locales/en.js
Original file line number Diff line number Diff line change
Expand Up @@ -1435,6 +1435,8 @@ export default {
'Switch the model for this session (--fast for suggestion model, --voice for voice transcription model, [model-id] to switch immediately).',
'Switch the model for this session (--fast for suggestion model, --voice for voice transcription model, --vision for the vision bridge model, [model-id] to switch immediately).':
'Switch the model for this session (--fast for suggestion model, --voice for voice transcription model, --vision for the vision bridge model, [model-id] to switch immediately).',
'Switch the model for this session (--fast for suggestion model, --voice for voice transcription model, --vision for the vision bridge model, --project to persist to project settings, --global to persist to user settings, [model-id] to switch immediately).':
'Switch the model for this session (--fast for suggestion model, --voice for voice transcription model, --vision for the vision bridge model, --project to persist to project settings, --global to persist to user settings, [model-id] to switch immediately).',
"⚠ '{{model}}' is not a known image-capable model; the vision bridge may fail on images.":
"⚠ '{{model}}' is not a known image-capable model; the vision bridge may fail on images.",
'Set a lighter model for prompt suggestions and speculative execution':
Expand All @@ -1444,6 +1446,10 @@ export default {
'Set the model for voice transcription',
'Set the image-capable model used to transcribe images for a text-only main model':
'Set the image-capable model used to transcribe images for a text-only main model',
'Persist the model selection to the project settings (workspace scope)':
'Persist the model selection to the project settings (workspace scope)',
'Persist the model selection to the user settings (global scope)':
'Persist the model selection to the user settings (global scope)',
'Select Fast Model': 'Select Fast Model',
'Select Vision Model': 'Select Vision Model',
'Select Voice Model': 'Select Voice Model',
Expand Down Expand Up @@ -1680,6 +1686,12 @@ export default {
// Dialogs - Model
// ============================================================================
'Select Model': 'Select Model',
'Select Model (this project)': 'Select Model (this project)',
'Select Model (global)': 'Select Model (global)',
' (this project)': ' (this project)',
' (global)': ' (global)',
'Model: {{model}} (project)': 'Model: {{model}} (project)',
'Model: {{model}} (global)': 'Model: {{model}} (global)',
'API Key': 'API Key',
'(default)': '(default)',
'(not set)': '(not set)',
Expand Down
12 changes: 12 additions & 0 deletions packages/cli/src/i18n/locales/zh-TW.js
Original file line number Diff line number Diff line change
Expand Up @@ -1232,6 +1232,8 @@ export default {
'切換此會話的模型(--fast 可設置建議模型,--voice 可設置語音轉寫模型,[model-id] 可立即切換)',
'Switch the model for this session (--fast for suggestion model, --voice for voice transcription model, --vision for the vision bridge model, [model-id] to switch immediately).':
'切換此會話的模型(--fast 可設置建議模型,--voice 可設置語音轉寫模型,--vision 可設置視覺橋接模型,[model-id] 可立即切換)',
'Switch the model for this session (--fast for suggestion model, --voice for voice transcription model, --vision for the vision bridge model, --project to persist to project settings, --global to persist to user settings, [model-id] to switch immediately).':
'切換此會話的模型(--fast 建議模型,--voice 語音轉寫模型,--vision 視覺橋接模型,--project 持久化到專案設定,--global 持久化到使用者設定,[model-id] 立即切換)',
"⚠ '{{model}}' is not a known image-capable model; the vision bridge may fail on images.":
"⚠ '{{model}}' 不是已知的圖像能力模型;視覺橋接處理圖片時可能會失敗。",
'Set a lighter model for prompt suggestions and speculative execution':
Expand All @@ -1240,6 +1242,10 @@ export default {
'Set the model for voice transcription': '設定語音轉寫模型',
'Set the image-capable model used to transcribe images for a text-only main model':
'設定用於為純文字主模型轉寫圖像的圖像能力模型',
'Persist the model selection to the project settings (workspace scope)':
'將模型選擇持久化到專案設定(工作區)',
'Persist the model selection to the user settings (global scope)':
'將模型選擇持久化到使用者設定(全域)',
'Select Fast Model': '選擇快速模型',
'Select Vision Model': '選擇視覺模型',
'Select Voice Model': '選擇語音模型',
Expand Down Expand Up @@ -1433,6 +1439,12 @@ export default {
'Invalid QWEN_DEFAULT_AUTH_TYPE value: "{{value}}". Valid values are: {{validValues}}':
'無效的 QWEN_DEFAULT_AUTH_TYPE 值:"{{value}}"。有效值為:{{validValues}}',
'Select Model': '選擇模型',
'Select Model (this project)': '選擇模型(當前專案)',
'Select Model (global)': '選擇模型(全域)',
' (this project)': '(當前專案)',
' (global)': '(全域)',
'Model: {{model}} (project)': '模型:{{model}}(專案)',
'Model: {{model}} (global)': '模型:{{model}}(全域)',
'API Key': 'API Key',
'(default)': '(默認)',
'(not set)': '(未設置)',
Expand Down
12 changes: 12 additions & 0 deletions packages/cli/src/i18n/locales/zh.js
Original file line number Diff line number Diff line change
Expand Up @@ -1346,6 +1346,8 @@ export default {
'切换此会话的模型(--fast 可设置建议模型,--voice 可设置语音转写模型,[model-id] 可立即切换)',
'Switch the model for this session (--fast for suggestion model, --voice for voice transcription model, --vision for the vision bridge model, [model-id] to switch immediately).':
'切换此会话的模型(--fast 可设置建议模型,--voice 可设置语音转写模型,--vision 可设置视觉桥接模型,[model-id] 可立即切换)',
'Switch the model for this session (--fast for suggestion model, --voice for voice transcription model, --vision for the vision bridge model, --project to persist to project settings, --global to persist to user settings, [model-id] to switch immediately).':
'切换此会话的模型(--fast 建议模型,--voice 语音转写模型,--vision 视觉桥接模型,--project 持久化到项目设置,--global 持久化到用户设置,[model-id] 立即切换)',
"⚠ '{{model}}' is not a known image-capable model; the vision bridge may fail on images.":
"⚠ '{{model}}' 不是已知的图像能力模型;视觉桥接处理图片时可能会失败。",
'Set a lighter model for prompt suggestions and speculative execution':
Expand All @@ -1354,6 +1356,10 @@ export default {
'Set the model for voice transcription': '设置语音转写模型',
'Set the image-capable model used to transcribe images for a text-only main model':
'设置用于为纯文本主模型转写图像的图像能力模型',
'Persist the model selection to the project settings (workspace scope)':
'将模型选择持久化到项目设置(工作区)',
'Persist the model selection to the user settings (global scope)':
'将模型选择持久化到用户设置(全局)',
'Select Fast Model': '选择快速模型',
'Select Vision Model': '选择视觉模型',
'Select Voice Model': '选择语音模型',
Expand Down Expand Up @@ -1575,6 +1581,12 @@ export default {
// Dialogs - Model
// ============================================================================
'Select Model': '选择模型',
'Select Model (this project)': '选择模型(当前项目)',
'Select Model (global)': '选择模型(全局)',
' (this project)': '(当前项目)',
' (global)': '(全局)',
'Model: {{model}} (project)': '模型:{{model}}(项目)',
'Model: {{model}} (global)': '模型:{{model}}(全局)',
'API Key': 'API Key',
'(default)': '(默认)',
'(not set)': '(未设置)',
Expand Down
3 changes: 3 additions & 0 deletions packages/cli/src/ui/AppContainer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1100,6 +1100,7 @@ export const AppContainer = (props: AppContainerProps) => {
isFastModelMode,
isVoiceModelMode,
isVisionModelMode,
modelDialogPersistScope,
openModelDialog,
closeModelDialog,
} = useModelCommand();
Expand Down Expand Up @@ -3667,6 +3668,7 @@ export const AppContainer = (props: AppContainerProps) => {
isFastModelMode,
isVoiceModelMode,
isVisionModelMode,
modelDialogPersistScope,
isTrustDialogOpen,
activeArenaDialog,
isPermissionsDialogOpen,
Expand Down Expand Up @@ -3806,6 +3808,7 @@ export const AppContainer = (props: AppContainerProps) => {
isFastModelMode,
isVoiceModelMode,
isVisionModelMode,
modelDialogPersistScope,
isTrustDialogOpen,
activeArenaDialog,
isPermissionsDialogOpen,
Expand Down
145 changes: 144 additions & 1 deletion packages/cli/src/ui/commands/modelCommand.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ describe('modelCommand', () => {
it('should have the correct name and description', () => {
expect(modelCommand.name).toBe('model');
expect(modelCommand.description).toBe(
'Switch the model for this session (--fast for suggestion model, --voice for voice transcription model, --vision for the vision bridge model, [model-id] to switch immediately).',
'Switch the model for this session (--fast for suggestion model, --voice for voice transcription model, --vision for the vision bridge model, --project to persist to project settings, --global to persist to user settings, [model-id] to switch immediately).',

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 only test change in this PR is the description string update. There are no tests for:

  • parseScopeFlags() function
  • resolveScope() with scope overrides
  • Scope-aware persistSetting() calls
  • Scope appearing in confirmation messages
  • persistScope in dialog return objects
  • Scope flag + --voice/--fast/--vision flag interactions

The Critical finding (persistScope silently dropped for subcommand dialogs) would have been caught by a single test asserting that /model --project --fast returns a dialog action with persistScope: 'workspace'.

— qwen3.7-max via Qwen Code /review

);
});

Expand Down Expand Up @@ -1669,4 +1669,147 @@ describe('modelCommand', () => {
});
});
});

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 new "scope flags" describe block covers dialog returns, fast-model direct-set, main-model direct-set, untrusted workspace, and no-flag default — good coverage of the happy path. However, several specific code paths in the diff remain untested:

  1. Voice model direct-set with scope (/model --project --voice <model-id>) — the persistSetting(settings, 'voiceModel', modelName, scopeOverride) call at modelCommand.ts:~503 is never exercised with SettingScope.Workspace.
  2. Vision model direct-set with scope (/model --project --vision <model-id>) — same gap at modelCommand.ts:~700.
  3. Inline prompt + scope flag rejection (/model --project qwen-max explain this) — the error path at modelCommand.ts:~780 is untested.
  4. Mutual exclusivity (/model --project --global) — the rejection at modelCommand.ts:~410 is untested.
  5. The switchMainModel scope test asserts setValue(scope, 'model.name', ...) but not the companion model.baseUrl tombstone or security.auth.selectedType writes at the same scope.

— qwen3.7-max via Qwen Code /review

describe('scope flags', () => {
function setupContext() {
const mockConfig = createMockConfig({
model: 'test-model',
authType: AuthType.USE_OPENAI,
});
(
mockConfig as Partial<Config> & { [key: string]: unknown }
).getAvailableModelsForAuthType = vi.fn().mockReturnValue([]);
(
mockConfig as Partial<Config> & { [key: string]: unknown }
).getAllConfiguredModels = vi.fn().mockReturnValue([]);
mockContext.services.config = mockConfig as Config;
return mockContext;
}

it('should include persistScope in dialog return for /model --project', async () => {
const ctx = setupContext();
const result = await modelCommand.action!(ctx, '--project');
expect(result).toEqual({
type: 'dialog',
dialog: 'model',
persistScope: 'workspace',
});
});

it('should include persistScope in dialog return for /model --global', async () => {
const ctx = setupContext();
const result = await modelCommand.action!(ctx, '--global');
expect(result).toEqual({
type: 'dialog',
dialog: 'model',
persistScope: 'user',
});
});

it('should include persistScope for /model --project --fast dialog', async () => {
const ctx = setupContext();
const result = await modelCommand.action!(ctx, '--project --fast');
expect(result).toEqual({
type: 'dialog',
dialog: 'fast-model',
persistScope: 'workspace',
});
});

it('should include persistScope for /model --global --voice dialog', async () => {
const ctx = setupContext();
const result = await modelCommand.action!(ctx, '--global --voice');
expect(result).toEqual({
type: 'dialog',
dialog: 'voice-model',
persistScope: 'user',
});
});

it('should include persistScope for /model --project --vision dialog', async () => {
const ctx = setupContext();
const result = await modelCommand.action!(ctx, '--project --vision');
expect(result).toEqual({
type: 'dialog',
dialog: 'vision-model',
persistScope: 'workspace',
});
});

it('should parse scope flags in any position', async () => {
const ctx = setupContext();
const result = await modelCommand.action!(ctx, '--fast --project');
expect(result).toEqual({
type: 'dialog',
dialog: 'fast-model',
persistScope: 'workspace',
});
});

it('should show scope suffix in fast model confirmation', async () => {
const setValue = vi.fn();
const settings = {
...createMockSettings(setValue),
_merged: {},
computeMergedSettings: vi.fn(),
} as unknown as LoadedSettings;
const ctx = setupContext();
ctx.services.settings = settings;
const cfg = ctx.services.config as unknown as Partial<Config> & {
[key: string]: unknown;
};
cfg.getAllConfiguredModels = vi
.fn()
.mockReturnValue([
{ id: 'qwen3-coder-flash', voiceOnly: false, fastOnly: true },
]);
cfg.setFastModel = vi.fn();
const result = await modelCommand.action!(
ctx,
'--project --fast qwen3-coder-flash',
);
expect(result).toMatchObject({
type: 'message',
content: expect.stringContaining('(project)'),
});
});

it('should show scope suffix in main model confirmation', async () => {
const setValue = vi.fn();
const settings = {
...createMockSettings(setValue),
_merged: {},
computeMergedSettings: vi.fn(),
} as unknown as LoadedSettings;
const mockGenerator = {
authType: AuthType.USE_OPENAI,
model: 'qwen-max',
};
const ctx = setupContext();
ctx.services.settings = settings;
const cfg = ctx.services.config as unknown as Partial<Config> & {
[key: string]: unknown;
};
cfg.getAvailableModelsForAuthType = vi
.fn()
.mockReturnValue([
{ id: 'qwen-max', voiceOnly: false, fastOnly: false },
]);
cfg.switchModel = vi.fn().mockResolvedValue(mockGenerator);
const result = await modelCommand.action!(ctx, '--project qwen-max');
expect(result).toMatchObject({
type: 'message',
content: expect.stringContaining('(project)'),
});
});

it('should not include persistScope when no scope flag is given', async () => {
const ctx = setupContext();
const result = await modelCommand.action!(ctx, '');
expect(result).toEqual({
type: 'dialog',
dialog: 'model',
});

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] Two error branches in modelCommand.ts lack test coverage:

  1. Mutual exclusion (lines ~410-418): --project --global together is rejected with "Cannot use both", but no test exercises this branch. A regression could silently accept both flags.

  2. Inline prompt + scope flag (lines ~778-790): --project qwen-max hello is rejected with "Cannot combine", but no test covers this. This guard was specifically added to prevent a misleading "(this project)" confirmation for persistence that never took effect.

Suggested tests:

it('should reject --project --global used together', async () => {
  const ctx = setupContext();
  const result = await modelCommand.action!(ctx, '--project --global');
  expect(result).toMatchObject({
    type: 'message',
    messageType: 'error',
    content: expect.stringContaining('Cannot use both'),
  });
});

it('should reject inline prompt combined with scope flag', async () => {
  // Setup config so qwen-max resolves as a valid model...
  const result = await modelCommand.action!(ctx, '--project qwen-max hello world');
  expect(result).toMatchObject({
    type: 'message',
    messageType: 'error',
    content: expect.stringContaining('Cannot combine'),
  });
});

— qwen3.7-max via Qwen Code /review

});
});
});
Loading