Skip to content
Draft
18 changes: 13 additions & 5 deletions packages/cli/src/acp-integration/acpAgent.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -859,6 +859,7 @@ vi.mock('../config/loadedSettingsAdapter.js', () => ({
vi.mock('../config/config.js', () => ({
loadCliConfig: vi.fn(),
buildDisabledSkillNamesProvider: vi.fn(() => () => new Set<string>()),
buildProbeResultStoreProvider: vi.fn(() => () => undefined),
SessionIdConflictError: class SessionIdConflictError extends Error {
sessionId: string;
constructor(sessionId: string, message: string) {
Expand Down Expand Up @@ -3691,10 +3692,11 @@ describe('QwenAgent MCP SSE/HTTP support', () => {
expect(argv).toMatchObject({
sessionId: '550e8400-e29b-41d4-a716-446655440000',
});
// Index 8 is `throwOnSessionIdConflict`: it must be true so a duplicate
// Index 9 is `throwOnSessionIdConflict`: it must be true so a duplicate
// caller-supplied id throws (mapped to a RequestError) instead of
// process.exit(1)-ing the shared ACP child.
expect(vi.mocked(loadCliConfig).mock.calls[0]![8]).toBe(true);
// process.exit(1)-ing the shared ACP child. (Index 8 is the settings
// watcher; index 6 is the probe-result store provider.)
expect(vi.mocked(loadCliConfig).mock.calls[0]![9]).toBe(true);

mockConnectionState.resolve();
await agentPromise;
Expand Down Expand Up @@ -10929,6 +10931,8 @@ describe('QwenAgent MCP SSE/HTTP support', () => {
'/tmp',
undefined,
expect.anything(),
// disabledSkillNamesProvider, probeResultStoreProvider
expect.any(Function),
expect.any(Function),
expect.anything(),
undefined,
Expand Down Expand Up @@ -16488,7 +16492,9 @@ describe('QwenAgent MCP SSE/HTTP support', () => {
],
});

const sessionMcpServers = vi.mocked(loadCliConfig).mock.calls[0]?.[6];
// Arg index 7 = sessionMcpServers (index 6 is the probe-result store
// provider).
const sessionMcpServers = vi.mocked(loadCliConfig).mock.calls[0]?.[7];
const localConfig = sessionMcpServers?.['local'] as unknown as {
_args: unknown[];
};
Expand Down Expand Up @@ -18847,7 +18853,9 @@ describe('QwenAgent loadSession / unstable_resumeSession', () => {
});
vi.mocked(loadSettings).mockReturnValue(makeRestoreSettings());
vi.mocked(loadCliConfig).mockImplementation(async (...args: unknown[]) => {
const hostPolicy = args[9] as
// Index 10 is `hostPolicy` (index 9 is `throwOnSessionIdConflict`;
// index 6 is the probe-result store provider).
const hostPolicy = args[10] as
| {
sessionRestore?: {
projectionSource: (sessionId: string) => Promise<unknown>;
Expand Down
3 changes: 3 additions & 0 deletions packages/cli/src/acp-integration/acpAgent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,7 @@ import { z } from 'zod';
import type { CliArgs } from '../config/config.js';
import {
buildDisabledSkillNamesProvider,
buildProbeResultStoreProvider,
loadCliConfig,
SessionIdConflictError,
} from '../config/config.js';
Expand Down Expand Up @@ -3755,6 +3756,7 @@ class QwenAgent implements Agent {
projectHooks: settings.getProjectHooks(),
},
buildDisabledSkillNamesProvider(settings),
buildProbeResultStoreProvider(settings),
),
);
config.setMcpTransportPool(this.mcpPool);
Expand Down Expand Up @@ -12487,6 +12489,7 @@ class QwenAgent implements Agent {
// session. ACP/Zed sessions otherwise leak persisted disabled skills
// into the first <available_skills> at cold start.
buildDisabledSkillNamesProvider(settings),
buildProbeResultStoreProvider(settings),
sessionMcpServers,
// The daemon owns the settings watcher lifecycle.
undefined,
Expand Down
1 change: 1 addition & 0 deletions packages/cli/src/acp-integration/acpAgent.worktree.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,7 @@ vi.mock('../config/settings-cache.js', async () => {
vi.mock('../config/config.js', () => ({
loadCliConfig: vi.fn(),
buildDisabledSkillNamesProvider: vi.fn(() => () => new Set<string>()),
buildProbeResultStoreProvider: vi.fn(() => () => undefined),
}));
vi.mock('./session/Session.js', () => ({
Session: vi.fn(),
Expand Down
10 changes: 10 additions & 0 deletions packages/cli/src/config/config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1594,6 +1594,7 @@ describe('loadCliConfig', () => {
undefined,
undefined,
undefined,
undefined,
sessionMcpServers,
);

Expand Down Expand Up @@ -1626,6 +1627,7 @@ describe('loadCliConfig', () => {
undefined,
undefined,
undefined,
undefined,
sessionMcpServers,
);

Expand Down Expand Up @@ -1688,6 +1690,7 @@ describe('loadCliConfig', () => {
undefined,
undefined,
undefined,
undefined,
sessionMcpServers,
);

Expand Down Expand Up @@ -1749,6 +1752,7 @@ describe('loadCliConfig', () => {
undefined,
undefined,
undefined,
undefined,
{
'ide-only': new ServerConfig.MCPServerConfig('ide-cmd'),
},
Expand Down Expand Up @@ -1818,6 +1822,7 @@ describe('loadCliConfig', () => {
undefined,
undefined,
undefined,
undefined,
false,
{ sessionRestore: { projectionSource } },
);
Expand Down Expand Up @@ -1876,6 +1881,7 @@ describe('loadCliConfig', () => {
undefined,
undefined,
undefined,
undefined,
false,
{ sessionRestore: { projectionSource } },
);
Expand Down Expand Up @@ -2006,6 +2012,7 @@ describe('loadCliConfig', () => {
undefined,
undefined,
undefined,
undefined,
true,
);

Expand All @@ -2029,6 +2036,7 @@ describe('loadCliConfig', () => {
undefined,
undefined,
undefined,
undefined,
true,
);

Expand All @@ -2050,6 +2058,7 @@ describe('loadCliConfig', () => {
undefined,
undefined,
undefined,
undefined,
true,
);

Expand Down Expand Up @@ -3715,6 +3724,7 @@ describe('loadCliConfig with includeDirectories', () => {
undefined,
undefined,
undefined,
undefined,
false,
{ provisionalWorkspace: true },
);
Expand Down
30 changes: 30 additions & 0 deletions packages/cli/src/config/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ import {
SchemaValidator,
type ConfigParameters,
type MCPServerConfig,
type ProbeResultStore,
type SkillLevel,
type WebSearchSettings,
MAX_SUBAGENT_DEPTH_LIMIT,
Expand Down Expand Up @@ -1481,6 +1482,25 @@ export function buildDisabledSkillNamesProvider(
return () => resolveSkillSettings(loadedSettings).disabledNames;
}

/**
* Builds the live-read closure for the persisted modality probe results
* (`probeResults` settings key, QwenLM/qwen-code#10309). Forwarded to
* `ConfigParameters.probeResultStoreProvider` so the model registry's
* modality resolution chain (explicit > probe > pattern) reflects verdicts
* written by the /model dialog's "Test image support" action without
* rebuilding `Config`.
*
* Like `buildDisabledSkillNamesProvider`, the closure is over the LIVE
* `LoadedSettings` instance (reading `merged` on every call), NOT over a
* `Settings` snapshot — `LoadedSettings.setValue` replaces `_merged`, so a
* snapshot closure would never observe newly written probe verdicts.
*/
export function buildProbeResultStoreProvider(
loadedSettings: LoadedSettings,
): () => ProbeResultStore | undefined {
return () => loadedSettings.merged.probeResults;
}

/**
* Thrown (instead of `process.exit(1)`) when a caller-supplied session id
* already exists and `throwOnSessionIdConflict` is set. The interactive CLI
Expand Down Expand Up @@ -1525,6 +1545,15 @@ export async function loadCliConfig(
* correctly.
*/
disabledSkillNamesProvider?: () => ReadonlySet<string>,
/**
* Live-read provider for the persisted modality probe results. Forwarded
* to `ConfigParameters` so the model registry's modality resolution chain
* sees probe verdicts written by the /model dialog within the same
* process. Callers MUST close over the live `LoadedSettings` instance —
* use `buildProbeResultStoreProvider(loadedSettings)` to construct it
* correctly.
*/
probeResultStoreProvider?: () => ProbeResultStore | undefined,
/**
* MCP servers injected by the embedding session (e.g. ACP / IDE clients).
* Treated as a session-level source at the TOP of the precedence stack — above
Expand Down Expand Up @@ -2339,6 +2368,7 @@ export async function loadCliConfig(
includePartialMessages,
modelProvidersConfig,
providerProtocolConfig,
probeResultStoreProvider,
generationConfigSources: resolvedCliConfig.sources,
generationConfig: resolvedCliConfig.generationConfig,
initialModelRegistryBaseUrl: resolvedCliConfig.registryBaseUrl,
Expand Down
14 changes: 14 additions & 0 deletions packages/cli/src/config/settingsSchema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import type {
ChatCompressionSettings,
ModelProvidersConfig,
ProviderProtocolConfig,
ModalityProbeRecord,
} from '@qwen-code/qwen-code-core';
import {
ApprovalMode,
Expand Down Expand Up @@ -380,6 +381,19 @@ const SETTINGS_SCHEMA = {
mergeStrategy: MergeStrategy.REPLACE,
},

// Persisted modality probe results (QwenLM/qwen-code#10309, phase 1).
probeResults: {
type: 'object',
label: 'Modality Probe Results',
category: 'Model',
requiresRestart: false,
default: {} as Record<string, ModalityProbeRecord>,
showInDialog: false,
mergeStrategy: MergeStrategy.SHALLOW_MERGE,
description:
'Persisted one-shot image modality probe verdicts, keyed by "authType|modelId|baseUrl". Written by the "Test image support" action in /model; feeds the modality resolution chain (explicit modalities > probe result > name-pattern table). Records are advisory metadata, never user declarations.',
},

plansDirectory: {
type: 'string',
label: 'Plans Directory',
Expand Down
3 changes: 3 additions & 0 deletions packages/cli/src/gemini.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,7 @@ vi.mock('./config/config.js', () => ({
parseArguments: vi.fn().mockResolvedValue({}),
isDebugMode: vi.fn(() => false),
buildDisabledSkillNamesProvider: vi.fn(() => () => new Set<string>()),
buildProbeResultStoreProvider: vi.fn(() => () => undefined),
// Mirrors SESSION_ID_REGEX in ./config/config.ts; keep them in sync.
isValidSessionId: vi.fn((value: string) =>
/^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}(-agent-[a-zA-Z0-9_.-]+)?$/i.test(
Expand Down Expand Up @@ -851,6 +852,8 @@ describe('gemini.tsx main function', () => {
userHooks: undefined,
projectHooks: undefined,
},
// disabledSkillNamesProvider, probeResultStoreProvider
expect.any(Function),
expect.any(Function),
undefined,
// settingsWatcher: not started in bare mode
Expand Down
3 changes: 3 additions & 0 deletions packages/cli/src/gemini.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ import { scrubAndReportInheritedLoaderEnv } from './config/shared-env-keys.js';
import { QWEN_CODE_SERVE_ENV } from './config/acp-channel-fallback.js';
import {
buildDisabledSkillNamesProvider,
buildProbeResultStoreProvider,
loadCliConfig,
parseArguments,
} from './config/config.js';
Expand Down Expand Up @@ -576,6 +577,7 @@ export async function main() {
projectHooks: settings.getProjectHooks(),
},
buildDisabledSkillNamesProvider(settings),
buildProbeResultStoreProvider(settings),
);

if (!settings.merged.security?.auth?.useExternal) {
Expand Down Expand Up @@ -872,6 +874,7 @@ export async function main() {
projectHooks: settings.getProjectHooks(),
},
buildDisabledSkillNamesProvider(settings),
buildProbeResultStoreProvider(settings),
undefined,
settingsWatcher,
);
Expand Down
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 @@ -543,6 +543,18 @@ export default {
'Using {{count}} tools': 'Using {{count}} tools',
'Enter to select, ↑↓ to navigate, Esc to close':
'Enter to select, ↑↓ to navigate, Esc to close',
't: test image support': 't: test image support',
'Image probe': 'Image probe',
'probe-tested': 'probe-tested',
'auto-detected': 'auto-detected',
manual: 'manual',
'testing…': 'testing…',
'accepts images': 'accepts images',
'text only': 'text only',
'inconclusive (auth/rate-limit/timeout) — nothing written':
'inconclusive (auth/rate-limit/timeout) — nothing written',
'Image probe verdict could not be saved.':
'Image probe verdict could not be saved.',
'Esc to go back': 'Esc to go back',
'Enter to confirm, Esc to cancel': 'Enter to confirm, Esc to cancel',
'Enter to select, ↑↓ to navigate, Esc to go back':
Expand Down
11 changes: 11 additions & 0 deletions packages/cli/src/i18n/locales/zh.js
Original file line number Diff line number Diff line change
Expand Up @@ -526,6 +526,17 @@ export default {
'Using {{count}} tools': '正在使用 {{count}} 个工具',
'Enter to select, ↑↓ to navigate, Esc to close':
'Enter 选择,↑↓ 导航,Esc 关闭',
't: test image support': 't: 测试图像支持',
'Image probe': '图像探测',
'probe-tested': '已实测',
'auto-detected': '自动检测',
manual: '手动设置',
'testing…': '测试中…',
'accepts images': '支持图像输入',
'text only': '仅文本',
'inconclusive (auth/rate-limit/timeout) — nothing written':
'结论不明(鉴权/限流/超时)— 未写入任何结果',
'Image probe verdict could not be saved.': '图像探测结论保存失败。',
'Esc to go back': '按 Esc 返回',
'Enter to confirm, Esc to cancel': 'Enter 确认,Esc 取消',
'Enter to select, ↑↓ to navigate, Esc to go back':
Expand Down
Loading