From 1cc3fcfc15b65a7ec662423a28564eba6ed9f236 Mon Sep 17 00:00:00 2001 From: 777genius Date: Thu, 20 Aug 2026 17:15:51 +0300 Subject: [PATCH 01/20] feat(opencode): add inherited default transport --- .../contracts/api.ts | 4 + .../contracts/channels.ts | 2 + .../contracts/types.ts | 5 + .../runtimeProviderManagementUseCases.ts | 8 + .../registerRuntimeProviderManagementIpc.ts | 37 ++++ ...teRuntimeProviderManagementFeature.test.ts | 3 + .../createRuntimeProviderManagementFeature.ts | 4 + ...TeamsRuntimeProviderManagementCliClient.ts | 170 +++++++++--------- .../runtimeProviderCliCommand.ts | 86 +++++++++ .../createRuntimeProviderManagementBridge.ts | 6 + src/renderer/api/httpClient.ts | 2 +- ...RuntimeProviderManagementCliClient.test.ts | 36 +++- ...gisterRuntimeProviderManagementIpc.test.ts | 15 ++ 13 files changed, 289 insertions(+), 89 deletions(-) create mode 100644 src/features/runtime-provider-management/main/infrastructure/runtimeProviderCliCommand.ts diff --git a/src/features/runtime-provider-management/contracts/api.ts b/src/features/runtime-provider-management/contracts/api.ts index 92d730c720..7b2cdf77bf 100644 --- a/src/features/runtime-provider-management/contracts/api.ts +++ b/src/features/runtime-provider-management/contracts/api.ts @@ -12,6 +12,7 @@ import type { RuntimeProviderCompanionStatusDto, RuntimeProviderManagementCancelModelTestInput, RuntimeProviderManagementCancelOAuthInput, + RuntimeProviderManagementClearProjectDefaultInput, RuntimeProviderManagementConfigureModelLimitsInput, RuntimeProviderManagementConnectApiKeyInput, RuntimeProviderManagementConnectInput, @@ -91,6 +92,9 @@ export interface RuntimeProviderManagementApi { setDefaultModel( input: RuntimeProviderManagementSetDefaultModelInput ): Promise; + clearProjectDefaultModel( + input: RuntimeProviderManagementClearProjectDefaultInput + ): Promise; configureModelLimits( input: RuntimeProviderManagementConfigureModelLimitsInput ): Promise; diff --git a/src/features/runtime-provider-management/contracts/channels.ts b/src/features/runtime-provider-management/contracts/channels.ts index 3c1d67c6e0..68e89ad98d 100644 --- a/src/features/runtime-provider-management/contracts/channels.ts +++ b/src/features/runtime-provider-management/contracts/channels.ts @@ -15,6 +15,8 @@ export const RUNTIME_PROVIDER_MANAGEMENT_CANCEL_MODEL_TEST = 'runtimeProviderManagement:cancelModelTest'; export const RUNTIME_PROVIDER_MANAGEMENT_SET_DEFAULT_MODEL = 'runtimeProviderManagement:setDefaultModel'; +export const RUNTIME_PROVIDER_MANAGEMENT_CLEAR_PROJECT_DEFAULT = + 'runtimeProviderManagement:clearProjectDefault'; export const RUNTIME_PROVIDER_MANAGEMENT_CONFIGURE_MODEL_LIMITS = 'runtimeProviderManagement:configureModelLimits'; export const RUNTIME_PROVIDER_MANAGEMENT_OAUTH_PROGRESS = 'runtimeProviderManagement:oauthProgress'; diff --git a/src/features/runtime-provider-management/contracts/types.ts b/src/features/runtime-provider-management/contracts/types.ts index 5bb7e595bb..4405fd5482 100644 --- a/src/features/runtime-provider-management/contracts/types.ts +++ b/src/features/runtime-provider-management/contracts/types.ts @@ -579,6 +579,11 @@ export interface RuntimeProviderManagementSetDefaultModelInput { projectPath?: string | null; } +export interface RuntimeProviderManagementClearProjectDefaultInput { + runtimeId: RuntimeProviderManagementRuntimeId; + projectPath?: string | null; +} + export interface RuntimeProviderManagementConfigureModelLimitsInput { runtimeId: RuntimeProviderManagementRuntimeId; providerId: string; diff --git a/src/features/runtime-provider-management/core/application/runtimeProviderManagementUseCases.ts b/src/features/runtime-provider-management/core/application/runtimeProviderManagementUseCases.ts index 0c1f8a8395..e78be563e8 100644 --- a/src/features/runtime-provider-management/core/application/runtimeProviderManagementUseCases.ts +++ b/src/features/runtime-provider-management/core/application/runtimeProviderManagementUseCases.ts @@ -1,5 +1,6 @@ import type { RuntimeProviderManagementPort } from './RuntimeProviderManagementPort'; import type { + RuntimeProviderManagementClearProjectDefaultInput, RuntimeProviderManagementConfigureModelLimitsInput, RuntimeProviderManagementConnectApiKeyInput, RuntimeProviderManagementConnectInput, @@ -82,6 +83,13 @@ export function setRuntimeProviderDefaultModel( return port.setDefaultModel(input); } +export function clearRuntimeProviderProjectDefault( + port: RuntimeProviderManagementPort, + input: RuntimeProviderManagementClearProjectDefaultInput +): Promise { + return port.clearProjectDefaultModel(input); +} + export function configureRuntimeProviderModelLimits( port: RuntimeProviderManagementPort, input: RuntimeProviderManagementConfigureModelLimitsInput diff --git a/src/features/runtime-provider-management/main/adapters/input/registerRuntimeProviderManagementIpc.ts b/src/features/runtime-provider-management/main/adapters/input/registerRuntimeProviderManagementIpc.ts index 44ebfa531d..ce457a06dc 100644 --- a/src/features/runtime-provider-management/main/adapters/input/registerRuntimeProviderManagementIpc.ts +++ b/src/features/runtime-provider-management/main/adapters/input/registerRuntimeProviderManagementIpc.ts @@ -12,6 +12,7 @@ import { RUNTIME_PROVIDER_COMPANION_INSTALL, RUNTIME_PROVIDER_COMPANION_STATUS, RUNTIME_PROVIDER_MANAGEMENT_CANCEL_MODEL_TEST, + RUNTIME_PROVIDER_MANAGEMENT_CLEAR_PROJECT_DEFAULT, RUNTIME_PROVIDER_MANAGEMENT_CONFIGURE_MODEL_LIMITS, RUNTIME_PROVIDER_MANAGEMENT_CONNECT, RUNTIME_PROVIDER_MANAGEMENT_CONNECT_API_KEY, @@ -46,6 +47,7 @@ import type { RuntimeProviderCompanionStatusDto, RuntimeProviderManagementCancelModelTestInput, RuntimeProviderManagementCancelOAuthInput, + RuntimeProviderManagementClearProjectDefaultInput, RuntimeProviderManagementConfigureModelLimitsInput, RuntimeProviderManagementConnectApiKeyInput, RuntimeProviderManagementConnectInput, @@ -583,6 +585,41 @@ export function registerRuntimeProviderManagementIpc( } ); + ipcMain.handle( + RUNTIME_PROVIDER_MANAGEMENT_CLEAR_PROJECT_DEFAULT, + async ( + _event, + input: RuntimeProviderManagementClearProjectDefaultInput + ): Promise => { + const projectPath = input?.projectPath?.trim(); + if (input?.runtimeId !== 'opencode' || !projectPath || projectPath.length > 4_096) { + return { + schemaVersion: 1, + runtimeId: 'opencode', + error: { + code: 'runtime-misconfigured', + message: 'A valid OpenCode project is required to clear its default model.', + recoverable: true, + }, + }; + } + try { + return await feature.clearProjectDefaultModel({ ...input, projectPath }); + } catch (error) { + const message = getRuntimeProviderIpcErrorMessage( + error, + 'Failed to clear project default model' + ); + logger.error('Failed to clear runtime provider project default model', message); + return { + schemaVersion: 1, + runtimeId: input.runtimeId, + error: createUnexpectedRuntimeProviderIpcError('runtime-unhealthy', message), + }; + } + } + ); + ipcMain.handle( RUNTIME_PROVIDER_MANAGEMENT_CONFIGURE_MODEL_LIMITS, async ( diff --git a/src/features/runtime-provider-management/main/composition/createRuntimeProviderManagementFeature.test.ts b/src/features/runtime-provider-management/main/composition/createRuntimeProviderManagementFeature.test.ts index dc2d2533ff..a211a6eb31 100644 --- a/src/features/runtime-provider-management/main/composition/createRuntimeProviderManagementFeature.test.ts +++ b/src/features/runtime-provider-management/main/composition/createRuntimeProviderManagementFeature.test.ts @@ -38,6 +38,7 @@ describe('createRuntimeProviderManagementFeature companion flow', () => { testModel, cancelModelTest: unsupported, setDefaultModel: unsupported, + clearProjectDefaultModel: unsupported, configureModelLimits: unsupported, submitOAuthCode: unsupported, cancelOAuth: unsupported, @@ -103,6 +104,7 @@ describe('createRuntimeProviderManagementFeature companion flow', () => { testModel, cancelModelTest: unsupported, setDefaultModel: unsupported, + clearProjectDefaultModel: unsupported, configureModelLimits: unsupported, submitOAuthCode: unsupported, cancelOAuth: unsupported, @@ -178,6 +180,7 @@ describe('createRuntimeProviderManagementFeature companion flow', () => { testModel, cancelModelTest: unsupported, setDefaultModel: unsupported, + clearProjectDefaultModel: unsupported, configureModelLimits: unsupported, submitOAuthCode: unsupported, cancelOAuth: unsupported, diff --git a/src/features/runtime-provider-management/main/composition/createRuntimeProviderManagementFeature.ts b/src/features/runtime-provider-management/main/composition/createRuntimeProviderManagementFeature.ts index 84749b3c7f..01f326a2f4 100644 --- a/src/features/runtime-provider-management/main/composition/createRuntimeProviderManagementFeature.ts +++ b/src/features/runtime-provider-management/main/composition/createRuntimeProviderManagementFeature.ts @@ -30,6 +30,7 @@ import type { RuntimeProviderManagementApi, RuntimeProviderManagementCancelModelTestInput, RuntimeProviderManagementCancelOAuthInput, + RuntimeProviderManagementClearProjectDefaultInput, RuntimeProviderManagementConfigureModelLimitsInput, RuntimeProviderManagementConnectApiKeyInput, RuntimeProviderManagementConnectInput, @@ -149,6 +150,9 @@ export function createRuntimeProviderManagementFeature( setDefaultModel: ( input: RuntimeProviderManagementSetDefaultModelInput ): Promise => port.setDefaultModel(input), + clearProjectDefaultModel: ( + input: RuntimeProviderManagementClearProjectDefaultInput + ): Promise => port.clearProjectDefaultModel(input), configureModelLimits: ( input: RuntimeProviderManagementConfigureModelLimitsInput ): Promise => port.configureModelLimits(input), diff --git a/src/features/runtime-provider-management/main/infrastructure/AgentTeamsRuntimeProviderManagementCliClient.ts b/src/features/runtime-provider-management/main/infrastructure/AgentTeamsRuntimeProviderManagementCliClient.ts index 4168f4a143..0f8eb7512a 100644 --- a/src/features/runtime-provider-management/main/infrastructure/AgentTeamsRuntimeProviderManagementCliClient.ts +++ b/src/features/runtime-provider-management/main/infrastructure/AgentTeamsRuntimeProviderManagementCliClient.ts @@ -1,9 +1,10 @@ +import { mkdtemp, rm } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; import path from 'node:path'; import { buildProviderAwareCliEnv } from '@main/services/runtime/providerAwareCliEnv'; import { ClaudeBinaryResolver } from '@main/services/team/ClaudeBinaryResolver'; import { execCli, killProcessTree, spawnCli } from '@main/utils/childProcess'; -import { getHomeDir } from '@main/utils/pathDecoder'; import { resolveInteractiveShellEnvBestEffort } from '@main/utils/shellEnv'; import { @@ -11,6 +12,17 @@ import { extractProfileIdFromSymlinkError, isOpenCodeNodeModulesSymlinkError, } from './openCodeWindowsNodeModulesJunction'; +import { + appendBoundedSpawnOutput, + appendOptionalArg, + appendProjectPathArgs, + type BoundedSpawnOutputBuffer, + createBoundedSpawnOutputBuffer, + normalizeProjectPath, + readBoundedSpawnOutput, + RUNTIME_PROVIDER_COMMAND_MAX_BUFFER_BYTES as COMMAND_MAX_BUFFER_BYTES, + runtimeProviderCommandOptions, +} from './runtimeProviderCliCommand'; import { binaryLooksLikeOpenCode, formatCommandForDisplay, @@ -28,6 +40,7 @@ import { import type { RuntimeProviderManagementCancelModelTestInput, RuntimeProviderManagementCancelOAuthInput, + RuntimeProviderManagementClearProjectDefaultInput, RuntimeProviderManagementConfigureModelLimitsInput, RuntimeProviderManagementConnectApiKeyInput, RuntimeProviderManagementConnectInput, @@ -60,8 +73,6 @@ const COMMAND_TIMEOUT_MS = 90_000; // cancellable from the UI. const OAUTH_COMMAND_TIMEOUT_MS = 17 * 60_000; const OAUTH_CANCEL_FORCE_KILL_DELAY_MS = 2_000; -const COMMAND_MAX_BUFFER_BYTES = 8 * 1024 * 1024; -const SPAWN_OUTPUT_TRUNCATED_MARKER = '...[truncated runtime provider command output]'; const DIRECTORY_RESPONSE_CACHE_TTL_MS = 30_000; const DEFAULT_DIRECTORY_RESPONSE_CACHE_TTL_MS = 2 * 60_000; const MAX_DIRECTORY_RESPONSE_CACHE_ENTRIES = 32; @@ -878,86 +889,6 @@ function createCommandContext( return { binaryPath, args, projectPath }; } -function normalizeProjectPath(projectPath: string | null | undefined): string | null { - const normalized = projectPath?.trim(); - return normalized ? normalized : null; -} - -function appendProjectPathArgs(args: string[], projectPath: string | null): string[] { - return projectPath ? [...args, '--project-path', projectPath] : args; -} - -function appendOptionalArg(args: string[], name: string, value: string | null | undefined): void { - const normalized = value?.trim(); - if (normalized) { - args.push(name, normalized); - } -} - -function runtimeProviderCommandOptions( - options: T, - projectPath: string | null -): T & { cwd?: string; maxBuffer: number } { - const isUsableCwd = (candidate: string | null | undefined): candidate is string => { - const normalized = candidate?.trim(); - if (!normalized) return false; - const resolved = path.resolve(normalized); - return resolved !== path.parse(resolved).root; - }; - const fallbackHome = [options.env.HOME, options.env.USERPROFILE, getHomeDir()] - .map((candidate) => candidate?.trim()) - .find(isUsableCwd); - const commandOptions = { - ...options, - maxBuffer: COMMAND_MAX_BUFFER_BYTES, - }; - const cwd = isUsableCwd(projectPath) ? projectPath.trim() : fallbackHome; - return cwd ? { ...commandOptions, cwd } : commandOptions; -} - -interface BoundedSpawnOutputBuffer { - chunks: Buffer[]; - bytes: number; - truncated: boolean; -} - -function createBoundedSpawnOutputBuffer(): BoundedSpawnOutputBuffer { - return { - chunks: [], - bytes: 0, - truncated: false, - }; -} - -function appendBoundedSpawnOutput(buffer: BoundedSpawnOutputBuffer, chunk: Buffer): void { - if (buffer.bytes >= COMMAND_MAX_BUFFER_BYTES) { - buffer.truncated = true; - return; - } - - const remaining = COMMAND_MAX_BUFFER_BYTES - buffer.bytes; - if (chunk.length > remaining) { - buffer.chunks.push(chunk.subarray(0, remaining)); - buffer.bytes += remaining; - buffer.truncated = true; - return; - } - - buffer.chunks.push(chunk); - buffer.bytes += chunk.length; -} - -function readBoundedSpawnOutput( - buffer: BoundedSpawnOutputBuffer, - options?: { includeTruncationMarker?: boolean } -): string { - const output = Buffer.concat(buffer.chunks, buffer.bytes).toString('utf8'); - if (!options?.includeTruncationMarker || !buffer.truncated) { - return output; - } - return `${SPAWN_OUTPUT_TRUNCATED_MARKER}\n${output}`; -} - async function resolveCliEnv(): Promise<{ binaryPath: string | null; env: NodeJS.ProcessEnv; @@ -2563,6 +2494,11 @@ export class AgentTeamsRuntimeProviderManagementCliClient implements RuntimeProv ); } + let neutralProjectPath: string | null = null; + if (input.scope === 'all_projects') { + neutralProjectPath = await mkdtemp(path.join(tmpdir(), 'agent-teams-opencode-default-')); + } + const commandProjectPath = neutralProjectPath ?? projectPath; const args = appendProjectPathArgs( [ 'runtime', @@ -2580,9 +2516,9 @@ export class AgentTeamsRuntimeProviderManagementCliClient implements RuntimeProv '--compact', '--json', ], - projectPath + commandProjectPath ); - const context = createCommandContext(binaryPath, args, projectPath); + const context = createCommandContext(binaryPath, args, commandProjectPath); const misconfigured = rejectWrongRuntimeBinary( input.runtimeId, context @@ -2596,7 +2532,7 @@ export class AgentTeamsRuntimeProviderManagementCliClient implements RuntimeProv args, runtimeProviderCommandOptions( { env, timeout: RUNTIME_PROVIDER_MODEL_PROBE_COMMAND_TIMEOUT_MS }, - projectPath + commandProjectPath ) ); return extractJsonObjectWithContext( @@ -2614,6 +2550,68 @@ export class AgentTeamsRuntimeProviderManagementCliClient implements RuntimeProv normalizeCommandFailure(error, context), 'model-test-failed' ); + } finally { + this.invalidateProviderResponseCaches(); + if (neutralProjectPath) { + await rm(neutralProjectPath, { recursive: true, force: true }).catch(() => undefined); + } + } + } + + async clearProjectDefaultModel( + input: RuntimeProviderManagementClearProjectDefaultInput + ): Promise { + this.invalidateProviderResponseCaches(); + const projectPath = normalizeProjectPath(input.projectPath); + const { binaryPath, env } = await resolveCliEnv(); + if (!binaryPath) { + return missingRuntimeBinaryResponse( + input.runtimeId, + projectPath + ); + } + + const args = appendProjectPathArgs( + [ + 'runtime', + 'providers', + 'clear-project-default', + '--runtime', + input.runtimeId, + '--compact', + '--json', + ], + projectPath + ); + const context = createCommandContext(binaryPath, args, projectPath); + const misconfigured = rejectWrongRuntimeBinary( + input.runtimeId, + context + ); + if (misconfigured) { + return misconfigured; + } + try { + const { stdout, stderr } = await execCli( + binaryPath, + args, + runtimeProviderCommandOptions({ env, timeout: COMMAND_TIMEOUT_MS }, projectPath) + ); + return extractJsonObjectWithContext( + stdout, + context, + stderr + ); + } catch (error) { + const response = extractJsonObjectFromError(error); + if (response) { + return response; + } + return commandFailureResponse( + input.runtimeId, + normalizeCommandFailure(error, context), + 'runtime-unhealthy' + ); } finally { this.invalidateProviderResponseCaches(); } diff --git a/src/features/runtime-provider-management/main/infrastructure/runtimeProviderCliCommand.ts b/src/features/runtime-provider-management/main/infrastructure/runtimeProviderCliCommand.ts new file mode 100644 index 0000000000..5fca716758 --- /dev/null +++ b/src/features/runtime-provider-management/main/infrastructure/runtimeProviderCliCommand.ts @@ -0,0 +1,86 @@ +import path from 'node:path'; + +import { getHomeDir } from '@main/utils/pathDecoder'; + +export const RUNTIME_PROVIDER_COMMAND_MAX_BUFFER_BYTES = 8 * 1024 * 1024; +const SPAWN_OUTPUT_TRUNCATED_MARKER = '...[truncated runtime provider command output]'; + +export function normalizeProjectPath( + projectPath: string | null | undefined +): string | null { + const normalized = projectPath?.trim(); + return normalized ? normalized : null; +} + +export function appendProjectPathArgs(args: string[], projectPath: string | null): string[] { + return projectPath ? [...args, '--project-path', projectPath] : args; +} + +export function appendOptionalArg( + args: string[], + name: string, + value: string | null | undefined +): void { + const normalized = value?.trim(); + if (normalized) args.push(name, normalized); +} + +export function runtimeProviderCommandOptions( + options: T, + projectPath: string | null +): T & { cwd?: string; maxBuffer: number } { + const isUsableCwd = (candidate: string | null | undefined): candidate is string => { + const normalized = candidate?.trim(); + if (!normalized) return false; + const resolved = path.resolve(normalized); + return resolved !== path.parse(resolved).root; + }; + const fallbackHome = [options.env.HOME, options.env.USERPROFILE, getHomeDir()] + .map((candidate) => candidate?.trim()) + .find(isUsableCwd); + const commandOptions = { + ...options, + maxBuffer: RUNTIME_PROVIDER_COMMAND_MAX_BUFFER_BYTES, + }; + const cwd = isUsableCwd(projectPath) ? projectPath.trim() : fallbackHome; + return cwd ? { ...commandOptions, cwd } : commandOptions; +} + +export interface BoundedSpawnOutputBuffer { + chunks: Buffer[]; + bytes: number; + truncated: boolean; +} + +export function createBoundedSpawnOutputBuffer(): BoundedSpawnOutputBuffer { + return { chunks: [], bytes: 0, truncated: false }; +} + +export function appendBoundedSpawnOutput( + buffer: BoundedSpawnOutputBuffer, + chunk: Buffer +): void { + if (buffer.bytes >= RUNTIME_PROVIDER_COMMAND_MAX_BUFFER_BYTES) { + buffer.truncated = true; + return; + } + const remaining = RUNTIME_PROVIDER_COMMAND_MAX_BUFFER_BYTES - buffer.bytes; + if (chunk.length > remaining) { + buffer.chunks.push(chunk.subarray(0, remaining)); + buffer.bytes += remaining; + buffer.truncated = true; + return; + } + buffer.chunks.push(chunk); + buffer.bytes += chunk.length; +} + +export function readBoundedSpawnOutput( + buffer: BoundedSpawnOutputBuffer, + options?: { includeTruncationMarker?: boolean } +): string { + const output = Buffer.concat(buffer.chunks, buffer.bytes).toString('utf8'); + return options?.includeTruncationMarker && buffer.truncated + ? `${SPAWN_OUTPUT_TRUNCATED_MARKER}\n${output}` + : output; +} diff --git a/src/features/runtime-provider-management/preload/createRuntimeProviderManagementBridge.ts b/src/features/runtime-provider-management/preload/createRuntimeProviderManagementBridge.ts index 72bdb5f87f..9238d722ce 100644 --- a/src/features/runtime-provider-management/preload/createRuntimeProviderManagementBridge.ts +++ b/src/features/runtime-provider-management/preload/createRuntimeProviderManagementBridge.ts @@ -9,6 +9,7 @@ import { RUNTIME_PROVIDER_COMPANION_PROGRESS, RUNTIME_PROVIDER_COMPANION_STATUS, RUNTIME_PROVIDER_MANAGEMENT_CANCEL_MODEL_TEST, + RUNTIME_PROVIDER_MANAGEMENT_CLEAR_PROJECT_DEFAULT, RUNTIME_PROVIDER_MANAGEMENT_CONFIGURE_MODEL_LIMITS, RUNTIME_PROVIDER_MANAGEMENT_CONNECT, RUNTIME_PROVIDER_MANAGEMENT_CONNECT_API_KEY, @@ -39,6 +40,7 @@ import type { RuntimeProviderCompanionStatusDto, RuntimeProviderManagementCancelModelTestInput, RuntimeProviderManagementCancelOAuthInput, + RuntimeProviderManagementClearProjectDefaultInput, RuntimeProviderManagementConfigureModelLimitsInput, RuntimeProviderManagementConnectApiKeyInput, RuntimeProviderManagementConnectInput, @@ -147,6 +149,10 @@ export function createRuntimeProviderManagementBridge( input: RuntimeProviderManagementSetDefaultModelInput ): Promise => ipcRenderer.invoke(RUNTIME_PROVIDER_MANAGEMENT_SET_DEFAULT_MODEL, input), + clearProjectDefaultModel: ( + input: RuntimeProviderManagementClearProjectDefaultInput + ): Promise => + ipcRenderer.invoke(RUNTIME_PROVIDER_MANAGEMENT_CLEAR_PROJECT_DEFAULT, input), configureModelLimits: ( input: RuntimeProviderManagementConfigureModelLimitsInput ): Promise => diff --git a/src/renderer/api/httpClient.ts b/src/renderer/api/httpClient.ts index 055eb6cf3e..0dc1e8d89e 100644 --- a/src/renderer/api/httpClient.ts +++ b/src/renderer/api/httpClient.ts @@ -1539,7 +1539,6 @@ export class HttpAPIClient implements ElectronAPI { return () => {}; }, }; - runtimeProviderManagement: RuntimeProviderManagementApi = { listLocalProviders: async (input) => ({ schemaVersion: 1, @@ -1665,6 +1664,7 @@ export class HttpAPIClient implements ElectronAPI { recoverable: true, }, }), + clearProjectDefaultModel: async (input) => ({ schemaVersion: 1, runtimeId: input.runtimeId, error: { code: 'unsupported-action', message: 'Runtime provider management is not available in browser mode.', recoverable: true } }), configureModelLimits: async (input) => ({ schemaVersion: 1, runtimeId: input.runtimeId, diff --git a/test/main/features/runtime-provider-management/AgentTeamsRuntimeProviderManagementCliClient.test.ts b/test/main/features/runtime-provider-management/AgentTeamsRuntimeProviderManagementCliClient.test.ts index db3eb6ba10..6c2923902d 100644 --- a/test/main/features/runtime-provider-management/AgentTeamsRuntimeProviderManagementCliClient.test.ts +++ b/test/main/features/runtime-provider-management/AgentTeamsRuntimeProviderManagementCliClient.test.ts @@ -2646,12 +2646,44 @@ describe('AgentTeamsRuntimeProviderManagementCliClient', () => { projectPath: '/Users/test/project', }); + const [, args, options] = execCliMock.mock.calls[0] ?? []; + const projectPathIndex = args.indexOf('--project-path'); + const neutralProjectPath = args[projectPathIndex + 1]; + expect(args).toEqual(expect.arrayContaining(['--scope', 'all-projects'])); + expect(neutralProjectPath).toMatch( + new RegExp(`^${os.tmpdir().replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}`) + ); + expect(neutralProjectPath).not.toBe('/Users/test/project'); + expect(options).toMatchObject({ cwd: neutralProjectPath }); + expect(fs.existsSync(neutralProjectPath)).toBe(false); + expect(execCliMock.mock.calls[0]?.[2]).toMatchObject({ timeout: 240_000 }); + }); + + it('clears only the selected project default through the dedicated CLI command', async () => { + execCliMock.mockResolvedValue({ + stdout: JSON.stringify({ + schemaVersion: 1, + runtimeId: 'opencode', + view: { providers: [], diagnostics: [] }, + }), + stderr: '', + }); + const client = new AgentTeamsRuntimeProviderManagementCliClient(); + + await client.clearProjectDefaultModel({ + runtimeId: 'opencode', + projectPath: '/Users/test/project', + }); + expect(execCliMock).toHaveBeenCalledWith( '/repo/cli-dev', - expect.arrayContaining(['--scope', 'all-projects']), + expect.arrayContaining([ + 'clear-project-default', + '--project-path', + '/Users/test/project', + ]), expect.objectContaining({ cwd: '/Users/test/project' }) ); - expect(execCliMock.mock.calls[0]?.[2]).toMatchObject({ timeout: 240_000 }); }); it('loads provider setup forms through the CLI contract', async () => { diff --git a/test/main/features/runtime-provider-management/registerRuntimeProviderManagementIpc.test.ts b/test/main/features/runtime-provider-management/registerRuntimeProviderManagementIpc.test.ts index 0a62535dc3..72a2888b76 100644 --- a/test/main/features/runtime-provider-management/registerRuntimeProviderManagementIpc.test.ts +++ b/test/main/features/runtime-provider-management/registerRuntimeProviderManagementIpc.test.ts @@ -9,6 +9,7 @@ import { RUNTIME_PROVIDER_COMPANION_CONNECT, RUNTIME_PROVIDER_COMPANION_INSTALL, RUNTIME_PROVIDER_COMPANION_STATUS, + RUNTIME_PROVIDER_MANAGEMENT_CLEAR_PROJECT_DEFAULT, RUNTIME_PROVIDER_MANAGEMENT_CONFIGURE_MODEL_LIMITS, RUNTIME_PROVIDER_MANAGEMENT_CONNECT, RUNTIME_PROVIDER_MANAGEMENT_CONNECT_API_KEY, @@ -587,6 +588,7 @@ describe('registerRuntimeProviderManagementIpc', () => { testModel: vi.fn(() => Promise.resolve(testResponse)), cancelModelTest: vi.fn(() => Promise.resolve({ ok: true })), setDefaultModel: vi.fn(() => Promise.resolve(viewResponse)), + clearProjectDefaultModel: vi.fn(() => Promise.resolve(viewResponse)), configureModelLimits: vi.fn(() => Promise.resolve(modelLimitsResponse)), }; @@ -688,6 +690,16 @@ describe('registerRuntimeProviderManagementIpc', () => { projectPath: '/tmp/local-project', }); expect(limitsResponse).toEqual(modelLimitsResponse); + + const clearResponse = await handlers.get(RUNTIME_PROVIDER_MANAGEMENT_CLEAR_PROJECT_DEFAULT)?.( + {}, + { runtimeId: 'opencode', projectPath: ' /tmp/test-project ' } + ); + expect(feature.clearProjectDefaultModel).toHaveBeenCalledWith({ + runtimeId: 'opencode', + projectPath: '/tmp/test-project', + }); + expect(clearResponse).toEqual(viewResponse); }); it('sanitizes unexpected IPC error messages before returning them to the renderer', async () => { @@ -720,6 +732,7 @@ describe('registerRuntimeProviderManagementIpc', () => { testModel: vi.fn(), cancelModelTest: vi.fn(() => Promise.resolve({ ok: true })), setDefaultModel: vi.fn(), + clearProjectDefaultModel: vi.fn(), configureModelLimits: vi.fn(), }; @@ -783,6 +796,7 @@ describe('registerRuntimeProviderManagementIpc', () => { testModel: vi.fn(), cancelModelTest: vi.fn(() => Promise.resolve({ ok: true })), setDefaultModel: vi.fn(), + clearProjectDefaultModel: vi.fn(), configureModelLimits: vi.fn(), }; @@ -827,6 +841,7 @@ describe('registerRuntimeProviderManagementIpc', () => { testModel: vi.fn(), cancelModelTest: vi.fn(() => Promise.resolve({ ok: true })), setDefaultModel: vi.fn(), + clearProjectDefaultModel: vi.fn(), configureModelLimits: vi.fn(), }; From ad8358143a25d79e6cd4f1e0de06166e7ed0811b Mon Sep 17 00:00:00 2001 From: 777genius Date: Thu, 20 Aug 2026 17:16:48 +0300 Subject: [PATCH 02/20] feat(opencode): add inherited default model UX --- ...opencode-default-model-inheritance-plan.md | 510 ++++++++++++++ .../renderer/locales/ar/settings.json | 17 +- .../renderer/locales/bn/settings.json | 17 +- .../renderer/locales/de/settings.json | 17 +- .../renderer/locales/en/settings.json | 17 +- .../renderer/locales/es/settings.json | 17 +- .../renderer/locales/fa/settings.json | 17 +- .../renderer/locales/fil/settings.json | 17 +- .../renderer/locales/fr/settings.json | 17 +- .../renderer/locales/hi/settings.json | 17 +- .../renderer/locales/id/settings.json | 17 +- .../renderer/locales/it/settings.json | 17 +- .../renderer/locales/ja/settings.json | 17 +- .../renderer/locales/ko/settings.json | 17 +- .../renderer/locales/mr/settings.json | 17 +- .../renderer/locales/ms/settings.json | 17 +- .../renderer/locales/nl/settings.json | 17 +- .../renderer/locales/pl/settings.json | 17 +- .../renderer/locales/pt/settings.json | 17 +- .../renderer/locales/ro/settings.json | 17 +- .../renderer/locales/ru/settings.json | 17 +- .../renderer/locales/sw/settings.json | 17 +- .../renderer/locales/ta/settings.json | 17 +- .../renderer/locales/te/settings.json | 17 +- .../renderer/locales/th/settings.json | 17 +- .../renderer/locales/tr/settings.json | 17 +- .../renderer/locales/uk/settings.json | 17 +- .../renderer/locales/ur/settings.json | 17 +- .../renderer/locales/vi/settings.json | 17 +- .../renderer/locales/zh/settings.json | 17 +- .../localization/renderer/resources.d.ts | 15 +- .../runtimeProviderManagementPresentation.ts | 156 ++++ .../hooks/useRuntimeProviderManagement.ts | 353 ++++------ .../OpenCodeDefaultModelInheritanceCard.tsx | 245 +++++++ .../ui/RuntimeProviderManagementPanelView.tsx | 665 +++--------------- .../openCodeDefaultModelInheritance.test.ts | 79 +++ .../openCodeDefaultModelInheritance.ts | 79 +++ ...RuntimeProviderManagementPanelView.test.ts | 476 ++----------- .../RuntimeProviderOnboardingView.test.ts | 3 +- .../useRuntimeProviderManagement.test.ts | 58 +- 40 files changed, 1876 insertions(+), 1256 deletions(-) create mode 100644 docs/research/opencode-default-model-inheritance-plan.md create mode 100644 src/features/runtime-provider-management/renderer/hooks/runtimeProviderManagementPresentation.ts create mode 100644 src/features/runtime-provider-management/renderer/ui/OpenCodeDefaultModelInheritanceCard.tsx create mode 100644 src/features/runtime-provider-management/renderer/view-models/openCodeDefaultModelInheritance.test.ts create mode 100644 src/features/runtime-provider-management/renderer/view-models/openCodeDefaultModelInheritance.ts diff --git a/docs/research/opencode-default-model-inheritance-plan.md b/docs/research/opencode-default-model-inheritance-plan.md new file mode 100644 index 0000000000..e3bccf7d73 --- /dev/null +++ b/docs/research/opencode-default-model-inheritance-plan.md @@ -0,0 +1,510 @@ +# OpenCode Default Model Inheritance UX Implementation Plan + +Status: implemented and verified on 2026-08-20 +Primary issue: `777genius/agent-teams-ai#446` +Target outcome: any usable live OpenCode route, including `openrouter/openrouter/free`, can be selected as the inherited OpenCode default without exposing configuration-scope mechanics to the user. + +## Summary + +Replace the current `All projects / This project` scope switch and limited configured-model list with an explicit inheritance view: + +1. A base `Default model` row used by projects without an override. +2. A contextual project row that either inherits the base default or shows its own override. +3. One provider-faceted model picker that can select live routes from connected/configured providers. +4. An explicit `Use default` action that removes a project override instead of copying the current base model into project state. +5. A resolved `Default - ` presentation in Create Team, while preserving explicit per-team model choices as a separate concept. + +The implementation must also remove the hidden provider-row side effect that currently stores a model for future Create Team dialogs. + +Estimated implementation size, including focused tests and transport wiring: **900-1,500 changed lines across the desktop and runtime repositories**. + +### Lean implementation decision + +Do not build a second model-browsing surface. The existing Providers tab already owns lazy provider loading, model search, filters, pagination, virtualization, access state, and execution results. Reuse it as the model picker: + +- `Change` on an inheritance row records the destination and opens the Providers tab. +- A compact banner says which default is being changed and provides `Cancel`. +- Model rows expose an explicit `Test and use` action for that destination. +- Outside an active change flow, model rows expose only `Test`. Default changes always start from the inheritance card, so the destination never has to be chosen twice. + +This keeps the inherited UX while avoiding duplicate catalog state, duplicate model-list components, and three new modal state machines. + +## Current understanding + +### Confirmed root causes + +- The live OpenRouter connection exposes 352 models and includes: + - display route: `openrouter/free` + - full OpenCode model id: `openrouter/openrouter/free` +- The provider catalog renders this route, but its row only exposes `Test`. +- Clicking the whole provider-model row silently calls `useModelForNewTeams()` and writes a Create Team preference. It does not change the OpenCode default. +- The Models tab renders only `view.configuredModels`. The runtime intentionally limits that collection to configured-local, built-in-free, and current-default routes, so a normal connected-provider route is missing until it is already the default. +- The runtime `setDefaultModel` use case already accepts any usable model found in the live provider catalog, probes it, and persists either a project or all-projects preference. +- The view already contains the inheritance data needed by the renderer: + - `projectDefaultModel` + - `allProjectsDefaultModel` + - `defaultModelSource` + - effective `defaultModel` +- There is no command or contract for removing a project override. Copying the base model into project preferences would look correct initially but would break inheritance when the base default changes later. +- All-projects mutation currently uses the selected project as its probe context. Project configuration can therefore make a global route appear usable or unusable for the wrong reason. +- Create Team separately persists its last explicit model selection in renderer storage. That preference can legitimately override the OpenCode default, but the current UI does not make the distinction obvious. + +### Canonical ownership + +- Runtime managed preferences are the source of truth for base and project defaults. +- `RuntimeProviderManagementViewDto` is the renderer read model for effective inheritance. +- Create Team renderer storage remains the source of truth only for the last explicit Create Team selection. +- Provider catalog state is discovery data. It must not mutate Create Team preferences from a row click. + +### Effective selection rule + +For an OpenCode team launch: + +```text +explicit team model + ?? project OpenCode override + ?? base OpenCode default + ?? existing OpenCode fallback +``` + +The renderer must explain this precedence without exposing internal terms such as `scope` or `validation context`. + +## Fixed product decisions + +### User-facing vocabulary + +- Do not show `All projects` as a standalone scope label. It is inaccurate when project overrides exist. +- Use `Default model` with helper text: `Used by projects that do not have their own model.` +- Use `This project: ` for the contextual row. +- When inherited, show `Uses default model: ` and action `Use another model`. +- When overridden, show the override and actions `Change` and `Use default`. +- Do not show `Validation context` in the normal UI. + +### Mutation behavior + +- `Change` on the base row opens the picker with the destination fixed to the base default. +- `Use another model` or `Change` on the project row opens the picker with the destination fixed to that project. +- The user is not asked to choose scope again inside the picker. +- A provider-catalog model row does not expose scope-specific writes without an active inheritance-card destination. +- Selecting a model is non-mutating. The only write action is `Test and use`. +- `Test and use` uses the runtime's atomic probe-before-write operation. Do not run a separate probe followed by an unrelated save. +- A failed, cancelled, timed-out, or stale operation leaves the stored default unchanged. +- `Use default` removes the project override. It must never write a snapshot of the current base model into the project preference. + +### Free Models Router presentation + +For `openrouter/openrouter/free`, show: + +- `Free Models Router` +- `OpenRouter`, `Free`, and connection/verification badges +- advisory text: `Chooses an available free model for each request. The underlying model may change.` + +Do not use the generic `Not recommended` label as the only explanation. The route remains selectable; the advisory describes the real trade-off. + +### Create Team behavior + +- Preserve the existing last-selection persistence behavior in this slice. +- Make the state visible: + - inherited option: `Default - Free Models Router` or a resolved fallback label + - explicit option/summary: `Explicit choice - ` +- Changing OpenCode defaults must not silently overwrite an intentional explicit Create Team choice. +- Provider Settings must no longer change the Create Team choice except through an explicitly worded onboarding action that already owns that behavior. + +## Non-goals + +- Do not launch teams, agents, terminals, tasks, or provisioning flows during verification. +- Do not redesign provider authentication or onboarding. +- Do not change the recommendation matrix for unrelated models. +- Do not add a general cross-provider search backend. The picker may remain provider-faceted and use the existing lazy provider-model API. +- Do not add global-default clearing unless a concrete UI requirement appears. This plan only needs project override clearing. +- Do not rewrite the existing Create Team persistence model. +- Do not broaden browser-mode support for runtime provider mutations; maintain the existing explicit unsupported response. +- Do not opportunistically refactor unrelated oversized legacy files. + +## Architecture and minimum edit set + +### Runtime repository: clear project override + +Repository: `/Users/belief/dev/projects/claude/agent_teams_orchestrator` + +Add a narrow `clear project default` vertical slice: + +1. Add `ClearProjectDefaultModelInput` to the runtime provider-management domain. +2. Add `clearProjectDefaultModel()` to `ProviderManagementCommandPort`. +3. Add the application delegator next to `setProviderDefaultModel`. +4. Extend managed preferences with an explicit atomic clear function that: + - preserves model-limit overrides; + - writes `defaultModelId: null`; + - updates `updatedAt`; + - uses the existing per-file queue and lock. +5. Extend `OpenCodeProviderManagementAdapter` with `clearProjectDefaultModel()`: + - require a real project path/profile; + - perform no model execution probe; + - clear only the project managed preference; + - invalidate directory/inventory caches; + - refresh the inventory and return a view resolving to the base default/fallback. +6. Add `runtime providers clear-project-default` to both CLI entry paths: + - source launcher argument parser; + - Commander registration; + - JSON response shape identical to other view mutations. +7. Do not overload `set-default` with empty model ids or magic sentinel values. + +Important repository condition: the runtime worktree currently contains unrelated uncommitted changes, including the adapter, domain types, tests, and source entrypoint. Before implementation, re-read those diffs and apply only compatible, narrow additions. Never revert or stage unrelated work. + +### Desktop contracts and transport + +Repository: `/Users/belief/dev/projects/claude/claude_team` + +1. Add `RuntimeProviderManagementClearProjectDefaultInput`. +2. Add `clearProjectDefaultModel()` to `RuntimeProviderManagementApi`. +3. Add a dedicated IPC channel and preload bridge method. +4. Validate: + - runtime is `opencode`; + - project path is a non-empty bounded string; + - no provider/model id is accepted because clearing is not model-specific. +5. Delegate through the feature facade and port. +6. Add the browser adapter's explicit `unsupported-action` response. +7. Extend `AgentTeamsRuntimeProviderManagementCliClient` to invoke `clear-project-default`, invalidate model/view caches before and after mutation, and preserve structured diagnostics. + +### Neutral base-default validation + +Implement neutral probing in the desktop main-process CLI client, not in renderer state: + +1. For `scope: all_projects`, create a disposable directory with `mkdtemp` under the OS temp directory. +2. Use that directory as both CLI cwd and `--project-path` for the set-default command. +3. Continue using the same runtime binary, managed data root, and provider authentication. +4. Remove the exact temporary directory in `finally` with narrow best-effort cleanup. +5. If temp creation fails, fail before running the mutation. +6. Project-scoped mutations continue using the selected project path. +7. Do not repurpose `HOME` or other broad environment variables. + +The global mutation response represents the neutral directory, so the renderer must not replace its current project view wholesale with that response. + +### Renderer view model + +Add a small pure presentation module under `renderer/view-models/` or `renderer/utils/` that derives: + +```ts +type OpenCodeDefaultModelPresentation = { + baseModelId: string | null; + baseDisplayName: string; + projectPath: string | null; + projectName: string | null; + projectOverrideModelId: string | null; + projectEffectiveModelId: string | null; + projectInherits: boolean; + effectiveSource: RuntimeProviderDefaultModelSourceDto | null; +}; +``` + +Rules: + +- Base row uses `allProjectsDefaultModel`, then the existing OpenCode config/fallback only for readout when no managed base exists. +- Project override exists only when `projectDefaultModel` is non-null. +- Project effective model is `projectDefaultModel ?? allProjectsDefaultModel ?? defaultModel`. +- A project override remains visibly effective when the base default changes. +- Unknown/stale model ids remain visible as raw ids with an `Unavailable` advisory; they are not silently dropped. + +Add pure mutation projection helpers so a persisted write can update scoped state even if the post-write refresh fails: + +- set base default while preserving a project override; +- set project override; +- clear project override and resolve the inherited model. + +### Renderer hook behavior + +Extend `useRuntimeProviderManagement` with: + +- a picker target: `base` or `project` plus the bound project snapshot; +- `clearProjectDefaultModel()` action; +- separate busy state for clearing the project override; +- scoped optimistic/read-model projection after successful persistence; +- a follow-up refresh for the currently active project. + +Concurrency rules: + +- Project mutations are bound to the project generation captured at start. A response for project A must never update project B. +- Base mutation is independent of the selected project. If the project changes while it runs, apply the base result and refresh the latest active project. +- Closing the picker cancels only picker-owned model probes and ignores late results. +- Only one default mutation may be active at a time. +- A saved mutation followed by refresh failure produces a warning, not a failure claim. +- Busy state must clear even if project context changes during a base mutation. + +### Renderer UI extraction + +The existing production files are frozen above the source-size limit: + +- `RuntimeProviderManagementPanelView.tsx`: 3309 lines +- `useRuntimeProviderManagement.ts`: 2118 lines +- `TeamModelSelector.tsx`: 3881 lines +- `CreateTeamDialog.tsx`: 3160 lines + +Do not grow them materially. Extract cohesive units and reduce their line counts where practical. + +Planned new units: + +- `OpenCodeDefaultModelInheritanceCard.tsx` + - base row; + - current-project inheritance/override row; + - project chooser only when there is no active project or the user explicitly changes context; + - direct, reversible `Use default` action for removing an override. +- `openCodeDefaultModelPresentation.ts` + - pure read-model projection and mutation merge rules. + +The existing provider/model catalog remains the picker. Add only: + +- a target banner above the catalog while a default change is active; +- explicit row actions for base/project default selection; +- route-specific advisory content. + +Reuse existing Radix-backed Dialog, AlertDialog, Select, Checkbox, Tabs, and Tooltip primitives. Do not add native `title` tooltips or hand-rolled modal controls. + +### Provider model catalog + +1. Make model rows non-mutating containers; remove `role=button`, `aria-pressed`, whole-row click, and keyboard save behavior. +2. Keep `Test` as an explicit action. +3. Show `Test and use` only while the inheritance card has bound the picker to a destination. +4. Do not require a project merely to set the base default; the main process supplies a neutral probe directory. +5. Require an actual project only for a project override. +6. Remove `Save for team picker` from the defaults/models management surface. Model choice for a team belongs in Create Team. +7. Keep onboarding's explicit verified-model acceptance behavior, but ensure its label states that it affects new teams. + +### Models tab + +Replace `OpenCodeModelScopeControls` and `ConfiguredOpenCodeModelsPanel` as the primary default-editing UI with `OpenCodeDefaultModelInheritanceCard`. + +The existing configured-routes list may remain below as an advanced diagnostics/limits section only if local model-limit controls still need it. It must not be the default picker and must not contain `Save for team picker` or duplicate default actions. + +### Create Team + +1. Resolve the effective OpenCode default from the project-scoped runtime catalog already loaded by Create Team. +2. Show the resolved model directly in the default option label. +3. Show an explicit-selection indicator when a non-empty model value is active. +4. Keep empty string as the launch contract for `use runtime default`. +5. Preserve legacy stored values, including `__default__` normalization. +6. If the saved explicit model is stale or unavailable, retain the existing fail-safe behavior and explain why it cannot be selected; do not silently reinterpret it as default before the catalog is authoritative. + +### Localization + +- Reuse existing keys where their meaning remains accurate. +- Add the minimum new keys for inheritance, clear override, picker destination, explicit choice, and Free Models Router advisory. +- Because catalog validation enforces source-locale key parity, add every new key to every configured locale or use an established generic key that already exists. +- Regenerate `resources.d.ts` with the project's i18n type command. +- Run catalog validation after changes. + +## Detailed implementation phases + +# Phase 0 - Preflight and ownership + +## Steps + +1. Recheck both git worktrees and current branches. +2. Record the exact uncommitted runtime files that overlap this change. +3. Create or switch to a correctly named feature branch only when implementation starts, following repository branch rules and without touching unrelated untracked files. +4. Confirm the desktop dev runtime points to `/Users/belief/dev/projects/claude/agent_teams_orchestrator/cli-source` for source verification. +5. Re-run the read-only `dev:mcp` reproduction to capture the before state if a fresh comparison is useful. + +## Exit criteria + +- Owned edit set is explicit. +- No unrelated dirty change will be reverted, staged, or reformatted. + +# Phase 1 - Runtime inheritance contract + +## Steps + +1. Implement atomic project preference clearing. +2. Add runtime domain/port/application method. +3. Add adapter behavior and view refresh. +4. Add both CLI entry paths. +5. Add focused runtime tests. + +## Exit criteria + +- Clearing a project override returns a view inherited from the base default. +- Model limits and global preferences are unchanged. +- No model probe is executed for clear. + +# Phase 2 - Desktop transport and neutral validation + +## Steps + +1. Add desktop contracts, IPC channel, bridge, facade, port, browser stub, and CLI client method. +2. Add neutral temporary project handling for base-default set. +3. Add defensive validation and cache invalidation. +4. Add focused main-process tests. + +## Exit criteria + +- Base set never uses the active user project as cwd/probe context. +- Project set and clear remain bound to the chosen project. +- Temporary directories are cleaned after success and failure. + +# Phase 3 - Pure renderer model and hook orchestration + +## Steps + +1. Add inheritance presentation projector. +2. Add scoped mutation projection helpers. +3. Extend hook state/actions for picker target and clear. +4. Separate global-vs-project stale response rules. +5. Preserve saved-but-refresh-failed warnings. +6. Add hook and projector tests before UI replacement. + +## Exit criteria + +- Renderer state always reports the correct effective model and source. +- Changing base does not visually replace a project override. +- Clearing override immediately resolves to base/default fallback. + +# Phase 4 - Inherited defaults UI and picker + +## Steps + +1. Extract the new defaults settings component. +2. Reuse the existing Providers tab and provider model list as the picker with a fixed-target banner. +3. Add one explicit `Test and use` provider-model action for the destination already selected in the inheritance card. +4. Remove hidden model-row writes and defaults-screen team-picker actions. +5. Add route-specific Free Models Router advisory. +6. Preserve local-model limits/diagnostics below the new primary UI where needed. +7. Add accessible labels, focus return, keyboard behavior, loading, empty, error, and cancellation states. + +## Exit criteria + +- `openrouter/openrouter/free` can be selected from the live OpenRouter model list and set as either base or project default. +- No standalone scope toggle or validation-context field remains. +- No model row click writes state. + +# Phase 5 - Create Team clarity + +## Steps + +1. Add a pure resolved-default label helper. +2. Render `Default - ` for OpenCode. +3. Render explicit-selection copy for non-empty values. +4. Add focused tests for default, project override, explicit saved model, pending catalog, and stale saved model. + +## Exit criteria + +- Users can tell whether Create Team will inherit the runtime default or launch an explicit model. +- Existing stored choices remain backward compatible. + +# Phase 6 - Verification and E2E + +Run focused verification first, then the Electron E2E. + +## Static and focused checks + +Desktop repository: + +```text +pnpm lint:fast:files -- +pnpm typecheck +pnpm vitest run +pnpm i18n:validate +pnpm guard:source-file-size +``` + +Runtime repository: + +```text +bun test +bun run typecheck (or the repository's available pinned typecheck command) +``` + +Do not run broad formatting or `pnpm lint:fix`. + +## Deterministic Electron E2E + +Use `pnpm dev:mcp` with: + +- a temporary `AGENT_TEAMS_ELECTRON_USER_DATA_DIR`; +- a new sandbox project directory; +- a temporary runtime-provider CLI fixture created outside the repository for this run only; +- CDP port 9222; +- no agent/team launch. + +Scenario: + +1. Open OpenCode Provider Settings -> Models. +2. Verify the base and project inheritance rows are visible. +3. Verify there is no `All projects / This project` segmented switch and no `Validation context` field. +4. Change the base default. +5. In the picker, select OpenRouter and search `openrouter/free`. +6. Verify `Free Models Router`, free/connected badges, and variable-model advisory. +7. Click `Test and use` and verify persisted base readout. +8. Set a different project override and verify it shadows the base. +9. Change the base again and verify the project remains unchanged. +10. Click `Use default` and verify the project inherits the new base. +11. Open Create Team for the sandbox project without launching it. +12. Verify the default option resolves to the inherited model. +13. Pick an explicit model and verify it is labelled as explicit. +14. Close dialogs, restart the isolated app if needed, and verify persisted base/project state. +15. Clean only the E2E-owned processes and temporary directories. + +Do not commit a general fake-runtime framework for this issue. Focused tests cover the CLI client contracts; the temporary fixture only supplies deterministic state for the one Electron E2E run. + +## Live OpenRouter compatibility smoke + +After deterministic E2E passes, run a read-only/live-provider smoke with the already connected OpenRouter account: + +1. Create a new sandbox project. +2. Start `dev:mcp` with the source runtime. +3. Open OpenRouter models and locate `openrouter/openrouter/free`. +4. Run `Test` in the sandbox context to confirm the real provider route remains executable. +5. Do not change the user's real base default. +6. Do not launch a team or agent. + +This separates UI/transport mutation proof from external provider availability and avoids modifying real global preferences. + +## Edge cases + +- No project selected: base default remains editable; project row offers a project chooser or explains that a project is required. +- Deleted/missing project: disable project mutation, keep base mutation available. +- Base default missing: show the effective OpenCode fallback and allow creating a managed base. +- Project override model absent from live catalog: keep the raw id visible with an unavailable advisory and allow `Use default`. +- Provider disconnects while picker is open: mutation fails closed, picker keeps selection and shows the runtime error. +- Route becomes unavailable after successful test but before refresh: persisted response is shown with refresh warning; later refresh may mark it unavailable. +- Project switches during project mutation: ignore stale response for the new project. +- Project switches during base mutation: complete the base mutation, refresh the newest project, and clear busy state. +- Picker closes during model search/test: cancel request group and ignore late results. +- Double click on `Test and use`: one mutation only. +- Refresh failure after successful write: show `saved, refresh needed`, never claim the write failed. +- App restart: derive inheritance from runtime preferences, not ephemeral renderer state. +- Legacy `__default__` Create Team value: normalize to inherited empty-string model as today. +- Existing explicit Create Team value: preserve and visibly label it. +- Free Models Router returns a different underlying model between probes: treat the route id as the persisted identity; advisory explains variability. + +## Rollback strategy + +- Renderer rollback: restore the old scope/configured-model panel while leaving the additive runtime clear command unused. +- Runtime rollback: the clear command is additive; older desktop builds do not call it. +- Contract compatibility: keep existing `setDefaultModel` unchanged. Add a new method/channel/CLI command rather than changing its request shape. +- Persistence compatibility: both old and new builds continue reading schema version 2 preferences with nullable `defaultModelId`. +- No migration script is required. + +## Verification result + +- Desktop typecheck, source-size guard, diff check, and focused lint completed with no errors. +- 284 focused desktop tests passed. +- 115 focused runtime managed-preference and adapter tests passed. +- Deterministic Electron `dev:mcp` covered base change, project override, base/override independence, `Use default`, persistence, Create Team labels, standalone `Test`, and 800x600 layout without launching a team. +- Live source-runtime smoke reported OpenRouter connected, found `openrouter/openrouter/free`, and returned `Model probe passed` in the sandbox project without changing the real default. +- New localization keys exist in every configured locale and generated types compile. The repository-wide localization validator remains red only on its existing unrelated dashboard/team parity backlog. +- The reconstructed runtime repository's broad TypeScript command remains red on its existing missing snapshot modules and unrelated legacy errors; the focused runtime slice tests and live `cli-source` execution passed. + +## Acceptance criteria + +1. A connected OpenRouter user can find and set `openrouter/openrouter/free` as the base OpenCode default. +2. The same route can be set as a project-only override. +3. The UI shows inheritance directly and contains no standalone scope toggle. +4. Base selection does not require a user-visible validation project. +5. Base probing runs in an app-owned neutral temporary directory. +6. Project probing runs in the selected project. +7. `Use default` removes the project override and later base changes flow through automatically. +8. Provider model rows have no hidden persistence behavior. +9. Create Team visibly distinguishes inherited default from explicit selection. +10. Failed probes do not persist defaults. +11. Project/context races cannot apply stale results to another project. +12. Focused tests, desktop typecheck, source-size guard, deterministic Electron E2E, and live OpenRouter test pass; repository-wide pre-existing i18n/runtime-typecheck baselines are recorded separately. +13. E2E does not launch agents or mutate real user-project defaults. diff --git a/src/features/localization/renderer/locales/ar/settings.json b/src/features/localization/renderer/locales/ar/settings.json index f6bfe7bc7f..713411f430 100644 --- a/src/features/localization/renderer/locales/ar/settings.json +++ b/src/features/localization/renderer/locales/ar/settings.json @@ -29,7 +29,7 @@ "test": "الاختبار" }, "defaults": { - "allProjects": "جميع المشاريع", + "allProjects": "النموذج الافتراضي", "allProjectsHint": "استخدام الاختبارات {{project}}- ينطبق التقصير ما لم يكن المشروع متجاوزا.", "loadingContexts": "...سياقات التحمل", "projectHint": "إنقاذ التجاوزات فقط {{project}}.", @@ -43,7 +43,20 @@ "setProjectDefault": "مخطط المشروع", "thisProject": "هذا المشروع", "title": "التخلف عن سداد الالتزامات المفتوحة", - "validationContext": "سياق التقييم" + "validationContext": "سياق التقييم", + "inheritanceDescription": "Projects use this model unless they choose another.", + "freeRouterAdvisory": "Chooses an available free model for each request. The underlying model may change.", + "change": "Change", + "useAnotherModel": "Use another model", + "inherits": "Uses default", + "override": "Project override", + "useDefault": "Use default", + "choosingAllProjects": "Choose a model for projects without an override.", + "choosingProject": "Choose a model only for {{project}}.", + "cancelChoosing": "Cancel", + "testAndUse": "Test and use", + "testAndUseAllProjects": "Test and use for all projects", + "testAndUseProject": "Test and use for this project" }, "diagnostics": { "copied": "أجهزة التشخيص", diff --git a/src/features/localization/renderer/locales/bn/settings.json b/src/features/localization/renderer/locales/bn/settings.json index 6eec443f7d..057590c78d 100644 --- a/src/features/localization/renderer/locales/bn/settings.json +++ b/src/features/localization/renderer/locales/bn/settings.json @@ -29,7 +29,7 @@ "test": "পরীক্ষা" }, "defaults": { - "allProjects": "সব প্রকল্প", + "allProjects": "ডিফল্ট মডেল", "allProjectsHint": "পরীক্ষা {{project}}কোনো প্রজেক্ট উপস্থিত না থাকলে ডিফল্ট মান প্রয়োগ করা হবে ।", "loadingContexts": "কনটেক্সট লোড করা হচ্ছে...", "projectHint": "শুধুমাত্র সংরক্ষণ করা শূণ্যস্থান {{project}}.", @@ -43,7 +43,20 @@ "setProjectDefault": "প্রজেক্ট ডিফল্ট মান নির্ধারণ করুন", "thisProject": "এই প্রজেক্ট", "title": "ডিফল্ট অঙ্ক", - "validationContext": "বৈধতা যাচাই" + "validationContext": "বৈধতা যাচাই", + "inheritanceDescription": "Projects use this model unless they choose another.", + "freeRouterAdvisory": "Chooses an available free model for each request. The underlying model may change.", + "change": "Change", + "useAnotherModel": "Use another model", + "inherits": "Uses default", + "override": "Project override", + "useDefault": "Use default", + "choosingAllProjects": "Choose a model for projects without an override.", + "choosingProject": "Choose a model only for {{project}}.", + "cancelChoosing": "Cancel", + "testAndUse": "Test and use", + "testAndUseAllProjects": "Test and use for all projects", + "testAndUseProject": "Test and use for this project" }, "diagnostics": { "copied": "ডায়াগনস্টিক কপি করেছেন", diff --git a/src/features/localization/renderer/locales/de/settings.json b/src/features/localization/renderer/locales/de/settings.json index f88429c18b..c9915e82e2 100644 --- a/src/features/localization/renderer/locales/de/settings.json +++ b/src/features/localization/renderer/locales/de/settings.json @@ -29,7 +29,7 @@ "test": "Prüfung" }, "defaults": { - "allProjects": "Alle Projekte", + "allProjects": "Standardmodell", "allProjectsHint": "Anwendung von Tests {{project}}. Default gilt, es sei denn, ein Projekt hat einen Override.", "loadingContexts": "Kontexte laden...", "projectHint": "Saving Overrides nur {{project}}.", @@ -43,7 +43,20 @@ "setProjectDefault": "Voreinstellung des Projekts", "thisProject": "Dieses Projekt", "title": "OpenCode-Standards", - "validationContext": "Validierungskontext" + "validationContext": "Validierungskontext", + "inheritanceDescription": "Projects use this model unless they choose another.", + "freeRouterAdvisory": "Chooses an available free model for each request. The underlying model may change.", + "change": "Change", + "useAnotherModel": "Use another model", + "inherits": "Uses default", + "override": "Project override", + "useDefault": "Use default", + "choosingAllProjects": "Choose a model for projects without an override.", + "choosingProject": "Choose a model only for {{project}}.", + "cancelChoosing": "Cancel", + "testAndUse": "Test and use", + "testAndUseAllProjects": "Test and use for all projects", + "testAndUseProject": "Test and use for this project" }, "diagnostics": { "copied": "Diagnostik kopiert", diff --git a/src/features/localization/renderer/locales/en/settings.json b/src/features/localization/renderer/locales/en/settings.json index a282db6fd9..b1462eb694 100644 --- a/src/features/localization/renderer/locales/en/settings.json +++ b/src/features/localization/renderer/locales/en/settings.json @@ -29,7 +29,7 @@ "test": "Test" }, "defaults": { - "allProjects": "All projects", + "allProjects": "Default model", "allProjectsHint": "Tests use {{project}}. Default applies unless a project has an override.", "loadingContexts": "Loading contexts...", "projectHint": "Saving overrides only {{project}}.", @@ -43,7 +43,20 @@ "setProjectDefault": "Set project default", "thisProject": "This project", "title": "OpenCode defaults", - "validationContext": "Validation context" + "validationContext": "Validation context", + "inheritanceDescription": "Projects use this model unless they choose another.", + "freeRouterAdvisory": "Chooses an available free model for each request. The underlying model may change.", + "change": "Change", + "useAnotherModel": "Use another model", + "inherits": "Uses default", + "override": "Project override", + "useDefault": "Use default", + "choosingAllProjects": "Choose a model for projects without an override.", + "choosingProject": "Choose a model only for {{project}}.", + "cancelChoosing": "Cancel", + "testAndUse": "Test and use", + "testAndUseAllProjects": "Test and use for all projects", + "testAndUseProject": "Test and use for this project" }, "diagnostics": { "copied": "Diagnostics copied", diff --git a/src/features/localization/renderer/locales/es/settings.json b/src/features/localization/renderer/locales/es/settings.json index 0a2d075feb..1367d68369 100644 --- a/src/features/localization/renderer/locales/es/settings.json +++ b/src/features/localization/renderer/locales/es/settings.json @@ -29,7 +29,7 @@ "test": "Prueba" }, "defaults": { - "allProjects": "Todos los proyectos", + "allProjects": "Modelo predeterminado", "allProjectsHint": "Las pruebas usan {{project}}. El incumplimiento se aplica a menos que un proyecto tenga una anulación.", "loadingContexts": "Cargando contextos...", "projectHint": "Salvar anula sólo {{project}}.", @@ -43,7 +43,20 @@ "setProjectDefault": "Establecer predeterminado del proyecto", "thisProject": "Este proyecto", "title": "Por defecto OpenCode", - "validationContext": "Contexto de validación" + "validationContext": "Contexto de validación", + "inheritanceDescription": "Projects use this model unless they choose another.", + "freeRouterAdvisory": "Chooses an available free model for each request. The underlying model may change.", + "change": "Change", + "useAnotherModel": "Use another model", + "inherits": "Uses default", + "override": "Project override", + "useDefault": "Use default", + "choosingAllProjects": "Choose a model for projects without an override.", + "choosingProject": "Choose a model only for {{project}}.", + "cancelChoosing": "Cancel", + "testAndUse": "Test and use", + "testAndUseAllProjects": "Test and use for all projects", + "testAndUseProject": "Test and use for this project" }, "diagnostics": { "copied": "Diagnósticos copiados", diff --git a/src/features/localization/renderer/locales/fa/settings.json b/src/features/localization/renderer/locales/fa/settings.json index 0de47f738a..86bad97a47 100644 --- a/src/features/localization/renderer/locales/fa/settings.json +++ b/src/features/localization/renderer/locales/fa/settings.json @@ -29,7 +29,7 @@ "test": "تست" }, "defaults": { - "allProjects": "همه پروژه‌ها", + "allProjects": "مدل پیش‌فرض", "allProjectsHint": "تست‌ها از {{project}} استفاده می‌کنند. پیش‌فرض اعمال می‌شود مگر آنکه پروژه‌ای بازنویسی داشته باشد.", "loadingContexts": "در حال بارگذاری بافت‌ها...", "projectHint": "ذخیره فقط {{project}} را بازنویسی می‌کند.", @@ -43,7 +43,20 @@ "setProjectDefault": "تنظیم پیش‌فرض پروژه", "thisProject": "این پروژه", "title": "پیش‌فرض‌های OpenCode", - "validationContext": "بافت اعتبارسنجی" + "validationContext": "بافت اعتبارسنجی", + "inheritanceDescription": "Projects use this model unless they choose another.", + "freeRouterAdvisory": "Chooses an available free model for each request. The underlying model may change.", + "change": "Change", + "useAnotherModel": "Use another model", + "inherits": "Uses default", + "override": "Project override", + "useDefault": "Use default", + "choosingAllProjects": "Choose a model for projects without an override.", + "choosingProject": "Choose a model only for {{project}}.", + "cancelChoosing": "Cancel", + "testAndUse": "Test and use", + "testAndUseAllProjects": "Test and use for all projects", + "testAndUseProject": "Test and use for this project" }, "diagnostics": { "copied": "عیب‌یابی کپی شد", diff --git a/src/features/localization/renderer/locales/fil/settings.json b/src/features/localization/renderer/locales/fil/settings.json index 3530eece80..7206926267 100644 --- a/src/features/localization/renderer/locales/fil/settings.json +++ b/src/features/localization/renderer/locales/fil/settings.json @@ -29,7 +29,7 @@ "test": "Subukan" }, "defaults": { - "allProjects": "Lahat ng proyekto", + "allProjects": "Default na modelo", "allProjectsHint": "Gumagamit ang mga test ng {{project}}. Nalalapat ang default maliban kung may override ang isang proyekto.", "loadingContexts": "Niloload ang mga context...", "projectHint": "Ino-override lang ng pag-save ang {{project}}.", @@ -43,7 +43,20 @@ "setProjectDefault": "Itakda ang default ng proyekto", "thisProject": "Proyektong ito", "title": "Mga OpenCode default", - "validationContext": "Validation context" + "validationContext": "Validation context", + "inheritanceDescription": "Projects use this model unless they choose another.", + "freeRouterAdvisory": "Chooses an available free model for each request. The underlying model may change.", + "change": "Change", + "useAnotherModel": "Use another model", + "inherits": "Uses default", + "override": "Project override", + "useDefault": "Use default", + "choosingAllProjects": "Choose a model for projects without an override.", + "choosingProject": "Choose a model only for {{project}}.", + "cancelChoosing": "Cancel", + "testAndUse": "Test and use", + "testAndUseAllProjects": "Test and use for all projects", + "testAndUseProject": "Test and use for this project" }, "diagnostics": { "copied": "Nakopya ang diagnostics", diff --git a/src/features/localization/renderer/locales/fr/settings.json b/src/features/localization/renderer/locales/fr/settings.json index f3b6532a17..a59d52b910 100644 --- a/src/features/localization/renderer/locales/fr/settings.json +++ b/src/features/localization/renderer/locales/fr/settings.json @@ -29,7 +29,7 @@ "test": "Essai" }, "defaults": { - "allProjects": "Tous les projets", + "allProjects": "Modèle par défaut", "allProjectsHint": "Utilisation des essais {{project}}. La valeur par défaut s'applique à moins qu'un projet ait une priorité.", "loadingContexts": "Chargement des contextes...", "projectHint": "Sauvegarder les dépassements seulement {{project}}.", @@ -43,7 +43,20 @@ "setProjectDefault": "Définir la valeur par défaut du projet", "thisProject": "Ce projet", "title": "Par défaut OpenCode", - "validationContext": "Contexte de validation" + "validationContext": "Contexte de validation", + "inheritanceDescription": "Projects use this model unless they choose another.", + "freeRouterAdvisory": "Chooses an available free model for each request. The underlying model may change.", + "change": "Change", + "useAnotherModel": "Use another model", + "inherits": "Uses default", + "override": "Project override", + "useDefault": "Use default", + "choosingAllProjects": "Choose a model for projects without an override.", + "choosingProject": "Choose a model only for {{project}}.", + "cancelChoosing": "Cancel", + "testAndUse": "Test and use", + "testAndUseAllProjects": "Test and use for all projects", + "testAndUseProject": "Test and use for this project" }, "diagnostics": { "copied": "Diagnostic copié", diff --git a/src/features/localization/renderer/locales/hi/settings.json b/src/features/localization/renderer/locales/hi/settings.json index 3b46c6e2c7..3301f6c027 100644 --- a/src/features/localization/renderer/locales/hi/settings.json +++ b/src/features/localization/renderer/locales/hi/settings.json @@ -29,7 +29,7 @@ "test": "टेस्ट" }, "defaults": { - "allProjects": "सभी परियोजनाएं", + "allProjects": "डिफ़ॉल्ट मॉडल", "allProjectsHint": "टेस्ट {{project}} का उपयोग करते हैं। डिफ़ॉल्ट तब तक लागू होता है जब तक किसी परियोजना में ओवरराइड नहीं होता।", "loadingContexts": "संदर्भ लोड हो रहा है...", "projectHint": "बचत ओवरराइड केवल {{project}}.", @@ -43,7 +43,20 @@ "setProjectDefault": "परियोजना डिफ़ॉल्ट सेट करें", "thisProject": "यह परियोजना", "title": "OpenCode डिफ़ॉल्ट", - "validationContext": "प्रमाणीकरण" + "validationContext": "प्रमाणीकरण", + "inheritanceDescription": "Projects use this model unless they choose another.", + "freeRouterAdvisory": "Chooses an available free model for each request. The underlying model may change.", + "change": "Change", + "useAnotherModel": "Use another model", + "inherits": "Uses default", + "override": "Project override", + "useDefault": "Use default", + "choosingAllProjects": "Choose a model for projects without an override.", + "choosingProject": "Choose a model only for {{project}}.", + "cancelChoosing": "Cancel", + "testAndUse": "Test and use", + "testAndUseAllProjects": "Test and use for all projects", + "testAndUseProject": "Test and use for this project" }, "diagnostics": { "copied": "निदान करना", diff --git a/src/features/localization/renderer/locales/id/settings.json b/src/features/localization/renderer/locales/id/settings.json index 8e733a3239..746c6dc703 100644 --- a/src/features/localization/renderer/locales/id/settings.json +++ b/src/features/localization/renderer/locales/id/settings.json @@ -29,7 +29,7 @@ "test": "Uji" }, "defaults": { - "allProjects": "Semua proyek", + "allProjects": "Model default", "allProjectsHint": "Penggunaan tes {{project}}Terapan baku kecuali sebuah projek memiliki sebuah override.", "loadingContexts": "Memuat konteks...", "projectHint": "Hanya menyimpan overrides {{project}}.", @@ -43,7 +43,20 @@ "setProjectDefault": "Set default projek", "thisProject": "Proyek ini", "title": "Default OpenCode", - "validationContext": "Konteks validasi" + "validationContext": "Konteks validasi", + "inheritanceDescription": "Projects use this model unless they choose another.", + "freeRouterAdvisory": "Chooses an available free model for each request. The underlying model may change.", + "change": "Change", + "useAnotherModel": "Use another model", + "inherits": "Uses default", + "override": "Project override", + "useDefault": "Use default", + "choosingAllProjects": "Choose a model for projects without an override.", + "choosingProject": "Choose a model only for {{project}}.", + "cancelChoosing": "Cancel", + "testAndUse": "Test and use", + "testAndUseAllProjects": "Test and use for all projects", + "testAndUseProject": "Test and use for this project" }, "diagnostics": { "copied": "Diagnostik disalin", diff --git a/src/features/localization/renderer/locales/it/settings.json b/src/features/localization/renderer/locales/it/settings.json index 0982466ff1..082a72ba2c 100644 --- a/src/features/localization/renderer/locales/it/settings.json +++ b/src/features/localization/renderer/locales/it/settings.json @@ -29,7 +29,7 @@ "test": "Testa" }, "defaults": { - "allProjects": "Tutti i progetti", + "allProjects": "Modello predefinito", "allProjectsHint": "I test usano {{project}}. L'impostazione predefinita si applica a meno che un progetto non abbia un override.", "loadingContexts": "Caricamento dei contesti...", "projectHint": "Il salvataggio sovrascrive solo {{project}}.", @@ -43,7 +43,20 @@ "setProjectDefault": "Imposta predefinito di progetto", "thisProject": "Questo progetto", "title": "Impostazioni predefinite OpenCode", - "validationContext": "Contesto di convalida" + "validationContext": "Contesto di convalida", + "inheritanceDescription": "Projects use this model unless they choose another.", + "freeRouterAdvisory": "Chooses an available free model for each request. The underlying model may change.", + "change": "Change", + "useAnotherModel": "Use another model", + "inherits": "Uses default", + "override": "Project override", + "useDefault": "Use default", + "choosingAllProjects": "Choose a model for projects without an override.", + "choosingProject": "Choose a model only for {{project}}.", + "cancelChoosing": "Cancel", + "testAndUse": "Test and use", + "testAndUseAllProjects": "Test and use for all projects", + "testAndUseProject": "Test and use for this project" }, "diagnostics": { "copied": "Diagnostica copiata", diff --git a/src/features/localization/renderer/locales/ja/settings.json b/src/features/localization/renderer/locales/ja/settings.json index a02a8d9f3b..fabfe8a702 100644 --- a/src/features/localization/renderer/locales/ja/settings.json +++ b/src/features/localization/renderer/locales/ja/settings.json @@ -29,7 +29,7 @@ "test": "テスト" }, "defaults": { - "allProjects": "すべてのプロジェクト", + "allProjects": "デフォルトモデル", "allProjectsHint": "試験は{{project}}を使用します。 プロジェクトがオーバーライドされていない限りデフォルトが適用されます。", "loadingContexts": "コンテキストをロードする...", "projectHint": "{{project}} のみオーバーライドを保存します。", @@ -43,7 +43,20 @@ "setProjectDefault": "プロジェクトのデフォルト設定", "thisProject": "プロジェクト", "title": "OpenCode のデフォルト", - "validationContext": "検証コンテキスト" + "validationContext": "検証コンテキスト", + "inheritanceDescription": "Projects use this model unless they choose another.", + "freeRouterAdvisory": "Chooses an available free model for each request. The underlying model may change.", + "change": "Change", + "useAnotherModel": "Use another model", + "inherits": "Uses default", + "override": "Project override", + "useDefault": "Use default", + "choosingAllProjects": "Choose a model for projects without an override.", + "choosingProject": "Choose a model only for {{project}}.", + "cancelChoosing": "Cancel", + "testAndUse": "Test and use", + "testAndUseAllProjects": "Test and use for all projects", + "testAndUseProject": "Test and use for this project" }, "diagnostics": { "copied": "コピーされた診断", diff --git a/src/features/localization/renderer/locales/ko/settings.json b/src/features/localization/renderer/locales/ko/settings.json index 8d41c962b8..e3977656a6 100644 --- a/src/features/localization/renderer/locales/ko/settings.json +++ b/src/features/localization/renderer/locales/ko/settings.json @@ -29,7 +29,7 @@ "test": "제품정보" }, "defaults": { - "allProjects": "모든 프로젝트", + "allProjects": "기본 모델", "allProjectsHint": "테스트 사용 {{project}}. Default는 프로젝트가 override가 없는 경우에 적용됩니다.", "loadingContexts": "로딩...", "projectHint": "저장 overrides만 {{project}}.", @@ -43,7 +43,20 @@ "setProjectDefault": "프로젝트 기본값 설정", "thisProject": "이 프로젝트", "title": "OpenCode 기본값", - "validationContext": "유효성 검사" + "validationContext": "유효성 검사", + "inheritanceDescription": "Projects use this model unless they choose another.", + "freeRouterAdvisory": "Chooses an available free model for each request. The underlying model may change.", + "change": "Change", + "useAnotherModel": "Use another model", + "inherits": "Uses default", + "override": "Project override", + "useDefault": "Use default", + "choosingAllProjects": "Choose a model for projects without an override.", + "choosingProject": "Choose a model only for {{project}}.", + "cancelChoosing": "Cancel", + "testAndUse": "Test and use", + "testAndUseAllProjects": "Test and use for all projects", + "testAndUseProject": "Test and use for this project" }, "diagnostics": { "copied": "진단", diff --git a/src/features/localization/renderer/locales/mr/settings.json b/src/features/localization/renderer/locales/mr/settings.json index 7109a12d23..b1960e8579 100644 --- a/src/features/localization/renderer/locales/mr/settings.json +++ b/src/features/localization/renderer/locales/mr/settings.json @@ -29,7 +29,7 @@ "test": "चाचणी करा" }, "defaults": { - "allProjects": "सर्व प्रकल्प", + "allProjects": "डीफॉल्ट मॉडेल", "allProjectsHint": "चाचण्या {{project}} वापरतात. प्रकल्पात ओव्हरराइड नसल्यास डीफॉल्ट लागू होतो.", "loadingContexts": "संदर्भ लोड होत आहेत...", "projectHint": "जतन केल्याने फक्त {{project}} ओव्हरराइड होतो.", @@ -43,7 +43,20 @@ "setProjectDefault": "प्रकल्प डीफॉल्ट सेट करा", "thisProject": "हा प्रकल्प", "title": "OpenCode डीफॉल्ट्स", - "validationContext": "पडताळणी संदर्भ" + "validationContext": "पडताळणी संदर्भ", + "inheritanceDescription": "Projects use this model unless they choose another.", + "freeRouterAdvisory": "Chooses an available free model for each request. The underlying model may change.", + "change": "Change", + "useAnotherModel": "Use another model", + "inherits": "Uses default", + "override": "Project override", + "useDefault": "Use default", + "choosingAllProjects": "Choose a model for projects without an override.", + "choosingProject": "Choose a model only for {{project}}.", + "cancelChoosing": "Cancel", + "testAndUse": "Test and use", + "testAndUseAllProjects": "Test and use for all projects", + "testAndUseProject": "Test and use for this project" }, "diagnostics": { "copied": "डायग्नॉस्टिक्स कॉपी केले", diff --git a/src/features/localization/renderer/locales/ms/settings.json b/src/features/localization/renderer/locales/ms/settings.json index 5f6822f1ec..e954ce6de6 100644 --- a/src/features/localization/renderer/locales/ms/settings.json +++ b/src/features/localization/renderer/locales/ms/settings.json @@ -29,7 +29,7 @@ "test": "Uji" }, "defaults": { - "allProjects": "Semua projek", + "allProjects": "Model lalai", "allProjectsHint": "Ujian menggunakan {{project}}. Lalai terpakai melainkan projek mempunyai penggantian.", "loadingContexts": "Memuatkan konteks...", "projectHint": "Menyimpan hanya menggantikan {{project}}.", @@ -43,7 +43,20 @@ "setProjectDefault": "Tetapkan lalai projek", "thisProject": "Projek ini", "title": "Lalai OpenCode", - "validationContext": "Konteks pengesahan" + "validationContext": "Konteks pengesahan", + "inheritanceDescription": "Projects use this model unless they choose another.", + "freeRouterAdvisory": "Chooses an available free model for each request. The underlying model may change.", + "change": "Change", + "useAnotherModel": "Use another model", + "inherits": "Uses default", + "override": "Project override", + "useDefault": "Use default", + "choosingAllProjects": "Choose a model for projects without an override.", + "choosingProject": "Choose a model only for {{project}}.", + "cancelChoosing": "Cancel", + "testAndUse": "Test and use", + "testAndUseAllProjects": "Test and use for all projects", + "testAndUseProject": "Test and use for this project" }, "diagnostics": { "copied": "Diagnostik disalin", diff --git a/src/features/localization/renderer/locales/nl/settings.json b/src/features/localization/renderer/locales/nl/settings.json index d3a682ce22..1501e9695a 100644 --- a/src/features/localization/renderer/locales/nl/settings.json +++ b/src/features/localization/renderer/locales/nl/settings.json @@ -29,7 +29,7 @@ "test": "Testen" }, "defaults": { - "allProjects": "Alle projecten", + "allProjects": "Standaardmodel", "allProjectsHint": "Tests gebruiken {{project}}. De standaard geldt tenzij een project een override heeft.", "loadingContexts": "Contexten laden...", "projectHint": "Opslaan overschrijft alleen {{project}}.", @@ -43,7 +43,20 @@ "setProjectDefault": "Projectstandaard instellen", "thisProject": "Dit project", "title": "OpenCode-standaarden", - "validationContext": "Validatiecontext" + "validationContext": "Validatiecontext", + "inheritanceDescription": "Projects use this model unless they choose another.", + "freeRouterAdvisory": "Chooses an available free model for each request. The underlying model may change.", + "change": "Change", + "useAnotherModel": "Use another model", + "inherits": "Uses default", + "override": "Project override", + "useDefault": "Use default", + "choosingAllProjects": "Choose a model for projects without an override.", + "choosingProject": "Choose a model only for {{project}}.", + "cancelChoosing": "Cancel", + "testAndUse": "Test and use", + "testAndUseAllProjects": "Test and use for all projects", + "testAndUseProject": "Test and use for this project" }, "diagnostics": { "copied": "Diagnostiek gekopieerd", diff --git a/src/features/localization/renderer/locales/pl/settings.json b/src/features/localization/renderer/locales/pl/settings.json index c5d38ab2b1..32ebacac3d 100644 --- a/src/features/localization/renderer/locales/pl/settings.json +++ b/src/features/localization/renderer/locales/pl/settings.json @@ -29,7 +29,7 @@ "test": "Testuj" }, "defaults": { - "allProjects": "Wszystkie projekty", + "allProjects": "Model domyślny", "allProjectsHint": "Testy używają {{project}}. Domyślne obowiązuje, chyba że projekt ma nadpisanie.", "loadingContexts": "Ładowanie kontekstów...", "projectHint": "Zapis nadpisuje tylko {{project}}.", @@ -43,7 +43,20 @@ "setProjectDefault": "Ustaw domyślne dla projektu", "thisProject": "Ten projekt", "title": "Domyślne OpenCode", - "validationContext": "Kontekst walidacji" + "validationContext": "Kontekst walidacji", + "inheritanceDescription": "Projects use this model unless they choose another.", + "freeRouterAdvisory": "Chooses an available free model for each request. The underlying model may change.", + "change": "Change", + "useAnotherModel": "Use another model", + "inherits": "Uses default", + "override": "Project override", + "useDefault": "Use default", + "choosingAllProjects": "Choose a model for projects without an override.", + "choosingProject": "Choose a model only for {{project}}.", + "cancelChoosing": "Cancel", + "testAndUse": "Test and use", + "testAndUseAllProjects": "Test and use for all projects", + "testAndUseProject": "Test and use for this project" }, "diagnostics": { "copied": "Skopiowano diagnostykę", diff --git a/src/features/localization/renderer/locales/pt/settings.json b/src/features/localization/renderer/locales/pt/settings.json index 8f820fb122..07f73737ad 100644 --- a/src/features/localization/renderer/locales/pt/settings.json +++ b/src/features/localization/renderer/locales/pt/settings.json @@ -29,7 +29,7 @@ "test": "Ensaio" }, "defaults": { - "allProjects": "Todos os projectos", + "allProjects": "Modelo predefinido", "allProjectsHint": "Os testes utilizam {{project}}. O padrão aplica- se a menos que um projeto tenha uma sobreposição.", "loadingContexts": "Carregando contextos...", "projectHint": "A poupança substitui apenas o {{project}}.", @@ -43,7 +43,20 @@ "setProjectDefault": "Definir o projecto por omissão", "thisProject": "Este projecto", "title": "Por omissão do OpenCode", - "validationContext": "Contexto de validação" + "validationContext": "Contexto de validação", + "inheritanceDescription": "Projects use this model unless they choose another.", + "freeRouterAdvisory": "Chooses an available free model for each request. The underlying model may change.", + "change": "Change", + "useAnotherModel": "Use another model", + "inherits": "Uses default", + "override": "Project override", + "useDefault": "Use default", + "choosingAllProjects": "Choose a model for projects without an override.", + "choosingProject": "Choose a model only for {{project}}.", + "cancelChoosing": "Cancel", + "testAndUse": "Test and use", + "testAndUseAllProjects": "Test and use for all projects", + "testAndUseProject": "Test and use for this project" }, "diagnostics": { "copied": "Diagnóstico copiado", diff --git a/src/features/localization/renderer/locales/ro/settings.json b/src/features/localization/renderer/locales/ro/settings.json index 3754caff69..c9eca1f61d 100644 --- a/src/features/localization/renderer/locales/ro/settings.json +++ b/src/features/localization/renderer/locales/ro/settings.json @@ -29,7 +29,7 @@ "test": "Testează" }, "defaults": { - "allProjects": "Toate proiectele", + "allProjects": "Model implicit", "allProjectsHint": "Testele folosesc {{project}}. Valoarea implicită se aplică dacă un proiect nu are o suprascriere.", "loadingContexts": "Se încarcă contextele...", "projectHint": "Salvarea suprascrie doar {{project}}.", @@ -43,7 +43,20 @@ "setProjectDefault": "Setează implicit pentru proiect", "thisProject": "Acest proiect", "title": "Valori implicite OpenCode", - "validationContext": "Context de validare" + "validationContext": "Context de validare", + "inheritanceDescription": "Projects use this model unless they choose another.", + "freeRouterAdvisory": "Chooses an available free model for each request. The underlying model may change.", + "change": "Change", + "useAnotherModel": "Use another model", + "inherits": "Uses default", + "override": "Project override", + "useDefault": "Use default", + "choosingAllProjects": "Choose a model for projects without an override.", + "choosingProject": "Choose a model only for {{project}}.", + "cancelChoosing": "Cancel", + "testAndUse": "Test and use", + "testAndUseAllProjects": "Test and use for all projects", + "testAndUseProject": "Test and use for this project" }, "diagnostics": { "copied": "Diagnostice copiate", diff --git a/src/features/localization/renderer/locales/ru/settings.json b/src/features/localization/renderer/locales/ru/settings.json index 3bb64b3b5d..30d4fc3ee0 100644 --- a/src/features/localization/renderer/locales/ru/settings.json +++ b/src/features/localization/renderer/locales/ru/settings.json @@ -29,7 +29,7 @@ "test": "Тест" }, "defaults": { - "allProjects": "Все проекты", + "allProjects": "Модель по умолчанию", "allProjectsHint": "Тесты используют {{project}}. Default применяется, если у проекта нет своего override.", "loadingContexts": "Загрузка contexts...", "projectHint": "Сохранение изменит override только для {{project}}.", @@ -43,7 +43,20 @@ "setProjectDefault": "Задать default для проекта", "thisProject": "Этот проект", "title": "OpenCode defaults", - "validationContext": "Validation context" + "validationContext": "Validation context", + "inheritanceDescription": "Проекты используют эту модель, если для них не выбрана другая.", + "freeRouterAdvisory": "Для каждого запроса выбирает доступную бесплатную модель. Конкретная модель может меняться.", + "change": "Изменить", + "useAnotherModel": "Использовать другую модель", + "inherits": "Использует общую", + "override": "Переопределение проекта", + "useDefault": "Использовать общую", + "choosingAllProjects": "Выберите модель для проектов без переопределения.", + "choosingProject": "Выберите модель только для {{project}}.", + "cancelChoosing": "Отмена", + "testAndUse": "Проверить и использовать", + "testAndUseAllProjects": "Проверить и использовать для всех проектов", + "testAndUseProject": "Проверить и использовать для этого проекта" }, "diagnostics": { "copied": "Diagnostics скопированы", diff --git a/src/features/localization/renderer/locales/sw/settings.json b/src/features/localization/renderer/locales/sw/settings.json index 12b949859a..65414ad455 100644 --- a/src/features/localization/renderer/locales/sw/settings.json +++ b/src/features/localization/renderer/locales/sw/settings.json @@ -29,7 +29,7 @@ "test": "Jaribu" }, "defaults": { - "allProjects": "Miradi yote", + "allProjects": "Muundo chaguo-msingi", "allProjectsHint": "Majaribio hutumia {{project}}. Chaguo-msingi hutumika isipokuwa mradi una ubatilishaji.", "loadingContexts": "Inapakia miktadha...", "projectHint": "Kuhifadhi kunabatilisha {{project}} pekee.", @@ -43,7 +43,20 @@ "setProjectDefault": "Weka chaguo-msingi cha mradi", "thisProject": "Mradi huu", "title": "Chaguo-msingi za OpenCode", - "validationContext": "Muktadha wa uthibitishaji" + "validationContext": "Muktadha wa uthibitishaji", + "inheritanceDescription": "Projects use this model unless they choose another.", + "freeRouterAdvisory": "Chooses an available free model for each request. The underlying model may change.", + "change": "Change", + "useAnotherModel": "Use another model", + "inherits": "Uses default", + "override": "Project override", + "useDefault": "Use default", + "choosingAllProjects": "Choose a model for projects without an override.", + "choosingProject": "Choose a model only for {{project}}.", + "cancelChoosing": "Cancel", + "testAndUse": "Test and use", + "testAndUseAllProjects": "Test and use for all projects", + "testAndUseProject": "Test and use for this project" }, "diagnostics": { "copied": "Utambuzi umenakiliwa", diff --git a/src/features/localization/renderer/locales/ta/settings.json b/src/features/localization/renderer/locales/ta/settings.json index 6fc2597077..003e33a7da 100644 --- a/src/features/localization/renderer/locales/ta/settings.json +++ b/src/features/localization/renderer/locales/ta/settings.json @@ -29,7 +29,7 @@ "test": "சோதி" }, "defaults": { - "allProjects": "அனைத்து projects", + "allProjects": "இயல்புநிலை மாடல்", "allProjectsHint": "சோதனைகள் {{project}} ஐப் பயன்படுத்துகின்றன. ஒரு project இல் override இல்லாவிட்டால் இயல்புநிலை பொருந்தும்.", "loadingContexts": "contexts ஐ ஏற்றுகிறது...", "projectHint": "சேமித்தல் {{project}} ஐ மட்டும் override செய்கிறது.", @@ -43,7 +43,20 @@ "setProjectDefault": "project இயல்புநிலையை அமை", "thisProject": "இந்த project", "title": "OpenCode இயல்புநிலைகள்", - "validationContext": "சரிபார்ப்பு context" + "validationContext": "சரிபார்ப்பு context", + "inheritanceDescription": "Projects use this model unless they choose another.", + "freeRouterAdvisory": "Chooses an available free model for each request. The underlying model may change.", + "change": "Change", + "useAnotherModel": "Use another model", + "inherits": "Uses default", + "override": "Project override", + "useDefault": "Use default", + "choosingAllProjects": "Choose a model for projects without an override.", + "choosingProject": "Choose a model only for {{project}}.", + "cancelChoosing": "Cancel", + "testAndUse": "Test and use", + "testAndUseAllProjects": "Test and use for all projects", + "testAndUseProject": "Test and use for this project" }, "diagnostics": { "copied": "Diagnostics நகலெடுக்கப்பட்டது", diff --git a/src/features/localization/renderer/locales/te/settings.json b/src/features/localization/renderer/locales/te/settings.json index 5d2755a795..cc9807d726 100644 --- a/src/features/localization/renderer/locales/te/settings.json +++ b/src/features/localization/renderer/locales/te/settings.json @@ -29,7 +29,7 @@ "test": "పరీక్షించు" }, "defaults": { - "allProjects": "అన్ని ప్రాజెక్ట్‌లు", + "allProjects": "డిఫాల్ట్ మోడల్", "allProjectsHint": "పరీక్షలు {{project}} ను ఉపయోగిస్తాయి. ప్రాజెక్ట్‌కు ఓవర్‌రైడ్ లేకపోతే డిఫాల్ట్ వర్తిస్తుంది.", "loadingContexts": "కాంటెక్స్ట్‌లను లోడ్ చేస్తోంది...", "projectHint": "సేవ్ చేయడం {{project}} ను మాత్రమే ఓవర్‌రైడ్ చేస్తుంది.", @@ -43,7 +43,20 @@ "setProjectDefault": "ప్రాజెక్ట్ డిఫాల్ట్‌ను సెట్ చేయి", "thisProject": "ఈ ప్రాజెక్ట్", "title": "OpenCode డిఫాల్ట్‌లు", - "validationContext": "ధృవీకరణ కాంటెక్స్ట్" + "validationContext": "ధృవీకరణ కాంటెక్స్ట్", + "inheritanceDescription": "Projects use this model unless they choose another.", + "freeRouterAdvisory": "Chooses an available free model for each request. The underlying model may change.", + "change": "Change", + "useAnotherModel": "Use another model", + "inherits": "Uses default", + "override": "Project override", + "useDefault": "Use default", + "choosingAllProjects": "Choose a model for projects without an override.", + "choosingProject": "Choose a model only for {{project}}.", + "cancelChoosing": "Cancel", + "testAndUse": "Test and use", + "testAndUseAllProjects": "Test and use for all projects", + "testAndUseProject": "Test and use for this project" }, "diagnostics": { "copied": "డయాగ్నొస్టిక్‌లు కాపీ చేయబడ్డాయి", diff --git a/src/features/localization/renderer/locales/th/settings.json b/src/features/localization/renderer/locales/th/settings.json index 8f2bca7272..49b37b08bd 100644 --- a/src/features/localization/renderer/locales/th/settings.json +++ b/src/features/localization/renderer/locales/th/settings.json @@ -29,7 +29,7 @@ "test": "ทดสอบ" }, "defaults": { - "allProjects": "ทุกโปรเจกต์", + "allProjects": "โมเดลเริ่มต้น", "allProjectsHint": "การทดสอบใช้ {{project}} ค่าเริ่มต้นจะใช้เว้นแต่โปรเจกต์มีการแทนที่", "loadingContexts": "กำลังโหลดบริบท...", "projectHint": "การบันทึกจะแทนที่เฉพาะ {{project}} เท่านั้น", @@ -43,7 +43,20 @@ "setProjectDefault": "ตั้งค่าเริ่มต้นโปรเจกต์", "thisProject": "โปรเจกต์นี้", "title": "ค่าเริ่มต้น OpenCode", - "validationContext": "บริบทการตรวจสอบ" + "validationContext": "บริบทการตรวจสอบ", + "inheritanceDescription": "Projects use this model unless they choose another.", + "freeRouterAdvisory": "Chooses an available free model for each request. The underlying model may change.", + "change": "Change", + "useAnotherModel": "Use another model", + "inherits": "Uses default", + "override": "Project override", + "useDefault": "Use default", + "choosingAllProjects": "Choose a model for projects without an override.", + "choosingProject": "Choose a model only for {{project}}.", + "cancelChoosing": "Cancel", + "testAndUse": "Test and use", + "testAndUseAllProjects": "Test and use for all projects", + "testAndUseProject": "Test and use for this project" }, "diagnostics": { "copied": "คัดลอกการวินิจฉัยแล้ว", diff --git a/src/features/localization/renderer/locales/tr/settings.json b/src/features/localization/renderer/locales/tr/settings.json index 60f385fa4c..1b18425e65 100644 --- a/src/features/localization/renderer/locales/tr/settings.json +++ b/src/features/localization/renderer/locales/tr/settings.json @@ -29,7 +29,7 @@ "test": "Test et" }, "defaults": { - "allProjects": "Tüm projeler", + "allProjects": "Varsayılan model", "allProjectsHint": "Testler {{project}} kullanır. Bir projenin geçersiz kılması olmadıkça varsayılan geçerlidir.", "loadingContexts": "Bağlamlar yükleniyor...", "projectHint": "Kaydetme yalnızca {{project}} öğesini geçersiz kılar.", @@ -43,7 +43,20 @@ "setProjectDefault": "Proje varsayılanını ayarla", "thisProject": "Bu proje", "title": "OpenCode varsayılanları", - "validationContext": "Doğrulama bağlamı" + "validationContext": "Doğrulama bağlamı", + "inheritanceDescription": "Projects use this model unless they choose another.", + "freeRouterAdvisory": "Chooses an available free model for each request. The underlying model may change.", + "change": "Change", + "useAnotherModel": "Use another model", + "inherits": "Uses default", + "override": "Project override", + "useDefault": "Use default", + "choosingAllProjects": "Choose a model for projects without an override.", + "choosingProject": "Choose a model only for {{project}}.", + "cancelChoosing": "Cancel", + "testAndUse": "Test and use", + "testAndUseAllProjects": "Test and use for all projects", + "testAndUseProject": "Test and use for this project" }, "diagnostics": { "copied": "Tanılama kopyalandı", diff --git a/src/features/localization/renderer/locales/uk/settings.json b/src/features/localization/renderer/locales/uk/settings.json index d91429d4dd..6a522e2c83 100644 --- a/src/features/localization/renderer/locales/uk/settings.json +++ b/src/features/localization/renderer/locales/uk/settings.json @@ -29,7 +29,7 @@ "test": "Тест" }, "defaults": { - "allProjects": "Усі проєкти", + "allProjects": "Модель за замовчуванням", "allProjectsHint": "Тести використовують {{project}}. За замовчуванням застосовується, якщо проєкт не має перевизначення.", "loadingContexts": "Завантаження контекстів...", "projectHint": "Збереження перевизначає лише {{project}}.", @@ -43,7 +43,20 @@ "setProjectDefault": "Встановити за замовчуванням для проєкту", "thisProject": "Цей проєкт", "title": "Значення за замовчуванням OpenCode", - "validationContext": "Контекст перевірки" + "validationContext": "Контекст перевірки", + "inheritanceDescription": "Проєкти використовують цю модель, якщо для них не вибрано іншу.", + "freeRouterAdvisory": "Для кожного запиту вибирає доступну безкоштовну модель. Конкретна модель може змінюватися.", + "change": "Змінити", + "useAnotherModel": "Використати іншу модель", + "inherits": "Використовує спільну", + "override": "Перевизначення проєкту", + "useDefault": "Використовувати спільну", + "choosingAllProjects": "Виберіть модель для проєктів без перевизначення.", + "choosingProject": "Виберіть модель лише для {{project}}.", + "cancelChoosing": "Скасувати", + "testAndUse": "Перевірити та використати", + "testAndUseAllProjects": "Перевірити й використовувати для всіх проєктів", + "testAndUseProject": "Перевірити й використовувати для цього проєкту" }, "diagnostics": { "copied": "Діагностику скопійовано", diff --git a/src/features/localization/renderer/locales/ur/settings.json b/src/features/localization/renderer/locales/ur/settings.json index cda1713afc..bdeddda5b6 100644 --- a/src/features/localization/renderer/locales/ur/settings.json +++ b/src/features/localization/renderer/locales/ur/settings.json @@ -29,7 +29,7 @@ "test": "جانچ" }, "defaults": { - "allProjects": "تمام منصوبوں", + "allProjects": "ڈیفالٹ ماڈل", "allProjectsHint": "ٹیسٹ کا استعمال {{project}}. . . .", "loadingContexts": "تاثرات لوڈ کریں...", "projectHint": "صرف بچوں کو محفوظ رکھنا {{project}}.", @@ -43,7 +43,20 @@ "setProjectDefault": "طے شدہ منصوبہ بندی", "thisProject": "یہ منصوبہ", "title": "کھولیں", - "validationContext": "سیاق‌وسباق" + "validationContext": "سیاق‌وسباق", + "inheritanceDescription": "Projects use this model unless they choose another.", + "freeRouterAdvisory": "Chooses an available free model for each request. The underlying model may change.", + "change": "Change", + "useAnotherModel": "Use another model", + "inherits": "Uses default", + "override": "Project override", + "useDefault": "Use default", + "choosingAllProjects": "Choose a model for projects without an override.", + "choosingProject": "Choose a model only for {{project}}.", + "cancelChoosing": "Cancel", + "testAndUse": "Test and use", + "testAndUseAllProjects": "Test and use for all projects", + "testAndUseProject": "Test and use for this project" }, "diagnostics": { "copied": "روزنامہیات نقل کرتے ہیں۔", diff --git a/src/features/localization/renderer/locales/vi/settings.json b/src/features/localization/renderer/locales/vi/settings.json index 999e69cb15..54edc3bbfb 100644 --- a/src/features/localization/renderer/locales/vi/settings.json +++ b/src/features/localization/renderer/locales/vi/settings.json @@ -29,7 +29,7 @@ "test": "Kiểm thử" }, "defaults": { - "allProjects": "Tất cả dự án", + "allProjects": "Mô hình mặc định", "allProjectsHint": "Kiểm thử sử dụng {{project}}. Mặc định áp dụng trừ khi một dự án có ghi đè riêng.", "loadingContexts": "Đang tải ngữ cảnh...", "projectHint": "Việc lưu chỉ ghi đè {{project}}.", @@ -43,7 +43,20 @@ "setProjectDefault": "Đặt mặc định cho dự án", "thisProject": "Dự án này", "title": "Mặc định OpenCode", - "validationContext": "Ngữ cảnh xác thực" + "validationContext": "Ngữ cảnh xác thực", + "inheritanceDescription": "Projects use this model unless they choose another.", + "freeRouterAdvisory": "Chooses an available free model for each request. The underlying model may change.", + "change": "Change", + "useAnotherModel": "Use another model", + "inherits": "Uses default", + "override": "Project override", + "useDefault": "Use default", + "choosingAllProjects": "Choose a model for projects without an override.", + "choosingProject": "Choose a model only for {{project}}.", + "cancelChoosing": "Cancel", + "testAndUse": "Test and use", + "testAndUseAllProjects": "Test and use for all projects", + "testAndUseProject": "Test and use for this project" }, "diagnostics": { "copied": "Đã sao chép chẩn đoán", diff --git a/src/features/localization/renderer/locales/zh/settings.json b/src/features/localization/renderer/locales/zh/settings.json index 1e7876e69b..ef5ee39224 100644 --- a/src/features/localization/renderer/locales/zh/settings.json +++ b/src/features/localization/renderer/locales/zh/settings.json @@ -29,7 +29,7 @@ "test": "测试" }, "defaults": { - "allProjects": "所有项目", + "allProjects": "默认模型", "allProjectsHint": "测试使用 {{project}}。除非项目有覆盖,否则应用默认值。", "loadingContexts": "正在加载上下文…", "projectHint": "保存仅覆盖 {{project}}。", @@ -43,7 +43,20 @@ "setProjectDefault": "设置项目默认值", "thisProject": "此项目", "title": "OpenCode 默认值", - "validationContext": "验证上下文" + "validationContext": "验证上下文", + "inheritanceDescription": "Projects use this model unless they choose another.", + "freeRouterAdvisory": "Chooses an available free model for each request. The underlying model may change.", + "change": "Change", + "useAnotherModel": "Use another model", + "inherits": "Uses default", + "override": "Project override", + "useDefault": "Use default", + "choosingAllProjects": "Choose a model for projects without an override.", + "choosingProject": "Choose a model only for {{project}}.", + "cancelChoosing": "Cancel", + "testAndUse": "Test and use", + "testAndUseAllProjects": "Test and use for all projects", + "testAndUseProject": "Test and use for this project" }, "diagnostics": { "copied": "已复制诊断信息", diff --git a/src/features/localization/renderer/resources.d.ts b/src/features/localization/renderer/resources.d.ts index ee892894a8..9be8cd4aa0 100644 --- a/src/features/localization/renderer/resources.d.ts +++ b/src/features/localization/renderer/resources.d.ts @@ -3189,9 +3189,17 @@ export default interface Resources { baseUrlPlaceholder: 'http://localhost:1234'; }; defaults: { - allProjects: 'All projects'; + allProjects: 'Default model'; allProjectsHint: 'Tests use {{project}}. Default applies unless a project has an override.'; + cancelChoosing: 'Cancel'; + change: 'Change'; + choosingAllProjects: 'Choose a model for projects without an override.'; + choosingProject: 'Choose a model only for {{project}}.'; + freeRouterAdvisory: 'Chooses an available free model for each request. The underlying model may change.'; + inheritanceDescription: 'Projects use this model unless they choose another.'; + inherits: 'Uses default'; loadingContexts: 'Loading contexts...'; + override: 'Project override'; projectHint: 'Saving overrides only {{project}}.'; projectOverrideContext: 'Project override context'; scopeDescriptionAllProjects: 'Default for every project that does not have its own OpenCode override.'; @@ -3201,8 +3209,13 @@ export default interface Resources { selectValidationContext: 'Select validation context'; setAllProjectsDefault: 'Set all-projects default'; setProjectDefault: 'Set project default'; + testAndUse: 'Test and use'; + testAndUseAllProjects: 'Test and use for all projects'; + testAndUseProject: 'Test and use for this project'; thisProject: 'This project'; title: 'OpenCode defaults'; + useAnotherModel: 'Use another model'; + useDefault: 'Use default'; validationContext: 'Validation context'; }; diagnostics: { diff --git a/src/features/runtime-provider-management/renderer/hooks/runtimeProviderManagementPresentation.ts b/src/features/runtime-provider-management/renderer/hooks/runtimeProviderManagementPresentation.ts new file mode 100644 index 0000000000..f8257f95e3 --- /dev/null +++ b/src/features/runtime-provider-management/renderer/hooks/runtimeProviderManagementPresentation.ts @@ -0,0 +1,156 @@ +import { getOpenCodeSourceDisplayName } from '@shared/utils/opencodeModelRef'; + +import type { + RuntimeProviderConnectionDto, + RuntimeProviderDirectoryEntryDto, + RuntimeProviderManagementErrorDto, + RuntimeProviderManagementViewDto, + RuntimeProviderOAuthProgressDto, + RuntimeProviderSetupFormDto, +} from '@features/runtime-provider-management/contracts'; + +export function presentProviderConnection( + provider: RuntimeProviderConnectionDto +): RuntimeProviderConnectionDto { + const displayName = getOpenCodeSourceDisplayName(provider.providerId, provider.displayName); + return displayName === provider.displayName ? provider : { ...provider, displayName }; +} + +export function presentDirectoryEntry( + provider: RuntimeProviderDirectoryEntryDto +): RuntimeProviderDirectoryEntryDto { + const displayName = getOpenCodeSourceDisplayName(provider.providerId, provider.displayName); + return displayName === provider.displayName ? provider : { ...provider, displayName }; +} + +export function presentManagementView( + view: RuntimeProviderManagementViewDto | null +): RuntimeProviderManagementViewDto | null { + if (!view) return null; + return { ...view, providers: view.providers.map(presentProviderConnection) }; +} + +function replaceProviderNameInText( + value: string | null, + currentDisplayName: string, + presentedDisplayName: string +): string | null { + if (!value || currentDisplayName === presentedDisplayName) return value; + return value.replace(currentDisplayName, presentedDisplayName); +} + +export function presentSetupForm( + form: RuntimeProviderSetupFormDto | null +): RuntimeProviderSetupFormDto | null { + if (!form) return null; + const displayName = + form.providerId.trim().toLowerCase() === 'xai' && + form.displayName.trim().toLowerCase() === 'supergrok' + ? form.displayName + : getOpenCodeSourceDisplayName(form.providerId, form.displayName); + if (displayName === form.displayName) return form; + return { + ...form, + displayName, + title: replaceProviderNameInText(form.title, form.displayName, displayName) ?? form.title, + description: replaceProviderNameInText(form.description, form.displayName, displayName), + submitLabel: + replaceProviderNameInText(form.submitLabel, form.displayName, displayName) ?? form.submitLabel, + }; +} + +export function presentOAuthProgress( + event: RuntimeProviderOAuthProgressDto +): RuntimeProviderOAuthProgressDto { + const displayName = getOpenCodeSourceDisplayName(event.providerId, event.displayName); + return displayName === event.displayName ? event : { ...event, displayName }; +} + +export function replaceProvider( + view: RuntimeProviderManagementViewDto | null, + provider: RuntimeProviderConnectionDto +): RuntimeProviderManagementViewDto | null { + if (!view) return view; + return { + ...view, + providers: view.providers.map((entry) => + entry.providerId === provider.providerId ? provider : entry + ), + }; +} + +export function replaceDirectoryProvider( + entries: readonly RuntimeProviderDirectoryEntryDto[], + provider: RuntimeProviderConnectionDto, + connectedMethod: 'api' | 'oauth' | null +): readonly RuntimeProviderDirectoryEntryDto[] { + return entries.map((entry) => { + if (entry.providerId !== provider.providerId) return entry; + const connectedAuthHint = + provider.connectedAuthHint ?? connectedMethod ?? entry.connectedAuthHint; + return { + ...entry, + state: provider.state, + setupKind: provider.state === 'connected' ? 'connected' : entry.setupKind, + connectedAuthHint, + ownership: provider.ownership, + recommended: provider.recommended, + modelCount: provider.modelCount, + defaultModelId: provider.defaultModelId, + authMethods: provider.authMethods, + actions: provider.actions, + detail: provider.detail, + }; + }); +} + +export function formatProviderConnectError( + displayName: string, + error: RuntimeProviderManagementErrorDto +): string { + const normalizedMessage = error.message.toLowerCase(); + const invalidApiKey = + /\binvalid[\s_-]+api[\s_-]*key\b/.test(normalizedMessage) || + /\bapi[\s_-]*key\s+(?:is\s+)?(?:invalid|expired|revoked)\b/.test(normalizedMessage); + if (invalidApiKey) { + return `${displayName} rejected this API key. The new credential was not kept. Copy the key from the correct account or subscription plan, then try again.`; + } + if ( + normalizedMessage.includes('access denied by security policy') || + normalizedMessage.includes('forbidden') + ) { + return `${displayName} rejected the verification request because of an account or security policy. The new credential was not kept. Check the key permissions and account restrictions, then try again.`; + } + if (error.code === 'auth-failed') { + return `${displayName} could not verify this credential with a real model request. The new credential was not kept.\n${error.message}`; + } + return error.message; +} + +export function formatProviderConnectSuccess(provider: RuntimeProviderConnectionDto): string { + return provider.verifiedModelId + ? `${provider.displayName} connected and verified with ${provider.verifiedModelId}.` + : `${provider.displayName} connected. Model execution was not verified during setup.`; +} + +export function formatProviderConnectCancellation(displayName: string): string { + return `${displayName} connection was cancelled. Your current credential was not changed.`; +} + +export function formatPostOperationRefreshWarning(successMessage: string): string { + return `${successMessage} The change is saved, but the latest provider status could not be refreshed. Refresh provider status to see the current state.`; +} + +export function formatCredentialRemovedMessage( + provider: RuntimeProviderConnectionDto | null +): string { + if (provider?.state !== 'connected') return 'Credential removed'; + const ownership = new Set(provider.ownership); + if (!ownership.has('managed') && ownership.has('local')) { + return 'Managed credential removed. Provider remains connected through local OpenCode credentials.'; + } + if (!ownership.has('managed') && ownership.size > 0) { + return 'Managed credential removed. Provider remains connected through another credential source.'; + } + return 'Credential removed'; +} diff --git a/src/features/runtime-provider-management/renderer/hooks/useRuntimeProviderManagement.ts b/src/features/runtime-provider-management/renderer/hooks/useRuntimeProviderManagement.ts index 4414664390..90c75d0038 100644 --- a/src/features/runtime-provider-management/renderer/hooks/useRuntimeProviderManagement.ts +++ b/src/features/runtime-provider-management/renderer/hooks/useRuntimeProviderManagement.ts @@ -6,22 +6,36 @@ import { recordProviderConnectionEnd, } from '@renderer/analytics/productAnalytics'; import { api } from '@renderer/api'; -import { getOpenCodeSourceDisplayName } from '@shared/utils/opencodeModelRef'; import { getRuntimeProviderCredentialUrl, selectInitialProviderId, selectRuntimeProviderSetupAuthOptionId, } from '../../core/domain'; +import { saveOpenCodeModelForNewTeams } from '../adapters/createTeamDefaultModelWriter'; import { - getOpenCodeModelForNewTeams, - saveOpenCodeModelForNewTeams, -} from '../adapters/createTeamDefaultModelWriter'; + projectBaseDefaultMutation, + projectClearedDefaultMutation, +} from '../view-models/openCodeDefaultModelInheritance'; import { getProviderConnectErrorDiagnostics, normalizeGitHubDeviceAuthorizationUrl, } from './runtimeProviderConnectionUi'; +import { + formatCredentialRemovedMessage, + formatPostOperationRefreshWarning, + formatProviderConnectCancellation, + formatProviderConnectError, + formatProviderConnectSuccess, + presentDirectoryEntry, + presentManagementView, + presentOAuthProgress, + presentProviderConnection, + presentSetupForm, + replaceDirectoryProvider, + replaceProvider, +} from './runtimeProviderManagementPresentation'; import { applyModelTestResultToModel, applyModelTestResultToView, @@ -35,7 +49,6 @@ import type { RuntimeProviderDirectoryEntryDto, RuntimeProviderDirectoryFilterDto, RuntimeProviderManagementErrorDiagnosticsDto, - RuntimeProviderManagementErrorDto, RuntimeProviderManagementRuntimeId, RuntimeProviderManagementViewDto, RuntimeProviderModelDto, @@ -72,66 +85,6 @@ const MODEL_PAGE_SIZE = 250; const MODEL_SEARCH_DEBOUNCE_MS = 300; const OAUTH_CONNECT_UI_TIMEOUT_MS = 18 * 60_000; -function presentProviderConnection( - provider: RuntimeProviderConnectionDto -): RuntimeProviderConnectionDto { - const displayName = getOpenCodeSourceDisplayName(provider.providerId, provider.displayName); - return displayName === provider.displayName ? provider : { ...provider, displayName }; -} - -function presentDirectoryEntry( - provider: RuntimeProviderDirectoryEntryDto -): RuntimeProviderDirectoryEntryDto { - const displayName = getOpenCodeSourceDisplayName(provider.providerId, provider.displayName); - return displayName === provider.displayName ? provider : { ...provider, displayName }; -} - -function presentManagementView( - view: RuntimeProviderManagementViewDto | null -): RuntimeProviderManagementViewDto | null { - if (!view) return null; - return { ...view, providers: view.providers.map(presentProviderConnection) }; -} - -function replaceProviderNameInText( - value: string | null, - currentDisplayName: string, - presentedDisplayName: string -): string | null { - if (!value || currentDisplayName === presentedDisplayName) return value; - return value.replace(currentDisplayName, presentedDisplayName); -} - -function presentSetupForm( - form: RuntimeProviderSetupFormDto | null -): RuntimeProviderSetupFormDto | null { - if (!form) return null; - // xAI is the catalog source, while SuperGrok is the user-facing subscription - // being connected. Keep the curated plan name in setup and error messages. - const displayName = - form.providerId.trim().toLowerCase() === 'xai' && - form.displayName.trim().toLowerCase() === 'supergrok' - ? form.displayName - : getOpenCodeSourceDisplayName(form.providerId, form.displayName); - if (displayName === form.displayName) return form; - return { - ...form, - displayName, - title: replaceProviderNameInText(form.title, form.displayName, displayName) ?? form.title, - description: replaceProviderNameInText(form.description, form.displayName, displayName), - submitLabel: - replaceProviderNameInText(form.submitLabel, form.displayName, displayName) ?? - form.submitLabel, - }; -} - -function presentOAuthProgress( - event: RuntimeProviderOAuthProgressDto -): RuntimeProviderOAuthProgressDto { - const displayName = getOpenCodeSourceDisplayName(event.providerId, event.displayName); - return displayName === event.displayName ? event : { ...event, displayName }; -} - interface ProjectContextSnapshot { path: string | null; generation: number; @@ -190,6 +143,7 @@ export interface RuntimeProviderManagementState { selectedModelId: string | null; testingModelIds: readonly string[]; savingDefaultModelId: string | null; + clearingProjectDefault?: boolean; modelResults: Readonly>; loading: boolean; savingProviderId: string | null; @@ -230,7 +184,8 @@ export interface RuntimeProviderManagementActions { providerId: string, modelId: string, scope?: RuntimeProviderDefaultScopeDto - ) => Promise; + ) => Promise; + clearProjectDefault: () => Promise; } export interface RuntimeProviderConnectOutcome { @@ -238,48 +193,6 @@ export interface RuntimeProviderConnectOutcome { readonly verifiedModelId: string | null; } -function replaceProvider( - view: RuntimeProviderManagementViewDto | null, - provider: RuntimeProviderConnectionDto -): RuntimeProviderManagementViewDto | null { - if (!view) { - return view; - } - return { - ...view, - providers: view.providers.map((entry) => - entry.providerId === provider.providerId ? provider : entry - ), - }; -} - -function replaceDirectoryProvider( - entries: readonly RuntimeProviderDirectoryEntryDto[], - provider: RuntimeProviderConnectionDto, - connectedMethod: 'api' | 'oauth' | null -): readonly RuntimeProviderDirectoryEntryDto[] { - return entries.map((entry) => { - if (entry.providerId !== provider.providerId) { - return entry; - } - const connectedAuthHint = - provider.connectedAuthHint ?? connectedMethod ?? entry.connectedAuthHint; - return { - ...entry, - state: provider.state, - setupKind: provider.state === 'connected' ? 'connected' : entry.setupKind, - connectedAuthHint, - ownership: provider.ownership, - recommended: provider.recommended, - modelCount: provider.modelCount, - defaultModelId: provider.defaultModelId, - authMethods: provider.authMethods, - actions: provider.actions, - detail: provider.detail, - }; - }); -} - function withUiTimeout(promise: Promise, message: string, timeoutMs = 70_000): Promise { return new Promise((resolve, reject) => { const timeout = window.setTimeout(() => { @@ -298,46 +211,6 @@ function withUiTimeout(promise: Promise, message: string, timeoutMs = 70_0 }); } -function formatProviderConnectError( - displayName: string, - error: RuntimeProviderManagementErrorDto -): string { - const normalizedMessage = error.message.toLowerCase(); - const invalidApiKey = - /\binvalid[\s_-]+api[\s_-]*key\b/.test(normalizedMessage) || - /\bapi[\s_-]*key\s+(?:is\s+)?(?:invalid|expired|revoked)\b/.test(normalizedMessage); - if (invalidApiKey) { - return `${displayName} rejected this API key. The new credential was not kept. Copy the key from the correct account or subscription plan, then try again.`; - } - - const blockedByPolicy = - normalizedMessage.includes('access denied by security policy') || - normalizedMessage.includes('forbidden'); - if (blockedByPolicy) { - return `${displayName} rejected the verification request because of an account or security policy. The new credential was not kept. Check the key permissions and account restrictions, then try again.`; - } - - if (error.code === 'auth-failed') { - return `${displayName} could not verify this credential with a real model request. The new credential was not kept.\n${error.message}`; - } - - return error.message; -} - -function formatProviderConnectSuccess(provider: RuntimeProviderConnectionDto): string { - return provider.verifiedModelId - ? `${provider.displayName} connected and verified with ${provider.verifiedModelId}.` - : `${provider.displayName} connected. Model execution was not verified during setup.`; -} - -function formatProviderConnectCancellation(displayName: string): string { - return `${displayName} connection was cancelled. Your current credential was not changed.`; -} - -function formatPostOperationRefreshWarning(successMessage: string): string { - return `${successMessage} The change is saved, but the latest provider status could not be refreshed. Refresh provider status to see the current state.`; -} - function isProviderConnectCancellation(error: unknown): boolean { const value = (() => { if (error instanceof Error) return error.message; @@ -355,29 +228,8 @@ function normalizeProjectContextPath(projectPath: string | null | undefined): st return projectPath?.trim() || null; } -function resolveSavedModelForNewTeams(models: readonly RuntimeProviderModelDto[]): string | null { - const savedModelId = getOpenCodeModelForNewTeams(); - if (!savedModelId) { - return null; - } - return models.some((model) => model.modelId === savedModelId) ? savedModelId : null; -} - -function formatCredentialRemovedMessage(provider: RuntimeProviderConnectionDto | null): string { - if (provider?.state !== 'connected') { - return 'Credential removed'; - } - - const ownership = new Set(provider.ownership); - if (!ownership.has('managed') && ownership.has('local')) { - return 'Managed credential removed. Provider remains connected through local OpenCode credentials.'; - } - - if (!ownership.has('managed') && ownership.size > 0) { - return 'Managed credential removed. Provider remains connected through another credential source.'; - } - - return 'Credential removed'; +function resolveEffectiveDefaultModel(models: readonly RuntimeProviderModelDto[]): string | null { + return models.find((model) => model.default)?.modelId ?? null; } function resolveSetupAuthOption( @@ -465,6 +317,7 @@ export function useRuntimeProviderManagement( const [selectedModelId, setSelectedModelId] = useState(null); const [testingModelIds, setTestingModelIds] = useState([]); const [savingDefaultModelId, setSavingDefaultModelId] = useState(null); + const [clearingProjectDefault, setClearingProjectDefault] = useState(false); const [modelResults, setModelResults] = useState< Record >({}); @@ -486,6 +339,7 @@ export function useRuntimeProviderManagement( const activeModelPickerProviderRef = useRef(null); const appliedInitialProviderRef = useRef(null); const activeOAuthOperationRef = useRef(null); + const defaultMutationInFlightRef = useRef(false); const activeOAuthPhaseRef = useRef(null); const cancelledOAuthOperationIdsRef = useRef(new Set()); const providerViewRef = useRef(view); @@ -629,6 +483,7 @@ export function useRuntimeProviderManagement( setTestingModelIds([]); setSavingProviderId(null); setSavingDefaultModelId(null); + setClearingProjectDefault(false); setModelResults({}); setSuccessMessage(null); setWarningMessage(null); @@ -669,6 +524,7 @@ export function useRuntimeProviderManagement( } const nextView = presentManagementView(response.view ?? null); setView(nextView); + setSelectedModelId(nextView?.defaultModel ?? null); setSelectedProviderId((current) => { if (current && nextView?.providers.some((provider) => provider.providerId === current)) { return current; @@ -972,12 +828,12 @@ export function useRuntimeProviderManagement( setModelsNextCursor(modelPage?.nextCursor ?? null); setSelectedModelId((current) => { if (append) { - return current ?? resolveSavedModelForNewTeams(nextModels); + return current ?? resolveEffectiveDefaultModel(nextModels); } if (current && nextModels.some((model) => model.modelId === current)) { return current; } - return resolveSavedModelForNewTeams(nextModels); + return resolveEffectiveDefaultModel(nextModels); }); } catch (modelsLoadError) { if (requestIsCurrent()) { @@ -1787,13 +1643,16 @@ export function useRuntimeProviderManagement( providerId: string, modelId: string, scope: RuntimeProviderDefaultScopeDto = 'project' - ): Promise => { + ): Promise => { + if (defaultMutationInFlightRef.current) return false; + defaultMutationInFlightRef.current = true; setSavingDefaultModelId(modelId); setError(null); setErrorDiagnostics(null); setSuccessMessage(null); setWarningMessage(null); const projectContext = getProjectContextSnapshot(); + const isGlobal = scope === 'all_projects'; try { const response = await withUiTimeout( api.runtimeProviderManagement.setDefaultModel({ @@ -1807,13 +1666,11 @@ export function useRuntimeProviderManagement( 'Set default model timed out', 250_000 ); - if (!isProjectContextCurrent(projectContext)) { - return; - } + if (!isGlobal && !isProjectContextCurrent(projectContext)) return false; if (response.error) { setError(response.error.message); setErrorDiagnostics(response.error.diagnostics ?? null); - return; + return false; } const proofResult: RuntimeProviderModelTestResultDto = { providerId, @@ -1823,67 +1680,135 @@ export function useRuntimeProviderManagement( message: 'Model probe passed', diagnostics: [], }; - if (response.view) { + if (!isGlobal && response.view) { setView(applyModelTestResultToView(response.view, proofResult)); } - const effectiveDefaultModelId = response.view?.defaultModel ?? modelId; setModelResults((current) => ({ ...current, [modelId]: proofResult, })); - setSelectedModelId(effectiveDefaultModelId); - setModels((current) => - current.map((model) => - applyModelTestResultToModel( - { - ...model, - default: model.modelId === effectiveDefaultModelId, - }, - proofResult + if (!isGlobal) { + const effectiveDefaultModelId = response.view?.defaultModel ?? modelId; + setSelectedModelId(effectiveDefaultModelId); + setModels((current) => + current.map((model) => + applyModelTestResultToModel( + { ...model, default: model.modelId === effectiveDefaultModelId }, + proofResult + ) ) - ) - ); + ); + } else { + const effectiveDefaultModelId = + providerViewRef.current?.projectDefaultModel ?? modelId; + setView((current) => projectBaseDefaultMutation(current, modelId)); + setSelectedModelId(effectiveDefaultModelId); + setModels((current) => + current.map((model) => ({ + ...model, + default: model.modelId === effectiveDefaultModelId, + })) + ); + } const success = - scope === 'all_projects' + isGlobal ? `All-projects OpenCode default set to ${modelId}` : `Project OpenCode default set to ${modelId}`; + let refreshed = true; try { - const externalRefreshResult = await options.onProviderChanged?.('configuration'); - if (!isProjectContextCurrent(projectContext)) { - return; - } - if (externalRefreshResult === false) { - setError(null); - setErrorDiagnostics(null); - setWarningMessage(formatPostOperationRefreshWarning(success)); - } else { - setSuccessMessage(success); - } + refreshed = (await onProviderChanged?.('configuration')) !== false; } catch { - if (!isProjectContextCurrent(projectContext)) { - return; + refreshed = false; + } + if (isGlobal) { + const refreshContext = getProjectContextSnapshot(); + let viewRefreshed = await refresh({ silent: true }); + if (!viewRefreshed && !isProjectContextCurrent(refreshContext)) { + viewRefreshed = await refresh({ silent: true }); } + setView((current) => applyModelTestResultToView(current, proofResult)); + setModels((current) => + current.map((model) => applyModelTestResultToModel(model, proofResult)) + ); + refreshed = refreshed && viewRefreshed; + } else if (!isProjectContextCurrent(projectContext)) { + return false; + } + if (refreshed) { + setSuccessMessage(success); + } else { setError(null); setErrorDiagnostics(null); setWarningMessage(formatPostOperationRefreshWarning(success)); } + return true; } catch (defaultError) { - if (!isProjectContextCurrent(projectContext)) { - return; - } + if (!isGlobal && !isProjectContextCurrent(projectContext)) return false; setError( defaultError instanceof Error ? defaultError.message : 'Failed to set OpenCode default' ); setErrorDiagnostics(null); + return false; } finally { - if (isProjectContextCurrent(projectContext)) { - setSavingDefaultModelId(null); - } + defaultMutationInFlightRef.current = false; + setSavingDefaultModelId(null); } }, - [getProjectContextSnapshot, isProjectContextCurrent, options] + [getProjectContextSnapshot, isProjectContextCurrent, onProviderChanged, options.runtimeId, refresh] ); + const clearProjectDefault = useCallback(async (): Promise => { + if (defaultMutationInFlightRef.current) return; + defaultMutationInFlightRef.current = true; + setClearingProjectDefault(true); + setError(null); + setErrorDiagnostics(null); + setSuccessMessage(null); + setWarningMessage(null); + const projectContext = getProjectContextSnapshot(); + try { + const response = await withUiTimeout( + api.runtimeProviderManagement.clearProjectDefaultModel({ + runtimeId: options.runtimeId, + projectPath: projectContext.path, + }), + 'Clear project default timed out' + ); + if (!isProjectContextCurrent(projectContext)) return; + if (response.error) { + setError(response.error.message); + setErrorDiagnostics(response.error.diagnostics ?? null); + return; + } + const nextView = presentManagementView( + response.view ?? projectClearedDefaultMutation(providerViewRef.current) + ); + setView(nextView); + setSelectedModelId(nextView?.defaultModel ?? null); + setModels((current) => + current.map((model) => ({ ...model, default: model.modelId === nextView?.defaultModel })) + ); + const success = nextView?.defaultModel + ? `Project override removed. This project now uses ${nextView.defaultModel}` + : 'Project override removed. This project now uses the OpenCode default.'; + let refreshed = true; + try { + refreshed = (await onProviderChanged?.('configuration')) !== false; + } catch { + refreshed = false; + } + if (!isProjectContextCurrent(projectContext)) return; + if (refreshed) setSuccessMessage(success); + else setWarningMessage(formatPostOperationRefreshWarning(success)); + } catch (clearError) { + if (!isProjectContextCurrent(projectContext)) return; + setError(clearError instanceof Error ? clearError.message : 'Failed to clear project default'); + } finally { + defaultMutationInFlightRef.current = false; + setClearingProjectDefault(false); + } + }, [getProjectContextSnapshot, isProjectContextCurrent, onProviderChanged, options.runtimeId]); + const selectProvider = useCallback( (providerId: string): void => { setupFormRequestSeq.current += 1; @@ -1996,6 +1921,7 @@ export function useRuntimeProviderManagement( selectedModelId, testingModelIds, savingDefaultModelId, + clearingProjectDefault, modelResults, loading, savingProviderId, @@ -2007,6 +1933,7 @@ export function useRuntimeProviderManagement( [ activeFormProviderId, connectionIntent, + clearingProjectDefault, apiKeyValue, selectedAuthOptionId, oauthProgress, @@ -2084,6 +2011,7 @@ export function useRuntimeProviderManagement( useModelForNewTeams, testModel, setDefaultModel, + clearProjectDefault, }), [ cancelConnect, @@ -2100,6 +2028,7 @@ export function useRuntimeProviderManagement( selectDirectoryProvider, selectProvider, setDefaultModel, + clearProjectDefault, setSetupMetadataValue, setAuthOption, startConnect, diff --git a/src/features/runtime-provider-management/renderer/ui/OpenCodeDefaultModelInheritanceCard.tsx b/src/features/runtime-provider-management/renderer/ui/OpenCodeDefaultModelInheritanceCard.tsx new file mode 100644 index 0000000000..d88c80627f --- /dev/null +++ b/src/features/runtime-provider-management/renderer/ui/OpenCodeDefaultModelInheritanceCard.tsx @@ -0,0 +1,245 @@ +import { useMemo } from 'react'; + +import { useAppTranslation } from '@features/localization/renderer'; +import { Badge } from '@renderer/components/ui/badge'; +import { Button } from '@renderer/components/ui/button'; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@renderer/components/ui/select'; +import { Loader2, RotateCcw } from 'lucide-react'; + +import { + OPENROUTER_FREE_MODEL_ID, + presentOpenCodeDefaultModelInheritance, +} from '../view-models/openCodeDefaultModelInheritance'; + +import type { RuntimeProviderDefaultScopeDto } from '../../contracts'; +import type { + RuntimeProviderManagementActions, + RuntimeProviderManagementState, +} from '../hooks/useRuntimeProviderManagement'; +import type { ProjectPathProject } from '@renderer/components/team/dialogs/projectPathProjects'; +import type { JSX } from 'react'; + +const NO_PROJECT = '__runtime-provider-no-project__'; + +function projectName(projectPath: string | null | undefined): string | null { + const normalized = projectPath?.trim().replace(/[\\/]+$/u, ''); + return normalized?.split(/[\\/]/u).at(-1) || null; +} + +export const OpenCodeDefaultModelInheritanceCard = ({ + state, + actions, + disabled, + projectPath, + projects, + projectsLoading, + projectError, + onProjectChange, + onChooseModel, +}: { + readonly state: RuntimeProviderManagementState; + readonly actions: RuntimeProviderManagementActions; + readonly disabled: boolean; + readonly projectPath: string | null; + readonly projects: readonly ProjectPathProject[]; + readonly projectsLoading: boolean; + readonly projectError: string | null; + readonly onProjectChange?: (projectPath: string | null) => void; + readonly onChooseModel: (target: RuntimeProviderDefaultScopeDto) => void; +}): JSX.Element => { + const { t } = useAppTranslation('settings'); + const options = useMemo(() => { + const unique = new Map(); + for (const project of projects) { + if (project.path.trim() && project.filesystemState !== 'deleted') { + unique.set(project.path.trim(), project); + } + } + if (projectPath && !unique.has(projectPath)) { + unique.set(projectPath, { + id: projectPath, + path: projectPath, + name: projectName(projectPath) ?? projectPath, + sessions: [], + totalSessions: 0, + createdAt: 0, + }); + } + return [...unique.values()]; + }, [projectPath, projects]); + const activeProjectName = + options.find((project) => project.path === projectPath)?.name ?? projectName(projectPath); + const model = presentOpenCodeDefaultModelInheritance({ + view: state.view, + projectPath, + projectName: activeProjectName, + }); + const busy = Boolean(state.savingDefaultModelId) || state.clearingProjectDefault; + + return ( +
+
+ {t('runtimeProvider.defaults.title')} +
+

+ {t('runtimeProvider.defaults.inheritanceDescription')} +

+ +
+
+
+
+ {t('runtimeProvider.defaults.allProjects')} +
+
+ {model.baseDisplayName} +
+ {model.baseModelId && model.baseDisplayName !== model.baseModelId ? ( +
+ {model.baseModelId} +
+ ) : null} + {model.baseModelId === OPENROUTER_FREE_MODEL_ID ? ( +
+ {t('runtimeProvider.defaults.freeRouterAdvisory')} +
+ ) : null} +
+ +
+ +
+
+
+
+ + {activeProjectName + ? `${t('runtimeProvider.defaults.thisProject')}: ${activeProjectName}` + : t('runtimeProvider.defaults.thisProject')} + + {projectPath ? ( + + {model.projectInherits + ? t('runtimeProvider.defaults.inherits') + : t('runtimeProvider.defaults.override')} + + ) : null} +
+
+ {projectPath + ? model.projectEffectiveDisplayName + : t('runtimeProvider.defaults.selectProjectContext')} +
+ {projectPath && + model.projectEffectiveModelId && + model.projectEffectiveDisplayName !== model.projectEffectiveModelId ? ( +
+ {model.projectEffectiveModelId} +
+ ) : null} +
+
+ +
+
+ +
+ + {model.projectOverrideModelId ? ( + + ) : null} +
+ {projectError ?
{projectError}
: null} +
+
+
+ ); +} + +export const OpenCodeDefaultTargetBanner = ({ + target, + projectName: targetProjectName, + onCancel, +}: { + readonly target: RuntimeProviderDefaultScopeDto; + readonly projectName: string | null; + readonly onCancel: () => void; +}): JSX.Element => { + const { t } = useAppTranslation('settings'); + return ( +
+ + {target === 'all_projects' + ? t('runtimeProvider.defaults.choosingAllProjects') + : t('runtimeProvider.defaults.choosingProject', { + project: targetProjectName ?? t('runtimeProvider.defaults.thisProject'), + })} + + +
+ ); +} diff --git a/src/features/runtime-provider-management/renderer/ui/RuntimeProviderManagementPanelView.tsx b/src/features/runtime-provider-management/renderer/ui/RuntimeProviderManagementPanelView.tsx index 4c0c9761a8..0e9eeeb3b3 100644 --- a/src/features/runtime-provider-management/renderer/ui/RuntimeProviderManagementPanelView.tsx +++ b/src/features/runtime-provider-management/renderer/ui/RuntimeProviderManagementPanelView.tsx @@ -15,12 +15,6 @@ import { SelectValue, } from '@renderer/components/ui/select'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '@renderer/components/ui/tabs'; -import { - Tooltip, - TooltipContent, - TooltipProvider, - TooltipTrigger, -} from '@renderer/components/ui/tooltip'; import { cn } from '@renderer/lib/utils'; import { compareOpenCodeTeamModelRecommendations, @@ -56,13 +50,16 @@ import { getProviderModelsLabel, getRuntimeProviderSetupPresentation, } from '../../core/domain'; +import { OPENROUTER_FREE_MODEL_ID } from '../view-models/openCodeDefaultModelInheritance'; +import { + OpenCodeDefaultModelInheritanceCard, + OpenCodeDefaultTargetBanner, +} from './OpenCodeDefaultModelInheritanceCard'; import { ProviderBrandIcon } from './providerBrandIcons'; import { - canTestOpenCodeModelRoute, canUseOpenCodeModelRoute, isUnknownOpenCodeModelRoute, - needsOpenCodeModelExecutionProof, } from './runtimeProviderModelAccess'; import { RuntimeProviderModelTestResult } from './RuntimeProviderModelTestResult'; import { @@ -116,6 +113,8 @@ interface ProviderRowProps { readonly busy: boolean; readonly disabled: boolean; readonly hasProjectContext: boolean; + readonly defaultTarget: RuntimeProviderDefaultScopeDto | null; + readonly onDefaultSaved: () => void; readonly actions: RuntimeProviderManagementActions; } @@ -128,7 +127,6 @@ interface RuntimeProviderErrorAlertProps { type OpenCodeSettingsSection = 'models' | 'providers'; type SettingsT = ReturnType['t']; -const NO_PROJECT_CONTEXT_VALUE = '__runtime-provider-no-project-context__'; const PROVIDER_MODEL_VIRTUALIZATION_THRESHOLD = 40; const PROVIDER_MODEL_ROW_ESTIMATE_PX = 112; @@ -205,77 +203,6 @@ function getProjectContextName(projectPath: string | null | undefined): string | return name || normalized; } -function getDefaultScopeDescription(scope: RuntimeProviderDefaultScopeDto, t: SettingsT): string { - return scope === 'all_projects' - ? t('runtimeProvider.defaults.scopeDescriptionAllProjects') - : t('runtimeProvider.defaults.scopeDescriptionProject'); -} - -function getDefaultScopeButtonLabel(scope: RuntimeProviderDefaultScopeDto, t: SettingsT): string { - return scope === 'all_projects' - ? t('runtimeProvider.defaults.setAllProjectsDefault') - : t('runtimeProvider.defaults.setProjectDefault'); -} - -function getContextControlLabel(scope: RuntimeProviderDefaultScopeDto, t: SettingsT): string { - return scope === 'all_projects' - ? t('runtimeProvider.defaults.validationContext') - : t('runtimeProvider.defaults.projectOverrideContext'); -} - -function getContextControlHint( - scope: RuntimeProviderDefaultScopeDto, - projectPath: string | null | undefined, - t: SettingsT -): string { - const projectName = getProjectContextName(projectPath) ?? projectPath?.trim(); - if (!projectName) { - return t('runtimeProvider.defaults.selectProjectHint'); - } - return scope === 'all_projects' - ? t('runtimeProvider.defaults.allProjectsHint', { project: projectName }) - : t('runtimeProvider.defaults.projectHint', { project: projectName }); -} - -function isDefaultForScope( - model: RuntimeProviderModelDto, - state: RuntimeProviderManagementState, - scope: RuntimeProviderDefaultScopeDto -): boolean { - const scopedDefault = - scope === 'all_projects' - ? state.view?.allProjectsDefaultModel - : state.view?.projectDefaultModel; - return scopedDefault === model.modelId; -} - -const DisabledActionTooltip = ({ - reason, - children, -}: { - readonly reason: string | undefined; - readonly children: JSX.Element; -}): JSX.Element => { - if (!reason) { - return children; - } - - return ( - - - - - {children} - - - - {reason} - - - - ); -}; - function directoryEntryMatchesQuery( provider: RuntimeProviderDirectoryEntryDto, query: string @@ -433,7 +360,7 @@ function eventStartedInInteractiveChild( return interactiveAncestor !== null && interactiveAncestor !== currentTarget; } -export function ProviderSetupFormPanel({ +export const ProviderSetupFormPanel = ({ provider, state, busy, @@ -447,7 +374,7 @@ export function ProviderSetupFormPanel({ readonly disabled: boolean; readonly preparing?: boolean; readonly actions: RuntimeProviderManagementActions; -}): JSX.Element { +}): JSX.Element => { const { t } = useAppTranslation('settings'); const form = state.setupForm?.providerId === provider.providerId ? state.setupForm : null; const loading = @@ -891,7 +818,7 @@ export function ProviderSetupFormPanel({ ); } -function RuntimeProviderLoadingPlaceholder(): JSX.Element { +const RuntimeProviderLoadingPlaceholder = (): JSX.Element => { const { t } = useAppTranslation('settings'); return (
{ return (
{Array.from({ length: 3 }).map((_, index) => ( @@ -1304,14 +1231,14 @@ function RuntimeProviderModelLoadingSkeleton(): JSX.Element { ); } -function ProviderActions({ +const ProviderActions = ({ provider, busy, disabled, onStartConnect, onStartReconnect, onForget, -}: ProviderActionsProps): JSX.Element { +}: ProviderActionsProps): JSX.Element => { const { t } = useAppTranslation('settings'); const connect = getProviderAction(provider, 'connect'); const reconnect = getProviderAction(provider, 'reconnect'); @@ -1395,7 +1322,7 @@ function ProviderActions({ ); } -function ProviderRow({ +const ProviderRow = ({ provider, state, active, @@ -1403,8 +1330,10 @@ function ProviderRow({ busy, disabled, hasProjectContext, + defaultTarget, + onDefaultSaved, actions, -}: ProviderRowProps): JSX.Element { +}: ProviderRowProps): JSX.Element => { const { t } = useAppTranslation('settings'); const connect = getProviderAction(provider, 'connect'); const test = getProviderAction(provider, 'test'); @@ -1529,6 +1458,8 @@ function ProviderRow({ provider={provider} disabled={disabled || busy} hasProjectContext={hasProjectContext} + defaultTarget={defaultTarget} + onDefaultSaved={onDefaultSaved} /> ) : null}
@@ -1537,7 +1468,7 @@ function ProviderRow({ ); } -function DirectoryProviderRow({ +const DirectoryProviderRow = ({ provider, state, active, @@ -1545,6 +1476,8 @@ function DirectoryProviderRow({ disabled, busy, hasProjectContext, + defaultTarget, + onDefaultSaved, actions, }: { readonly provider: RuntimeProviderDirectoryEntryDto; @@ -1554,8 +1487,10 @@ function DirectoryProviderRow({ readonly disabled: boolean; readonly busy: boolean; readonly hasProjectContext: boolean; + readonly defaultTarget: RuntimeProviderDefaultScopeDto | null; + readonly onDefaultSaved: () => void; readonly actions: RuntimeProviderManagementActions; -}): JSX.Element { +}): JSX.Element => { const { t } = useAppTranslation('settings'); const connect = getDirectoryAction(provider, 'connect'); const reconnect = getDirectoryAction(provider, 'reconnect'); @@ -1741,6 +1676,8 @@ function DirectoryProviderRow({ provider={directoryEntryToProviderConnection(provider)} disabled={disabled || busy} hasProjectContext={hasProjectContext} + defaultTarget={defaultTarget} + onDefaultSaved={onDefaultSaved} /> ) : null}
@@ -1749,13 +1686,7 @@ function DirectoryProviderRow({ ); } -function ModelBadges({ - model, - usedForNewTeams, -}: { - readonly model: RuntimeProviderModelDto; - readonly usedForNewTeams: boolean; -}): JSX.Element | null { +const ModelBadges = ({ model }: { readonly model: RuntimeProviderModelDto }): JSX.Element | null => { const { t } = useAppTranslation('settings'); const modelRecommendation = getOpenCodeTeamModelRecommendation(model.modelId); const routeStatus = getOpenCodeModelRoutePresentationStatus(model); @@ -1779,7 +1710,6 @@ function ModelBadges({ if ( !freeModel && !model.default && - !usedForNewTeams && !modelRecommendation && !localRoute && !configuredRoute && @@ -1824,12 +1754,6 @@ function ModelBadges({ {modelRecommendation.label} ) : null} - {usedForNewTeams ? ( - - - {t('runtimeProvider.badges.usedInTeamPicker')} - - ) : null} {freeModel ? ( {t('runtimeProvider.badges.free')} @@ -1917,35 +1841,6 @@ function getOpenCodeRouteUnavailableTitle( return undefined; } -function getDisabledActionReason(input: { - readonly disabled: boolean; - readonly contextRequiredTitle?: string; - readonly unavailableTitle?: string; - readonly busy: boolean; - readonly busyTitle: string; - readonly alreadyDefault?: boolean; - readonly alreadyDefaultTitle?: string; - readonly capabilityAvailable: boolean; - readonly t: SettingsT; -}): string | undefined { - if (input.disabled) { - return input.t('runtimeProvider.models.actionsUnavailable'); - } - if (input.contextRequiredTitle) { - return input.contextRequiredTitle; - } - if (input.busy) { - return input.busyTitle; - } - if (input.alreadyDefault) { - return input.alreadyDefaultTitle; - } - if (!input.capabilityAvailable) { - return input.unavailableTitle ?? input.t('runtimeProvider.models.routeUnavailableGeneric'); - } - return undefined; -} - function getOpenCodeModelSearchText(model: RuntimeProviderModelDto): string { const recommendation = getOpenCodeTeamModelRecommendation(model.modelId); const routeStatus = getOpenCodeModelRoutePresentationStatus(model); @@ -2135,7 +2030,7 @@ function formatModelResultMessage(message: string): { return { summary: decoded, details: null }; } -function ModelRow({ +const ModelRow = ({ provider, model, selected, @@ -2143,6 +2038,9 @@ function ModelRow({ hasProjectContext, testing, result, + defaultTarget, + savingDefault, + onDefaultSaved, actions, }: { readonly provider: RuntimeProviderConnectionDto; @@ -2152,44 +2050,22 @@ function ModelRow({ readonly hasProjectContext: boolean; readonly testing: boolean; readonly result: RuntimeProviderModelTestResultDto | undefined; + readonly defaultTarget: RuntimeProviderDefaultScopeDto | null; + readonly savingDefault: boolean; + readonly onDefaultSaved: () => void; readonly actions: RuntimeProviderManagementActions; -}): JSX.Element { +}): JSX.Element => { const { t } = useAppTranslation('settings'); const modelDisabled = disabled || !canUseOpenCodeModelRoute(model); const unavailableTitle = getOpenCodeRouteUnavailableTitle(model, t); - const chooseModel = (): void => { - if (!modelDisabled) { - actions.useModelForNewTeams(model.modelId); - } - }; - const handleKeyDown = (event: KeyboardEvent): void => { - if (event.key !== 'Enter' && event.key !== ' ') { - return; - } - if (eventStartedInInteractiveChild(event.currentTarget, event.target)) { - return; - } - event.stopPropagation(); - event.preventDefault(); - chooseModel(); - }; + const targets: readonly RuntimeProviderDefaultScopeDto[] = defaultTarget ? [defaultTarget] : []; return (
{ - event.stopPropagation(); - chooseModel(); - }} - onKeyDown={handleKeyDown} + className="rounded-md border px-3 py-2.5" style={{ borderColor: selected ? 'rgba(96, 165, 250, 0.45)' : 'var(--color-border-subtle)', backgroundColor: selected ? 'rgba(96, 165, 250, 0.06)' : 'rgba(255,255,255,0.02)', @@ -2209,7 +2085,12 @@ function ModelRow({ > {model.modelId}
- + + {model.modelId === OPENROUTER_FREE_MODEL_ID ? ( +
+ {t('runtimeProvider.defaults.freeRouterAdvisory')} +
+ ) : null}
- - - - ); -} - -function OpenCodeModelScopeControls({ - defaultScope, - onDefaultScopeChange, - projectPath, - projects, - loading, - disabled, - error, - onProjectContextChange, -}: { - readonly defaultScope: RuntimeProviderDefaultScopeDto; - readonly onDefaultScopeChange: (scope: RuntimeProviderDefaultScopeDto) => void; - readonly projectPath: string | null | undefined; - readonly projects: readonly ProjectPathProject[]; - readonly loading: boolean; - readonly disabled: boolean; - readonly error: string | null; - readonly onProjectContextChange?: (projectPath: string | null) => void; -}): JSX.Element { - const { t } = useAppTranslation('settings'); - const selectedValue = projectPath?.trim() || NO_PROJECT_CONTEXT_VALUE; - const projectOptions = useMemo(() => { - const seen = new Set(); - const options = projects.filter((project) => { - const normalized = project.path.trim(); - if (!normalized || seen.has(normalized) || project.filesystemState === 'deleted') { - return false; - } - seen.add(normalized); - return true; - }); - const currentPath = projectPath?.trim(); - if (currentPath && !seen.has(currentPath)) { - options.unshift({ - id: currentPath, - path: currentPath, - name: getProjectContextName(currentPath) ?? currentPath, - sessions: [], - totalSessions: 0, - createdAt: 0, - }); - } - return options; - }, [projectPath, projects]); - const contextPlaceholder = loading - ? t('runtimeProvider.defaults.loadingContexts') - : defaultScope === 'all_projects' - ? t('runtimeProvider.defaults.selectValidationContext') - : t('runtimeProvider.defaults.selectProjectContext'); - - return ( -
-
-
-
- {t('runtimeProvider.defaults.title')} -
-
- {getDefaultScopeDescription(defaultScope, t)} -
-
-
- {(['all_projects', 'project'] as const).map((scope) => ( - - ))} -
-
- -
-
- -
- -
-
-
- {getContextControlHint(defaultScope, projectPath, t)} -
-
- - {error ? ( -
- {error} -
- ) : null} -
- ); -} - -function ConfiguredOpenCodeModelsPanel({ - state, - actions, - disabled, - defaultScope, - hasProjectContext, -}: { - readonly state: RuntimeProviderManagementState; - readonly actions: RuntimeProviderManagementActions; - readonly disabled: boolean; - readonly defaultScope: RuntimeProviderDefaultScopeDto; - readonly hasProjectContext: boolean; -}): JSX.Element | null { - const { t } = useAppTranslation('settings'); - const models = useMemo(() => state.view?.configuredModels ?? [], [state.view?.configuredModels]); - const [query, setQuery] = useState(''); - const normalizedQuery = query.trim().toLowerCase(); - const visibleModels = useMemo( - () => - normalizedQuery - ? models.filter((model) => getOpenCodeModelSearchText(model).includes(normalizedQuery)) - : models, - [models, normalizedQuery] - ); - if (models.length === 0) { - return null; - } - - return ( -
-
-
-
- {t('runtimeProvider.models.launchableTitle')} -
-
- {t('runtimeProvider.models.launchableDescription')} -
-
-
- - setQuery(event.target.value)} - placeholder={t('runtimeProvider.modelRoutes.searchPlaceholder')} - className="h-9 pl-10 pr-3 text-sm leading-5" - style={{ paddingLeft: 40 }} - /> + {savingDefault ? : null} + {t('runtimeProvider.defaults.testAndUse')} + + ))}
- -
- {!hasProjectContext ? ( -
- {t('runtimeProvider.models.validationContextRequired')} -
- ) : null} - {visibleModels.length === 0 ? ( -
- {t('runtimeProvider.models.noRoutesMatch', { query: query.trim() })} -
- ) : null} - {visibleModels.map((model) => { - const selected = state.selectedModelId === model.modelId; - const testing = state.testingModelIds.includes(model.modelId); - const savingDefault = state.savingDefaultModelId === model.modelId; - const result = state.modelResults[model.modelId]; - const unavailableTitle = getOpenCodeRouteUnavailableTitle(model, t); - const contextRequiredTitle = hasProjectContext - ? undefined - : t('runtimeProvider.models.selectProjectBeforeTestingDefaults'); - const alreadyDefaultForScope = isDefaultForScope(model, state, defaultScope); - const testCapabilityAvailable = canTestOpenCodeModelRoute(model); - const useCapabilityAvailable = canUseOpenCodeModelRoute(model); - const defaultCapabilityAvailable = - useCapabilityAvailable || needsOpenCodeModelExecutionProof(model); - const canTest = !disabled && hasProjectContext && !testing && testCapabilityAvailable; - const canUse = !disabled && useCapabilityAvailable; - const canSetDefault = - !disabled && - hasProjectContext && - !savingDefault && - !alreadyDefaultForScope && - defaultCapabilityAvailable; - const testDisabledReason = canTest - ? undefined - : getDisabledActionReason({ - disabled, - contextRequiredTitle, - unavailableTitle, - busy: testing, - busyTitle: t('runtimeProvider.models.testInProgress'), - capabilityAvailable: testCapabilityAvailable, - t, - }); - const useDisabledReason = canUse - ? undefined - : getDisabledActionReason({ - disabled, - unavailableTitle, - busy: false, - busyTitle: '', - capabilityAvailable: useCapabilityAvailable, - t, - }); - const setDefaultDisabledReason = canSetDefault - ? undefined - : getDisabledActionReason({ - disabled, - contextRequiredTitle, - unavailableTitle, - busy: savingDefault, - busyTitle: t('runtimeProvider.models.defaultSaveInProgress'), - alreadyDefault: alreadyDefaultForScope, - alreadyDefaultTitle: t('runtimeProvider.models.alreadyDefault'), - capabilityAvailable: defaultCapabilityAvailable, - t, - }); - return ( -
-
-
-
- {model.displayName} -
-
- {model.modelId} - {model.sourceLabel} -
- -
-
- - - - - - - - - -
-
- -
- ); - })} -
+
); } -function ProviderModelList({ +const ProviderModelList = ({ state, actions, provider, disabled, hasProjectContext, + defaultTarget, + onDefaultSaved, }: { readonly state: RuntimeProviderManagementState; readonly actions: RuntimeProviderManagementActions; readonly provider: RuntimeProviderConnectionDto; readonly disabled: boolean; readonly hasProjectContext: boolean; -}): JSX.Element { + readonly defaultTarget: RuntimeProviderDefaultScopeDto | null; + readonly onDefaultSaved: () => void; +}): JSX.Element => { const { t } = useAppTranslation('settings'); const pickerOpen = state.modelPickerProviderId === provider.providerId; const modelListRef = useRef(null); @@ -2777,6 +2335,9 @@ function ProviderModelList({ hasProjectContext={hasProjectContext} testing={state.testingModelIds.includes(model.modelId)} result={state.modelResults[model.modelId]} + defaultTarget={defaultTarget} + savingDefault={Boolean(state.savingDefaultModelId || state.clearingProjectDefault)} + onDefaultSaved={onDefaultSaved} actions={actions} /> ); @@ -2925,7 +2486,7 @@ function ProviderModelList({ ); } -export function RuntimeProviderManagementPanelView({ +export const RuntimeProviderManagementPanelView = ({ state, actions, disabled, @@ -2934,10 +2495,10 @@ export function RuntimeProviderManagementPanelView({ projectContextLoading = false, projectContextError = null, onProjectContextChange, -}: RuntimeProviderManagementPanelViewProps): JSX.Element { +}: RuntimeProviderManagementPanelViewProps): JSX.Element => { const { t } = useAppTranslation('settings'); const [selectedSection, setSelectedSection] = useState(null); - const [defaultScope, setDefaultScope] = useState('all_projects'); + const [defaultTarget, setDefaultTarget] = useState(null); const providerQuery = state.providerQuery.trim().toLowerCase(); const filteredProviders = providerQuery ? state.providers.filter((provider) => @@ -2973,9 +2534,11 @@ export function RuntimeProviderManagementPanelView({ ? t('runtimeProvider.providers.catalog') : t('runtimeProvider.providers.countFallback'); const launchableModelCount = state.view?.configuredModels?.length ?? 0; - const modelsLoading = state.loading && launchableModelCount === 0; const activeSection = selectedSection ?? 'providers'; const hasProjectContext = Boolean(projectPath?.trim()); + const activeProjectName = + projectContextProjects.find((project) => project.path === projectPath)?.name ?? + getProjectContextName(projectPath); const activeAuthOption = state.setupForm?.authOptions?.find( (option) => option.id === state.selectedAuthOptionId ); @@ -3098,46 +2661,30 @@ export function RuntimeProviderManagementPanelView({ - - { + setDefaultTarget(target); + setSelectedSection('providers'); + }} /> - {modelsLoading ? ( -
-
- - {t('runtimeProvider.models.loadingRoutes')} -
- -
- ) : null} - {!modelsLoading && launchableModelCount === 0 ? ( -
- {t('runtimeProvider.models.noneReported')} -
- ) : null}
+ {defaultTarget ? ( + setDefaultTarget(null)} + /> + ) : null}
{ + setDefaultTarget(null); + setSelectedSection('models'); + }} actions={actions} /> ))} @@ -3251,6 +2803,11 @@ export function RuntimeProviderManagementPanelView({ busy={state.savingProviderId === provider.providerId} disabled={disabled || state.loading} hasProjectContext={hasProjectContext} + defaultTarget={defaultTarget} + onDefaultSaved={() => { + setDefaultTarget(null); + setSelectedSection('models'); + }} actions={actions} /> ))} diff --git a/src/features/runtime-provider-management/renderer/view-models/openCodeDefaultModelInheritance.test.ts b/src/features/runtime-provider-management/renderer/view-models/openCodeDefaultModelInheritance.test.ts new file mode 100644 index 0000000000..f2dabafd03 --- /dev/null +++ b/src/features/runtime-provider-management/renderer/view-models/openCodeDefaultModelInheritance.test.ts @@ -0,0 +1,79 @@ +import { describe, expect, it } from 'vitest'; + +import { + presentOpenCodeDefaultModelInheritance, + projectBaseDefaultMutation, + projectClearedDefaultMutation, +} from './openCodeDefaultModelInheritance'; + +import type { RuntimeProviderManagementViewDto } from '@features/runtime-provider-management/contracts'; + +const view = { + runtimeId: 'opencode', + title: 'OpenCode', + runtime: { + state: 'ready', + cliPath: null, + version: null, + managedProfile: 'active', + localAuth: 'synced', + }, + providers: [], + configuredModels: [], + diagnostics: [], + projectPath: '/tmp/project', + defaultModel: 'openrouter/project', + projectDefaultModel: 'openrouter/project', + allProjectsDefaultModel: 'openrouter/base', + defaultModelSource: 'project', + fallbackModel: null, +} as RuntimeProviderManagementViewDto; + +describe('openCodeDefaultModelInheritance', () => { + it('keeps a project override effective when the base changes', () => { + expect(projectBaseDefaultMutation(view, 'openrouter/new-base')).toMatchObject({ + allProjectsDefaultModel: 'openrouter/new-base', + projectDefaultModel: 'openrouter/project', + defaultModel: 'openrouter/project', + defaultModelSource: 'project', + }); + }); + + it('resolves to the base after clearing the project override', () => { + expect(projectClearedDefaultMutation(view)).toMatchObject({ + projectDefaultModel: null, + defaultModel: 'openrouter/base', + defaultModelSource: 'all_projects', + }); + }); + + it('falls back safely when an older response omits the all-projects field', () => { + expect( + projectClearedDefaultMutation({ + ...view, + allProjectsDefaultModel: null, + fallbackModel: 'opencode/fallback', + }) + ).toMatchObject({ + projectDefaultModel: null, + defaultModel: 'opencode/fallback', + defaultModelSource: 'fallback', + }); + }); + + it('presents the Free Models Router with its stable product name', () => { + const presentation = presentOpenCodeDefaultModelInheritance({ + view: { + ...view, + defaultModel: 'openrouter/openrouter/free', + projectDefaultModel: null, + allProjectsDefaultModel: 'openrouter/openrouter/free', + defaultModelSource: 'all_projects', + }, + projectPath: '/tmp/project', + projectName: 'Test project', + }); + expect(presentation.baseDisplayName).toBe('Free Models Router'); + expect(presentation.projectInherits).toBe(true); + }); +}); diff --git a/src/features/runtime-provider-management/renderer/view-models/openCodeDefaultModelInheritance.ts b/src/features/runtime-provider-management/renderer/view-models/openCodeDefaultModelInheritance.ts new file mode 100644 index 0000000000..bd8e034e3c --- /dev/null +++ b/src/features/runtime-provider-management/renderer/view-models/openCodeDefaultModelInheritance.ts @@ -0,0 +1,79 @@ +import type { + RuntimeProviderManagementViewDto, + RuntimeProviderModelDto, +} from '@features/runtime-provider-management/contracts'; + +export const OPENROUTER_FREE_MODEL_ID = 'openrouter/openrouter/free'; + +export interface OpenCodeDefaultModelPresentation { + readonly baseModelId: string | null; + readonly baseDisplayName: string; + readonly projectPath: string | null; + readonly projectName: string | null; + readonly projectOverrideModelId: string | null; + readonly projectEffectiveModelId: string | null; + readonly projectEffectiveDisplayName: string; + readonly projectInherits: boolean; + readonly effectiveSource: RuntimeProviderManagementViewDto['defaultModelSource']; +} + +function displayModel( + modelId: string | null, + models: readonly RuntimeProviderModelDto[] +): string { + if (!modelId) return 'OpenCode default'; + if (modelId === OPENROUTER_FREE_MODEL_ID) return 'Free Models Router'; + return models.find((model) => model.modelId === modelId)?.displayName ?? modelId; +} + +export function presentOpenCodeDefaultModelInheritance(input: { + readonly view: RuntimeProviderManagementViewDto | null; + readonly projectPath: string | null; + readonly projectName: string | null; +}): OpenCodeDefaultModelPresentation { + const { view } = input; + const models = view?.configuredModels ?? []; + const baseModelId = + view?.allProjectsDefaultModel ?? + (view?.defaultModelSource === 'project' ? null : (view?.defaultModel ?? null)); + const projectOverrideModelId = view?.projectDefaultModel ?? null; + const projectEffectiveModelId = projectOverrideModelId ?? baseModelId ?? view?.defaultModel ?? null; + return { + baseModelId, + baseDisplayName: displayModel(baseModelId, models), + projectPath: input.projectPath, + projectName: input.projectName, + projectOverrideModelId, + projectEffectiveModelId, + projectEffectiveDisplayName: displayModel(projectEffectiveModelId, models), + projectInherits: projectOverrideModelId === null, + effectiveSource: view?.defaultModelSource ?? null, + }; +} + +export function projectBaseDefaultMutation( + view: RuntimeProviderManagementViewDto | null, + modelId: string +): RuntimeProviderManagementViewDto | null { + if (!view) return view; + const projectOverride = view.projectDefaultModel; + return { + ...view, + allProjectsDefaultModel: modelId, + defaultModel: projectOverride ?? modelId, + defaultModelSource: projectOverride ? 'project' : 'all_projects', + }; +} + +export function projectClearedDefaultMutation( + view: RuntimeProviderManagementViewDto | null +): RuntimeProviderManagementViewDto | null { + if (!view) return view; + const inherited = view.allProjectsDefaultModel ?? view.fallbackModel ?? null; + return { + ...view, + projectDefaultModel: null, + defaultModel: inherited, + defaultModelSource: view.allProjectsDefaultModel ? 'all_projects' : inherited ? 'fallback' : null, + }; +} diff --git a/test/renderer/features/runtime-provider-management/RuntimeProviderManagementPanelView.test.ts b/test/renderer/features/runtime-provider-management/RuntimeProviderManagementPanelView.test.ts index 09943b9bac..8c25245a2a 100644 --- a/test/renderer/features/runtime-provider-management/RuntimeProviderManagementPanelView.test.ts +++ b/test/renderer/features/runtime-provider-management/RuntimeProviderManagementPanelView.test.ts @@ -141,20 +141,11 @@ function createActions(): RuntimeProviderManagementActions { diagnostics: [], }) ), - setDefaultModel: vi.fn(() => Promise.resolve()), + setDefaultModel: vi.fn(() => Promise.resolve(true)), + clearProjectDefault: vi.fn(() => Promise.resolve()), }; } -function setInputValue(input: HTMLInputElement, value: string): void { - const setter = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value')?.set; - if (!setter) { - throw new Error('HTMLInputElement value setter not found'); - } - - setter.call(input, value); - input.dispatchEvent(new Event('input', { bubbles: true })); -} - async function selectOpenCodeTab(host: HTMLElement, label: 'Models' | 'Providers'): Promise { const trigger = Array.from(host.querySelectorAll('[role="tab"]')).find( (button) => button.textContent?.trim().startsWith(label) @@ -209,18 +200,19 @@ describe('RuntimeProviderManagementPanelView', () => { await selectOpenCodeTab(host, 'Models'); - expect(host.textContent).toContain('Loading OpenCode model routes'); + expect(host.textContent).toContain('OpenCode defaults'); + expect(host.textContent).toContain('Projects use this model unless they choose another.'); + expect(host.textContent).toContain('Default model'); expect( host.querySelector('[data-testid="runtime-provider-model-loading-skeleton"]') - ).not.toBeNull(); - expect(host.querySelectorAll('.skeleton-shimmer').length).toBeGreaterThanOrEqual(8); + ).toBeNull(); expect(host.textContent).toContain('Refresh'); const refreshButton = Array.from(host.querySelectorAll('button')).find((button) => button.textContent?.includes('Refresh') ); expect(refreshButton?.disabled).toBe(true); - expect(host.textContent).not.toContain('No launchable OpenCode model routes were reported yet'); + expect(host.textContent).not.toContain('Validation context'); }); it('requests the full managed view only after the Models tab is opened', async () => { @@ -754,15 +746,16 @@ describe('RuntimeProviderManagementPanelView', () => { await selectOpenCodeTab(host, 'Models'); expect(host.textContent).toContain('OpenCode defaults'); - expect(host.textContent).toContain('Validation context'); - expect(host.textContent).toContain('Tests use 321. Default applies unless'); + expect(host.textContent).toContain('321'); + expect(host.textContent).toContain('Uses default'); + expect(host.textContent).not.toContain('Validation context'); expect(host.textContent).not.toContain('Project context: 321'); expect(host.textContent).not.toContain('Current context: 321'); expect(host.textContent).not.toContain('Managing selected project profile'); expect(host.textContent).not.toContain('/Users/belief/dev/projects/321'); }); - it('renders configured OpenCode model routes with local proof actions', async () => { + it('opens the provider catalog with an explicit all-projects destination', async () => { const host = document.createElement('div'); document.body.appendChild(host); const root = createRoot(host); @@ -774,231 +767,8 @@ describe('RuntimeProviderManagementPanelView', () => { sourceLabel: 'llama.cpp', free: false, default: false, - availability: 'untested' as const, - accessKind: 'configured_authless' as const, - routeKind: 'configured_local' as const, - proofState: 'needs_probe' as const, - requiresExecutionProof: true, - accessReason: 'Execution proof required', - }; - - await act(async () => { - root.render( - React.createElement(RuntimeProviderManagementPanelView, { - state: createState({ - view: { - ...createState().view!, - configuredModels: [configuredModel], - }, - selectedModelId: 'llama.cpp/qwen-test:0.5b', - }), - actions, - disabled: false, - projectPath: '/tmp/project', - }) - ); - await Promise.resolve(); - }); - - await selectOpenCodeTab(host, 'Models'); - - const row = host.querySelector( - '[data-testid="configured-opencode-model-row-llama.cpp/qwen-test:0.5b"]' - ); - expect(host.textContent).toContain('OpenCode model routes'); - expect(host.textContent).toContain('Known routes from OpenCode config'); - expect(row?.textContent).toContain('local'); - expect(row?.textContent).toContain('known route'); - expect(row?.textContent).toContain('needs test'); - - const buttons = Array.from(row?.querySelectorAll('button') ?? []); - await act(async () => { - buttons.find((button) => button.textContent?.includes('Test'))?.click(); - await Promise.resolve(); - }); - await act(async () => { - buttons.find((button) => button.textContent?.includes('Save for team picker'))?.click(); - await Promise.resolve(); - }); - await act(async () => { - buttons.find((button) => button.textContent?.includes('Set all-projects default'))?.click(); - await Promise.resolve(); - }); - - expect(actions.testModel).toHaveBeenCalledWith('llama.cpp', 'llama.cpp/qwen-test:0.5b'); - expect(actions.useModelForNewTeams).not.toHaveBeenCalled(); - expect(actions.setDefaultModel).toHaveBeenCalledWith( - 'llama.cpp', - 'llama.cpp/qwen-test:0.5b', - 'all_projects' - ); - }); - - it('pretty-prints structured model probe errors without escaped JSON', async () => { - const host = document.createElement('div'); - document.body.appendChild(host); - const root = createRoot(host); - const modelId = 'lmstudio/meta/llama-3.3-70b'; - const nestedError = { - code: 500, - message: 'Compute error.', - type: 'server_error', - }; - const payload = { - error: JSON.stringify(nestedError), - attempts: [{ route: 'predict', status: 500 }], - }; - const configuredModel = { - providerId: 'lmstudio', - modelId, - displayName: 'meta/llama-3.3-70b', - sourceLabel: 'LMStudio', - free: false, - default: false, - availability: 'unavailable' as const, - accessKind: 'execution_failed' as const, - routeKind: 'configured_local' as const, - proofState: 'failed' as const, - requiresExecutionProof: true, - accessReason: 'Compute error', - }; - - await act(async () => { - root.render( - React.createElement(RuntimeProviderManagementPanelView, { - state: createState({ - view: { - ...createState().view!, - configuredModels: [configuredModel], - }, - modelResults: { - [modelId]: { - providerId: 'lmstudio', - modelId, - ok: false, - availability: 'unavailable', - message: JSON.stringify( - `Engine protocol {model=llama} returned an error: ${JSON.stringify(payload)}` - ), - diagnostics: ['OpenCode session status retry - Cannot connect to API'], - failureCode: 'provider_endpoint_unreachable', - effectiveBaseUrl: 'http://127.0.0.1:11434/v1', - providerSource: 'config', - }, - }, - }), - actions: createActions(), - disabled: false, - projectPath: '/tmp/project', - }) - ); - await Promise.resolve(); - }); - - await selectOpenCodeTab(host, 'Models'); - - const result = host.querySelector( - `[data-testid="runtime-provider-model-result-${modelId}"]` - ); - const details = result?.querySelector('pre'); - expect(result?.textContent).toContain('Engine protocol {model=llama} returned an error'); - expect(result?.textContent).not.toContain('\\"'); - expect(details?.textContent).toBe( - JSON.stringify({ error: nestedError, attempts: payload.attempts }, null, 2) - ); - expect( - result?.querySelector( - `[data-testid="runtime-provider-model-metadata-${modelId}"]` - )?.textContent - ).toBe( - JSON.stringify( - { - failureCode: 'provider_endpoint_unreachable', - effectiveBaseUrl: 'http://127.0.0.1:11434/v1', - providerSource: 'config', - }, - null, - 2 - ) - ); - expect(result?.textContent).toContain('OpenCode session status retry - Cannot connect to API'); - - await act(async () => { - root.render( - React.createElement(RuntimeProviderManagementPanelView, { - state: createState({ - view: { - ...createState().view!, - configuredModels: [configuredModel], - }, - modelResults: { - [modelId]: { - providerId: 'lmstudio', - modelId, - ok: false, - availability: 'unavailable', - message: 'Engine [predict] failed: {not valid JSON}', - diagnostics: [], - }, - }, - }), - actions: createActions(), - disabled: false, - projectPath: '/tmp/project', - }) - ); - await Promise.resolve(); - }); - - expect(result?.textContent).toContain('Engine [predict] failed: {not valid JSON}'); - expect(result?.querySelector('pre')).toBeNull(); - - const longMalformedPrefix = `Engine "predict failed ${'{'.repeat(5_000)}`; - await act(async () => { - root.render( - React.createElement(RuntimeProviderManagementPanelView, { - state: createState({ - view: { - ...createState().view!, - configuredModels: [configuredModel], - }, - modelResults: { - [modelId]: { - providerId: 'lmstudio', - modelId, - ok: false, - availability: 'unavailable', - message: `${longMalformedPrefix}: ${JSON.stringify(payload)}`, - diagnostics: [], - }, - }, - }), - actions: createActions(), - disabled: false, - projectPath: '/tmp/project', - }) - ); - await Promise.resolve(); - }); - - expect(result?.querySelector('pre')?.textContent).toBe( - JSON.stringify({ error: nestedError, attempts: payload.attempts }, null, 2) - ); - }); - - it('renders Kiro as configured instead of local or free across route badges and search', async () => { - const host = document.createElement('div'); - document.body.appendChild(host); - const root = createRoot(host); - const kiroModel = { - providerId: 'kiro', - modelId: 'kiro/auto', - displayName: 'auto', - sourceLabel: 'Kiro', - free: true, - default: false, availability: 'available' as const, - accessKind: 'credentialed' as const, + accessKind: 'verified' as const, routeKind: 'configured_local' as const, proofState: 'verified' as const, requiresExecutionProof: false, @@ -1011,12 +781,12 @@ describe('RuntimeProviderManagementPanelView', () => { state: createState({ view: { ...createState().view!, - configuredModels: [kiroModel], + configuredModels: [configuredModel], }, }), - actions: createActions(), + actions, disabled: false, - projectPath: '/tmp/project', + projectPath: '/tmp/project-a', }) ); await Promise.resolve(); @@ -1024,55 +794,24 @@ describe('RuntimeProviderManagementPanelView', () => { await selectOpenCodeTab(host, 'Models'); - const row = host.querySelector( - '[data-testid="configured-opencode-model-row-kiro/auto"]' - ); - expect(row?.textContent).toContain('configured'); - expect(row?.textContent).toContain('known route'); - expect(row?.textContent).not.toContain('local'); - expect(row?.textContent).not.toContain('free'); - - const searchInput = host.querySelector( - 'input[placeholder="Search model routes"]' + const changeButtons = Array.from(host.querySelectorAll('button')).filter( + (button) => button.textContent?.trim() === 'Change' ); - expect(searchInput).not.toBeNull(); - await act(async () => { - setInputValue(searchInput!, 'local'); + changeButtons[0]?.click(); await Promise.resolve(); }); - expect( - host.querySelector('[data-testid="configured-opencode-model-row-kiro/auto"]') - ).toBeNull(); - await act(async () => { - setInputValue(searchInput!, 'configured'); - await Promise.resolve(); - }); - expect( - host.querySelector('[data-testid="configured-opencode-model-row-kiro/auto"]') - ).not.toBeNull(); + expect(host.textContent).toContain('Choose a model for projects without an override.'); + expect(host.querySelector('[data-testid="runtime-provider-catalog-list"]')).not.toBeNull(); + expect(actions.setDefaultModel).not.toHaveBeenCalled(); }); - it('can set an all-projects OpenCode default from the model scope controls', async () => { + it('clears a project override through the explicit Use default action', async () => { const host = document.createElement('div'); document.body.appendChild(host); const root = createRoot(host); const actions = createActions(); - const configuredModel = { - providerId: 'llama.cpp', - modelId: 'llama.cpp/qwen-test:0.5b', - displayName: 'qwen-test:0.5b', - sourceLabel: 'llama.cpp', - free: false, - default: false, - availability: 'available' as const, - accessKind: 'verified' as const, - routeKind: 'configured_local' as const, - proofState: 'verified' as const, - requiresExecutionProof: false, - accessReason: null, - }; await act(async () => { root.render( @@ -1080,7 +819,10 @@ describe('RuntimeProviderManagementPanelView', () => { state: createState({ view: { ...createState().view!, - configuredModels: [configuredModel], + defaultModel: 'openrouter/project-model', + projectDefaultModel: 'openrouter/project-model', + allProjectsDefaultModel: 'openrouter/base-model', + defaultModelSource: 'project', }, }), actions, @@ -1090,104 +832,19 @@ describe('RuntimeProviderManagementPanelView', () => { ); await Promise.resolve(); }); - await selectOpenCodeTab(host, 'Models'); + expect(host.textContent).toContain('Project override'); await act(async () => { Array.from(host.querySelectorAll('button')) - .find((button) => button.textContent?.includes('Set all-projects default')) + .find((button) => button.textContent?.trim() === 'Use default') ?.click(); await Promise.resolve(); }); - - expect(host.textContent).toContain( - 'Default for every project that does not have its own OpenCode override' - ); - expect(host.textContent).toContain('Validation context'); - expect(actions.setDefaultModel).toHaveBeenCalledWith( - 'llama.cpp', - 'llama.cpp/qwen-test:0.5b', - 'all_projects' - ); + expect(actions.clearProjectDefault).toHaveBeenCalledOnce(); }); - it('filters launchable OpenCode model routes by route text', async () => { - const host = document.createElement('div'); - document.body.appendChild(host); - const root = createRoot(host); - const localModel = { - providerId: 'llama.cpp', - modelId: 'llama.cpp/qwen-test:0.5b', - displayName: 'qwen-test:0.5b', - sourceLabel: 'llama.cpp', - free: false, - default: false, - availability: 'available' as const, - accessKind: 'verified' as const, - routeKind: 'configured_local' as const, - proofState: 'verified' as const, - requiresExecutionProof: false, - accessReason: null, - }; - const freeModel = { - providerId: 'opencode', - modelId: 'opencode/big-pickle', - displayName: 'big-pickle', - sourceLabel: 'OpenCode', - free: true, - default: false, - availability: 'available' as const, - accessKind: 'builtin_free' as const, - routeKind: 'builtin_free' as const, - proofState: 'not_required' as const, - requiresExecutionProof: false, - accessReason: null, - }; - - await act(async () => { - root.render( - React.createElement(RuntimeProviderManagementPanelView, { - state: createState({ - view: { - ...createState().view!, - configuredModels: [localModel, freeModel], - }, - }), - actions: createActions(), - disabled: false, - projectPath: '/tmp/project-a', - }) - ); - await Promise.resolve(); - }); - - await selectOpenCodeTab(host, 'Models'); - - const searchInput = host.querySelector( - 'input[placeholder="Search model routes"]' - ); - expect(searchInput).not.toBeNull(); - expect(host.textContent).toContain('qwen-test:0.5b'); - expect(host.textContent).toContain('big-pickle'); - - await act(async () => { - setInputValue(searchInput!, 'pickle'); - await Promise.resolve(); - }); - - expect(host.textContent).not.toContain('qwen-test:0.5b'); - expect(host.textContent).toContain('big-pickle'); - - await act(async () => { - setInputValue(searchInput!, 'missing-route'); - await Promise.resolve(); - }); - - expect(host.textContent).toContain('No OpenCode model routes match'); - expect(host.textContent).toContain('missing-route'); - }); - - it('opens providers first and keeps launchable routes in a separate tab', async () => { + it('opens providers first and keeps inheritance in a separate tab', async () => { const host = document.createElement('div'); document.body.appendChild(host); const root = createRoot(host); @@ -1232,32 +889,15 @@ describe('RuntimeProviderManagementPanelView', () => { await selectOpenCodeTab(host, 'Models'); - expect(host.textContent).toContain('OpenCode model routes'); - expect(host.textContent).toContain('llama.cpp/qwen-test:0.5b'); - expect(host.textContent).toContain( - 'Select a validation context above to enable Test and Set default' - ); + expect(host.textContent).toContain('OpenCode defaults'); + expect(host.textContent).toContain('Projects use this model unless they choose another.'); expect(host.querySelector('[data-testid="runtime-provider-row-openrouter"]')).toBeNull(); - - const row = host.querySelector( - '[data-testid="configured-opencode-model-row-llama.cpp/qwen-test:0.5b"]' - ); - const buttons = Array.from(row?.querySelectorAll('button') ?? []); - expect(buttons.map((button) => [button.textContent?.trim(), button.disabled])).toEqual([ - ['Test', true], - ['Save for team picker', true], - ['Set all-projects default', true], - ]); expect( - Array.from(row?.querySelectorAll('[title]') ?? []).some( - (element) => - element.getAttribute('title') === - 'Select a project context before testing or saving OpenCode defaults.' - ) - ).toBe(true); + host.querySelector('[data-testid="configured-opencode-model-row-llama.cpp/qwen-test:0.5b"]') + ).toBeNull(); }); - it('shows unknown OpenCode defaults without enabling launch actions', async () => { + it('keeps an unknown OpenCode default visible as its raw model id', async () => { const host = document.createElement('div'); document.body.appendChild(host); const root = createRoot(host); @@ -1284,6 +924,9 @@ describe('RuntimeProviderManagementPanelView', () => { view: { ...createState().view!, configuredModels: [unknownDefaultModel], + defaultModel: unknownDefaultModel.modelId, + allProjectsDefaultModel: unknownDefaultModel.modelId, + defaultModelSource: 'all_projects', }, }), actions, @@ -1295,25 +938,8 @@ describe('RuntimeProviderManagementPanelView', () => { await selectOpenCodeTab(host, 'Models'); - const row = host.querySelector( - '[data-testid="configured-opencode-model-row-openrouter/moonshotai/kimi-k2"]' - ); - expect(row?.textContent).toContain('unknown'); - expect(row?.textContent).toContain('default'); - - const buttons = Array.from(row?.querySelectorAll('button') ?? []); - expect(buttons.map((button) => button.disabled)).toEqual([true, true, true]); - expect( - Array.from(row?.querySelectorAll('[title]') ?? []).some( - (element) => - element.getAttribute('title') === - 'This model is the current OpenCode default, but it is not available in the live catalog yet.' - ) - ).toBe(true); - await act(async () => { - buttons.forEach((button) => button.click()); - await Promise.resolve(); - }); + expect(host.textContent).toContain('openrouter/moonshotai/kimi-k2'); + expect(host.querySelector('[data-testid="opencode-default-inheritance"]')).not.toBeNull(); expect(actions.testModel).not.toHaveBeenCalled(); expect(actions.useModelForNewTeams).not.toHaveBeenCalled(); expect(actions.setDefaultModel).not.toHaveBeenCalled(); @@ -3031,7 +2657,8 @@ describe('RuntimeProviderManagementPanelView', () => { }); expect(host.textContent).toContain('openrouter/openai/gpt-oss-20b:free'); - expect(host.textContent).toContain('Saved for team picker'); + expect(host.textContent).not.toContain('Set all-projects default'); + expect(host.textContent).not.toContain('Set project default'); expect(host.textContent).toContain('Model probe passed'); expect(host.textContent).toContain('Recommended'); expect(host.textContent).toContain('Not recommended'); @@ -3097,9 +2724,11 @@ describe('RuntimeProviderManagementPanelView', () => { await Promise.resolve(); }); - expect(actions.useModelForNewTeams).toHaveBeenCalledWith('openrouter/openai/gpt-oss-20b:free'); + expect(actions.useModelForNewTeams).not.toHaveBeenCalled(); expect(actions.selectProvider).not.toHaveBeenCalled(); + expect(actions.setDefaultModel).not.toHaveBeenCalled(); + vi.mocked(actions.useModelForNewTeams).mockClear(); await act(async () => { const notRecommendedRow = host.querySelector( @@ -3233,17 +2862,11 @@ describe('RuntimeProviderManagementPanelView', () => { const verifiedRow = host.querySelector( '[data-testid="runtime-provider-model-row-github-copilot/gpt-5-mini"]' ); - expect(verifiedRow?.getAttribute('role')).toBe('button'); + expect(verifiedRow?.getAttribute('role')).toBeNull(); expect( host.querySelector('[data-testid="runtime-provider-copilot-access-summary"]')?.textContent ).toContain('1 reported, 1 verified'); - await act(async () => { - verifiedRow?.click(); - await Promise.resolve(); - }); - expect(actions.useModelForNewTeams).toHaveBeenCalledWith( - 'github-copilot/gpt-5-mini' - ); + expect(actions.setDefaultModel).not.toHaveBeenCalled(); }); it('marks deprecated catalog models and prevents selecting them for new teams', async () => { @@ -3833,9 +3456,8 @@ describe('RuntimeProviderManagementPanelView', () => { await Promise.resolve(); }); - expect(actions.useModelForNewTeams).toHaveBeenCalledWith( - 'openrouter/google/gemini-3-flash-preview' - ); + expect(actions.useModelForNewTeams).not.toHaveBeenCalled(); + expect(actions.setDefaultModel).not.toHaveBeenCalled(); expect(actions.selectDirectoryProvider).not.toHaveBeenCalled(); expect(host.textContent).toContain('google/gemini-3-flash-preview'); expect(host.textContent).not.toContain('No models found.'); diff --git a/test/renderer/features/runtime-provider-management/RuntimeProviderOnboardingView.test.ts b/test/renderer/features/runtime-provider-management/RuntimeProviderOnboardingView.test.ts index 320024a98f..88217c047c 100644 --- a/test/renderer/features/runtime-provider-management/RuntimeProviderOnboardingView.test.ts +++ b/test/renderer/features/runtime-provider-management/RuntimeProviderOnboardingView.test.ts @@ -106,7 +106,8 @@ function managementActions(): RuntimeProviderManagementActions { message: 'Model probe passed', diagnostics: [], })), - setDefaultModel: vi.fn(async () => undefined), + setDefaultModel: vi.fn(async () => true), + clearProjectDefault: vi.fn(async () => undefined), }; } diff --git a/test/renderer/features/runtime-provider-management/useRuntimeProviderManagement.test.ts b/test/renderer/features/runtime-provider-management/useRuntimeProviderManagement.test.ts index 875e6724a2..433e25dfdc 100644 --- a/test/renderer/features/runtime-provider-management/useRuntimeProviderManagement.test.ts +++ b/test/renderer/features/runtime-provider-management/useRuntimeProviderManagement.test.ts @@ -1150,7 +1150,7 @@ describe('useRuntimeProviderManagement', () => { await Promise.resolve(); }); - let setDefault: Promise | null = null; + let setDefault: Promise | null = null; await act(async () => { setDefault = actions?.setDefaultModel('llama.cpp', projectAModelId, 'project') ?? null; await Promise.resolve(); @@ -1189,7 +1189,7 @@ describe('useRuntimeProviderManagement', () => { expect(state?.view?.projectPath).toBe('/tmp/project-b'); expect(state?.view?.defaultModel).toBe('opencode/project-b'); - expect(state?.selectedModelId).toBeNull(); + expect(state?.selectedModelId).toBe('opencode/project-b'); expect(state?.successMessage).toBeNull(); await act(async () => { @@ -3327,13 +3327,19 @@ describe('useRuntimeProviderManagement', () => { value: { runtimeProviderManagement: { setDefaultModel, + loadView: setDefaultModel, }, } as unknown as ElectronAPI, }); const root = createRoot(host); await act(async () => { - root.render(React.createElement(Harness)); + root.render( + React.createElement(ConfigurableHarness, { + enabled: true, + loadViewOnEnable: false, + }) + ); await Promise.resolve(); }); @@ -3360,4 +3366,50 @@ describe('useRuntimeProviderManagement', () => { accessKind: 'verified', }); }); + + it('clears a project override and immediately resolves to the all-projects default', async () => { + const clearProjectDefaultModel = vi.fn(() => + Promise.resolve({ + schemaVersion: 1 as const, + runtimeId: 'opencode' as const, + view: { + ...createRuntimeView(), + projectPath: '/tmp/project-a', + projectDefaultModel: null, + allProjectsDefaultModel: 'openrouter/base-model', + defaultModel: 'openrouter/base-model', + defaultModelSource: 'all_projects' as const, + }, + }) + ); + Object.defineProperty(window, 'electronAPI', { + configurable: true, + value: { + runtimeProviderManagement: { + clearProjectDefaultModel, + loadView: clearProjectDefaultModel, + }, + } as unknown as ElectronAPI, + }); + const root = createRoot(host); + await act(async () => { + root.render(React.createElement(EnabledHarness, { projectPath: '/tmp/project-a' })); + await Promise.resolve(); + }); + + clearProjectDefaultModel.mockClear(); + + await act(async () => { + await actions?.clearProjectDefault(); + await Promise.resolve(); + }); + + expect(clearProjectDefaultModel).toHaveBeenCalledWith({ + runtimeId: 'opencode', + projectPath: '/tmp/project-a', + }); + expect(state?.view?.projectDefaultModel).toBeNull(); + expect(state?.view?.defaultModel).toBe('openrouter/base-model'); + expect(state?.selectedModelId).toBe('openrouter/base-model'); + }); }); From 22137acf492c572b030aae5937b15a86d5e9defa Mon Sep 17 00:00:00 2001 From: 777genius Date: Thu, 20 Aug 2026 17:17:25 +0300 Subject: [PATCH 03/20] feat(team): clarify inherited OpenCode model selection --- .../renderer/locales/ar/team.json | 4 +- .../renderer/locales/bn/team.json | 4 +- .../renderer/locales/de/team.json | 4 +- .../renderer/locales/en/team.json | 4 +- .../renderer/locales/es/team.json | 4 +- .../renderer/locales/fa/team.json | 4 +- .../renderer/locales/fil/team.json | 4 +- .../renderer/locales/fr/team.json | 4 +- .../renderer/locales/hi/team.json | 4 +- .../renderer/locales/id/team.json | 4 +- .../renderer/locales/it/team.json | 4 +- .../renderer/locales/ja/team.json | 4 +- .../renderer/locales/ko/team.json | 4 +- .../renderer/locales/mr/team.json | 4 +- .../renderer/locales/ms/team.json | 4 +- .../renderer/locales/nl/team.json | 4 +- .../renderer/locales/pl/team.json | 4 +- .../renderer/locales/pt/team.json | 4 +- .../renderer/locales/ro/team.json | 4 +- .../renderer/locales/ru/team.json | 4 +- .../renderer/locales/sw/team.json | 4 +- .../renderer/locales/ta/team.json | 4 +- .../renderer/locales/te/team.json | 4 +- .../renderer/locales/th/team.json | 4 +- .../renderer/locales/tr/team.json | 4 +- .../renderer/locales/uk/team.json | 4 +- .../renderer/locales/ur/team.json | 4 +- .../renderer/locales/vi/team.json | 4 +- .../renderer/locales/zh/team.json | 4 +- .../localization/renderer/resources.d.ts | 2 + .../team/dialogs/TeamModelSelector.tsx | 93 ++++++++----------- .../team/dialogs/teamModelFreshness.ts | 48 ++++++++++ 32 files changed, 175 insertions(+), 84 deletions(-) create mode 100644 src/renderer/components/team/dialogs/teamModelFreshness.ts diff --git a/src/features/localization/renderer/locales/ar/team.json b/src/features/localization/renderer/locales/ar/team.json index 248ef1cbf6..a4fb0e55f2 100644 --- a/src/features/localization/renderer/locales/ar/team.json +++ b/src/features/localization/renderer/locales/ar/team.json @@ -899,7 +899,9 @@ "runtime": "ويستخدم التخلف عن الدفع في الوقت المناسب للمزود المختار." }, "multimodelOff": "مضاعف", - "unavailableInRuntime": "غير متاح في الوقت الحالي" + "unavailableInRuntime": "غير متاح في الوقت الحالي", + "defaultWithResolved": "Default - {{model}}", + "explicitChoice": "Explicit choice - {{model}}" }, "taskDetail": { "actions": { diff --git a/src/features/localization/renderer/locales/bn/team.json b/src/features/localization/renderer/locales/bn/team.json index 6c9436bc19..8963f1a979 100644 --- a/src/features/localization/renderer/locales/bn/team.json +++ b/src/features/localization/renderer/locales/bn/team.json @@ -899,7 +899,9 @@ "runtime": "নির্বাচিত উপলব্ধকারীর ডিফল্ট পদ্ধতি ব্যবহার করতে ব্যবহৃত হয় ।" }, "multimodelOff": "মাল্টি মডেল বন্ধ করুন", - "unavailableInRuntime": "বর্তমানে চলমান নয়" + "unavailableInRuntime": "বর্তমানে চলমান নয়", + "defaultWithResolved": "Default - {{model}}", + "explicitChoice": "Explicit choice - {{model}}" }, "taskDetail": { "actions": { diff --git a/src/features/localization/renderer/locales/de/team.json b/src/features/localization/renderer/locales/de/team.json index 1bd37a94ce..4aece6f844 100644 --- a/src/features/localization/renderer/locales/de/team.json +++ b/src/features/localization/renderer/locales/de/team.json @@ -899,7 +899,9 @@ "runtime": "Benutzt die Laufzeiteinstellung für den ausgewählten Anbieter." }, "multimodelOff": "Multimodel ab", - "unavailableInRuntime": "Nicht verfügbar in der aktuellen Laufzeit" + "unavailableInRuntime": "Nicht verfügbar in der aktuellen Laufzeit", + "defaultWithResolved": "Default - {{model}}", + "explicitChoice": "Explicit choice - {{model}}" }, "taskDetail": { "actions": { diff --git a/src/features/localization/renderer/locales/en/team.json b/src/features/localization/renderer/locales/en/team.json index b924955e62..7c9905d04c 100644 --- a/src/features/localization/renderer/locales/en/team.json +++ b/src/features/localization/renderer/locales/en/team.json @@ -934,7 +934,9 @@ "runtime": "Uses the runtime default for the selected provider." }, "multimodelOff": "Multimodel off", - "unavailableInRuntime": "Unavailable in current runtime" + "unavailableInRuntime": "Unavailable in current runtime", + "defaultWithResolved": "Default - {{model}}", + "explicitChoice": "Explicit choice - {{model}}" }, "taskDetail": { "actions": { diff --git a/src/features/localization/renderer/locales/es/team.json b/src/features/localization/renderer/locales/es/team.json index f4b95dfb7e..b4e8e34c12 100644 --- a/src/features/localization/renderer/locales/es/team.json +++ b/src/features/localization/renderer/locales/es/team.json @@ -899,7 +899,9 @@ "runtime": "Utiliza el predeterminado de tiempo de ejecución para el proveedor seleccionado." }, "multimodelOff": "Multimodel off", - "unavailableInRuntime": "No disponible en tiempo de ejecución actual" + "unavailableInRuntime": "No disponible en tiempo de ejecución actual", + "defaultWithResolved": "Default - {{model}}", + "explicitChoice": "Explicit choice - {{model}}" }, "taskDetail": { "actions": { diff --git a/src/features/localization/renderer/locales/fa/team.json b/src/features/localization/renderer/locales/fa/team.json index 237e01830c..7325e168a9 100644 --- a/src/features/localization/renderer/locales/fa/team.json +++ b/src/features/localization/renderer/locales/fa/team.json @@ -899,7 +899,9 @@ "runtime": "از پیش‌فرض زمان اجرا برای ارائه‌دهنده انتخاب‌شده استفاده می‌کند." }, "multimodelOff": "چندمدلی خاموش", - "unavailableInRuntime": "در زمان اجرای فعلی در دسترس نیست" + "unavailableInRuntime": "در زمان اجرای فعلی در دسترس نیست", + "defaultWithResolved": "Default - {{model}}", + "explicitChoice": "Explicit choice - {{model}}" }, "taskDetail": { "actions": { diff --git a/src/features/localization/renderer/locales/fil/team.json b/src/features/localization/renderer/locales/fil/team.json index e29b776f3a..f9c5c83818 100644 --- a/src/features/localization/renderer/locales/fil/team.json +++ b/src/features/localization/renderer/locales/fil/team.json @@ -899,7 +899,9 @@ "runtime": "Ginagamit ang runtime default para sa napiling provider." }, "multimodelOff": "Naka-off ang Multimodel", - "unavailableInRuntime": "Hindi available sa kasalukuyang runtime" + "unavailableInRuntime": "Hindi available sa kasalukuyang runtime", + "defaultWithResolved": "Default - {{model}}", + "explicitChoice": "Explicit choice - {{model}}" }, "taskDetail": { "actions": { diff --git a/src/features/localization/renderer/locales/fr/team.json b/src/features/localization/renderer/locales/fr/team.json index db0fc1c98e..a34e223455 100644 --- a/src/features/localization/renderer/locales/fr/team.json +++ b/src/features/localization/renderer/locales/fr/team.json @@ -899,7 +899,9 @@ "runtime": "Utilise la valeur par défaut d'exécution pour le fournisseur sélectionné." }, "multimodelOff": "Modèle multiple désactivé", - "unavailableInRuntime": "Indisponible dans l'exécution actuelle" + "unavailableInRuntime": "Indisponible dans l'exécution actuelle", + "defaultWithResolved": "Default - {{model}}", + "explicitChoice": "Explicit choice - {{model}}" }, "taskDetail": { "actions": { diff --git a/src/features/localization/renderer/locales/hi/team.json b/src/features/localization/renderer/locales/hi/team.json index 5ef4ccc0e5..931baf1236 100644 --- a/src/features/localization/renderer/locales/hi/team.json +++ b/src/features/localization/renderer/locales/hi/team.json @@ -899,7 +899,9 @@ "runtime": "चयनित प्रदाता के लिए रनटाइम डिफ़ॉल्ट का उपयोग करता है।" }, "multimodelOff": "मल्टीमॉडल बंद", - "unavailableInRuntime": "वर्तमान समय में अनुपलब्ध" + "unavailableInRuntime": "वर्तमान समय में अनुपलब्ध", + "defaultWithResolved": "Default - {{model}}", + "explicitChoice": "Explicit choice - {{model}}" }, "taskDetail": { "actions": { diff --git a/src/features/localization/renderer/locales/id/team.json b/src/features/localization/renderer/locales/id/team.json index ffc8b6da74..a2018a6193 100644 --- a/src/features/localization/renderer/locales/id/team.json +++ b/src/features/localization/renderer/locales/id/team.json @@ -899,7 +899,9 @@ "runtime": "Menggunakan waktu-jalan default bagi penyedia yang dipilih." }, "multimodelOff": "Matikan multimodel", - "unavailableInRuntime": "Tidak tersedia dalam waktu-jalan saat ini" + "unavailableInRuntime": "Tidak tersedia dalam waktu-jalan saat ini", + "defaultWithResolved": "Default - {{model}}", + "explicitChoice": "Explicit choice - {{model}}" }, "taskDetail": { "actions": { diff --git a/src/features/localization/renderer/locales/it/team.json b/src/features/localization/renderer/locales/it/team.json index 7cfde0089d..1ef3de898e 100644 --- a/src/features/localization/renderer/locales/it/team.json +++ b/src/features/localization/renderer/locales/it/team.json @@ -899,7 +899,9 @@ "runtime": "Usa il valore predefinito del runtime per il provider selezionato." }, "multimodelOff": "Multimodello disattivato", - "unavailableInRuntime": "Non disponibile nel runtime attuale" + "unavailableInRuntime": "Non disponibile nel runtime attuale", + "defaultWithResolved": "Default - {{model}}", + "explicitChoice": "Explicit choice - {{model}}" }, "taskDetail": { "actions": { diff --git a/src/features/localization/renderer/locales/ja/team.json b/src/features/localization/renderer/locales/ja/team.json index eeb2306516..8b8817506f 100644 --- a/src/features/localization/renderer/locales/ja/team.json +++ b/src/features/localization/renderer/locales/ja/team.json @@ -899,7 +899,9 @@ "runtime": "選択したプロバイダのランタイムデフォルトを使用します。" }, "multimodelOff": "マルチモデルオフ", - "unavailableInRuntime": "現在のランタイムで利用不可" + "unavailableInRuntime": "現在のランタイムで利用不可", + "defaultWithResolved": "Default - {{model}}", + "explicitChoice": "Explicit choice - {{model}}" }, "taskDetail": { "actions": { diff --git a/src/features/localization/renderer/locales/ko/team.json b/src/features/localization/renderer/locales/ko/team.json index 03fd356edb..31cb13cc22 100644 --- a/src/features/localization/renderer/locales/ko/team.json +++ b/src/features/localization/renderer/locales/ko/team.json @@ -899,7 +899,9 @@ "runtime": "선택한 공급자의 runtime default를 사용합니다." }, "multimodelOff": "Multimodel 오프", - "unavailableInRuntime": "현재 런타임에서 사용 가능" + "unavailableInRuntime": "현재 런타임에서 사용 가능", + "defaultWithResolved": "Default - {{model}}", + "explicitChoice": "Explicit choice - {{model}}" }, "taskDetail": { "actions": { diff --git a/src/features/localization/renderer/locales/mr/team.json b/src/features/localization/renderer/locales/mr/team.json index 8464c5fc66..adf510d8aa 100644 --- a/src/features/localization/renderer/locales/mr/team.json +++ b/src/features/localization/renderer/locales/mr/team.json @@ -899,7 +899,9 @@ "runtime": "निवडलेल्या प्रोव्हायडरसाठी रनटाइम डीफॉल्ट वापरते." }, "multimodelOff": "मल्टीमॉडेल बंद", - "unavailableInRuntime": "सध्याच्या रनटाइममध्ये उपलब्ध नाही" + "unavailableInRuntime": "सध्याच्या रनटाइममध्ये उपलब्ध नाही", + "defaultWithResolved": "Default - {{model}}", + "explicitChoice": "Explicit choice - {{model}}" }, "taskDetail": { "actions": { diff --git a/src/features/localization/renderer/locales/ms/team.json b/src/features/localization/renderer/locales/ms/team.json index 1b31092505..480e56a8cc 100644 --- a/src/features/localization/renderer/locales/ms/team.json +++ b/src/features/localization/renderer/locales/ms/team.json @@ -899,7 +899,9 @@ "runtime": "Menggunakan lalai runtime untuk pembekal yang dipilih." }, "multimodelOff": "Multimodel mati", - "unavailableInRuntime": "Tidak tersedia dalam runtime semasa" + "unavailableInRuntime": "Tidak tersedia dalam runtime semasa", + "defaultWithResolved": "Default - {{model}}", + "explicitChoice": "Explicit choice - {{model}}" }, "taskDetail": { "actions": { diff --git a/src/features/localization/renderer/locales/nl/team.json b/src/features/localization/renderer/locales/nl/team.json index 79353b72a2..63839c6e96 100644 --- a/src/features/localization/renderer/locales/nl/team.json +++ b/src/features/localization/renderer/locales/nl/team.json @@ -899,7 +899,9 @@ "runtime": "Gebruikt de runtimestandaard voor de geselecteerde provider." }, "multimodelOff": "Multimodel uit", - "unavailableInRuntime": "Niet beschikbaar in huidige runtime" + "unavailableInRuntime": "Niet beschikbaar in huidige runtime", + "defaultWithResolved": "Default - {{model}}", + "explicitChoice": "Explicit choice - {{model}}" }, "taskDetail": { "actions": { diff --git a/src/features/localization/renderer/locales/pl/team.json b/src/features/localization/renderer/locales/pl/team.json index c46f90cacd..c33a923af9 100644 --- a/src/features/localization/renderer/locales/pl/team.json +++ b/src/features/localization/renderer/locales/pl/team.json @@ -899,7 +899,9 @@ "runtime": "Używa domyślnego modelu środowiska uruchomieniowego dla wybranego dostawcy." }, "multimodelOff": "Wielomodelowy wył.", - "unavailableInRuntime": "Niedostępne w bieżącym środowisku uruchomieniowym" + "unavailableInRuntime": "Niedostępne w bieżącym środowisku uruchomieniowym", + "defaultWithResolved": "Default - {{model}}", + "explicitChoice": "Explicit choice - {{model}}" }, "taskDetail": { "actions": { diff --git a/src/features/localization/renderer/locales/pt/team.json b/src/features/localization/renderer/locales/pt/team.json index ebe2e1378b..867aaca897 100644 --- a/src/features/localization/renderer/locales/pt/team.json +++ b/src/features/localization/renderer/locales/pt/team.json @@ -899,7 +899,9 @@ "runtime": "Usa o padrão de tempo de execução para o provedor selecionado." }, "multimodelOff": "Multimodelo desligado", - "unavailableInRuntime": "Indisponível no tempo de execução atual" + "unavailableInRuntime": "Indisponível no tempo de execução atual", + "defaultWithResolved": "Default - {{model}}", + "explicitChoice": "Explicit choice - {{model}}" }, "taskDetail": { "actions": { diff --git a/src/features/localization/renderer/locales/ro/team.json b/src/features/localization/renderer/locales/ro/team.json index dd747721de..8e37726398 100644 --- a/src/features/localization/renderer/locales/ro/team.json +++ b/src/features/localization/renderer/locales/ro/team.json @@ -899,7 +899,9 @@ "runtime": "Folosește valoarea implicită a runtime-ului pentru furnizorul selectat." }, "multimodelOff": "Multimodel dezactivat", - "unavailableInRuntime": "Indisponibil în runtime-ul curent" + "unavailableInRuntime": "Indisponibil în runtime-ul curent", + "defaultWithResolved": "Default - {{model}}", + "explicitChoice": "Explicit choice - {{model}}" }, "taskDetail": { "actions": { diff --git a/src/features/localization/renderer/locales/ru/team.json b/src/features/localization/renderer/locales/ru/team.json index 7dc87f7220..9d9b62f882 100644 --- a/src/features/localization/renderer/locales/ru/team.json +++ b/src/features/localization/renderer/locales/ru/team.json @@ -931,7 +931,9 @@ "runtime": "Использует значение по умолчанию из рантайма для выбранного провайдера." }, "multimodelOff": "Multimodel выключен", - "unavailableInRuntime": "Недоступно в текущем рантайме" + "unavailableInRuntime": "Недоступно в текущем рантайме", + "defaultWithResolved": "По умолчанию - {{model}}", + "explicitChoice": "Выбрано явно - {{model}}" }, "taskDetail": { "actions": { diff --git a/src/features/localization/renderer/locales/sw/team.json b/src/features/localization/renderer/locales/sw/team.json index e6b49f048f..bc371e3de1 100644 --- a/src/features/localization/renderer/locales/sw/team.json +++ b/src/features/localization/renderer/locales/sw/team.json @@ -899,7 +899,9 @@ "runtime": "Inatumia chaguo-msingi ya runtime kwa mtoa huduma aliyechaguliwa." }, "multimodelOff": "Modeli nyingi imezimwa", - "unavailableInRuntime": "Haipatikani katika runtime ya sasa" + "unavailableInRuntime": "Haipatikani katika runtime ya sasa", + "defaultWithResolved": "Default - {{model}}", + "explicitChoice": "Explicit choice - {{model}}" }, "taskDetail": { "actions": { diff --git a/src/features/localization/renderer/locales/ta/team.json b/src/features/localization/renderer/locales/ta/team.json index 5b0a7e0aef..ef7b2cbcaa 100644 --- a/src/features/localization/renderer/locales/ta/team.json +++ b/src/features/localization/renderer/locales/ta/team.json @@ -899,7 +899,9 @@ "runtime": "தேர்ந்தெடுத்த வழங்குநருக்கான runtime இயல்புநிலையைப் பயன்படுத்துகிறது." }, "multimodelOff": "Multimodel off", - "unavailableInRuntime": "தற்போதைய runtime இல் கிடைக்கவில்லை" + "unavailableInRuntime": "தற்போதைய runtime இல் கிடைக்கவில்லை", + "defaultWithResolved": "Default - {{model}}", + "explicitChoice": "Explicit choice - {{model}}" }, "taskDetail": { "actions": { diff --git a/src/features/localization/renderer/locales/te/team.json b/src/features/localization/renderer/locales/te/team.json index a2f0eb46d8..cde8d0999a 100644 --- a/src/features/localization/renderer/locales/te/team.json +++ b/src/features/localization/renderer/locales/te/team.json @@ -899,7 +899,9 @@ "runtime": "ఎంచుకున్న ప్రొవైడర్ కోసం రన్‌టైమ్ డిఫాల్ట్‌ను ఉపయోగిస్తుంది." }, "multimodelOff": "Multimodel ఆఫ్", - "unavailableInRuntime": "ప్రస్తుత రన్‌టైమ్‌లో అందుబాటులో లేదు" + "unavailableInRuntime": "ప్రస్తుత రన్‌టైమ్‌లో అందుబాటులో లేదు", + "defaultWithResolved": "Default - {{model}}", + "explicitChoice": "Explicit choice - {{model}}" }, "taskDetail": { "actions": { diff --git a/src/features/localization/renderer/locales/th/team.json b/src/features/localization/renderer/locales/th/team.json index d3411bfe1d..85197e0ccd 100644 --- a/src/features/localization/renderer/locales/th/team.json +++ b/src/features/localization/renderer/locales/th/team.json @@ -899,7 +899,9 @@ "runtime": "ใช้ค่าเริ่มต้นของรันไทม์สำหรับผู้ให้บริการที่เลือก" }, "multimodelOff": "ปิด Multimodel", - "unavailableInRuntime": "ไม่พร้อมใช้งานในรันไทม์ปัจจุบัน" + "unavailableInRuntime": "ไม่พร้อมใช้งานในรันไทม์ปัจจุบัน", + "defaultWithResolved": "Default - {{model}}", + "explicitChoice": "Explicit choice - {{model}}" }, "taskDetail": { "actions": { diff --git a/src/features/localization/renderer/locales/tr/team.json b/src/features/localization/renderer/locales/tr/team.json index dd8b3ecf5e..f6f1d705b7 100644 --- a/src/features/localization/renderer/locales/tr/team.json +++ b/src/features/localization/renderer/locales/tr/team.json @@ -899,7 +899,9 @@ "runtime": "Seçili sağlayıcı için çalışma zamanı varsayılanını kullanır." }, "multimodelOff": "Çoklu model kapalı", - "unavailableInRuntime": "Geçerli çalışma zamanında kullanılamıyor" + "unavailableInRuntime": "Geçerli çalışma zamanında kullanılamıyor", + "defaultWithResolved": "Default - {{model}}", + "explicitChoice": "Explicit choice - {{model}}" }, "taskDetail": { "actions": { diff --git a/src/features/localization/renderer/locales/uk/team.json b/src/features/localization/renderer/locales/uk/team.json index e6e52fc332..33b5ca00f9 100644 --- a/src/features/localization/renderer/locales/uk/team.json +++ b/src/features/localization/renderer/locales/uk/team.json @@ -901,7 +901,9 @@ "runtime": "Використовує значення середовища за замовчуванням для вибраного провайдера." }, "multimodelOff": "Мультимодель вимкнено", - "unavailableInRuntime": "Недоступно в поточному середовищі" + "unavailableInRuntime": "Недоступно в поточному середовищі", + "defaultWithResolved": "За замовчуванням - {{model}}", + "explicitChoice": "Вибрано явно - {{model}}" }, "taskDetail": { "actions": { diff --git a/src/features/localization/renderer/locales/ur/team.json b/src/features/localization/renderer/locales/ur/team.json index 8b315145b1..432a7b0e4b 100644 --- a/src/features/localization/renderer/locales/ur/team.json +++ b/src/features/localization/renderer/locales/ur/team.json @@ -899,7 +899,9 @@ "runtime": "منتخب کردہ فراہم کُنندہ کے لیے چلائیں" }, "multimodelOff": "معطل", - "unavailableInRuntime": "موجودہ دور میں غیر متصل" + "unavailableInRuntime": "موجودہ دور میں غیر متصل", + "defaultWithResolved": "Default - {{model}}", + "explicitChoice": "Explicit choice - {{model}}" }, "taskDetail": { "actions": { diff --git a/src/features/localization/renderer/locales/vi/team.json b/src/features/localization/renderer/locales/vi/team.json index 7fdebd421b..4a19989160 100644 --- a/src/features/localization/renderer/locales/vi/team.json +++ b/src/features/localization/renderer/locales/vi/team.json @@ -899,7 +899,9 @@ "runtime": "Dùng mặc định runtime cho nhà cung cấp đã chọn." }, "multimodelOff": "Đa mô hình tắt", - "unavailableInRuntime": "Không khả dụng trong runtime hiện tại" + "unavailableInRuntime": "Không khả dụng trong runtime hiện tại", + "defaultWithResolved": "Default - {{model}}", + "explicitChoice": "Explicit choice - {{model}}" }, "taskDetail": { "actions": { diff --git a/src/features/localization/renderer/locales/zh/team.json b/src/features/localization/renderer/locales/zh/team.json index d37fcfda79..deb3535b8c 100644 --- a/src/features/localization/renderer/locales/zh/team.json +++ b/src/features/localization/renderer/locales/zh/team.json @@ -899,7 +899,9 @@ "runtime": "使用所选提供商的运行时默认值。" }, "multimodelOff": "多模型关闭", - "unavailableInRuntime": "当前运行时不可用" + "unavailableInRuntime": "当前运行时不可用", + "defaultWithResolved": "Default - {{model}}", + "explicitChoice": "Explicit choice - {{model}}" }, "taskDetail": { "actions": { diff --git a/src/features/localization/renderer/resources.d.ts b/src/features/localization/renderer/resources.d.ts index 9be8cd4aa0..07e1044e7c 100644 --- a/src/features/localization/renderer/resources.d.ts +++ b/src/features/localization/renderer/resources.d.ts @@ -4801,6 +4801,7 @@ export default interface Resources { openCodeWithResolved: 'Uses the OpenCode default model.\nCurrently resolves to {{model}}.'; runtime: 'Uses the runtime default for the selected provider.'; }; + defaultWithResolved: 'Default - {{model}}'; empty: { freeOpenCode: 'No free OpenCode models are available in the current runtime list.'; noModels: 'No models are available in the current runtime list.'; @@ -4808,6 +4809,7 @@ export default interface Resources { recommendedFreeOpenCode: 'No recommended free OpenCode models are available in the current runtime list.'; recommendedOpenCode: 'No recommended OpenCode models are available in the current runtime list.'; }; + explicitChoice: 'Explicit choice - {{model}}'; fastMode: { codexLabel: 'Fast mode (2x credits)'; defaultFast: 'Default (Fast)'; diff --git a/src/renderer/components/team/dialogs/TeamModelSelector.tsx b/src/renderer/components/team/dialogs/TeamModelSelector.tsx index dd288cc948..2ebb6cfda8 100644 --- a/src/renderer/components/team/dialogs/TeamModelSelector.tsx +++ b/src/renderer/components/team/dialogs/TeamModelSelector.tsx @@ -50,7 +50,6 @@ import { type TeamRuntimeModelOption, } from '@renderer/utils/teamModelAvailability'; import { - compareTeamModelVersionsDescending, getRuntimeAwareProviderScopedTeamModelLabel, getTeamModelSourceBadgeLabel, } from '@renderer/utils/teamModelCatalog'; @@ -104,6 +103,7 @@ import { hasFreeOpenCodeModelRoute, shouldShowOpenCodeRuntimeLoading, } from './openCodeRuntimeStatusUi'; +import { compareModelFreshness, isRecentlyReleasedModel } from './teamModelFreshness'; import { getActiveOpenCodeStickyHeadingIndex, getOpenCodeModelGridColumnCount, @@ -238,7 +238,6 @@ const OPENCODE_MODEL_GROUP_HEADING_ESTIMATE_PX = 38; const OPENCODE_MODEL_ROW_ESTIMATE_PX = 74; const OPENCODE_SCOPED_CATALOG_RETRY_DELAYS_MS = [2_000, 5_000, 10_000] as const; const MAX_BROWSER_TIMEOUT_MS = 2_147_483_647; -const NEW_MODEL_BADGE_WINDOW_MS = 30 * 24 * 60 * 60 * 1000; const OPENCODE_LOCAL_MODELS_TAB_ID = 'opencode-local-models'; const PROVIDERS: ProviderDef[] = [ { id: 'anthropic', label: 'Anthropic', comingSoon: false }, @@ -639,54 +638,6 @@ function getOpenCodeModelPricingInfo( }; } -function getModelReleaseTimestamp( - catalogModel: ProviderModelCatalogItem | null | undefined -): number | null { - const releaseDate = catalogModel?.metadata?.releaseDate?.trim(); - if (!releaseDate) { - return null; - } - const timestamp = Date.parse(releaseDate); - return Number.isFinite(timestamp) ? timestamp : null; -} - -function isRecentlyReleasedModel( - catalogModel: ProviderModelCatalogItem | null | undefined, - nowMs = Date.now() -): boolean { - const releasedAt = getModelReleaseTimestamp(catalogModel); - if (releasedAt === null) { - return false; - } - const ageMs = nowMs - releasedAt; - return ageMs >= 0 && ageMs <= NEW_MODEL_BADGE_WINDOW_MS; -} - -function compareModelFreshness( - left: { option: TeamRuntimeModelOption; catalogModel: ProviderModelCatalogItem | null }, - right: { option: TeamRuntimeModelOption; catalogModel: ProviderModelCatalogItem | null } -): number { - const releaseDateOrder = compareModelReleaseDates(left, right); - if (releaseDateOrder !== 0) { - return releaseDateOrder; - } - return compareTeamModelVersionsDescending(left.option.value, right.option.value); -} - -function compareModelReleaseDates( - left: { catalogModel: ProviderModelCatalogItem | null }, - right: { catalogModel: ProviderModelCatalogItem | null } -): number { - const leftReleasedAt = getModelReleaseTimestamp(left.catalogModel); - const rightReleasedAt = getModelReleaseTimestamp(right.catalogModel); - if (leftReleasedAt !== rightReleasedAt) { - if (leftReleasedAt === null) return 1; - if (rightReleasedAt === null) return -1; - return rightReleasedAt - leftReleasedAt; - } - return 0; -} - function shouldHydrateProviderModelCatalog( providerId: TeamProviderId, providerStatus: CliProviderStatus | null | undefined @@ -1431,6 +1382,23 @@ export const TeamModelSelector: React.FC = ({ } return t('modelSelector.defaultTooltip.runtime'); }, [effectiveProviderId, runtimeProviderStatus, t]); + const openCodeDefaultOptionLabel = useMemo(() => { + if (effectiveProviderId !== 'opencode') return t('modelSelector.defaultModel'); + const resolvedModel = + runtimeProviderStatus?.modelCatalog?.defaultLaunchModel ?? + runtimeProviderStatus?.modelCatalog?.defaultModelId ?? + null; + if (!resolvedModel) return t('modelSelector.defaultModel'); + const resolvedLabel = + resolvedModel === 'openrouter/openrouter/free' + ? 'Free Models Router' + : (getRuntimeAwareProviderScopedTeamModelLabel( + 'opencode', + resolvedModel, + runtimeProviderStatus + ) ?? resolvedModel); + return t('modelSelector.defaultWithResolved', { model: resolvedLabel }); + }, [effectiveProviderId, runtimeProviderStatus, t]); const getProviderOverrideDisabledReason = (candidateProviderId: string): string | null => { if (!isTeamProviderId(candidateProviderId)) { return null; @@ -1793,7 +1761,7 @@ export const TeamModelSelector: React.FC = ({ const pendingOptions: TeamRuntimeModelOption[] = [ { value: '', - label: t('modelSelector.defaultModel'), + label: openCodeDefaultOptionLabel, badgeLabel: t('modelSelector.defaultModel'), }, ]; @@ -1820,14 +1788,20 @@ export const TeamModelSelector: React.FC = ({ openCodeLocalModelOverlay.modelIds.has(option.value) ) : unscopedRuntimeOptions; + const presentedRuntimeOptions = + effectiveProviderId === 'opencode' + ? runtimeOptions.map((option) => + option.value.trim() ? option : { ...option, label: openCodeDefaultOptionLabel } + ) + : runtimeOptions; if ( effectiveProviderId !== 'opencode' || (openCodeLocalModelOverlay.options.length === 0 && !selectedLocalModelFallbackOption) ) { - return runtimeOptions; + return presentedRuntimeOptions; } - const optionByValue = new Map(runtimeOptions.map((option) => [option.value, option])); + const optionByValue = new Map(presentedRuntimeOptions.map((option) => [option.value, option])); if ( selectedLocalModelFallbackOption && !optionByValue.has(selectedLocalModelFallbackOption.value) @@ -1841,6 +1815,7 @@ export const TeamModelSelector: React.FC = ({ }, [ effectiveProviderId, openCodeLocalProviderLookupAuthoritative, + openCodeDefaultOptionLabel, openCodeLocalModelOverlay.modelIds, openCodeLocalModelOverlay.options, runtimeProviderStatus, @@ -2859,6 +2834,10 @@ export const TeamModelSelector: React.FC = ({ const showFreeRibbon = openCodePricingInfo?.free === true || openCodeRouteKind === 'builtin_free'; const isSelectedModel = normalizedValue === opt.value; + const optionDisplayLabel = + effectiveProviderId === 'opencode' && isSelectedModel && opt.value.trim() + ? t('modelSelector.explicitChoice', { model: opt.label }) + : opt.label; const isFlatOpenCodeCell = effectiveProviderId === 'opencode'; const flatCellBackgroundClass = 'bg-[color-mix(in_srgb,var(--color-surface-raised)_58%,var(--color-surface)_42%)]'; @@ -2871,7 +2850,11 @@ export const TeamModelSelector: React.FC = ({ data-testid="team-model-selector-model-option" aria-pressed={localModelCanAdd ? undefined : isSelectedModel} aria-disabled={!modelInteractable} - aria-label={modelButtonDescription ? `${opt.label}. ${modelButtonDescription}` : undefined} + aria-label={ + modelButtonDescription + ? `${optionDisplayLabel}. ${modelButtonDescription}` + : optionDisplayLabel + } className={cn( isFlatOpenCodeCell ? 'relative flex min-h-[58px] items-center justify-start gap-1.5 overflow-hidden border-0 border-b border-r border-[var(--color-border-subtle)] px-3 py-2 text-left text-xs font-medium transition-[background-color,color] duration-150' @@ -2938,7 +2921,7 @@ export const TeamModelSelector: React.FC = ({ )} > ['models'][number]; +const NEW_MODEL_BADGE_WINDOW_MS = 30 * 24 * 60 * 60 * 1000; + +function getModelReleaseTimestamp( + catalogModel: ProviderModelCatalogItem | null | undefined +): number | null { + const releaseDate = catalogModel?.metadata?.releaseDate?.trim(); + if (!releaseDate) return null; + const timestamp = Date.parse(releaseDate); + return Number.isFinite(timestamp) ? timestamp : null; +} + +export function isRecentlyReleasedModel( + catalogModel: ProviderModelCatalogItem | null | undefined, + nowMs = Date.now() +): boolean { + const releasedAt = getModelReleaseTimestamp(catalogModel); + if (releasedAt === null) return false; + const ageMs = nowMs - releasedAt; + return ageMs >= 0 && ageMs <= NEW_MODEL_BADGE_WINDOW_MS; +} + +function compareModelReleaseDates( + left: { catalogModel: ProviderModelCatalogItem | null }, + right: { catalogModel: ProviderModelCatalogItem | null } +): number { + const leftReleasedAt = getModelReleaseTimestamp(left.catalogModel); + const rightReleasedAt = getModelReleaseTimestamp(right.catalogModel); + if (leftReleasedAt === rightReleasedAt) return 0; + if (leftReleasedAt === null) return 1; + if (rightReleasedAt === null) return -1; + return rightReleasedAt - leftReleasedAt; +} + +export function compareModelFreshness( + left: { option: TeamRuntimeModelOption; catalogModel: ProviderModelCatalogItem | null }, + right: { option: TeamRuntimeModelOption; catalogModel: ProviderModelCatalogItem | null } +): number { + return ( + compareModelReleaseDates(left, right) || + compareTeamModelVersionsDescending(left.option.value, right.option.value) + ); +} From 49bf4888430ef036491a727d50318599c52c9fe5 Mon Sep 17 00:00:00 2001 From: 777genius Date: Fri, 21 Aug 2026 00:14:46 +0300 Subject: [PATCH 04/20] fix(opencode): harden inherited default workflow --- scripts/ci/source-file-size-baseline.json | 8 +- .../core/domain/providerManagementView.ts | 33 +++ .../registerRuntimeProviderManagementIpc.ts | 1 + ...TeamsRuntimeProviderManagementCliClient.ts | 6 +- .../hooks/useRuntimeProviderManagement.ts | 177 ++++++++---- .../OpenCodeDefaultModelInheritanceCard.tsx | 108 +++++-- .../ui/RuntimeProviderManagementPanelView.tsx | 271 ++++++++++++++---- .../openCodeDefaultModelInheritance.test.ts | 19 +- .../openCodeDefaultModelInheritance.ts | 18 +- ...RuntimeProviderManagementCliClient.test.ts | 22 ++ ...gisterRuntimeProviderManagementIpc.test.ts | 17 +- .../TeamModelSelectorDisabledState.test.ts | 14 + ...RuntimeProviderManagementPanelView.test.ts | 239 ++++++++++++++- .../RuntimeProviderOnboardingView.test.ts | 1 + .../providerManagementView.test.ts | 53 ++++ .../useRuntimeProviderManagement.test.ts | 151 +++++++++- 16 files changed, 967 insertions(+), 171 deletions(-) diff --git a/scripts/ci/source-file-size-baseline.json b/scripts/ci/source-file-size-baseline.json index 39b42d7357..ab03e0805c 100644 --- a/scripts/ci/source-file-size-baseline.json +++ b/scripts/ci/source-file-size-baseline.json @@ -25,13 +25,13 @@ "src/features/organizations/core/domain/organizationGraph.ts": 963, "src/features/organizations/renderer/adapters/organizationGraphAdapter.ts": 1664, "src/features/organizations/renderer/ui/OrganizationEditorPanel.tsx": 942, - "src/features/runtime-provider-management/main/infrastructure/AgentTeamsRuntimeProviderManagementCliClient.ts": 2691, + "src/features/runtime-provider-management/main/infrastructure/AgentTeamsRuntimeProviderManagementCliClient.ts": 2689, "src/features/runtime-provider-management/main/infrastructure/cli-companion/RuntimeProviderCliCompanionService.ts": 838, "src/features/runtime-provider-management/main/infrastructure/OpenCodeLocalProviderConnector.ts": 1018, - "src/features/runtime-provider-management/renderer/hooks/useRuntimeProviderManagement.ts": 2118, + "src/features/runtime-provider-management/renderer/hooks/useRuntimeProviderManagement.ts": 2102, "src/features/runtime-provider-management/renderer/RuntimeLocalProviderSetupDialog.tsx": 1676, "src/features/runtime-provider-management/renderer/RuntimeProviderQuickConnect.tsx": 826, - "src/features/runtime-provider-management/renderer/ui/RuntimeProviderManagementPanelView.tsx": 3309, + "src/features/runtime-provider-management/renderer/ui/RuntimeProviderManagementPanelView.tsx": 3026, "src/features/terminal-workspace/renderer/ui/TerminalWorkspacePanel.tsx": 4513, "src/features/token-usage/core/domain/snapshotProjection.ts": 937, "src/features/token-usage/renderer/view-models/tokenUsageViewModel.ts": 1481, @@ -121,7 +121,7 @@ "src/renderer/components/team/dialogs/providerPrepareDiagnostics.ts": 1640, "src/renderer/components/team/dialogs/ProvisioningProviderStatusList.tsx": 1206, "src/renderer/components/team/dialogs/TaskDetailDialog.tsx": 1717, - "src/renderer/components/team/dialogs/TeamModelSelector.tsx": 3881, + "src/renderer/components/team/dialogs/TeamModelSelector.tsx": 3864, "src/renderer/components/team/editor/EditorFileTree.tsx": 907, "src/renderer/components/team/editor/ProjectEditorOverlay.tsx": 929, "src/renderer/components/team/kanban/KanbanBoard.tsx": 919, diff --git a/src/features/runtime-provider-management/core/domain/providerManagementView.ts b/src/features/runtime-provider-management/core/domain/providerManagementView.ts index fca59d0146..c70dc76166 100644 --- a/src/features/runtime-provider-management/core/domain/providerManagementView.ts +++ b/src/features/runtime-provider-management/core/domain/providerManagementView.ts @@ -1,3 +1,5 @@ +import runtimeLock from '../../../../../runtime.lock.json'; + import type { RuntimeProviderActionDescriptorDto, RuntimeProviderActionIdDto, @@ -97,3 +99,34 @@ export function getProviderModelsLabel(provider: RuntimeProviderConnectionDto): } return `${provider.modelCount} model${provider.modelCount === 1 ? '' : 's'}`; } + +export function getProjectPathName(projectPath: string | null | undefined): string | null { + const normalized = projectPath?.trim().replace(/[\\/]+$/u, ''); + return normalized?.split(/[\\/]/u).at(-1)?.trim() || null; +} + +export function supportsScopedDefaultModelInheritance( + view: RuntimeProviderManagementViewDto | null, + bundledRuntimeVersion: string = runtimeLock.version +): boolean { + if ( + !view || + ![ + 'configuredModels', + 'projectPath', + 'projectDefaultModel', + 'allProjectsDefaultModel', + 'defaultModelSource', + ].every((field) => field in view) + ) { + return false; + } + const match = /^(\d+)\.(\d+)\.(\d+)(?:$|[-+])/u.exec(bundledRuntimeVersion.trim()); + if (!match || match[0].includes('-')) return false; + const version = match.slice(1, 4).map(Number); + const minimum = [0, 0, 75]; + for (let index = 0; index < minimum.length; index += 1) { + if (version[index]! !== minimum[index]!) return version[index]! > minimum[index]!; + } + return true; +} diff --git a/src/features/runtime-provider-management/main/adapters/input/registerRuntimeProviderManagementIpc.ts b/src/features/runtime-provider-management/main/adapters/input/registerRuntimeProviderManagementIpc.ts index ce457a06dc..cf019d203d 100644 --- a/src/features/runtime-provider-management/main/adapters/input/registerRuntimeProviderManagementIpc.ts +++ b/src/features/runtime-provider-management/main/adapters/input/registerRuntimeProviderManagementIpc.ts @@ -707,6 +707,7 @@ export function removeRuntimeProviderManagementIpc(ipcMain: IpcMain): void { ipcMain.removeHandler(RUNTIME_PROVIDER_MANAGEMENT_TEST_MODEL); ipcMain.removeHandler(RUNTIME_PROVIDER_MANAGEMENT_CANCEL_MODEL_TEST); ipcMain.removeHandler(RUNTIME_PROVIDER_MANAGEMENT_SET_DEFAULT_MODEL); + ipcMain.removeHandler(RUNTIME_PROVIDER_MANAGEMENT_CLEAR_PROJECT_DEFAULT); ipcMain.removeHandler(RUNTIME_PROVIDER_MANAGEMENT_CONFIGURE_MODEL_LIMITS); ipcMain.removeHandler(RUNTIME_PROVIDER_MANAGEMENT_OAUTH_CODE); ipcMain.removeHandler(RUNTIME_PROVIDER_MANAGEMENT_OAUTH_CANCEL); diff --git a/src/features/runtime-provider-management/main/infrastructure/AgentTeamsRuntimeProviderManagementCliClient.ts b/src/features/runtime-provider-management/main/infrastructure/AgentTeamsRuntimeProviderManagementCliClient.ts index 0f8eb7512a..ae5a7b3b96 100644 --- a/src/features/runtime-provider-management/main/infrastructure/AgentTeamsRuntimeProviderManagementCliClient.ts +++ b/src/features/runtime-provider-management/main/infrastructure/AgentTeamsRuntimeProviderManagementCliClient.ts @@ -2523,9 +2523,9 @@ export class AgentTeamsRuntimeProviderManagementCliClient implements RuntimeProv input.runtimeId, context ); - if (misconfigured) { - return misconfigured; - } + if (misconfigured && neutralProjectPath) + await rm(neutralProjectPath, { recursive: true, force: true }).catch(() => undefined); + if (misconfigured) return misconfigured; try { const { stdout, stderr } = await execCli( binaryPath, diff --git a/src/features/runtime-provider-management/renderer/hooks/useRuntimeProviderManagement.ts b/src/features/runtime-provider-management/renderer/hooks/useRuntimeProviderManagement.ts index 90c75d0038..0be8b77b47 100644 --- a/src/features/runtime-provider-management/renderer/hooks/useRuntimeProviderManagement.ts +++ b/src/features/runtime-provider-management/renderer/hooks/useRuntimeProviderManagement.ts @@ -1,5 +1,6 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import { useAppTranslation } from '@features/localization/renderer'; import { classifyAnalyticsError, elapsedMsSince, @@ -11,6 +12,7 @@ import { getRuntimeProviderCredentialUrl, selectInitialProviderId, selectRuntimeProviderSetupAuthOptionId, + supportsScopedDefaultModelInheritance, } from '../../core/domain'; import { saveOpenCodeModelForNewTeams } from '../adapters/createTeamDefaultModelWriter'; import { @@ -68,6 +70,7 @@ interface UseRuntimeProviderManagementOptions { projectPath?: string | null; initialProviderId?: string | null; initialProviderAction?: RuntimeProviderConnectionIntent | 'select' | null; + bundledRuntimeVersion?: string; onProviderChanged?: ( changeKind: RuntimeProviderChangeKind ) => Promise | boolean | void; @@ -143,7 +146,7 @@ export interface RuntimeProviderManagementState { selectedModelId: string | null; testingModelIds: readonly string[]; savingDefaultModelId: string | null; - clearingProjectDefault?: boolean; + clearingProjectDefault: boolean; modelResults: Readonly>; loading: boolean; savingProviderId: string | null; @@ -183,9 +186,10 @@ export interface RuntimeProviderManagementActions { setDefaultModel: ( providerId: string, modelId: string, - scope?: RuntimeProviderDefaultScopeDto + scope?: RuntimeProviderDefaultScopeDto, + intendedProjectPath?: string | null ) => Promise; - clearProjectDefault: () => Promise; + clearProjectDefault: (projectPath: string) => Promise; } export interface RuntimeProviderConnectOutcome { @@ -260,6 +264,7 @@ function createOAuthOperationId(): string { export function useRuntimeProviderManagement( options: UseRuntimeProviderManagementOptions ): [RuntimeProviderManagementState, RuntimeProviderManagementActions] { + const { t } = useAppTranslation('settings'); const onProviderChanged = options.onProviderChanged; const [view, setView] = useState(null); const [selectedProviderId, setSelectedProviderId] = useState(null); @@ -482,8 +487,7 @@ export function useRuntimeProviderManagement( setSelectedModelId(null); setTestingModelIds([]); setSavingProviderId(null); - setSavingDefaultModelId(null); - setClearingProjectDefault(false); + setView(null); setModelResults({}); setSuccessMessage(null); setWarningMessage(null); @@ -1642,7 +1646,8 @@ export function useRuntimeProviderManagement( async ( providerId: string, modelId: string, - scope: RuntimeProviderDefaultScopeDto = 'project' + scope: RuntimeProviderDefaultScopeDto = 'project', + intendedProjectPath?: string | null ): Promise => { if (defaultMutationInFlightRef.current) return false; defaultMutationInFlightRef.current = true; @@ -1653,6 +1658,19 @@ export function useRuntimeProviderManagement( setWarningMessage(null); const projectContext = getProjectContextSnapshot(); const isGlobal = scope === 'all_projects'; + if ( + intendedProjectPath !== undefined && + normalizeProjectContextPath(intendedProjectPath) !== projectContext.path + ) { + defaultMutationInFlightRef.current = false; + setSavingDefaultModelId(null); + return false; + } + if (!isGlobal && !projectContext.path) { + defaultMutationInFlightRef.current = false; + setSavingDefaultModelId(null); + return false; + } try { const response = await withUiTimeout( api.runtimeProviderManagement.setDefaultModel({ @@ -1699,8 +1717,7 @@ export function useRuntimeProviderManagement( ) ); } else { - const effectiveDefaultModelId = - providerViewRef.current?.projectDefaultModel ?? modelId; + const effectiveDefaultModelId = providerViewRef.current?.projectDefaultModel ?? modelId; setView((current) => projectBaseDefaultMutation(current, modelId)); setSelectedModelId(effectiveDefaultModelId); setModels((current) => @@ -1710,10 +1727,9 @@ export function useRuntimeProviderManagement( })) ); } - const success = - isGlobal - ? `All-projects OpenCode default set to ${modelId}` - : `Project OpenCode default set to ${modelId}`; + const success = `${t( + isGlobal ? 'runtimeProvider.defaults.allProjects' : 'runtimeProvider.defaults.thisProject' + )}: ${modelId}`; let refreshed = true; try { refreshed = (await onProviderChanged?.('configuration')) !== false; @@ -1754,60 +1770,101 @@ export function useRuntimeProviderManagement( setSavingDefaultModelId(null); } }, - [getProjectContextSnapshot, isProjectContextCurrent, onProviderChanged, options.runtimeId, refresh] + [ + getProjectContextSnapshot, + isProjectContextCurrent, + onProviderChanged, + options.runtimeId, + refresh, + t, + ] ); - const clearProjectDefault = useCallback(async (): Promise => { - if (defaultMutationInFlightRef.current) return; - defaultMutationInFlightRef.current = true; - setClearingProjectDefault(true); - setError(null); - setErrorDiagnostics(null); - setSuccessMessage(null); - setWarningMessage(null); - const projectContext = getProjectContextSnapshot(); - try { - const response = await withUiTimeout( - api.runtimeProviderManagement.clearProjectDefaultModel({ - runtimeId: options.runtimeId, - projectPath: projectContext.path, - }), - 'Clear project default timed out' - ); - if (!isProjectContextCurrent(projectContext)) return; - if (response.error) { - setError(response.error.message); - setErrorDiagnostics(response.error.diagnostics ?? null); + const clearProjectDefault = useCallback( + async (intendedProjectPath: string): Promise => { + if (defaultMutationInFlightRef.current) return; + if ( + !supportsScopedDefaultModelInheritance( + providerViewRef.current, + options.bundledRuntimeVersion + ) + ) { return; } - const nextView = presentManagementView( - response.view ?? projectClearedDefaultMutation(providerViewRef.current) - ); - setView(nextView); - setSelectedModelId(nextView?.defaultModel ?? null); - setModels((current) => - current.map((model) => ({ ...model, default: model.modelId === nextView?.defaultModel })) - ); - const success = nextView?.defaultModel - ? `Project override removed. This project now uses ${nextView.defaultModel}` - : 'Project override removed. This project now uses the OpenCode default.'; - let refreshed = true; + const projectContext = getProjectContextSnapshot(); + const normalizedIntendedProjectPath = normalizeProjectContextPath(intendedProjectPath); + if ( + !normalizedIntendedProjectPath || + normalizedIntendedProjectPath !== projectContext.path || + normalizeProjectContextPath(providerViewRef.current?.projectPath) !== + normalizedIntendedProjectPath + ) { + return; + } + defaultMutationInFlightRef.current = true; + setClearingProjectDefault(true); + setError(null); + setErrorDiagnostics(null); + setSuccessMessage(null); + setWarningMessage(null); try { - refreshed = (await onProviderChanged?.('configuration')) !== false; - } catch { - refreshed = false; + const response = await withUiTimeout( + api.runtimeProviderManagement.clearProjectDefaultModel({ + runtimeId: options.runtimeId, + projectPath: normalizedIntendedProjectPath, + }), + 'Clear project default timed out' + ); + if (!isProjectContextCurrent(projectContext)) return; + if (response.error) { + setError(response.error.message); + setErrorDiagnostics(response.error.diagnostics ?? null); + return; + } + const nextView = presentManagementView( + response.view ?? projectClearedDefaultMutation(providerViewRef.current) + ); + setView(nextView); + setSelectedModelId(nextView?.defaultModel ?? null); + setModels((current) => + current.map((model) => ({ ...model, default: model.modelId === nextView?.defaultModel })) + ); + const translatedOpenCodeDefault = t('runtimeProvider.summary.defaultModel', { + model: '', + }) + .trim() + .replace(/[::]\s*$/u, ''); + const success = `${t('runtimeProvider.defaults.thisProject')}: ${t( + 'runtimeProvider.defaults.inherits' + )} ${nextView?.defaultModel ?? translatedOpenCodeDefault}`; + let refreshed = true; + try { + refreshed = (await onProviderChanged?.('configuration')) !== false; + } catch { + refreshed = false; + } + if (!isProjectContextCurrent(projectContext)) return; + if (refreshed) setSuccessMessage(success); + else setWarningMessage(formatPostOperationRefreshWarning(success)); + } catch (clearError) { + if (!isProjectContextCurrent(projectContext)) return; + setError( + clearError instanceof Error ? clearError.message : 'Failed to clear project default' + ); + } finally { + defaultMutationInFlightRef.current = false; + setClearingProjectDefault(false); } - if (!isProjectContextCurrent(projectContext)) return; - if (refreshed) setSuccessMessage(success); - else setWarningMessage(formatPostOperationRefreshWarning(success)); - } catch (clearError) { - if (!isProjectContextCurrent(projectContext)) return; - setError(clearError instanceof Error ? clearError.message : 'Failed to clear project default'); - } finally { - defaultMutationInFlightRef.current = false; - setClearingProjectDefault(false); - } - }, [getProjectContextSnapshot, isProjectContextCurrent, onProviderChanged, options.runtimeId]); + }, + [ + getProjectContextSnapshot, + isProjectContextCurrent, + onProviderChanged, + options.bundledRuntimeVersion, + options.runtimeId, + t, + ] + ); const selectProvider = useCallback( (providerId: string): void => { diff --git a/src/features/runtime-provider-management/renderer/ui/OpenCodeDefaultModelInheritanceCard.tsx b/src/features/runtime-provider-management/renderer/ui/OpenCodeDefaultModelInheritanceCard.tsx index d88c80627f..3d6071b445 100644 --- a/src/features/runtime-provider-management/renderer/ui/OpenCodeDefaultModelInheritanceCard.tsx +++ b/src/features/runtime-provider-management/renderer/ui/OpenCodeDefaultModelInheritanceCard.tsx @@ -12,26 +12,24 @@ import { } from '@renderer/components/ui/select'; import { Loader2, RotateCcw } from 'lucide-react'; +import { getProjectPathName } from '../../core/domain'; import { OPENROUTER_FREE_MODEL_ID, presentOpenCodeDefaultModelInheritance, } from '../view-models/openCodeDefaultModelInheritance'; +import { canUseOpenCodeModelRoute } from './runtimeProviderModelAccess'; + import type { RuntimeProviderDefaultScopeDto } from '../../contracts'; import type { RuntimeProviderManagementActions, RuntimeProviderManagementState, } from '../hooks/useRuntimeProviderManagement'; import type { ProjectPathProject } from '@renderer/components/team/dialogs/projectPathProjects'; -import type { JSX } from 'react'; +import type { JSX, Ref } from 'react'; const NO_PROJECT = '__runtime-provider-no-project__'; -function projectName(projectPath: string | null | undefined): string | null { - const normalized = projectPath?.trim().replace(/[\\/]+$/u, ''); - return normalized?.split(/[\\/]/u).at(-1) || null; -} - export const OpenCodeDefaultModelInheritanceCard = ({ state, actions, @@ -42,6 +40,8 @@ export const OpenCodeDefaultModelInheritanceCard = ({ projectError, onProjectChange, onChooseModel, + allProjectsActionRef, + projectActionRef, }: { readonly state: RuntimeProviderManagementState; readonly actions: RuntimeProviderManagementActions; @@ -52,6 +52,8 @@ export const OpenCodeDefaultModelInheritanceCard = ({ readonly projectError: string | null; readonly onProjectChange?: (projectPath: string | null) => void; readonly onChooseModel: (target: RuntimeProviderDefaultScopeDto) => void; + readonly allProjectsActionRef?: Ref; + readonly projectActionRef?: Ref; }): JSX.Element => { const { t } = useAppTranslation('settings'); const options = useMemo(() => { @@ -65,7 +67,7 @@ export const OpenCodeDefaultModelInheritanceCard = ({ unique.set(projectPath, { id: projectPath, path: projectPath, - name: projectName(projectPath) ?? projectPath, + name: getProjectPathName(projectPath) ?? projectPath, sessions: [], totalSessions: 0, createdAt: 0, @@ -74,12 +76,32 @@ export const OpenCodeDefaultModelInheritanceCard = ({ return [...unique.values()]; }, [projectPath, projects]); const activeProjectName = - options.find((project) => project.path === projectPath)?.name ?? projectName(projectPath); + options.find((project) => project.path === projectPath)?.name ?? + getProjectPathName(projectPath); const model = presentOpenCodeDefaultModelInheritance({ view: state.view, projectPath, projectName: activeProjectName, }); + const translatedOpenCodeDefault = t('runtimeProvider.summary.defaultModel', { model: '' }) + .trim() + .replace(/[::]\s*$/u, ''); + const getUnavailableReason = (modelId: string | null): string | null => { + if (!modelId) return null; + const configuredModel = state.view?.configuredModels?.find( + (entry) => entry.modelId === modelId + ); + if (configuredModel && canUseOpenCodeModelRoute(configuredModel)) return null; + return ( + configuredModel?.accessReason ?? + (configuredModel + ? t('runtimeProvider.models.routeUnavailableGeneric') + : t('runtimeProvider.models.routeUnavailableUnknown')) + ); + }; + const baseUnavailableReason = getUnavailableReason(model.baseModelId); + const projectUnavailableReason = getUnavailableReason(model.projectEffectiveModelId); + const unavailableLabel = t('providerRuntime.connectionUi.status.unavailableInCurrentRuntime'); const busy = Boolean(state.savingDefaultModelId) || state.clearingProjectDefault; return ( @@ -105,10 +127,16 @@ export const OpenCodeDefaultModelInheritanceCard = ({ {t('runtimeProvider.defaults.allProjects')}
- {model.baseDisplayName} + {baseUnavailableReason + ? unavailableLabel + : (model.baseDisplayName ?? translatedOpenCodeDefault)}
- {model.baseModelId && model.baseDisplayName !== model.baseModelId ? ( -
+ {model.baseModelId && + (baseUnavailableReason || model.baseDisplayName !== model.baseModelId) ? ( +
{model.baseModelId}
) : null} @@ -117,12 +145,19 @@ export const OpenCodeDefaultModelInheritanceCard = ({ {t('runtimeProvider.defaults.freeRouterAdvisory')}
) : null} + {baseUnavailableReason ? ( +
{baseUnavailableReason}
+ ) : null}
) : null} - {projectError ?
{projectError}
: null} + {projectError ? ( +
+ {projectError} +
+ ) : null} ); -} +}; export const OpenCodeDefaultTargetBanner = ({ target, projectName: targetProjectName, onCancel, + focusRef, }: { readonly target: RuntimeProviderDefaultScopeDto; readonly projectName: string | null; readonly onCancel: () => void; + readonly focusRef?: Ref; }): JSX.Element => { const { t } = useAppTranslation('settings'); return ( -
+
{target === 'all_projects' ? t('runtimeProvider.defaults.choosingAllProjects') @@ -242,4 +310,4 @@ export const OpenCodeDefaultTargetBanner = ({
); -} +}; diff --git a/src/features/runtime-provider-management/renderer/ui/RuntimeProviderManagementPanelView.tsx b/src/features/runtime-provider-management/renderer/ui/RuntimeProviderManagementPanelView.tsx index 0e9eeeb3b3..05993f19b6 100644 --- a/src/features/runtime-provider-management/renderer/ui/RuntimeProviderManagementPanelView.tsx +++ b/src/features/runtime-provider-management/renderer/ui/RuntimeProviderManagementPanelView.tsx @@ -46,9 +46,11 @@ import { import { formatProviderState, + getProjectPathName, getProviderAction, getProviderModelsLabel, getRuntimeProviderSetupPresentation, + supportsScopedDefaultModelInheritance, } from '../../core/domain'; import { OPENROUTER_FREE_MODEL_ID } from '../view-models/openCodeDefaultModelInheritance'; @@ -60,6 +62,7 @@ import { ProviderBrandIcon } from './providerBrandIcons'; import { canUseOpenCodeModelRoute, isUnknownOpenCodeModelRoute, + needsOpenCodeModelExecutionProof, } from './runtimeProviderModelAccess'; import { RuntimeProviderModelTestResult } from './RuntimeProviderModelTestResult'; import { @@ -94,6 +97,7 @@ interface RuntimeProviderManagementPanelViewProps { readonly projectContextLoading?: boolean; readonly projectContextError?: string | null; readonly onProjectContextChange?: (projectPath: string | null) => void; + readonly bundledRuntimeVersion?: string; } interface ProviderActionsProps { @@ -114,6 +118,7 @@ interface ProviderRowProps { readonly disabled: boolean; readonly hasProjectContext: boolean; readonly defaultTarget: RuntimeProviderDefaultScopeDto | null; + readonly intendedProjectPath: string | null; readonly onDefaultSaved: () => void; readonly actions: RuntimeProviderManagementActions; } @@ -125,6 +130,10 @@ interface RuntimeProviderErrorAlertProps { } type OpenCodeSettingsSection = 'models' | 'providers'; +interface DefaultModelSelectionTarget { + readonly scope: RuntimeProviderDefaultScopeDto; + readonly projectPath: string | null; +} type SettingsT = ReturnType['t']; const PROVIDER_MODEL_VIRTUALIZATION_THRESHOLD = 40; @@ -193,16 +202,6 @@ function formatOpenCodeProviderCount(count: number): string { return `${count} OpenCode provider${count === 1 ? '' : 's'}`; } -function getProjectContextName(projectPath: string | null | undefined): string | null { - const trimmed = projectPath?.trim(); - if (!trimmed) { - return null; - } - const normalized = trimmed.replace(/[\\/]+$/, ''); - const name = normalized.split(/[\\/]/).pop()?.trim(); - return name || normalized; -} - function directoryEntryMatchesQuery( provider: RuntimeProviderDirectoryEntryDto, query: string @@ -816,7 +815,7 @@ export const ProviderSetupFormPanel = ({
); -} +}; const RuntimeProviderLoadingPlaceholder = (): JSX.Element => { const { t } = useAppTranslation('settings'); @@ -923,7 +922,7 @@ const RuntimeProviderLoadingPlaceholder = (): JSX.Element => { ); -} +}; function formatRuntimeProviderDiagnosticsCopyText( message: string, @@ -1229,7 +1228,7 @@ const RuntimeProviderModelLoadingSkeleton = (): JSX.Element => { ))} ); -} +}; const ProviderActions = ({ provider, @@ -1320,7 +1319,7 @@ const ProviderActions = ({ ) : null} ); -} +}; const ProviderRow = ({ provider, @@ -1331,6 +1330,7 @@ const ProviderRow = ({ disabled, hasProjectContext, defaultTarget, + intendedProjectPath, onDefaultSaved, actions, }: ProviderRowProps): JSX.Element => { @@ -1459,6 +1459,7 @@ const ProviderRow = ({ disabled={disabled || busy} hasProjectContext={hasProjectContext} defaultTarget={defaultTarget} + intendedProjectPath={intendedProjectPath} onDefaultSaved={onDefaultSaved} /> ) : null} @@ -1466,7 +1467,7 @@ const ProviderRow = ({ ) : null} ); -} +}; const DirectoryProviderRow = ({ provider, @@ -1477,6 +1478,7 @@ const DirectoryProviderRow = ({ busy, hasProjectContext, defaultTarget, + intendedProjectPath, onDefaultSaved, actions, }: { @@ -1488,6 +1490,7 @@ const DirectoryProviderRow = ({ readonly busy: boolean; readonly hasProjectContext: boolean; readonly defaultTarget: RuntimeProviderDefaultScopeDto | null; + readonly intendedProjectPath: string | null; readonly onDefaultSaved: () => void; readonly actions: RuntimeProviderManagementActions; }): JSX.Element => { @@ -1677,6 +1680,7 @@ const DirectoryProviderRow = ({ disabled={disabled || busy} hasProjectContext={hasProjectContext} defaultTarget={defaultTarget} + intendedProjectPath={intendedProjectPath} onDefaultSaved={onDefaultSaved} /> ) : null} @@ -1684,9 +1688,13 @@ const DirectoryProviderRow = ({ ) : null} ); -} +}; -const ModelBadges = ({ model }: { readonly model: RuntimeProviderModelDto }): JSX.Element | null => { +const ModelBadges = ({ + model, +}: { + readonly model: RuntimeProviderModelDto; +}): JSX.Element | null => { const { t } = useAppTranslation('settings'); const modelRecommendation = getOpenCodeTeamModelRecommendation(model.modelId); const routeStatus = getOpenCodeModelRoutePresentationStatus(model); @@ -1813,7 +1821,7 @@ const ModelBadges = ({ model }: { readonly model: RuntimeProviderModelDto }): JS ) : null} ); -} +}; function isFreeRuntimeProviderModel(model: RuntimeProviderModelDto): boolean { return isOpenCodeModelExplicitlyFree(model); @@ -2039,6 +2047,7 @@ const ModelRow = ({ testing, result, defaultTarget, + intendedProjectPath, savingDefault, onDefaultSaved, actions, @@ -2051,19 +2060,18 @@ const ModelRow = ({ readonly testing: boolean; readonly result: RuntimeProviderModelTestResultDto | undefined; readonly defaultTarget: RuntimeProviderDefaultScopeDto | null; + readonly intendedProjectPath: string | null; readonly savingDefault: boolean; readonly onDefaultSaved: () => void; readonly actions: RuntimeProviderManagementActions; }): JSX.Element => { const { t } = useAppTranslation('settings'); - const modelDisabled = disabled || !canUseOpenCodeModelRoute(model); + const modelDisabled = + disabled || (!canUseOpenCodeModelRoute(model) && !needsOpenCodeModelExecutionProof(model)); const unavailableTitle = getOpenCodeRouteUnavailableTitle(model, t); - const targets: readonly RuntimeProviderDefaultScopeDto[] = defaultTarget ? [defaultTarget] : []; return (
- {targets.map((target) => ( + {defaultTarget ? ( - ))} + ) : null}
+ {unavailableTitle ? ( +
{unavailableTitle}
+ ) : null} ); -} +}; const ProviderModelList = ({ state, @@ -2155,6 +2173,7 @@ const ProviderModelList = ({ disabled, hasProjectContext, defaultTarget, + intendedProjectPath, onDefaultSaved, }: { readonly state: RuntimeProviderManagementState; @@ -2163,6 +2182,7 @@ const ProviderModelList = ({ readonly disabled: boolean; readonly hasProjectContext: boolean; readonly defaultTarget: RuntimeProviderDefaultScopeDto | null; + readonly intendedProjectPath: string | null; readonly onDefaultSaved: () => void; }): JSX.Element => { const { t } = useAppTranslation('settings'); @@ -2336,6 +2356,7 @@ const ProviderModelList = ({ testing={state.testingModelIds.includes(model.modelId)} result={state.modelResults[model.modelId]} defaultTarget={defaultTarget} + intendedProjectPath={intendedProjectPath} savingDefault={Boolean(state.savingDefaultModelId || state.clearingProjectDefault)} onDefaultSaved={onDefaultSaved} actions={actions} @@ -2484,7 +2505,90 @@ const ProviderModelList = ({ ); -} +}; + +const LegacyConfiguredModelsPanel = ({ + state, + actions, + disabled, + hasProjectContext, +}: { + readonly state: RuntimeProviderManagementState; + readonly actions: RuntimeProviderManagementActions; + readonly disabled: boolean; + readonly hasProjectContext: boolean; +}): JSX.Element => { + const { t } = useAppTranslation('settings'); + const models = state.view?.configuredModels ?? []; + if (models.length === 0) { + return ( +
+ {t('runtimeProvider.models.noneReported')} +
+ ); + } + return ( +
+ {models.map((model) => { + const unavailableTitle = getOpenCodeRouteUnavailableTitle(model, t); + const testing = state.testingModelIds.includes(model.modelId); + return ( +
+
+
+
+ {model.displayName} +
+
+ {model.modelId} +
+ {unavailableTitle ? ( +
{unavailableTitle}
+ ) : null} +
+
+ + +
+
+
+ ); + })} +
+ ); +}; export const RuntimeProviderManagementPanelView = ({ state, @@ -2495,10 +2599,20 @@ export const RuntimeProviderManagementPanelView = ({ projectContextLoading = false, projectContextError = null, onProjectContextChange, + bundledRuntimeVersion, }: RuntimeProviderManagementPanelViewProps): JSX.Element => { const { t } = useAppTranslation('settings'); const [selectedSection, setSelectedSection] = useState(null); - const [defaultTarget, setDefaultTarget] = useState(null); + const [defaultTarget, setDefaultTarget] = useState(null); + const pickerBannerRef = useRef(null); + const modelsTabTriggerRef = useRef(null); + const allProjectsActionRef = useRef(null); + const projectActionRef = useRef(null); + const returnFocusScopeRef = useRef(null); + const focusPickerBanner = useCallback((banner: HTMLDivElement | null): void => { + pickerBannerRef.current = banner; + banner?.focus(); + }, []); const providerQuery = state.providerQuery.trim().toLowerCase(); const filteredProviders = providerQuery ? state.providers.filter((provider) => @@ -2534,11 +2648,16 @@ export const RuntimeProviderManagementPanelView = ({ ? t('runtimeProvider.providers.catalog') : t('runtimeProvider.providers.countFallback'); const launchableModelCount = state.view?.configuredModels?.length ?? 0; + const scopedDefaultsSupported = supportsScopedDefaultModelInheritance( + state.view, + bundledRuntimeVersion + ); const activeSection = selectedSection ?? 'providers'; const hasProjectContext = Boolean(projectPath?.trim()); + const normalizedProjectPath = projectPath?.trim() || null; const activeProjectName = projectContextProjects.find((project) => project.path === projectPath)?.name ?? - getProjectContextName(projectPath); + getProjectPathName(projectPath); const activeAuthOption = state.setupForm?.authOptions?.find( (option) => option.id === state.selectedAuthOptionId ); @@ -2547,6 +2666,28 @@ export const RuntimeProviderManagementPanelView = ({ state.savingProviderId && activeSetupMethod && activeSetupMethod !== 'oauth' ); + useEffect(() => { + if (defaultTarget && defaultTarget.projectPath !== normalizedProjectPath) { + setDefaultTarget(null); + actions.closeModelPicker(); + } + }, [actions, defaultTarget, normalizedProjectPath]); + + useEffect(() => { + const returnScope = returnFocusScopeRef.current; + if (defaultTarget !== null || !returnScope || activeSection !== 'models') return; + const scopedAction = + returnScope === 'all_projects' ? allProjectsActionRef.current : projectActionRef.current; + if (scopedAction && !scopedAction.disabled) scopedAction.focus(); + else modelsTabTriggerRef.current?.focus(); + returnFocusScopeRef.current = null; + }, [activeSection, defaultTarget]); + + const finishDefaultSelection = (): void => { + setDefaultTarget(null); + setSelectedSection('models'); + }; + return (
{state.error ? ( @@ -2617,6 +2758,7 @@ export const RuntimeProviderManagementPanelView = ({
- { - setDefaultTarget(target); - setSelectedSection('providers'); - }} - /> + {scopedDefaultsSupported ? ( + { + setDefaultTarget(null); + actions.closeModelPicker(); + onProjectContextChange?.(nextProjectPath); + }} + onChooseModel={(target) => { + returnFocusScopeRef.current = target; + setDefaultTarget({ scope: target, projectPath: normalizedProjectPath }); + setSelectedSection('providers'); + }} + allProjectsActionRef={allProjectsActionRef} + projectActionRef={projectActionRef} + /> + ) : ( + + )} {defaultTarget ? ( setDefaultTarget(null)} + onCancel={finishDefaultSelection} + focusRef={focusPickerBanner} /> ) : null}
@@ -2763,10 +2922,10 @@ export const RuntimeProviderManagementPanelView = ({ busy={state.savingProviderId === provider.providerId} disabled={disabled || state.directoryLoading} hasProjectContext={hasProjectContext} - defaultTarget={defaultTarget} + defaultTarget={defaultTarget?.scope ?? null} + intendedProjectPath={defaultTarget?.projectPath ?? null} onDefaultSaved={() => { - setDefaultTarget(null); - setSelectedSection('models'); + finishDefaultSelection(); }} actions={actions} /> @@ -2803,10 +2962,10 @@ export const RuntimeProviderManagementPanelView = ({ busy={state.savingProviderId === provider.providerId} disabled={disabled || state.loading} hasProjectContext={hasProjectContext} - defaultTarget={defaultTarget} + defaultTarget={defaultTarget?.scope ?? null} + intendedProjectPath={defaultTarget?.projectPath ?? null} onDefaultSaved={() => { - setDefaultTarget(null); - setSelectedSection('models'); + finishDefaultSelection(); }} actions={actions} /> @@ -2863,4 +3022,4 @@ export const RuntimeProviderManagementPanelView = ({
); -} +}; diff --git a/src/features/runtime-provider-management/renderer/view-models/openCodeDefaultModelInheritance.test.ts b/src/features/runtime-provider-management/renderer/view-models/openCodeDefaultModelInheritance.test.ts index f2dabafd03..01c6ba0d06 100644 --- a/src/features/runtime-provider-management/renderer/view-models/openCodeDefaultModelInheritance.test.ts +++ b/src/features/runtime-provider-management/renderer/view-models/openCodeDefaultModelInheritance.test.ts @@ -21,7 +21,7 @@ const view = { providers: [], configuredModels: [], diagnostics: [], - projectPath: '/tmp/project', + projectPath: '/workspace/project', defaultModel: 'openrouter/project', projectDefaultModel: 'openrouter/project', allProjectsDefaultModel: 'openrouter/base', @@ -61,6 +61,21 @@ describe('openCodeDefaultModelInheritance', () => { }); }); + it('projects the fallback model as the base while a project override is active', () => { + const presentation = presentOpenCodeDefaultModelInheritance({ + view: { + ...view, + allProjectsDefaultModel: null, + fallbackModel: 'opencode/fallback', + }, + projectPath: '/workspace/project', + projectName: 'Test project', + }); + + expect(presentation.baseModelId).toBe('opencode/fallback'); + expect(presentation.baseDisplayName).toBe('opencode/fallback'); + }); + it('presents the Free Models Router with its stable product name', () => { const presentation = presentOpenCodeDefaultModelInheritance({ view: { @@ -70,7 +85,7 @@ describe('openCodeDefaultModelInheritance', () => { allProjectsDefaultModel: 'openrouter/openrouter/free', defaultModelSource: 'all_projects', }, - projectPath: '/tmp/project', + projectPath: '/workspace/project', projectName: 'Test project', }); expect(presentation.baseDisplayName).toBe('Free Models Router'); diff --git a/src/features/runtime-provider-management/renderer/view-models/openCodeDefaultModelInheritance.ts b/src/features/runtime-provider-management/renderer/view-models/openCodeDefaultModelInheritance.ts index bd8e034e3c..0d5314b1bb 100644 --- a/src/features/runtime-provider-management/renderer/view-models/openCodeDefaultModelInheritance.ts +++ b/src/features/runtime-provider-management/renderer/view-models/openCodeDefaultModelInheritance.ts @@ -7,12 +7,12 @@ export const OPENROUTER_FREE_MODEL_ID = 'openrouter/openrouter/free'; export interface OpenCodeDefaultModelPresentation { readonly baseModelId: string | null; - readonly baseDisplayName: string; + readonly baseDisplayName: string | null; readonly projectPath: string | null; readonly projectName: string | null; readonly projectOverrideModelId: string | null; readonly projectEffectiveModelId: string | null; - readonly projectEffectiveDisplayName: string; + readonly projectEffectiveDisplayName: string | null; readonly projectInherits: boolean; readonly effectiveSource: RuntimeProviderManagementViewDto['defaultModelSource']; } @@ -20,8 +20,8 @@ export interface OpenCodeDefaultModelPresentation { function displayModel( modelId: string | null, models: readonly RuntimeProviderModelDto[] -): string { - if (!modelId) return 'OpenCode default'; +): string | null { + if (!modelId) return null; if (modelId === OPENROUTER_FREE_MODEL_ID) return 'Free Models Router'; return models.find((model) => model.modelId === modelId)?.displayName ?? modelId; } @@ -35,9 +35,11 @@ export function presentOpenCodeDefaultModelInheritance(input: { const models = view?.configuredModels ?? []; const baseModelId = view?.allProjectsDefaultModel ?? + view?.fallbackModel ?? (view?.defaultModelSource === 'project' ? null : (view?.defaultModel ?? null)); const projectOverrideModelId = view?.projectDefaultModel ?? null; - const projectEffectiveModelId = projectOverrideModelId ?? baseModelId ?? view?.defaultModel ?? null; + const projectEffectiveModelId = + projectOverrideModelId ?? baseModelId ?? view?.defaultModel ?? null; return { baseModelId, baseDisplayName: displayModel(baseModelId, models), @@ -74,6 +76,10 @@ export function projectClearedDefaultMutation( ...view, projectDefaultModel: null, defaultModel: inherited, - defaultModelSource: view.allProjectsDefaultModel ? 'all_projects' : inherited ? 'fallback' : null, + defaultModelSource: view.allProjectsDefaultModel + ? 'all_projects' + : inherited + ? 'fallback' + : null, }; } diff --git a/test/main/features/runtime-provider-management/AgentTeamsRuntimeProviderManagementCliClient.test.ts b/test/main/features/runtime-provider-management/AgentTeamsRuntimeProviderManagementCliClient.test.ts index 6c2923902d..320eb2aa27 100644 --- a/test/main/features/runtime-provider-management/AgentTeamsRuntimeProviderManagementCliClient.test.ts +++ b/test/main/features/runtime-provider-management/AgentTeamsRuntimeProviderManagementCliClient.test.ts @@ -2659,6 +2659,28 @@ describe('AgentTeamsRuntimeProviderManagementCliClient', () => { expect(execCliMock.mock.calls[0]?.[2]).toMatchObject({ timeout: 240_000 }); }); + it('removes the neutral project directory when the runtime binary is rejected', async () => { + resolveBinaryMock.mockResolvedValue('/opt/homebrew/bin/opencode'); + const before = new Set( + fs.readdirSync(os.tmpdir()).filter((entry) => entry.startsWith('agent-teams-opencode-default-')) + ); + const client = new AgentTeamsRuntimeProviderManagementCliClient(); + + const response = await client.setDefaultModel({ + runtimeId: 'opencode', + providerId: 'openrouter', + modelId: 'openrouter/qwen/qwen3-coder', + scope: 'all_projects', + projectPath: path.join(os.tmpdir(), 'project'), + }); + + const after = fs + .readdirSync(os.tmpdir()) + .filter((entry) => entry.startsWith('agent-teams-opencode-default-')); + expect(response.error?.code).toBe('runtime-misconfigured'); + expect(after.filter((entry) => !before.has(entry))).toEqual([]); + }); + it('clears only the selected project default through the dedicated CLI command', async () => { execCliMock.mockResolvedValue({ stdout: JSON.stringify({ diff --git a/test/main/features/runtime-provider-management/registerRuntimeProviderManagementIpc.test.ts b/test/main/features/runtime-provider-management/registerRuntimeProviderManagementIpc.test.ts index 72a2888b76..8c67c5a351 100644 --- a/test/main/features/runtime-provider-management/registerRuntimeProviderManagementIpc.test.ts +++ b/test/main/features/runtime-provider-management/registerRuntimeProviderManagementIpc.test.ts @@ -18,7 +18,10 @@ import { RUNTIME_PROVIDER_MANAGEMENT_SETUP_FORM, RUNTIME_PROVIDER_MANAGEMENT_VIEW, } from '../../../../src/features/runtime-provider-management/contracts'; -import { registerRuntimeProviderManagementIpc } from '../../../../src/features/runtime-provider-management/main'; +import { + registerRuntimeProviderManagementIpc, + removeRuntimeProviderManagementIpc, +} from '../../../../src/features/runtime-provider-management/main'; import type { RuntimeProviderManagementDirectoryResponse, @@ -58,6 +61,18 @@ function createCompanionFeatureStubs(): Pick< } describe('registerRuntimeProviderManagementIpc', () => { + it('removes the scoped-default handler during teardown', () => { + const ipcMain = { + removeHandler: vi.fn(), + } as unknown as IpcMain; + + removeRuntimeProviderManagementIpc(ipcMain); + + expect(ipcMain.removeHandler).toHaveBeenCalledWith( + RUNTIME_PROVIDER_MANAGEMENT_CLEAR_PROJECT_DEFAULT + ); + }); + it('validates and routes local provider list, scan, probe, and configuration requests', async () => { const handlers = new Map Promise>(); const ipcMain = { diff --git a/test/renderer/components/team/TeamModelSelectorDisabledState.test.ts b/test/renderer/components/team/TeamModelSelectorDisabledState.test.ts index 737c0f2260..22ccfccce2 100644 --- a/test/renderer/components/team/TeamModelSelectorDisabledState.test.ts +++ b/test/renderer/components/team/TeamModelSelectorDisabledState.test.ts @@ -4738,9 +4738,23 @@ describe('TeamModelSelector disabled Codex models', () => { }); expect(host.textContent).toContain('big-pickle'); + expect(host.textContent).toContain('Default - big-pickle'); expect(host.textContent).toContain('GPT-5.4'); expect(host.textContent).not.toContain('Loading models'); + await act(async () => { + root.render( + React.createElement(TeamModelSelector, { + providerId: 'opencode', + onProviderChange: () => undefined, + value: 'openai/gpt-5.4', + onValueChange: () => undefined, + }) + ); + await Promise.resolve(); + }); + expect(host.textContent).toContain('Explicit choice - GPT-5.4'); + await act(async () => { root.unmount(); await Promise.resolve(); diff --git a/test/renderer/features/runtime-provider-management/RuntimeProviderManagementPanelView.test.ts b/test/renderer/features/runtime-provider-management/RuntimeProviderManagementPanelView.test.ts index 8c25245a2a..364c6ae02d 100644 --- a/test/renderer/features/runtime-provider-management/RuntimeProviderManagementPanelView.test.ts +++ b/test/renderer/features/runtime-provider-management/RuntimeProviderManagementPanelView.test.ts @@ -47,6 +47,11 @@ function createState( detail: null, }, ], + configuredModels: [], + projectPath: null, + projectDefaultModel: null, + allProjectsDefaultModel: null, + defaultModelSource: null, defaultModel: null, fallbackModel: null, diagnostics: [], @@ -91,6 +96,7 @@ function createState( selectedModelId: null, testingModelIds: [], savingDefaultModelId: null, + clearingProjectDefault: false, modelResults: {}, loading: false, savingProviderId: null, @@ -200,9 +206,8 @@ describe('RuntimeProviderManagementPanelView', () => { await selectOpenCodeTab(host, 'Models'); - expect(host.textContent).toContain('OpenCode defaults'); - expect(host.textContent).toContain('Projects use this model unless they choose another.'); - expect(host.textContent).toContain('Default model'); + expect(host.textContent).not.toContain('OpenCode defaults'); + expect(host.textContent).toContain('No OpenCode model routes were reported yet'); expect( host.querySelector('[data-testid="runtime-provider-model-loading-skeleton"]') ).toBeNull(); @@ -215,6 +220,92 @@ describe('RuntimeProviderManagementPanelView', () => { expect(host.textContent).not.toContain('Validation context'); }); + it('keeps bundled v0.0.74 legacy default editing while scoped inheritance and clear stay gated', async () => { + const host = document.createElement('div'); + document.body.appendChild(host); + const root = createRoot(host); + const baseView = createState().view!; + const actions = createActions(); + const legacyModel = { + providerId: 'openrouter', + modelId: 'openrouter/openai/gpt-oss-20b:free', + displayName: 'GPT OSS 20B', + sourceLabel: 'OpenRouter', + free: true, + default: true, + availability: 'untested' as const, + accessKind: 'configured_authless' as const, + proofState: 'needs_probe' as const, + requiresExecutionProof: true, + accessReason: 'Execution proof required', + }; + const otherLegacyModel = { + ...legacyModel, + modelId: 'openrouter/qwen/qwen3-coder:free', + displayName: 'Qwen3 Coder', + default: false, + }; + const legacyView = { + ...baseView, + runtime: { ...baseView.runtime, version: '1.14.24' }, + configuredModels: [legacyModel, otherLegacyModel], + projectPath: null, + projectDefaultModel: null, + allProjectsDefaultModel: legacyModel.modelId, + defaultModelSource: 'all_projects' as const, + defaultModel: legacyModel.modelId, + }; + + await act(async () => { + root.render( + React.createElement(RuntimeProviderManagementPanelView, { + state: createState({ view: legacyView }), + actions, + disabled: false, + }) + ); + await Promise.resolve(); + }); + await selectOpenCodeTab(host, 'Models'); + + expect(host.querySelector('[data-testid="runtime-provider-legacy-models"]')).not.toBeNull(); + expect(host.querySelector('[data-testid="opencode-default-inheritance"]')).toBeNull(); + expect(host.textContent).not.toContain('Use default'); + expect(host.textContent).not.toContain('Save for team picker'); + const setLegacyDefault = Array.from(host.querySelectorAll('button')).find( + (button) => button.textContent?.trim() === 'Set all-projects default' + ); + expect(setLegacyDefault?.disabled).toBe(false); + await act(async () => { + setLegacyDefault?.click(); + await Promise.resolve(); + }); + expect(actions.setDefaultModel).toHaveBeenCalledWith( + 'openrouter', + legacyModel.modelId, + 'all_projects' + ); + + await act(async () => { + root.render( + React.createElement(RuntimeProviderManagementPanelView, { + state: createState({ + view: legacyView, + savingDefaultModelId: legacyModel.modelId, + }), + actions, + disabled: false, + }) + ); + await Promise.resolve(); + }); + const allLegacyDefaultButtons = Array.from(host.querySelectorAll('button')).filter( + (button) => button.textContent?.includes('Set all-projects default') + ); + expect(allLegacyDefaultButtons).toHaveLength(2); + expect(allLegacyDefaultButtons.every((button) => button.disabled)).toBe(true); + }); + it('requests the full managed view only after the Models tab is opened', async () => { const host = document.createElement('div'); document.body.appendChild(host); @@ -738,6 +829,7 @@ describe('RuntimeProviderManagementPanelView', () => { actions: createActions(), disabled: false, projectPath: '/Users/belief/dev/projects/321', + bundledRuntimeVersion: '0.0.75', }) ); await Promise.resolve(); @@ -786,7 +878,7 @@ describe('RuntimeProviderManagementPanelView', () => { }), actions, disabled: false, - projectPath: '/tmp/project-a', + bundledRuntimeVersion: '0.0.75', }) ); await Promise.resolve(); @@ -798,13 +890,108 @@ describe('RuntimeProviderManagementPanelView', () => { (button) => button.textContent?.trim() === 'Change' ); await act(async () => { + changeButtons[0]?.focus(); changeButtons[0]?.click(); await Promise.resolve(); }); expect(host.textContent).toContain('Choose a model for projects without an override.'); expect(host.querySelector('[data-testid="runtime-provider-catalog-list"]')).not.toBeNull(); + expect(document.activeElement).toBe( + host.querySelector('[data-testid="opencode-default-target-banner"]') + ); expect(actions.setDefaultModel).not.toHaveBeenCalled(); + + await act(async () => { + Array.from(host.querySelectorAll('[role="tab"]')) + .find((tab) => tab.textContent?.includes('Models')) + ?.dispatchEvent(new MouseEvent('click', { bubbles: true })); + await Promise.resolve(); + }); + await act(async () => { + Array.from(host.querySelectorAll('[role="tab"]')) + .find((tab) => tab.textContent?.includes('Providers')) + ?.dispatchEvent(new MouseEvent('click', { bubbles: true })); + await Promise.resolve(); + }); + expect(document.activeElement).toBe( + host.querySelector('[data-testid="opencode-default-target-banner"]') + ); + + await act(async () => { + Array.from(host.querySelectorAll('button')) + .find((button) => button.textContent?.trim() === 'Cancel') + ?.click(); + await Promise.resolve(); + }); + const restoredAllProjectsAction = host.querySelector( + 'button[aria-label="Change: Default model"]' + ); + expect(document.activeElement).toBe( + Array.from(host.querySelectorAll('[role="tab"]')).find( + (tab) => tab.textContent?.includes('Models') + ) + ); + + await act(async () => { + restoredAllProjectsAction?.click(); + await Promise.resolve(); + }); + expect(document.activeElement).toBe( + host.querySelector('[data-testid="opencode-default-target-banner"]') + ); + + const connectedProvider = { + ...createState().view!.providers[0]!, + state: 'connected' as const, + actions: [ + { + id: 'test' as const, + label: 'Test', + enabled: true, + disabledReason: null, + requiresSecret: false, + ownershipScope: 'managed' as const, + }, + ], + }; + await act(async () => { + root.render( + React.createElement(RuntimeProviderManagementPanelView, { + state: createState({ + providers: [connectedProvider], + selectedProviderId: 'openrouter', + modelPickerProviderId: 'openrouter', + modelPickerMode: 'runtime-default', + models: [configuredModel], + }), + actions, + disabled: false, + bundledRuntimeVersion: '0.0.75', + }) + ); + await Promise.resolve(); + }); + + const testAndUseButton = Array.from(host.querySelectorAll('button')).find( + (button) => button.textContent?.trim() === 'Test and use' + ); + expect(testAndUseButton?.disabled).toBe(false); + await act(async () => { + testAndUseButton?.click(); + await Promise.resolve(); + }); + expect(actions.setDefaultModel).toHaveBeenCalledWith( + 'openrouter', + configuredModel.modelId, + 'all_projects', + null + ); + expect(document.activeElement).toBe( + Array.from(host.querySelectorAll('[role="tab"]')).find( + (tab) => tab.textContent?.includes('Models') + ) + ); }); it('clears a project override through the explicit Use default action', async () => { @@ -828,6 +1015,7 @@ describe('RuntimeProviderManagementPanelView', () => { actions, disabled: false, projectPath: '/tmp/project-a', + bundledRuntimeVersion: '0.0.75', }) ); await Promise.resolve(); @@ -876,6 +1064,7 @@ describe('RuntimeProviderManagementPanelView', () => { }), actions: createActions(), disabled: false, + bundledRuntimeVersion: '0.0.75', }) ); await Promise.resolve(); @@ -897,7 +1086,7 @@ describe('RuntimeProviderManagementPanelView', () => { ).toBeNull(); }); - it('keeps an unknown OpenCode default visible as its raw model id', async () => { + it('marks unavailable stored defaults and names their scoped actions accessibly', async () => { const host = document.createElement('div'); document.body.appendChild(host); const root = createRoot(host); @@ -905,7 +1094,7 @@ describe('RuntimeProviderManagementPanelView', () => { const unknownDefaultModel = { providerId: 'openrouter', modelId: 'openrouter/moonshotai/kimi-k2', - displayName: 'moonshotai/kimi-k2', + displayName: 'openrouter/moonshotai/kimi-k2', sourceLabel: 'OpenRouter', free: false, default: true, @@ -924,13 +1113,27 @@ describe('RuntimeProviderManagementPanelView', () => { view: { ...createState().view!, configuredModels: [unknownDefaultModel], + projectPath: '/workspace/project-a', + projectDefaultModel: unknownDefaultModel.modelId, defaultModel: unknownDefaultModel.modelId, allProjectsDefaultModel: unknownDefaultModel.modelId, - defaultModelSource: 'all_projects', + defaultModelSource: 'project', }, }), actions, disabled: false, + projectPath: '/workspace/project-a', + bundledRuntimeVersion: '0.0.75', + projectContextProjects: [ + { + id: 'project-a', + path: '/workspace/project-a', + name: 'Project Alpha', + sessions: [], + totalSessions: 0, + createdAt: 0, + }, + ], }) ); await Promise.resolve(); @@ -938,8 +1141,22 @@ describe('RuntimeProviderManagementPanelView', () => { await selectOpenCodeTab(host, 'Models'); - expect(host.textContent).toContain('openrouter/moonshotai/kimi-k2'); + expect(host.textContent).toContain('Unavailable in current runtime'); + expect(host.textContent).toContain('Model was not found in the live catalog'); expect(host.querySelector('[data-testid="opencode-default-inheritance"]')).not.toBeNull(); + expect( + host.querySelector('[data-testid="opencode-default-base-model-id"]')?.textContent + ).toBe(unknownDefaultModel.modelId); + expect( + host.querySelector('[data-testid="opencode-default-project-model-id"]')?.textContent + ).toBe(unknownDefaultModel.modelId); + expect(host.querySelector('button[aria-label="Change: Default model"]')).not.toBeNull(); + expect( + host.querySelector('button[aria-label="Change: This project: Project Alpha"]') + ).not.toBeNull(); + expect( + host.querySelector('button[aria-label="Use default: This project: Project Alpha"]') + ).not.toBeNull(); expect(actions.testModel).not.toHaveBeenCalled(); expect(actions.useModelForNewTeams).not.toHaveBeenCalled(); expect(actions.setDefaultModel).not.toHaveBeenCalled(); @@ -2934,8 +3151,8 @@ describe('RuntimeProviderManagementPanelView', () => { '[data-testid="runtime-provider-model-row-google/gemini-old"]' ); expect(row?.textContent).toContain('deprecated'); - expect(row?.getAttribute('aria-disabled')).toBe('true'); - expect(row?.getAttribute('aria-label')).toContain('OpenCode marks this model as deprecated'); + expect(row?.getAttribute('aria-disabled')).toBeNull(); + expect(row?.textContent).toContain('OpenCode marks this model as deprecated'); await act(async () => { row?.dispatchEvent(new MouseEvent('click', { bubbles: true })); @@ -3377,7 +3594,7 @@ describe('RuntimeProviderManagementPanelView', () => { ); expect(row?.getAttribute('role')).toBeNull(); - expect(row?.getAttribute('aria-disabled')).toBe('true'); + expect(row?.getAttribute('aria-disabled')).toBeNull(); expect(row?.tabIndex).toBe(-1); await act(async () => { diff --git a/test/renderer/features/runtime-provider-management/RuntimeProviderOnboardingView.test.ts b/test/renderer/features/runtime-provider-management/RuntimeProviderOnboardingView.test.ts index 88217c047c..efa75835b6 100644 --- a/test/renderer/features/runtime-provider-management/RuntimeProviderOnboardingView.test.ts +++ b/test/renderer/features/runtime-provider-management/RuntimeProviderOnboardingView.test.ts @@ -60,6 +60,7 @@ function managementState( selectedModelId: null, testingModelIds: [], savingDefaultModelId: null, + clearingProjectDefault: false, modelResults: {}, loading: false, savingProviderId: null, diff --git a/test/renderer/features/runtime-provider-management/providerManagementView.test.ts b/test/renderer/features/runtime-provider-management/providerManagementView.test.ts index 641fd8ef80..b6065e6a28 100644 --- a/test/renderer/features/runtime-provider-management/providerManagementView.test.ts +++ b/test/renderer/features/runtime-provider-management/providerManagementView.test.ts @@ -4,6 +4,7 @@ import { canConnectWithApiKey, canForgetManagedCredential, selectInitialProviderId, + supportsScopedDefaultModelInheritance, } from '../../../../src/features/runtime-provider-management/core/domain'; import type { @@ -113,4 +114,56 @@ describe('runtime provider management domain', () => { ) ).toBe(true); }); + + it('gates scoped default inheritance on the first compatible stable runtime revision', () => { + const compatibleView = { + ...view([]), + configuredModels: [], + projectPath: null, + projectDefaultModel: null, + allProjectsDefaultModel: null, + defaultModelSource: null, + }; + expect( + supportsScopedDefaultModelInheritance( + { + ...compatibleView, + runtime: { ...compatibleView.runtime, version: '1.14.24' }, + }, + '0.0.74' + ) + ).toBe(false); + expect( + supportsScopedDefaultModelInheritance( + { + ...compatibleView, + runtime: { ...compatibleView.runtime, version: '1.14.24' }, + }, + '0.0.75' + ) + ).toBe(true); + expect( + supportsScopedDefaultModelInheritance( + { + ...compatibleView, + runtime: { ...compatibleView.runtime, version: '1.14.24' }, + }, + '0.0.75-beta.1' + ) + ).toBe(false); + expect( + supportsScopedDefaultModelInheritance( + { + ...view([]), + configuredModels: [], + projectPath: null, + projectDefaultModel: null, + allProjectsDefaultModel: null, + runtime: { ...compatibleView.runtime, version: '1.14.24' }, + }, + '99.0.0' + ) + ).toBe(false); + expect(supportsScopedDefaultModelInheritance(compatibleView)).toBe(false); + }); }); diff --git a/test/renderer/features/runtime-provider-management/useRuntimeProviderManagement.test.ts b/test/renderer/features/runtime-provider-management/useRuntimeProviderManagement.test.ts index 433e25dfdc..b05bdc04f8 100644 --- a/test/renderer/features/runtime-provider-management/useRuntimeProviderManagement.test.ts +++ b/test/renderer/features/runtime-provider-management/useRuntimeProviderManagement.test.ts @@ -61,6 +61,11 @@ function createRuntimeView( localAuth: 'synced', }, providers, + configuredModels: [], + projectPath: null, + projectDefaultModel: null, + allProjectsDefaultModel: null, + defaultModelSource: null, defaultModel: null, fallbackModel: null, diagnostics: [], @@ -135,11 +140,15 @@ describe('useRuntimeProviderManagement', () => { return React.createElement('div'); } - function EnabledHarness(props: { projectPath?: string | null }): React.ReactElement { + function EnabledHarness(props: { + projectPath?: string | null; + bundledRuntimeVersion?: string; + }): React.ReactElement { const hook = useRuntimeProviderManagement({ runtimeId: 'opencode', enabled: true, projectPath: props.projectPath, + bundledRuntimeVersion: props.bundledRuntimeVersion, }); state = hook[0]; actions = hook[1]; @@ -155,6 +164,7 @@ describe('useRuntimeProviderManagement', () => { searchDirectoryOnQueryChange?: boolean; initialProviderId?: string | null; initialProviderAction?: 'connect' | 'reconnect' | 'select' | null; + bundledRuntimeVersion?: string; onProviderChanged?: ( changeKind: RuntimeProviderChangeKind ) => Promise | boolean | void; @@ -169,6 +179,7 @@ describe('useRuntimeProviderManagement', () => { searchDirectoryOnQueryChange: props.searchDirectoryOnQueryChange, initialProviderId: props.initialProviderId, initialProviderAction: props.initialProviderAction, + bundledRuntimeVersion: props.bundledRuntimeVersion, onProviderChanged: props.onProviderChanged, }); state = hook[0]; @@ -1172,7 +1183,7 @@ describe('useRuntimeProviderManagement', () => { }); expect(state?.view?.projectPath).toBe('/tmp/project-b'); - expect(state?.savingDefaultModelId).toBeNull(); + expect(state?.savingDefaultModelId).toBe(projectAModelId); await act(async () => { resolveSetDefault?.({ @@ -1191,6 +1202,7 @@ describe('useRuntimeProviderManagement', () => { expect(state?.view?.defaultModel).toBe('opencode/project-b'); expect(state?.selectedModelId).toBe('opencode/project-b'); expect(state?.successMessage).toBeNull(); + expect(state?.savingDefaultModelId).toBeNull(); await act(async () => { root.unmount(); @@ -3238,18 +3250,28 @@ describe('useRuntimeProviderManagement', () => { value: { runtimeProviderManagement: { setDefaultModel, + loadView: vi.fn(() => + Promise.resolve({ + schemaVersion: 1 as const, + runtimeId: 'opencode' as const, + view: { + ...createRuntimeView(), + projectPath: '/tmp/project-a', + }, + }) + ), }, } as unknown as ElectronAPI, }); const root = createRoot(host); await act(async () => { - root.render(React.createElement(Harness)); + root.render(React.createElement(EnabledHarness, { projectPath: '/tmp/project-a' })); await Promise.resolve(); }); await act(async () => { - await actions?.setDefaultModel('llama.cpp', modelId); + await actions?.setDefaultModel('llama.cpp', modelId, 'project', '/tmp/project-a'); await Promise.resolve(); }); @@ -3259,7 +3281,7 @@ describe('useRuntimeProviderManagement', () => { modelId, probe: true, scope: 'project', - projectPath: null, + projectPath: '/tmp/project-a', }); expect(state?.view?.configuredModels?.[0]).toMatchObject({ modelId, @@ -3337,7 +3359,7 @@ describe('useRuntimeProviderManagement', () => { root.render( React.createElement(ConfigurableHarness, { enabled: true, - loadViewOnEnable: false, + loadViewOnEnable: true, }) ); await Promise.resolve(); @@ -3367,7 +3389,7 @@ describe('useRuntimeProviderManagement', () => { }); }); - it('clears a project override and immediately resolves to the all-projects default', async () => { + it('does not clear a project override before the bundled runtime supports scoped defaults', async () => { const clearProjectDefaultModel = vi.fn(() => Promise.resolve({ schemaVersion: 1 as const, @@ -3400,7 +3422,61 @@ describe('useRuntimeProviderManagement', () => { clearProjectDefaultModel.mockClear(); await act(async () => { - await actions?.clearProjectDefault(); + await actions?.clearProjectDefault('/tmp/project-a'); + await Promise.resolve(); + }); + + expect(clearProjectDefaultModel).not.toHaveBeenCalled(); + }); + + it('clears a valid project override with a compatible bundled runtime', async () => { + const loadView = vi.fn(() => + Promise.resolve({ + schemaVersion: 1 as const, + runtimeId: 'opencode' as const, + view: { + ...createRuntimeView(), + projectPath: '/tmp/project-a', + projectDefaultModel: 'openrouter/project-model', + allProjectsDefaultModel: 'openrouter/base-model', + defaultModel: 'openrouter/project-model', + defaultModelSource: 'project' as const, + }, + }) + ); + const clearProjectDefaultModel = vi.fn(() => + Promise.resolve({ + schemaVersion: 1 as const, + runtimeId: 'opencode' as const, + view: { + ...createRuntimeView(), + projectPath: '/tmp/project-a', + projectDefaultModel: null, + allProjectsDefaultModel: 'openrouter/base-model', + defaultModel: 'openrouter/base-model', + defaultModelSource: 'all_projects' as const, + }, + }) + ); + Object.defineProperty(window, 'electronAPI', { + configurable: true, + value: { + runtimeProviderManagement: { loadView, clearProjectDefaultModel }, + } as unknown as ElectronAPI, + }); + const root = createRoot(host); + await act(async () => { + root.render( + React.createElement(EnabledHarness, { + projectPath: '/tmp/project-a', + bundledRuntimeVersion: '0.0.75', + }) + ); + await Promise.resolve(); + }); + + await act(async () => { + await actions?.clearProjectDefault('/tmp/project-a'); await Promise.resolve(); }); @@ -3411,5 +3487,64 @@ describe('useRuntimeProviderManagement', () => { expect(state?.view?.projectDefaultModel).toBeNull(); expect(state?.view?.defaultModel).toBe('openrouter/base-model'); expect(state?.selectedModelId).toBe('openrouter/base-model'); + expect(state?.successMessage).toBe( + 'This project: Uses default openrouter/base-model' + ); + }); + + it('does not clear project B while its view is still refreshing after switching from A', async () => { + const clearProjectDefaultModel = vi.fn(); + const projectBView = new Promise(() => undefined); + Object.defineProperty(window, 'electronAPI', { + configurable: true, + value: { + runtimeProviderManagement: { + clearProjectDefaultModel, + loadView: vi.fn((input: { projectPath?: string | null }) => { + if (input.projectPath === '/tmp/project-b') return projectBView; + return Promise.resolve({ + schemaVersion: 1 as const, + runtimeId: 'opencode' as const, + view: { + ...createRuntimeView(), + configuredModels: [], + projectPath: '/tmp/project-a', + projectDefaultModel: 'openrouter/project-a', + allProjectsDefaultModel: 'openrouter/base', + defaultModelSource: 'project' as const, + }, + }); + }), + }, + } as unknown as ElectronAPI, + }); + const root = createRoot(host); + await act(async () => { + root.render( + React.createElement(EnabledHarness, { + projectPath: '/tmp/project-a', + bundledRuntimeVersion: '0.0.75', + }) + ); + await Promise.resolve(); + }); + expect(state?.view?.projectPath).toBe('/tmp/project-a'); + + await act(async () => { + root.render( + React.createElement(EnabledHarness, { + projectPath: '/tmp/project-b', + bundledRuntimeVersion: '0.0.75', + }) + ); + await Promise.resolve(); + }); + expect(state?.view).toBeNull(); + + await act(async () => { + await actions?.clearProjectDefault('/tmp/project-b'); + }); + + expect(clearProjectDefaultModel).not.toHaveBeenCalled(); }); }); From 224a4d7ad4a22913ac1904fa34f0e1d9b23bdb5f Mon Sep 17 00:00:00 2001 From: 777genius Date: Fri, 21 Aug 2026 00:15:12 +0300 Subject: [PATCH 05/20] fix(i18n): localize inherited default workflow --- .../renderer/locales/ar/settings.json | 26 +++++++++---------- .../renderer/locales/ar/team.json | 4 +-- .../renderer/locales/bn/settings.json | 26 +++++++++---------- .../renderer/locales/bn/team.json | 4 +-- .../renderer/locales/de/settings.json | 26 +++++++++---------- .../renderer/locales/de/team.json | 4 +-- .../renderer/locales/es/settings.json | 26 +++++++++---------- .../renderer/locales/es/team.json | 4 +-- .../renderer/locales/fa/settings.json | 26 +++++++++---------- .../renderer/locales/fa/team.json | 4 +-- .../renderer/locales/fil/settings.json | 26 +++++++++---------- .../renderer/locales/fil/team.json | 4 +-- .../renderer/locales/fr/settings.json | 26 +++++++++---------- .../renderer/locales/fr/team.json | 4 +-- .../renderer/locales/hi/settings.json | 26 +++++++++---------- .../renderer/locales/hi/team.json | 4 +-- .../renderer/locales/id/settings.json | 26 +++++++++---------- .../renderer/locales/id/team.json | 4 +-- .../renderer/locales/it/settings.json | 26 +++++++++---------- .../renderer/locales/it/team.json | 4 +-- .../renderer/locales/ja/settings.json | 26 +++++++++---------- .../renderer/locales/ja/team.json | 4 +-- .../renderer/locales/ko/settings.json | 26 +++++++++---------- .../renderer/locales/ko/team.json | 6 ++--- .../renderer/locales/mr/settings.json | 26 +++++++++---------- .../renderer/locales/mr/team.json | 4 +-- .../renderer/locales/ms/settings.json | 26 +++++++++---------- .../renderer/locales/ms/team.json | 4 +-- .../renderer/locales/nl/settings.json | 26 +++++++++---------- .../renderer/locales/nl/team.json | 4 +-- .../renderer/locales/pl/settings.json | 26 +++++++++---------- .../renderer/locales/pl/team.json | 4 +-- .../renderer/locales/pt/settings.json | 26 +++++++++---------- .../renderer/locales/pt/team.json | 4 +-- .../renderer/locales/ro/settings.json | 26 +++++++++---------- .../renderer/locales/ro/team.json | 4 +-- .../renderer/locales/sw/settings.json | 26 +++++++++---------- .../renderer/locales/sw/team.json | 4 +-- .../renderer/locales/ta/settings.json | 26 +++++++++---------- .../renderer/locales/ta/team.json | 4 +-- .../renderer/locales/te/settings.json | 26 +++++++++---------- .../renderer/locales/te/team.json | 4 +-- .../renderer/locales/th/settings.json | 26 +++++++++---------- .../renderer/locales/th/team.json | 4 +-- .../renderer/locales/tr/settings.json | 26 +++++++++---------- .../renderer/locales/tr/team.json | 4 +-- .../renderer/locales/uk/settings.json | 4 +-- .../renderer/locales/ur/settings.json | 26 +++++++++---------- .../renderer/locales/ur/team.json | 4 +-- .../renderer/locales/vi/settings.json | 26 +++++++++---------- .../renderer/locales/vi/team.json | 4 +-- .../renderer/locales/zh/settings.json | 26 +++++++++---------- .../renderer/locales/zh/team.json | 4 +-- 53 files changed, 393 insertions(+), 393 deletions(-) diff --git a/src/features/localization/renderer/locales/ar/settings.json b/src/features/localization/renderer/locales/ar/settings.json index 713411f430..9a1ebe2e6e 100644 --- a/src/features/localization/renderer/locales/ar/settings.json +++ b/src/features/localization/renderer/locales/ar/settings.json @@ -44,19 +44,19 @@ "thisProject": "هذا المشروع", "title": "التخلف عن سداد الالتزامات المفتوحة", "validationContext": "سياق التقييم", - "inheritanceDescription": "Projects use this model unless they choose another.", - "freeRouterAdvisory": "Chooses an available free model for each request. The underlying model may change.", - "change": "Change", - "useAnotherModel": "Use another model", - "inherits": "Uses default", - "override": "Project override", - "useDefault": "Use default", - "choosingAllProjects": "Choose a model for projects without an override.", - "choosingProject": "Choose a model only for {{project}}.", - "cancelChoosing": "Cancel", - "testAndUse": "Test and use", - "testAndUseAllProjects": "Test and use for all projects", - "testAndUseProject": "Test and use for this project" + "inheritanceDescription": "تستخدم المشاريع هذا النموذج ما لم تختر نموذجًا آخر.", + "freeRouterAdvisory": "يختار نموذجًا مجانيًا متاحًا لكل طلب. قد يتغير النموذج الأساسي.", + "change": "تغيير", + "useAnotherModel": "استخدام نموذج آخر", + "inherits": "يستخدم النموذج الافتراضي", + "override": "تجاوز خاص بالمشروع", + "useDefault": "استخدام النموذج الافتراضي", + "choosingAllProjects": "اختر نموذجًا للمشاريع التي ليس لديها تجاوز.", + "choosingProject": "اختر نموذجًا لـ {{project}} فقط.", + "cancelChoosing": "إلغاء", + "testAndUse": "اختبار واستخدام", + "testAndUseAllProjects": "اختبار واستخدام لجميع المشاريع", + "testAndUseProject": "اختبار واستخدام لهذا المشروع" }, "diagnostics": { "copied": "أجهزة التشخيص", diff --git a/src/features/localization/renderer/locales/ar/team.json b/src/features/localization/renderer/locales/ar/team.json index a4fb0e55f2..0c01be1bba 100644 --- a/src/features/localization/renderer/locales/ar/team.json +++ b/src/features/localization/renderer/locales/ar/team.json @@ -900,8 +900,8 @@ }, "multimodelOff": "مضاعف", "unavailableInRuntime": "غير متاح في الوقت الحالي", - "defaultWithResolved": "Default - {{model}}", - "explicitChoice": "Explicit choice - {{model}}" + "defaultWithResolved": "الافتراضي - {{model}}", + "explicitChoice": "اختيار صريح - {{model}}" }, "taskDetail": { "actions": { diff --git a/src/features/localization/renderer/locales/bn/settings.json b/src/features/localization/renderer/locales/bn/settings.json index 057590c78d..d6eda7dddc 100644 --- a/src/features/localization/renderer/locales/bn/settings.json +++ b/src/features/localization/renderer/locales/bn/settings.json @@ -44,19 +44,19 @@ "thisProject": "এই প্রজেক্ট", "title": "ডিফল্ট অঙ্ক", "validationContext": "বৈধতা যাচাই", - "inheritanceDescription": "Projects use this model unless they choose another.", - "freeRouterAdvisory": "Chooses an available free model for each request. The underlying model may change.", - "change": "Change", - "useAnotherModel": "Use another model", - "inherits": "Uses default", - "override": "Project override", - "useDefault": "Use default", - "choosingAllProjects": "Choose a model for projects without an override.", - "choosingProject": "Choose a model only for {{project}}.", - "cancelChoosing": "Cancel", - "testAndUse": "Test and use", - "testAndUseAllProjects": "Test and use for all projects", - "testAndUseProject": "Test and use for this project" + "inheritanceDescription": "প্রকল্পগুলি অন্য কোনো মডেল না বেছে নিলে এই মডেলটি ব্যবহার করে।", + "freeRouterAdvisory": "প্রতিটি অনুরোধের জন্য একটি উপলভ্য বিনামূল্যের মডেল বেছে নেয়। অন্তর্নিহিত মডেলটি পরিবর্তিত হতে পারে।", + "change": "পরিবর্তন করুন", + "useAnotherModel": "অন্য মডেল ব্যবহার করুন", + "inherits": "ডিফল্ট ব্যবহার করে", + "override": "প্রকল্পের ওভাররাইড", + "useDefault": "ডিফল্ট ব্যবহার করুন", + "choosingAllProjects": "ওভাররাইড নেই এমন প্রকল্পগুলির জন্য একটি মডেল বেছে নিন।", + "choosingProject": "শুধু {{project}}-এর জন্য একটি মডেল বেছে নিন।", + "cancelChoosing": "বাতিল করুন", + "testAndUse": "পরীক্ষা করে ব্যবহার করুন", + "testAndUseAllProjects": "সব প্রকল্পের জন্য পরীক্ষা করে ব্যবহার করুন", + "testAndUseProject": "এই প্রকল্পের জন্য পরীক্ষা করে ব্যবহার করুন" }, "diagnostics": { "copied": "ডায়াগনস্টিক কপি করেছেন", diff --git a/src/features/localization/renderer/locales/bn/team.json b/src/features/localization/renderer/locales/bn/team.json index 8963f1a979..674fe96cfe 100644 --- a/src/features/localization/renderer/locales/bn/team.json +++ b/src/features/localization/renderer/locales/bn/team.json @@ -900,8 +900,8 @@ }, "multimodelOff": "মাল্টি মডেল বন্ধ করুন", "unavailableInRuntime": "বর্তমানে চলমান নয়", - "defaultWithResolved": "Default - {{model}}", - "explicitChoice": "Explicit choice - {{model}}" + "defaultWithResolved": "ডিফল্ট - {{model}}", + "explicitChoice": "স্পষ্ট পছন্দ - {{model}}" }, "taskDetail": { "actions": { diff --git a/src/features/localization/renderer/locales/de/settings.json b/src/features/localization/renderer/locales/de/settings.json index c9915e82e2..3f924ae071 100644 --- a/src/features/localization/renderer/locales/de/settings.json +++ b/src/features/localization/renderer/locales/de/settings.json @@ -44,19 +44,19 @@ "thisProject": "Dieses Projekt", "title": "OpenCode-Standards", "validationContext": "Validierungskontext", - "inheritanceDescription": "Projects use this model unless they choose another.", - "freeRouterAdvisory": "Chooses an available free model for each request. The underlying model may change.", - "change": "Change", - "useAnotherModel": "Use another model", - "inherits": "Uses default", - "override": "Project override", - "useDefault": "Use default", - "choosingAllProjects": "Choose a model for projects without an override.", - "choosingProject": "Choose a model only for {{project}}.", - "cancelChoosing": "Cancel", - "testAndUse": "Test and use", - "testAndUseAllProjects": "Test and use for all projects", - "testAndUseProject": "Test and use for this project" + "inheritanceDescription": "Projekte verwenden dieses Modell, sofern sie kein anderes auswählen.", + "freeRouterAdvisory": "Wählt für jede Anfrage ein verfügbares kostenloses Modell aus. Das zugrunde liegende Modell kann sich ändern.", + "change": "Ändern", + "useAnotherModel": "Anderes Modell verwenden", + "inherits": "Verwendet den Standard", + "override": "Projektüberschreibung", + "useDefault": "Standard verwenden", + "choosingAllProjects": "Wählen Sie ein Modell für Projekte ohne Überschreibung.", + "choosingProject": "Wählen Sie ein Modell nur für {{project}}.", + "cancelChoosing": "Abbrechen", + "testAndUse": "Testen und verwenden", + "testAndUseAllProjects": "Für alle Projekte testen und verwenden", + "testAndUseProject": "Für dieses Projekt testen und verwenden" }, "diagnostics": { "copied": "Diagnostik kopiert", diff --git a/src/features/localization/renderer/locales/de/team.json b/src/features/localization/renderer/locales/de/team.json index 4aece6f844..a7f43fd31b 100644 --- a/src/features/localization/renderer/locales/de/team.json +++ b/src/features/localization/renderer/locales/de/team.json @@ -900,8 +900,8 @@ }, "multimodelOff": "Multimodel ab", "unavailableInRuntime": "Nicht verfügbar in der aktuellen Laufzeit", - "defaultWithResolved": "Default - {{model}}", - "explicitChoice": "Explicit choice - {{model}}" + "defaultWithResolved": "Standard - {{model}}", + "explicitChoice": "Explizite Auswahl - {{model}}" }, "taskDetail": { "actions": { diff --git a/src/features/localization/renderer/locales/es/settings.json b/src/features/localization/renderer/locales/es/settings.json index 1367d68369..3b536b354d 100644 --- a/src/features/localization/renderer/locales/es/settings.json +++ b/src/features/localization/renderer/locales/es/settings.json @@ -44,19 +44,19 @@ "thisProject": "Este proyecto", "title": "Por defecto OpenCode", "validationContext": "Contexto de validación", - "inheritanceDescription": "Projects use this model unless they choose another.", - "freeRouterAdvisory": "Chooses an available free model for each request. The underlying model may change.", - "change": "Change", - "useAnotherModel": "Use another model", - "inherits": "Uses default", - "override": "Project override", - "useDefault": "Use default", - "choosingAllProjects": "Choose a model for projects without an override.", - "choosingProject": "Choose a model only for {{project}}.", - "cancelChoosing": "Cancel", - "testAndUse": "Test and use", - "testAndUseAllProjects": "Test and use for all projects", - "testAndUseProject": "Test and use for this project" + "inheritanceDescription": "Los proyectos usan este modelo a menos que elijan otro.", + "freeRouterAdvisory": "Elige un modelo gratuito disponible para cada solicitud. El modelo subyacente puede cambiar.", + "change": "Cambiar", + "useAnotherModel": "Usar otro modelo", + "inherits": "Usa el predeterminado", + "override": "Anulación del proyecto", + "useDefault": "Usar el predeterminado", + "choosingAllProjects": "Elige un modelo para los proyectos sin una anulación.", + "choosingProject": "Elige un modelo solo para {{project}}.", + "cancelChoosing": "Cancelar", + "testAndUse": "Probar y usar", + "testAndUseAllProjects": "Probar y usar para todos los proyectos", + "testAndUseProject": "Probar y usar para este proyecto" }, "diagnostics": { "copied": "Diagnósticos copiados", diff --git a/src/features/localization/renderer/locales/es/team.json b/src/features/localization/renderer/locales/es/team.json index b4e8e34c12..af34b77ae8 100644 --- a/src/features/localization/renderer/locales/es/team.json +++ b/src/features/localization/renderer/locales/es/team.json @@ -900,8 +900,8 @@ }, "multimodelOff": "Multimodel off", "unavailableInRuntime": "No disponible en tiempo de ejecución actual", - "defaultWithResolved": "Default - {{model}}", - "explicitChoice": "Explicit choice - {{model}}" + "defaultWithResolved": "Predeterminado - {{model}}", + "explicitChoice": "Elección explícita - {{model}}" }, "taskDetail": { "actions": { diff --git a/src/features/localization/renderer/locales/fa/settings.json b/src/features/localization/renderer/locales/fa/settings.json index 86bad97a47..71d01ace42 100644 --- a/src/features/localization/renderer/locales/fa/settings.json +++ b/src/features/localization/renderer/locales/fa/settings.json @@ -44,19 +44,19 @@ "thisProject": "این پروژه", "title": "پیش‌فرض‌های OpenCode", "validationContext": "بافت اعتبارسنجی", - "inheritanceDescription": "Projects use this model unless they choose another.", - "freeRouterAdvisory": "Chooses an available free model for each request. The underlying model may change.", - "change": "Change", - "useAnotherModel": "Use another model", - "inherits": "Uses default", - "override": "Project override", - "useDefault": "Use default", - "choosingAllProjects": "Choose a model for projects without an override.", - "choosingProject": "Choose a model only for {{project}}.", - "cancelChoosing": "Cancel", - "testAndUse": "Test and use", - "testAndUseAllProjects": "Test and use for all projects", - "testAndUseProject": "Test and use for this project" + "inheritanceDescription": "پروژه‌ها از این مدل استفاده می‌کنند، مگر اینکه مدل دیگری را انتخاب کنند.", + "freeRouterAdvisory": "برای هر درخواست یک مدل رایگانِ در دسترس انتخاب می‌کند. مدل زیربنایی ممکن است تغییر کند.", + "change": "تغییر", + "useAnotherModel": "استفاده از مدل دیگر", + "inherits": "از مدل پیش‌فرض استفاده می‌کند", + "override": "بازنویسی پروژه", + "useDefault": "استفاده از مدل پیش‌فرض", + "choosingAllProjects": "برای پروژه‌های بدون بازنویسی یک مدل انتخاب کنید.", + "choosingProject": "فقط برای {{project}} یک مدل انتخاب کنید.", + "cancelChoosing": "لغو", + "testAndUse": "آزمایش و استفاده", + "testAndUseAllProjects": "آزمایش و استفاده برای همه پروژه‌ها", + "testAndUseProject": "آزمایش و استفاده برای این پروژه" }, "diagnostics": { "copied": "عیب‌یابی کپی شد", diff --git a/src/features/localization/renderer/locales/fa/team.json b/src/features/localization/renderer/locales/fa/team.json index 7325e168a9..3fcd99591d 100644 --- a/src/features/localization/renderer/locales/fa/team.json +++ b/src/features/localization/renderer/locales/fa/team.json @@ -900,8 +900,8 @@ }, "multimodelOff": "چندمدلی خاموش", "unavailableInRuntime": "در زمان اجرای فعلی در دسترس نیست", - "defaultWithResolved": "Default - {{model}}", - "explicitChoice": "Explicit choice - {{model}}" + "defaultWithResolved": "پیش‌فرض - {{model}}", + "explicitChoice": "انتخاب صریح - {{model}}" }, "taskDetail": { "actions": { diff --git a/src/features/localization/renderer/locales/fil/settings.json b/src/features/localization/renderer/locales/fil/settings.json index 7206926267..f7d8c38e49 100644 --- a/src/features/localization/renderer/locales/fil/settings.json +++ b/src/features/localization/renderer/locales/fil/settings.json @@ -44,19 +44,19 @@ "thisProject": "Proyektong ito", "title": "Mga OpenCode default", "validationContext": "Validation context", - "inheritanceDescription": "Projects use this model unless they choose another.", - "freeRouterAdvisory": "Chooses an available free model for each request. The underlying model may change.", - "change": "Change", - "useAnotherModel": "Use another model", - "inherits": "Uses default", - "override": "Project override", - "useDefault": "Use default", - "choosingAllProjects": "Choose a model for projects without an override.", - "choosingProject": "Choose a model only for {{project}}.", - "cancelChoosing": "Cancel", - "testAndUse": "Test and use", - "testAndUseAllProjects": "Test and use for all projects", - "testAndUseProject": "Test and use for this project" + "inheritanceDescription": "Ginagamit ng mga proyekto ang modelong ito maliban kung pumili sila ng iba.", + "freeRouterAdvisory": "Pumipili ng available na libreng modelo para sa bawat request. Maaaring magbago ang aktuwal na modelo.", + "change": "Baguhin", + "useAnotherModel": "Gumamit ng ibang modelo", + "inherits": "Gumagamit ng default", + "override": "Override ng proyekto", + "useDefault": "Gamitin ang default", + "choosingAllProjects": "Pumili ng modelo para sa mga proyektong walang override.", + "choosingProject": "Pumili ng modelo para lamang sa {{project}}.", + "cancelChoosing": "Kanselahin", + "testAndUse": "Subukan at gamitin", + "testAndUseAllProjects": "Subukan at gamitin para sa lahat ng proyekto", + "testAndUseProject": "Subukan at gamitin para sa proyektong ito" }, "diagnostics": { "copied": "Nakopya ang diagnostics", diff --git a/src/features/localization/renderer/locales/fil/team.json b/src/features/localization/renderer/locales/fil/team.json index f9c5c83818..b774fe8f77 100644 --- a/src/features/localization/renderer/locales/fil/team.json +++ b/src/features/localization/renderer/locales/fil/team.json @@ -900,8 +900,8 @@ }, "multimodelOff": "Naka-off ang Multimodel", "unavailableInRuntime": "Hindi available sa kasalukuyang runtime", - "defaultWithResolved": "Default - {{model}}", - "explicitChoice": "Explicit choice - {{model}}" + "defaultWithResolved": "Nakatakda - {{model}}", + "explicitChoice": "Tahasang pinili - {{model}}" }, "taskDetail": { "actions": { diff --git a/src/features/localization/renderer/locales/fr/settings.json b/src/features/localization/renderer/locales/fr/settings.json index a59d52b910..cbbdac81a9 100644 --- a/src/features/localization/renderer/locales/fr/settings.json +++ b/src/features/localization/renderer/locales/fr/settings.json @@ -44,19 +44,19 @@ "thisProject": "Ce projet", "title": "Par défaut OpenCode", "validationContext": "Contexte de validation", - "inheritanceDescription": "Projects use this model unless they choose another.", - "freeRouterAdvisory": "Chooses an available free model for each request. The underlying model may change.", - "change": "Change", - "useAnotherModel": "Use another model", - "inherits": "Uses default", - "override": "Project override", - "useDefault": "Use default", - "choosingAllProjects": "Choose a model for projects without an override.", - "choosingProject": "Choose a model only for {{project}}.", - "cancelChoosing": "Cancel", - "testAndUse": "Test and use", - "testAndUseAllProjects": "Test and use for all projects", - "testAndUseProject": "Test and use for this project" + "inheritanceDescription": "Les projets utilisent ce modèle sauf s’ils en choisissent un autre.", + "freeRouterAdvisory": "Choisit un modèle gratuit disponible pour chaque requête. Le modèle sous-jacent peut changer.", + "change": "Modifier", + "useAnotherModel": "Utiliser un autre modèle", + "inherits": "Utilise le modèle par défaut", + "override": "Remplacement du projet", + "useDefault": "Utiliser le modèle par défaut", + "choosingAllProjects": "Choisissez un modèle pour les projets sans remplacement.", + "choosingProject": "Choisissez un modèle uniquement pour {{project}}.", + "cancelChoosing": "Annuler", + "testAndUse": "Tester et utiliser", + "testAndUseAllProjects": "Tester et utiliser pour tous les projets", + "testAndUseProject": "Tester et utiliser pour ce projet" }, "diagnostics": { "copied": "Diagnostic copié", diff --git a/src/features/localization/renderer/locales/fr/team.json b/src/features/localization/renderer/locales/fr/team.json index a34e223455..68b89f4436 100644 --- a/src/features/localization/renderer/locales/fr/team.json +++ b/src/features/localization/renderer/locales/fr/team.json @@ -900,8 +900,8 @@ }, "multimodelOff": "Modèle multiple désactivé", "unavailableInRuntime": "Indisponible dans l'exécution actuelle", - "defaultWithResolved": "Default - {{model}}", - "explicitChoice": "Explicit choice - {{model}}" + "defaultWithResolved": "Par défaut - {{model}}", + "explicitChoice": "Choix explicite - {{model}}" }, "taskDetail": { "actions": { diff --git a/src/features/localization/renderer/locales/hi/settings.json b/src/features/localization/renderer/locales/hi/settings.json index 3301f6c027..4ea722a5ef 100644 --- a/src/features/localization/renderer/locales/hi/settings.json +++ b/src/features/localization/renderer/locales/hi/settings.json @@ -44,19 +44,19 @@ "thisProject": "यह परियोजना", "title": "OpenCode डिफ़ॉल्ट", "validationContext": "प्रमाणीकरण", - "inheritanceDescription": "Projects use this model unless they choose another.", - "freeRouterAdvisory": "Chooses an available free model for each request. The underlying model may change.", - "change": "Change", - "useAnotherModel": "Use another model", - "inherits": "Uses default", - "override": "Project override", - "useDefault": "Use default", - "choosingAllProjects": "Choose a model for projects without an override.", - "choosingProject": "Choose a model only for {{project}}.", - "cancelChoosing": "Cancel", - "testAndUse": "Test and use", - "testAndUseAllProjects": "Test and use for all projects", - "testAndUseProject": "Test and use for this project" + "inheritanceDescription": "प्रोजेक्ट किसी अन्य मॉडल को न चुनने पर इस मॉडल का उपयोग करते हैं।", + "freeRouterAdvisory": "हर अनुरोध के लिए उपलब्ध मुफ़्त मॉडल चुनता है। अंतर्निहित मॉडल बदल सकता है।", + "change": "बदलें", + "useAnotherModel": "किसी अन्य मॉडल का उपयोग करें", + "inherits": "डिफ़ॉल्ट का उपयोग करता है", + "override": "प्रोजेक्ट ओवरराइड", + "useDefault": "डिफ़ॉल्ट का उपयोग करें", + "choosingAllProjects": "बिना ओवरराइड वाले प्रोजेक्ट के लिए मॉडल चुनें।", + "choosingProject": "केवल {{project}} के लिए मॉडल चुनें।", + "cancelChoosing": "रद्द करें", + "testAndUse": "जाँचें और उपयोग करें", + "testAndUseAllProjects": "सभी प्रोजेक्ट के लिए जाँचें और उपयोग करें", + "testAndUseProject": "इस प्रोजेक्ट के लिए जाँचें और उपयोग करें" }, "diagnostics": { "copied": "निदान करना", diff --git a/src/features/localization/renderer/locales/hi/team.json b/src/features/localization/renderer/locales/hi/team.json index 931baf1236..0934ce1868 100644 --- a/src/features/localization/renderer/locales/hi/team.json +++ b/src/features/localization/renderer/locales/hi/team.json @@ -900,8 +900,8 @@ }, "multimodelOff": "मल्टीमॉडल बंद", "unavailableInRuntime": "वर्तमान समय में अनुपलब्ध", - "defaultWithResolved": "Default - {{model}}", - "explicitChoice": "Explicit choice - {{model}}" + "defaultWithResolved": "डिफ़ॉल्ट - {{model}}", + "explicitChoice": "स्पष्ट चयन - {{model}}" }, "taskDetail": { "actions": { diff --git a/src/features/localization/renderer/locales/id/settings.json b/src/features/localization/renderer/locales/id/settings.json index 746c6dc703..297a8e6e1d 100644 --- a/src/features/localization/renderer/locales/id/settings.json +++ b/src/features/localization/renderer/locales/id/settings.json @@ -44,19 +44,19 @@ "thisProject": "Proyek ini", "title": "Default OpenCode", "validationContext": "Konteks validasi", - "inheritanceDescription": "Projects use this model unless they choose another.", - "freeRouterAdvisory": "Chooses an available free model for each request. The underlying model may change.", - "change": "Change", - "useAnotherModel": "Use another model", - "inherits": "Uses default", - "override": "Project override", - "useDefault": "Use default", - "choosingAllProjects": "Choose a model for projects without an override.", - "choosingProject": "Choose a model only for {{project}}.", - "cancelChoosing": "Cancel", - "testAndUse": "Test and use", - "testAndUseAllProjects": "Test and use for all projects", - "testAndUseProject": "Test and use for this project" + "inheritanceDescription": "Proyek menggunakan model ini kecuali memilih model lain.", + "freeRouterAdvisory": "Memilih model gratis yang tersedia untuk setiap permintaan. Model yang mendasarinya dapat berubah.", + "change": "Ubah", + "useAnotherModel": "Gunakan model lain", + "inherits": "Menggunakan model default", + "override": "Pengesampingan proyek", + "useDefault": "Gunakan default", + "choosingAllProjects": "Pilih model untuk proyek tanpa pengesampingan.", + "choosingProject": "Pilih model hanya untuk {{project}}.", + "cancelChoosing": "Batal", + "testAndUse": "Uji dan gunakan", + "testAndUseAllProjects": "Uji dan gunakan untuk semua proyek", + "testAndUseProject": "Uji dan gunakan untuk proyek ini" }, "diagnostics": { "copied": "Diagnostik disalin", diff --git a/src/features/localization/renderer/locales/id/team.json b/src/features/localization/renderer/locales/id/team.json index a2018a6193..e9a5b55883 100644 --- a/src/features/localization/renderer/locales/id/team.json +++ b/src/features/localization/renderer/locales/id/team.json @@ -900,8 +900,8 @@ }, "multimodelOff": "Matikan multimodel", "unavailableInRuntime": "Tidak tersedia dalam waktu-jalan saat ini", - "defaultWithResolved": "Default - {{model}}", - "explicitChoice": "Explicit choice - {{model}}" + "defaultWithResolved": "Bawaan - {{model}}", + "explicitChoice": "Pilihan eksplisit - {{model}}" }, "taskDetail": { "actions": { diff --git a/src/features/localization/renderer/locales/it/settings.json b/src/features/localization/renderer/locales/it/settings.json index 082a72ba2c..59ba34931b 100644 --- a/src/features/localization/renderer/locales/it/settings.json +++ b/src/features/localization/renderer/locales/it/settings.json @@ -44,19 +44,19 @@ "thisProject": "Questo progetto", "title": "Impostazioni predefinite OpenCode", "validationContext": "Contesto di convalida", - "inheritanceDescription": "Projects use this model unless they choose another.", - "freeRouterAdvisory": "Chooses an available free model for each request. The underlying model may change.", - "change": "Change", - "useAnotherModel": "Use another model", - "inherits": "Uses default", - "override": "Project override", - "useDefault": "Use default", - "choosingAllProjects": "Choose a model for projects without an override.", - "choosingProject": "Choose a model only for {{project}}.", - "cancelChoosing": "Cancel", - "testAndUse": "Test and use", - "testAndUseAllProjects": "Test and use for all projects", - "testAndUseProject": "Test and use for this project" + "inheritanceDescription": "I progetti usano questo modello a meno che non ne scelgano un altro.", + "freeRouterAdvisory": "Sceglie un modello gratuito disponibile per ogni richiesta. Il modello sottostante può cambiare.", + "change": "Cambia", + "useAnotherModel": "Usa un altro modello", + "inherits": "Usa il predefinito", + "override": "Sostituzione del progetto", + "useDefault": "Usa il predefinito", + "choosingAllProjects": "Scegli un modello per i progetti senza sostituzione.", + "choosingProject": "Scegli un modello solo per {{project}}.", + "cancelChoosing": "Annulla", + "testAndUse": "Testa e usa", + "testAndUseAllProjects": "Testa e usa per tutti i progetti", + "testAndUseProject": "Testa e usa per questo progetto" }, "diagnostics": { "copied": "Diagnostica copiata", diff --git a/src/features/localization/renderer/locales/it/team.json b/src/features/localization/renderer/locales/it/team.json index 1ef3de898e..a3a2849311 100644 --- a/src/features/localization/renderer/locales/it/team.json +++ b/src/features/localization/renderer/locales/it/team.json @@ -900,8 +900,8 @@ }, "multimodelOff": "Multimodello disattivato", "unavailableInRuntime": "Non disponibile nel runtime attuale", - "defaultWithResolved": "Default - {{model}}", - "explicitChoice": "Explicit choice - {{model}}" + "defaultWithResolved": "Predefinito - {{model}}", + "explicitChoice": "Scelta esplicita - {{model}}" }, "taskDetail": { "actions": { diff --git a/src/features/localization/renderer/locales/ja/settings.json b/src/features/localization/renderer/locales/ja/settings.json index fabfe8a702..6a76540cea 100644 --- a/src/features/localization/renderer/locales/ja/settings.json +++ b/src/features/localization/renderer/locales/ja/settings.json @@ -44,19 +44,19 @@ "thisProject": "プロジェクト", "title": "OpenCode のデフォルト", "validationContext": "検証コンテキスト", - "inheritanceDescription": "Projects use this model unless they choose another.", - "freeRouterAdvisory": "Chooses an available free model for each request. The underlying model may change.", - "change": "Change", - "useAnotherModel": "Use another model", - "inherits": "Uses default", - "override": "Project override", - "useDefault": "Use default", - "choosingAllProjects": "Choose a model for projects without an override.", - "choosingProject": "Choose a model only for {{project}}.", - "cancelChoosing": "Cancel", - "testAndUse": "Test and use", - "testAndUseAllProjects": "Test and use for all projects", - "testAndUseProject": "Test and use for this project" + "inheritanceDescription": "プロジェクトは、別のモデルを選択しない限りこのモデルを使用します。", + "freeRouterAdvisory": "リクエストごとに利用可能な無料モデルを選択します。実際に使われるモデルは変わる場合があります。", + "change": "変更", + "useAnotherModel": "別のモデルを使用", + "inherits": "デフォルトを使用中", + "override": "プロジェクトの上書き", + "useDefault": "デフォルトを使用", + "choosingAllProjects": "上書きのないプロジェクト用のモデルを選択してください。", + "choosingProject": "{{project}} のみに使用するモデルを選択してください。", + "cancelChoosing": "キャンセル", + "testAndUse": "テストして使用", + "testAndUseAllProjects": "すべてのプロジェクトでテストして使用", + "testAndUseProject": "このプロジェクトでテストして使用" }, "diagnostics": { "copied": "コピーされた診断", diff --git a/src/features/localization/renderer/locales/ja/team.json b/src/features/localization/renderer/locales/ja/team.json index 8b8817506f..c792efa3c1 100644 --- a/src/features/localization/renderer/locales/ja/team.json +++ b/src/features/localization/renderer/locales/ja/team.json @@ -900,8 +900,8 @@ }, "multimodelOff": "マルチモデルオフ", "unavailableInRuntime": "現在のランタイムで利用不可", - "defaultWithResolved": "Default - {{model}}", - "explicitChoice": "Explicit choice - {{model}}" + "defaultWithResolved": "デフォルト - {{model}}", + "explicitChoice": "明示的な選択 - {{model}}" }, "taskDetail": { "actions": { diff --git a/src/features/localization/renderer/locales/ko/settings.json b/src/features/localization/renderer/locales/ko/settings.json index e3977656a6..b0ef9cc0af 100644 --- a/src/features/localization/renderer/locales/ko/settings.json +++ b/src/features/localization/renderer/locales/ko/settings.json @@ -44,19 +44,19 @@ "thisProject": "이 프로젝트", "title": "OpenCode 기본값", "validationContext": "유효성 검사", - "inheritanceDescription": "Projects use this model unless they choose another.", - "freeRouterAdvisory": "Chooses an available free model for each request. The underlying model may change.", - "change": "Change", - "useAnotherModel": "Use another model", - "inherits": "Uses default", - "override": "Project override", - "useDefault": "Use default", - "choosingAllProjects": "Choose a model for projects without an override.", - "choosingProject": "Choose a model only for {{project}}.", - "cancelChoosing": "Cancel", - "testAndUse": "Test and use", - "testAndUseAllProjects": "Test and use for all projects", - "testAndUseProject": "Test and use for this project" + "inheritanceDescription": "프로젝트에서 다른 모델을 선택하지 않으면 이 모델을 사용합니다.", + "freeRouterAdvisory": "각 요청에 사용 가능한 무료 모델을 선택합니다. 실제 사용되는 모델은 변경될 수 있습니다.", + "change": "변경", + "useAnotherModel": "다른 모델 사용", + "inherits": "기본값 사용 중", + "override": "프로젝트 재정의", + "useDefault": "기본값 사용", + "choosingAllProjects": "재정의가 없는 프로젝트에 사용할 모델을 선택하세요.", + "choosingProject": "{{project}}에만 사용할 모델을 선택하세요.", + "cancelChoosing": "취소", + "testAndUse": "테스트 후 사용", + "testAndUseAllProjects": "모든 프로젝트에 테스트 후 사용", + "testAndUseProject": "이 프로젝트에 테스트 후 사용" }, "diagnostics": { "copied": "진단", diff --git a/src/features/localization/renderer/locales/ko/team.json b/src/features/localization/renderer/locales/ko/team.json index 31cb13cc22..88a4b24c20 100644 --- a/src/features/localization/renderer/locales/ko/team.json +++ b/src/features/localization/renderer/locales/ko/team.json @@ -899,9 +899,9 @@ "runtime": "선택한 공급자의 runtime default를 사용합니다." }, "multimodelOff": "Multimodel 오프", - "unavailableInRuntime": "현재 런타임에서 사용 가능", - "defaultWithResolved": "Default - {{model}}", - "explicitChoice": "Explicit choice - {{model}}" + "unavailableInRuntime": "현재 런타임에서 사용할 수 없음", + "defaultWithResolved": "기본값 - {{model}}", + "explicitChoice": "명시적 선택 - {{model}}" }, "taskDetail": { "actions": { diff --git a/src/features/localization/renderer/locales/mr/settings.json b/src/features/localization/renderer/locales/mr/settings.json index b1960e8579..c1cd560cc4 100644 --- a/src/features/localization/renderer/locales/mr/settings.json +++ b/src/features/localization/renderer/locales/mr/settings.json @@ -44,19 +44,19 @@ "thisProject": "हा प्रकल्प", "title": "OpenCode डीफॉल्ट्स", "validationContext": "पडताळणी संदर्भ", - "inheritanceDescription": "Projects use this model unless they choose another.", - "freeRouterAdvisory": "Chooses an available free model for each request. The underlying model may change.", - "change": "Change", - "useAnotherModel": "Use another model", - "inherits": "Uses default", - "override": "Project override", - "useDefault": "Use default", - "choosingAllProjects": "Choose a model for projects without an override.", - "choosingProject": "Choose a model only for {{project}}.", - "cancelChoosing": "Cancel", - "testAndUse": "Test and use", - "testAndUseAllProjects": "Test and use for all projects", - "testAndUseProject": "Test and use for this project" + "inheritanceDescription": "प्रकल्पांनी दुसरे मॉडेल निवडले नाही तर ते हे मॉडेल वापरतात.", + "freeRouterAdvisory": "प्रत्येक विनंतीसाठी उपलब्ध मोफत मॉडेल निवडते. प्रत्यक्ष मॉडेल बदलू शकते.", + "change": "बदला", + "useAnotherModel": "दुसरे मॉडेल वापरा", + "inherits": "डीफॉल्ट वापरते", + "override": "प्रकल्प ओव्हरराइड", + "useDefault": "डीफॉल्ट वापरा", + "choosingAllProjects": "ओव्हरराइड नसलेल्या प्रकल्पांसाठी मॉडेल निवडा.", + "choosingProject": "फक्त {{project}} साठी मॉडेल निवडा.", + "cancelChoosing": "रद्द करा", + "testAndUse": "चाचणी करा आणि वापरा", + "testAndUseAllProjects": "सर्व प्रकल्पांसाठी चाचणी करा आणि वापरा", + "testAndUseProject": "या प्रकल्पासाठी चाचणी करा आणि वापरा" }, "diagnostics": { "copied": "डायग्नॉस्टिक्स कॉपी केले", diff --git a/src/features/localization/renderer/locales/mr/team.json b/src/features/localization/renderer/locales/mr/team.json index adf510d8aa..de7164a37f 100644 --- a/src/features/localization/renderer/locales/mr/team.json +++ b/src/features/localization/renderer/locales/mr/team.json @@ -900,8 +900,8 @@ }, "multimodelOff": "मल्टीमॉडेल बंद", "unavailableInRuntime": "सध्याच्या रनटाइममध्ये उपलब्ध नाही", - "defaultWithResolved": "Default - {{model}}", - "explicitChoice": "Explicit choice - {{model}}" + "defaultWithResolved": "डीफॉल्ट - {{model}}", + "explicitChoice": "स्पष्ट निवड - {{model}}" }, "taskDetail": { "actions": { diff --git a/src/features/localization/renderer/locales/ms/settings.json b/src/features/localization/renderer/locales/ms/settings.json index e954ce6de6..dc92b41c35 100644 --- a/src/features/localization/renderer/locales/ms/settings.json +++ b/src/features/localization/renderer/locales/ms/settings.json @@ -44,19 +44,19 @@ "thisProject": "Projek ini", "title": "Lalai OpenCode", "validationContext": "Konteks pengesahan", - "inheritanceDescription": "Projects use this model unless they choose another.", - "freeRouterAdvisory": "Chooses an available free model for each request. The underlying model may change.", - "change": "Change", - "useAnotherModel": "Use another model", - "inherits": "Uses default", - "override": "Project override", - "useDefault": "Use default", - "choosingAllProjects": "Choose a model for projects without an override.", - "choosingProject": "Choose a model only for {{project}}.", - "cancelChoosing": "Cancel", - "testAndUse": "Test and use", - "testAndUseAllProjects": "Test and use for all projects", - "testAndUseProject": "Test and use for this project" + "inheritanceDescription": "Projek menggunakan model ini melainkan mereka memilih model lain.", + "freeRouterAdvisory": "Memilih model percuma yang tersedia bagi setiap permintaan. Model sebenar mungkin berubah.", + "change": "Ubah", + "useAnotherModel": "Gunakan model lain", + "inherits": "Menggunakan lalai", + "override": "Penggantian projek", + "useDefault": "Gunakan lalai", + "choosingAllProjects": "Pilih model untuk projek tanpa penggantian.", + "choosingProject": "Pilih model untuk {{project}} sahaja.", + "cancelChoosing": "Batal", + "testAndUse": "Uji dan gunakan", + "testAndUseAllProjects": "Uji dan gunakan untuk semua projek", + "testAndUseProject": "Uji dan gunakan untuk projek ini" }, "diagnostics": { "copied": "Diagnostik disalin", diff --git a/src/features/localization/renderer/locales/ms/team.json b/src/features/localization/renderer/locales/ms/team.json index 480e56a8cc..d58f398316 100644 --- a/src/features/localization/renderer/locales/ms/team.json +++ b/src/features/localization/renderer/locales/ms/team.json @@ -900,8 +900,8 @@ }, "multimodelOff": "Multimodel mati", "unavailableInRuntime": "Tidak tersedia dalam runtime semasa", - "defaultWithResolved": "Default - {{model}}", - "explicitChoice": "Explicit choice - {{model}}" + "defaultWithResolved": "Lalai - {{model}}", + "explicitChoice": "Pilihan khusus - {{model}}" }, "taskDetail": { "actions": { diff --git a/src/features/localization/renderer/locales/nl/settings.json b/src/features/localization/renderer/locales/nl/settings.json index 1501e9695a..f8d53e91ff 100644 --- a/src/features/localization/renderer/locales/nl/settings.json +++ b/src/features/localization/renderer/locales/nl/settings.json @@ -44,19 +44,19 @@ "thisProject": "Dit project", "title": "OpenCode-standaarden", "validationContext": "Validatiecontext", - "inheritanceDescription": "Projects use this model unless they choose another.", - "freeRouterAdvisory": "Chooses an available free model for each request. The underlying model may change.", - "change": "Change", - "useAnotherModel": "Use another model", - "inherits": "Uses default", - "override": "Project override", - "useDefault": "Use default", - "choosingAllProjects": "Choose a model for projects without an override.", - "choosingProject": "Choose a model only for {{project}}.", - "cancelChoosing": "Cancel", - "testAndUse": "Test and use", - "testAndUseAllProjects": "Test and use for all projects", - "testAndUseProject": "Test and use for this project" + "inheritanceDescription": "Projecten gebruiken dit model tenzij ze een ander model kiezen.", + "freeRouterAdvisory": "Kiest voor elke aanvraag een beschikbaar gratis model. Het onderliggende model kan veranderen.", + "change": "Wijzigen", + "useAnotherModel": "Een ander model gebruiken", + "inherits": "Gebruikt de standaard", + "override": "Projectoverschrijving", + "useDefault": "Standaard gebruiken", + "choosingAllProjects": "Kies een model voor projecten zonder overschrijving.", + "choosingProject": "Kies alleen voor {{project}} een model.", + "cancelChoosing": "Annuleren", + "testAndUse": "Testen en gebruiken", + "testAndUseAllProjects": "Testen en gebruiken voor alle projecten", + "testAndUseProject": "Testen en gebruiken voor dit project" }, "diagnostics": { "copied": "Diagnostiek gekopieerd", diff --git a/src/features/localization/renderer/locales/nl/team.json b/src/features/localization/renderer/locales/nl/team.json index 63839c6e96..cd5036f6e9 100644 --- a/src/features/localization/renderer/locales/nl/team.json +++ b/src/features/localization/renderer/locales/nl/team.json @@ -900,8 +900,8 @@ }, "multimodelOff": "Multimodel uit", "unavailableInRuntime": "Niet beschikbaar in huidige runtime", - "defaultWithResolved": "Default - {{model}}", - "explicitChoice": "Explicit choice - {{model}}" + "defaultWithResolved": "Standaard - {{model}}", + "explicitChoice": "Expliciete keuze - {{model}}" }, "taskDetail": { "actions": { diff --git a/src/features/localization/renderer/locales/pl/settings.json b/src/features/localization/renderer/locales/pl/settings.json index 32ebacac3d..3c96470ad6 100644 --- a/src/features/localization/renderer/locales/pl/settings.json +++ b/src/features/localization/renderer/locales/pl/settings.json @@ -44,19 +44,19 @@ "thisProject": "Ten projekt", "title": "Domyślne OpenCode", "validationContext": "Kontekst walidacji", - "inheritanceDescription": "Projects use this model unless they choose another.", - "freeRouterAdvisory": "Chooses an available free model for each request. The underlying model may change.", - "change": "Change", - "useAnotherModel": "Use another model", - "inherits": "Uses default", - "override": "Project override", - "useDefault": "Use default", - "choosingAllProjects": "Choose a model for projects without an override.", - "choosingProject": "Choose a model only for {{project}}.", - "cancelChoosing": "Cancel", - "testAndUse": "Test and use", - "testAndUseAllProjects": "Test and use for all projects", - "testAndUseProject": "Test and use for this project" + "inheritanceDescription": "Projekty używają tego modelu, chyba że wybiorą inny.", + "freeRouterAdvisory": "Wybiera dostępny bezpłatny model dla każdego żądania. Używany model może się zmieniać.", + "change": "Zmień", + "useAnotherModel": "Użyj innego modelu", + "inherits": "Używa domyślnego", + "override": "Nadpisanie projektu", + "useDefault": "Użyj domyślnego", + "choosingAllProjects": "Wybierz model dla projektów bez nadpisania.", + "choosingProject": "Wybierz model tylko dla {{project}}.", + "cancelChoosing": "Anuluj", + "testAndUse": "Przetestuj i użyj", + "testAndUseAllProjects": "Przetestuj i użyj dla wszystkich projektów", + "testAndUseProject": "Przetestuj i użyj dla tego projektu" }, "diagnostics": { "copied": "Skopiowano diagnostykę", diff --git a/src/features/localization/renderer/locales/pl/team.json b/src/features/localization/renderer/locales/pl/team.json index c33a923af9..be9513f8e9 100644 --- a/src/features/localization/renderer/locales/pl/team.json +++ b/src/features/localization/renderer/locales/pl/team.json @@ -900,8 +900,8 @@ }, "multimodelOff": "Wielomodelowy wył.", "unavailableInRuntime": "Niedostępne w bieżącym środowisku uruchomieniowym", - "defaultWithResolved": "Default - {{model}}", - "explicitChoice": "Explicit choice - {{model}}" + "defaultWithResolved": "Domyślny - {{model}}", + "explicitChoice": "Jawny wybór - {{model}}" }, "taskDetail": { "actions": { diff --git a/src/features/localization/renderer/locales/pt/settings.json b/src/features/localization/renderer/locales/pt/settings.json index 07f73737ad..ef2462c17e 100644 --- a/src/features/localization/renderer/locales/pt/settings.json +++ b/src/features/localization/renderer/locales/pt/settings.json @@ -44,19 +44,19 @@ "thisProject": "Este projecto", "title": "Por omissão do OpenCode", "validationContext": "Contexto de validação", - "inheritanceDescription": "Projects use this model unless they choose another.", - "freeRouterAdvisory": "Chooses an available free model for each request. The underlying model may change.", - "change": "Change", - "useAnotherModel": "Use another model", - "inherits": "Uses default", - "override": "Project override", - "useDefault": "Use default", - "choosingAllProjects": "Choose a model for projects without an override.", - "choosingProject": "Choose a model only for {{project}}.", - "cancelChoosing": "Cancel", - "testAndUse": "Test and use", - "testAndUseAllProjects": "Test and use for all projects", - "testAndUseProject": "Test and use for this project" + "inheritanceDescription": "Os projetos usam este modelo, a menos que escolham outro.", + "freeRouterAdvisory": "Escolhe um modelo gratuito disponível para cada pedido. O modelo subjacente pode mudar.", + "change": "Alterar", + "useAnotherModel": "Usar outro modelo", + "inherits": "Usa o predefinido", + "override": "Substituição do projeto", + "useDefault": "Usar o predefinido", + "choosingAllProjects": "Escolha um modelo para projetos sem substituição.", + "choosingProject": "Escolha um modelo apenas para {{project}}.", + "cancelChoosing": "Cancelar", + "testAndUse": "Testar e usar", + "testAndUseAllProjects": "Testar e usar para todos os projetos", + "testAndUseProject": "Testar e usar para este projeto" }, "diagnostics": { "copied": "Diagnóstico copiado", diff --git a/src/features/localization/renderer/locales/pt/team.json b/src/features/localization/renderer/locales/pt/team.json index 867aaca897..c55b48a33f 100644 --- a/src/features/localization/renderer/locales/pt/team.json +++ b/src/features/localization/renderer/locales/pt/team.json @@ -900,8 +900,8 @@ }, "multimodelOff": "Multimodelo desligado", "unavailableInRuntime": "Indisponível no tempo de execução atual", - "defaultWithResolved": "Default - {{model}}", - "explicitChoice": "Explicit choice - {{model}}" + "defaultWithResolved": "Predefinido - {{model}}", + "explicitChoice": "Escolha explícita - {{model}}" }, "taskDetail": { "actions": { diff --git a/src/features/localization/renderer/locales/ro/settings.json b/src/features/localization/renderer/locales/ro/settings.json index c9eca1f61d..df010677e3 100644 --- a/src/features/localization/renderer/locales/ro/settings.json +++ b/src/features/localization/renderer/locales/ro/settings.json @@ -44,19 +44,19 @@ "thisProject": "Acest proiect", "title": "Valori implicite OpenCode", "validationContext": "Context de validare", - "inheritanceDescription": "Projects use this model unless they choose another.", - "freeRouterAdvisory": "Chooses an available free model for each request. The underlying model may change.", - "change": "Change", - "useAnotherModel": "Use another model", - "inherits": "Uses default", - "override": "Project override", - "useDefault": "Use default", - "choosingAllProjects": "Choose a model for projects without an override.", - "choosingProject": "Choose a model only for {{project}}.", - "cancelChoosing": "Cancel", - "testAndUse": "Test and use", - "testAndUseAllProjects": "Test and use for all projects", - "testAndUseProject": "Test and use for this project" + "inheritanceDescription": "Proiectele folosesc acest model dacă nu aleg altul.", + "freeRouterAdvisory": "Alege un model gratuit disponibil pentru fiecare solicitare. Modelul utilizat se poate schimba.", + "change": "Schimbă", + "useAnotherModel": "Folosește alt model", + "inherits": "Folosește modelul implicit", + "override": "Suprascriere pentru proiect", + "useDefault": "Folosește modelul implicit", + "choosingAllProjects": "Alege un model pentru proiectele fără suprascriere.", + "choosingProject": "Alege un model doar pentru {{project}}.", + "cancelChoosing": "Anulează", + "testAndUse": "Testează și folosește", + "testAndUseAllProjects": "Testează și folosește pentru toate proiectele", + "testAndUseProject": "Testează și folosește pentru acest proiect" }, "diagnostics": { "copied": "Diagnostice copiate", diff --git a/src/features/localization/renderer/locales/ro/team.json b/src/features/localization/renderer/locales/ro/team.json index 8e37726398..a42c4bbf41 100644 --- a/src/features/localization/renderer/locales/ro/team.json +++ b/src/features/localization/renderer/locales/ro/team.json @@ -900,8 +900,8 @@ }, "multimodelOff": "Multimodel dezactivat", "unavailableInRuntime": "Indisponibil în runtime-ul curent", - "defaultWithResolved": "Default - {{model}}", - "explicitChoice": "Explicit choice - {{model}}" + "defaultWithResolved": "Implicit - {{model}}", + "explicitChoice": "Alegere explicită - {{model}}" }, "taskDetail": { "actions": { diff --git a/src/features/localization/renderer/locales/sw/settings.json b/src/features/localization/renderer/locales/sw/settings.json index 65414ad455..93e1ab48a6 100644 --- a/src/features/localization/renderer/locales/sw/settings.json +++ b/src/features/localization/renderer/locales/sw/settings.json @@ -44,19 +44,19 @@ "thisProject": "Mradi huu", "title": "Chaguo-msingi za OpenCode", "validationContext": "Muktadha wa uthibitishaji", - "inheritanceDescription": "Projects use this model unless they choose another.", - "freeRouterAdvisory": "Chooses an available free model for each request. The underlying model may change.", - "change": "Change", - "useAnotherModel": "Use another model", - "inherits": "Uses default", - "override": "Project override", - "useDefault": "Use default", - "choosingAllProjects": "Choose a model for projects without an override.", - "choosingProject": "Choose a model only for {{project}}.", - "cancelChoosing": "Cancel", - "testAndUse": "Test and use", - "testAndUseAllProjects": "Test and use for all projects", - "testAndUseProject": "Test and use for this project" + "inheritanceDescription": "Miradi hutumia muundo huu isipokuwa ichague mwingine.", + "freeRouterAdvisory": "Huchagua muundo wa bure unaopatikana kwa kila ombi. Muundo unaotumika unaweza kubadilika.", + "change": "Badilisha", + "useAnotherModel": "Tumia muundo mwingine", + "inherits": "Hutumia chaguo-msingi", + "override": "Ubatilishaji wa mradi", + "useDefault": "Tumia chaguo-msingi", + "choosingAllProjects": "Chagua muundo kwa miradi isiyo na ubatilishaji.", + "choosingProject": "Chagua muundo kwa {{project}} pekee.", + "cancelChoosing": "Ghairi", + "testAndUse": "Jaribu na utumie", + "testAndUseAllProjects": "Jaribu na utumie kwa miradi yote", + "testAndUseProject": "Jaribu na utumie kwa mradi huu" }, "diagnostics": { "copied": "Utambuzi umenakiliwa", diff --git a/src/features/localization/renderer/locales/sw/team.json b/src/features/localization/renderer/locales/sw/team.json index bc371e3de1..9c16bb9f99 100644 --- a/src/features/localization/renderer/locales/sw/team.json +++ b/src/features/localization/renderer/locales/sw/team.json @@ -900,8 +900,8 @@ }, "multimodelOff": "Modeli nyingi imezimwa", "unavailableInRuntime": "Haipatikani katika runtime ya sasa", - "defaultWithResolved": "Default - {{model}}", - "explicitChoice": "Explicit choice - {{model}}" + "defaultWithResolved": "Chaguo-msingi - {{model}}", + "explicitChoice": "Chaguo mahususi - {{model}}" }, "taskDetail": { "actions": { diff --git a/src/features/localization/renderer/locales/ta/settings.json b/src/features/localization/renderer/locales/ta/settings.json index 003e33a7da..b338952049 100644 --- a/src/features/localization/renderer/locales/ta/settings.json +++ b/src/features/localization/renderer/locales/ta/settings.json @@ -44,19 +44,19 @@ "thisProject": "இந்த project", "title": "OpenCode இயல்புநிலைகள்", "validationContext": "சரிபார்ப்பு context", - "inheritanceDescription": "Projects use this model unless they choose another.", - "freeRouterAdvisory": "Chooses an available free model for each request. The underlying model may change.", - "change": "Change", - "useAnotherModel": "Use another model", - "inherits": "Uses default", - "override": "Project override", - "useDefault": "Use default", - "choosingAllProjects": "Choose a model for projects without an override.", - "choosingProject": "Choose a model only for {{project}}.", - "cancelChoosing": "Cancel", - "testAndUse": "Test and use", - "testAndUseAllProjects": "Test and use for all projects", - "testAndUseProject": "Test and use for this project" + "inheritanceDescription": "திட்டங்கள் வேறு மாதிரியைத் தேர்ந்தெடுக்காவிட்டால் இந்த மாதிரியைப் பயன்படுத்தும்.", + "freeRouterAdvisory": "ஒவ்வொரு கோரிக்கைக்கும் கிடைக்கக்கூடிய இலவச மாதிரியைத் தேர்ந்தெடுக்கும். பயன்படுத்தப்படும் மாதிரி மாறலாம்.", + "change": "மாற்று", + "useAnotherModel": "வேறு மாதிரியைப் பயன்படுத்து", + "inherits": "இயல்புநிலையைப் பயன்படுத்துகிறது", + "override": "திட்ட மேலெழுதல்", + "useDefault": "இயல்புநிலையைப் பயன்படுத்து", + "choosingAllProjects": "மேலெழுதல் இல்லாத திட்டங்களுக்கு ஒரு மாதிரியைத் தேர்ந்தெடுக்கவும்.", + "choosingProject": "{{project}} க்கு மட்டும் ஒரு மாதிரியைத் தேர்ந்தெடுக்கவும்.", + "cancelChoosing": "ரத்துசெய்", + "testAndUse": "சோதித்துப் பயன்படுத்து", + "testAndUseAllProjects": "அனைத்து திட்டங்களுக்கும் சோதித்துப் பயன்படுத்து", + "testAndUseProject": "இந்தத் திட்டத்திற்குச் சோதித்துப் பயன்படுத்து" }, "diagnostics": { "copied": "Diagnostics நகலெடுக்கப்பட்டது", diff --git a/src/features/localization/renderer/locales/ta/team.json b/src/features/localization/renderer/locales/ta/team.json index ef7b2cbcaa..aeffac5c3b 100644 --- a/src/features/localization/renderer/locales/ta/team.json +++ b/src/features/localization/renderer/locales/ta/team.json @@ -900,8 +900,8 @@ }, "multimodelOff": "Multimodel off", "unavailableInRuntime": "தற்போதைய runtime இல் கிடைக்கவில்லை", - "defaultWithResolved": "Default - {{model}}", - "explicitChoice": "Explicit choice - {{model}}" + "defaultWithResolved": "இயல்புநிலை - {{model}}", + "explicitChoice": "வெளிப்படையான தேர்வு - {{model}}" }, "taskDetail": { "actions": { diff --git a/src/features/localization/renderer/locales/te/settings.json b/src/features/localization/renderer/locales/te/settings.json index cc9807d726..d84b4b89f3 100644 --- a/src/features/localization/renderer/locales/te/settings.json +++ b/src/features/localization/renderer/locales/te/settings.json @@ -44,19 +44,19 @@ "thisProject": "ఈ ప్రాజెక్ట్", "title": "OpenCode డిఫాల్ట్‌లు", "validationContext": "ధృవీకరణ కాంటెక్స్ట్", - "inheritanceDescription": "Projects use this model unless they choose another.", - "freeRouterAdvisory": "Chooses an available free model for each request. The underlying model may change.", - "change": "Change", - "useAnotherModel": "Use another model", - "inherits": "Uses default", - "override": "Project override", - "useDefault": "Use default", - "choosingAllProjects": "Choose a model for projects without an override.", - "choosingProject": "Choose a model only for {{project}}.", - "cancelChoosing": "Cancel", - "testAndUse": "Test and use", - "testAndUseAllProjects": "Test and use for all projects", - "testAndUseProject": "Test and use for this project" + "inheritanceDescription": "ప్రాజెక్ట్‌లు వేరే మోడల్‌ను ఎంచుకోకపోతే ఈ మోడల్‌ను ఉపయోగిస్తాయి.", + "freeRouterAdvisory": "ప్రతి అభ్యర్థనకు అందుబాటులో ఉన్న ఉచిత మోడల్‌ను ఎంచుకుంటుంది. ఉపయోగించే మోడల్ మారవచ్చు.", + "change": "మార్చు", + "useAnotherModel": "మరొక మోడల్‌ను ఉపయోగించు", + "inherits": "డిఫాల్ట్‌ను ఉపయోగిస్తుంది", + "override": "ప్రాజెక్ట్ ఓవర్‌రైడ్", + "useDefault": "డిఫాల్ట్‌ను ఉపయోగించు", + "choosingAllProjects": "ఓవర్‌రైడ్ లేని ప్రాజెక్ట్‌ల కోసం మోడల్‌ను ఎంచుకోండి.", + "choosingProject": "{{project}} కోసం మాత్రమే మోడల్‌ను ఎంచుకోండి.", + "cancelChoosing": "రద్దు చేయి", + "testAndUse": "పరీక్షించి ఉపయోగించు", + "testAndUseAllProjects": "అన్ని ప్రాజెక్ట్‌ల కోసం పరీక్షించి ఉపయోగించు", + "testAndUseProject": "ఈ ప్రాజెక్ట్ కోసం పరీక్షించి ఉపయోగించు" }, "diagnostics": { "copied": "డయాగ్నొస్టిక్‌లు కాపీ చేయబడ్డాయి", diff --git a/src/features/localization/renderer/locales/te/team.json b/src/features/localization/renderer/locales/te/team.json index cde8d0999a..0417f97041 100644 --- a/src/features/localization/renderer/locales/te/team.json +++ b/src/features/localization/renderer/locales/te/team.json @@ -900,8 +900,8 @@ }, "multimodelOff": "Multimodel ఆఫ్", "unavailableInRuntime": "ప్రస్తుత రన్‌టైమ్‌లో అందుబాటులో లేదు", - "defaultWithResolved": "Default - {{model}}", - "explicitChoice": "Explicit choice - {{model}}" + "defaultWithResolved": "డిఫాల్ట్ - {{model}}", + "explicitChoice": "స్పష్టమైన ఎంపిక - {{model}}" }, "taskDetail": { "actions": { diff --git a/src/features/localization/renderer/locales/th/settings.json b/src/features/localization/renderer/locales/th/settings.json index 49b37b08bd..5ef735fe8d 100644 --- a/src/features/localization/renderer/locales/th/settings.json +++ b/src/features/localization/renderer/locales/th/settings.json @@ -44,19 +44,19 @@ "thisProject": "โปรเจกต์นี้", "title": "ค่าเริ่มต้น OpenCode", "validationContext": "บริบทการตรวจสอบ", - "inheritanceDescription": "Projects use this model unless they choose another.", - "freeRouterAdvisory": "Chooses an available free model for each request. The underlying model may change.", - "change": "Change", - "useAnotherModel": "Use another model", - "inherits": "Uses default", - "override": "Project override", - "useDefault": "Use default", - "choosingAllProjects": "Choose a model for projects without an override.", - "choosingProject": "Choose a model only for {{project}}.", - "cancelChoosing": "Cancel", - "testAndUse": "Test and use", - "testAndUseAllProjects": "Test and use for all projects", - "testAndUseProject": "Test and use for this project" + "inheritanceDescription": "โปรเจกต์จะใช้โมเดลนี้ เว้นแต่จะเลือกโมเดลอื่น", + "freeRouterAdvisory": "เลือกโมเดลฟรีที่พร้อมใช้งานสำหรับแต่ละคำขอ โมเดลที่ใช้จริงอาจเปลี่ยนแปลงได้", + "change": "เปลี่ยน", + "useAnotherModel": "ใช้โมเดลอื่น", + "inherits": "ใช้ค่าเริ่มต้น", + "override": "การแทนที่ของโปรเจกต์", + "useDefault": "ใช้ค่าเริ่มต้น", + "choosingAllProjects": "เลือกโมเดลสำหรับโปรเจกต์ที่ไม่มีการแทนที่", + "choosingProject": "เลือกโมเดลสำหรับ {{project}} เท่านั้น", + "cancelChoosing": "ยกเลิก", + "testAndUse": "ทดสอบและใช้", + "testAndUseAllProjects": "ทดสอบและใช้กับทุกโปรเจกต์", + "testAndUseProject": "ทดสอบและใช้กับโปรเจกต์นี้" }, "diagnostics": { "copied": "คัดลอกการวินิจฉัยแล้ว", diff --git a/src/features/localization/renderer/locales/th/team.json b/src/features/localization/renderer/locales/th/team.json index 85197e0ccd..b0a7f78091 100644 --- a/src/features/localization/renderer/locales/th/team.json +++ b/src/features/localization/renderer/locales/th/team.json @@ -900,8 +900,8 @@ }, "multimodelOff": "ปิด Multimodel", "unavailableInRuntime": "ไม่พร้อมใช้งานในรันไทม์ปัจจุบัน", - "defaultWithResolved": "Default - {{model}}", - "explicitChoice": "Explicit choice - {{model}}" + "defaultWithResolved": "ค่าเริ่มต้น - {{model}}", + "explicitChoice": "ตัวเลือกที่ระบุ - {{model}}" }, "taskDetail": { "actions": { diff --git a/src/features/localization/renderer/locales/tr/settings.json b/src/features/localization/renderer/locales/tr/settings.json index 1b18425e65..0768cee081 100644 --- a/src/features/localization/renderer/locales/tr/settings.json +++ b/src/features/localization/renderer/locales/tr/settings.json @@ -44,19 +44,19 @@ "thisProject": "Bu proje", "title": "OpenCode varsayılanları", "validationContext": "Doğrulama bağlamı", - "inheritanceDescription": "Projects use this model unless they choose another.", - "freeRouterAdvisory": "Chooses an available free model for each request. The underlying model may change.", - "change": "Change", - "useAnotherModel": "Use another model", - "inherits": "Uses default", - "override": "Project override", - "useDefault": "Use default", - "choosingAllProjects": "Choose a model for projects without an override.", - "choosingProject": "Choose a model only for {{project}}.", - "cancelChoosing": "Cancel", - "testAndUse": "Test and use", - "testAndUseAllProjects": "Test and use for all projects", - "testAndUseProject": "Test and use for this project" + "inheritanceDescription": "Projeler başka bir model seçmedikçe bu modeli kullanır.", + "freeRouterAdvisory": "Her istek için kullanılabilir ücretsiz bir model seçer. Kullanılan model değişebilir.", + "change": "Değiştir", + "useAnotherModel": "Başka bir model kullan", + "inherits": "Varsayılanı kullanıyor", + "override": "Proje geçersiz kılması", + "useDefault": "Varsayılanı kullan", + "choosingAllProjects": "Geçersiz kılması olmayan projeler için bir model seçin.", + "choosingProject": "Yalnızca {{project}} için bir model seçin.", + "cancelChoosing": "İptal", + "testAndUse": "Test et ve kullan", + "testAndUseAllProjects": "Tüm projeler için test et ve kullan", + "testAndUseProject": "Bu proje için test et ve kullan" }, "diagnostics": { "copied": "Tanılama kopyalandı", diff --git a/src/features/localization/renderer/locales/tr/team.json b/src/features/localization/renderer/locales/tr/team.json index f6f1d705b7..9b52194f97 100644 --- a/src/features/localization/renderer/locales/tr/team.json +++ b/src/features/localization/renderer/locales/tr/team.json @@ -900,8 +900,8 @@ }, "multimodelOff": "Çoklu model kapalı", "unavailableInRuntime": "Geçerli çalışma zamanında kullanılamıyor", - "defaultWithResolved": "Default - {{model}}", - "explicitChoice": "Explicit choice - {{model}}" + "defaultWithResolved": "Varsayılan - {{model}}", + "explicitChoice": "Açık seçim - {{model}}" }, "taskDetail": { "actions": { diff --git a/src/features/localization/renderer/locales/uk/settings.json b/src/features/localization/renderer/locales/uk/settings.json index 6a522e2c83..439b1340e2 100644 --- a/src/features/localization/renderer/locales/uk/settings.json +++ b/src/features/localization/renderer/locales/uk/settings.json @@ -48,9 +48,9 @@ "freeRouterAdvisory": "Для кожного запиту вибирає доступну безкоштовну модель. Конкретна модель може змінюватися.", "change": "Змінити", "useAnotherModel": "Використати іншу модель", - "inherits": "Використовує спільну", + "inherits": "Використовує модель за замовчуванням", "override": "Перевизначення проєкту", - "useDefault": "Використовувати спільну", + "useDefault": "Використовувати модель за замовчуванням", "choosingAllProjects": "Виберіть модель для проєктів без перевизначення.", "choosingProject": "Виберіть модель лише для {{project}}.", "cancelChoosing": "Скасувати", diff --git a/src/features/localization/renderer/locales/ur/settings.json b/src/features/localization/renderer/locales/ur/settings.json index bdeddda5b6..e9758a4f2c 100644 --- a/src/features/localization/renderer/locales/ur/settings.json +++ b/src/features/localization/renderer/locales/ur/settings.json @@ -44,19 +44,19 @@ "thisProject": "یہ منصوبہ", "title": "کھولیں", "validationContext": "سیاق‌وسباق", - "inheritanceDescription": "Projects use this model unless they choose another.", - "freeRouterAdvisory": "Chooses an available free model for each request. The underlying model may change.", - "change": "Change", - "useAnotherModel": "Use another model", - "inherits": "Uses default", - "override": "Project override", - "useDefault": "Use default", - "choosingAllProjects": "Choose a model for projects without an override.", - "choosingProject": "Choose a model only for {{project}}.", - "cancelChoosing": "Cancel", - "testAndUse": "Test and use", - "testAndUseAllProjects": "Test and use for all projects", - "testAndUseProject": "Test and use for this project" + "inheritanceDescription": "پروجیکٹس کوئی دوسرا ماڈل منتخب نہ کریں تو یہ ماڈل استعمال کرتے ہیں۔", + "freeRouterAdvisory": "ہر درخواست کے لیے دستیاب مفت ماڈل منتخب کرتا ہے۔ استعمال ہونے والا ماڈل بدل سکتا ہے۔", + "change": "تبدیل کریں", + "useAnotherModel": "دوسرا ماڈل استعمال کریں", + "inherits": "ڈیفالٹ استعمال کرتا ہے", + "override": "پروجیکٹ اوور رائیڈ", + "useDefault": "ڈیفالٹ استعمال کریں", + "choosingAllProjects": "اوور رائیڈ کے بغیر پروجیکٹس کے لیے ماڈل منتخب کریں۔", + "choosingProject": "صرف {{project}} کے لیے ماڈل منتخب کریں۔", + "cancelChoosing": "منسوخ کریں", + "testAndUse": "جانچیں اور استعمال کریں", + "testAndUseAllProjects": "تمام پروجیکٹس کے لیے جانچیں اور استعمال کریں", + "testAndUseProject": "اس پروجیکٹ کے لیے جانچیں اور استعمال کریں" }, "diagnostics": { "copied": "روزنامہیات نقل کرتے ہیں۔", diff --git a/src/features/localization/renderer/locales/ur/team.json b/src/features/localization/renderer/locales/ur/team.json index 432a7b0e4b..dea42e8a9e 100644 --- a/src/features/localization/renderer/locales/ur/team.json +++ b/src/features/localization/renderer/locales/ur/team.json @@ -900,8 +900,8 @@ }, "multimodelOff": "معطل", "unavailableInRuntime": "موجودہ دور میں غیر متصل", - "defaultWithResolved": "Default - {{model}}", - "explicitChoice": "Explicit choice - {{model}}" + "defaultWithResolved": "ڈیفالٹ - {{model}}", + "explicitChoice": "واضح انتخاب - {{model}}" }, "taskDetail": { "actions": { diff --git a/src/features/localization/renderer/locales/vi/settings.json b/src/features/localization/renderer/locales/vi/settings.json index 54edc3bbfb..ebe4661585 100644 --- a/src/features/localization/renderer/locales/vi/settings.json +++ b/src/features/localization/renderer/locales/vi/settings.json @@ -44,19 +44,19 @@ "thisProject": "Dự án này", "title": "Mặc định OpenCode", "validationContext": "Ngữ cảnh xác thực", - "inheritanceDescription": "Projects use this model unless they choose another.", - "freeRouterAdvisory": "Chooses an available free model for each request. The underlying model may change.", - "change": "Change", - "useAnotherModel": "Use another model", - "inherits": "Uses default", - "override": "Project override", - "useDefault": "Use default", - "choosingAllProjects": "Choose a model for projects without an override.", - "choosingProject": "Choose a model only for {{project}}.", - "cancelChoosing": "Cancel", - "testAndUse": "Test and use", - "testAndUseAllProjects": "Test and use for all projects", - "testAndUseProject": "Test and use for this project" + "inheritanceDescription": "Các dự án dùng mô hình này trừ khi chọn mô hình khác.", + "freeRouterAdvisory": "Chọn một mô hình miễn phí khả dụng cho mỗi yêu cầu. Mô hình thực tế có thể thay đổi.", + "change": "Thay đổi", + "useAnotherModel": "Dùng mô hình khác", + "inherits": "Dùng mặc định", + "override": "Ghi đè của dự án", + "useDefault": "Dùng mặc định", + "choosingAllProjects": "Chọn mô hình cho các dự án không có ghi đè.", + "choosingProject": "Chỉ chọn mô hình cho {{project}}.", + "cancelChoosing": "Hủy", + "testAndUse": "Kiểm tra và dùng", + "testAndUseAllProjects": "Kiểm tra và dùng cho tất cả dự án", + "testAndUseProject": "Kiểm tra và dùng cho dự án này" }, "diagnostics": { "copied": "Đã sao chép chẩn đoán", diff --git a/src/features/localization/renderer/locales/vi/team.json b/src/features/localization/renderer/locales/vi/team.json index 4a19989160..2c69e17ec5 100644 --- a/src/features/localization/renderer/locales/vi/team.json +++ b/src/features/localization/renderer/locales/vi/team.json @@ -900,8 +900,8 @@ }, "multimodelOff": "Đa mô hình tắt", "unavailableInRuntime": "Không khả dụng trong runtime hiện tại", - "defaultWithResolved": "Default - {{model}}", - "explicitChoice": "Explicit choice - {{model}}" + "defaultWithResolved": "Mặc định - {{model}}", + "explicitChoice": "Lựa chọn rõ ràng - {{model}}" }, "taskDetail": { "actions": { diff --git a/src/features/localization/renderer/locales/zh/settings.json b/src/features/localization/renderer/locales/zh/settings.json index ef5ee39224..d1bf814ea9 100644 --- a/src/features/localization/renderer/locales/zh/settings.json +++ b/src/features/localization/renderer/locales/zh/settings.json @@ -44,19 +44,19 @@ "thisProject": "此项目", "title": "OpenCode 默认值", "validationContext": "验证上下文", - "inheritanceDescription": "Projects use this model unless they choose another.", - "freeRouterAdvisory": "Chooses an available free model for each request. The underlying model may change.", - "change": "Change", - "useAnotherModel": "Use another model", - "inherits": "Uses default", - "override": "Project override", - "useDefault": "Use default", - "choosingAllProjects": "Choose a model for projects without an override.", - "choosingProject": "Choose a model only for {{project}}.", - "cancelChoosing": "Cancel", - "testAndUse": "Test and use", - "testAndUseAllProjects": "Test and use for all projects", - "testAndUseProject": "Test and use for this project" + "inheritanceDescription": "项目会使用此模型,除非选择其他模型。", + "freeRouterAdvisory": "为每个请求选择一个可用的免费模型。实际使用的模型可能会变化。", + "change": "更改", + "useAnotherModel": "使用其他模型", + "inherits": "使用默认设置", + "override": "项目覆盖设置", + "useDefault": "使用默认设置", + "choosingAllProjects": "为没有覆盖设置的项目选择模型。", + "choosingProject": "仅为 {{project}} 选择模型。", + "cancelChoosing": "取消", + "testAndUse": "测试并使用", + "testAndUseAllProjects": "为所有项目测试并使用", + "testAndUseProject": "为此项目测试并使用" }, "diagnostics": { "copied": "已复制诊断信息", diff --git a/src/features/localization/renderer/locales/zh/team.json b/src/features/localization/renderer/locales/zh/team.json index deb3535b8c..1e57eb712d 100644 --- a/src/features/localization/renderer/locales/zh/team.json +++ b/src/features/localization/renderer/locales/zh/team.json @@ -900,8 +900,8 @@ }, "multimodelOff": "多模型关闭", "unavailableInRuntime": "当前运行时不可用", - "defaultWithResolved": "Default - {{model}}", - "explicitChoice": "Explicit choice - {{model}}" + "defaultWithResolved": "默认 - {{model}}", + "explicitChoice": "明确选择 - {{model}}" }, "taskDetail": { "actions": { From 812be2648ff51e7cf0b542dcb7c26d4b5392abdf Mon Sep 17 00:00:00 2001 From: 777genius Date: Fri, 21 Aug 2026 00:15:21 +0300 Subject: [PATCH 06/20] docs(opencode): make inheritance plan portable --- ...opencode-default-model-inheritance-plan.md | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/docs/research/opencode-default-model-inheritance-plan.md b/docs/research/opencode-default-model-inheritance-plan.md index e3bccf7d73..05e9135e2d 100644 --- a/docs/research/opencode-default-model-inheritance-plan.md +++ b/docs/research/opencode-default-model-inheritance-plan.md @@ -125,7 +125,7 @@ Do not use the generic `Not recommended` label as the only explanation. The rout ### Runtime repository: clear project override -Repository: `/Users/belief/dev/projects/claude/agent_teams_orchestrator` +Repository: the sibling `agent_teams_orchestrator` runtime repository Add a narrow `clear project default` vertical slice: @@ -149,11 +149,9 @@ Add a narrow `clear project default` vertical slice: - JSON response shape identical to other view mutations. 7. Do not overload `set-default` with empty model ids or magic sentinel values. -Important repository condition: the runtime worktree currently contains unrelated uncommitted changes, including the adapter, domain types, tests, and source entrypoint. Before implementation, re-read those diffs and apply only compatible, narrow additions. Never revert or stage unrelated work. - ### Desktop contracts and transport -Repository: `/Users/belief/dev/projects/claude/claude_team` +Repository: this desktop repository (`agent-teams-ai`) 1. Add `RuntimeProviderManagementClearProjectDefaultInput`. 2. Add `clearProjectDefaultModel()` to `RuntimeProviderManagementApi`. @@ -165,6 +163,7 @@ Repository: `/Users/belief/dev/projects/claude/claude_team` 5. Delegate through the feature facade and port. 6. Add the browser adapter's explicit `unsupported-action` response. 7. Extend `AgentTeamsRuntimeProviderManagementCliClient` to invoke `clear-project-default`, invalidate model/view caches before and after mutation, and preserve structured diagnostics. +8. Detect scoped-default support from the additive response fields. Until the bundled runtime exposes them, fail closed to the legacy configured-model UI and never call the new scoped mutation or clear command. ### Neutral base-default validation @@ -298,16 +297,16 @@ The existing configured-routes list may remain below as an advanced diagnostics/ ## Steps -1. Recheck both git worktrees and current branches. -2. Record the exact uncommitted runtime files that overlap this change. -3. Create or switch to a correctly named feature branch only when implementation starts, following repository branch rules and without touching unrelated untracked files. -4. Confirm the desktop dev runtime points to `/Users/belief/dev/projects/claude/agent_teams_orchestrator/cli-source` for source verification. +1. Confirm the desktop and runtime repository checkouts and their ownership boundaries. +2. Confirm the owned edit set in each repository before implementation. +3. Create or switch to a correctly named feature branch only when implementation starts, following each repository's branch rules. +4. Confirm the desktop dev runtime points to the runtime repository's `cli-source` launcher for source verification. 5. Re-run the read-only `dev:mcp` reproduction to capture the before state if a fresh comparison is useful. ## Exit criteria - Owned edit set is explicit. -- No unrelated dirty change will be reverted, staged, or reformatted. +- Files outside the owned edit set will not be reverted, staged, or reformatted. # Phase 1 - Runtime inheritance contract @@ -479,7 +478,7 @@ This separates UI/transport mutation proof from external provider availability a - Renderer rollback: restore the old scope/configured-model panel while leaving the additive runtime clear command unused. - Runtime rollback: the clear command is additive; older desktop builds do not call it. -- Contract compatibility: keep existing `setDefaultModel` unchanged. Add a new method/channel/CLI command rather than changing its request shape. +- Contract compatibility: keep existing `setDefaultModel` unchanged. Add a new method/channel/CLI command rather than changing its request shape. A newer desktop paired with an older bundled runtime uses the legacy model UI until a later runtime lock update exposes the additive fields. - Persistence compatibility: both old and new builds continue reading schema version 2 preferences with nullable `defaultModelId`. - No migration script is required. From d1c48dfac20d5f3239dcff1af2460d9cf65190fd Mon Sep 17 00:00:00 2001 From: 777genius Date: Fri, 21 Aug 2026 00:19:22 +0300 Subject: [PATCH 07/20] refactor(opencode): keep default guard within size cap --- scripts/ci/source-file-size-baseline.json | 4 ++-- .../renderer/hooks/useRuntimeProviderManagement.ts | 13 +++++-------- 2 files changed, 7 insertions(+), 10 deletions(-) diff --git a/scripts/ci/source-file-size-baseline.json b/scripts/ci/source-file-size-baseline.json index ab03e0805c..d47142f54a 100644 --- a/scripts/ci/source-file-size-baseline.json +++ b/scripts/ci/source-file-size-baseline.json @@ -28,10 +28,10 @@ "src/features/runtime-provider-management/main/infrastructure/AgentTeamsRuntimeProviderManagementCliClient.ts": 2689, "src/features/runtime-provider-management/main/infrastructure/cli-companion/RuntimeProviderCliCompanionService.ts": 838, "src/features/runtime-provider-management/main/infrastructure/OpenCodeLocalProviderConnector.ts": 1018, - "src/features/runtime-provider-management/renderer/hooks/useRuntimeProviderManagement.ts": 2102, + "src/features/runtime-provider-management/renderer/hooks/useRuntimeProviderManagement.ts": 2101, "src/features/runtime-provider-management/renderer/RuntimeLocalProviderSetupDialog.tsx": 1676, "src/features/runtime-provider-management/renderer/RuntimeProviderQuickConnect.tsx": 826, - "src/features/runtime-provider-management/renderer/ui/RuntimeProviderManagementPanelView.tsx": 3026, + "src/features/runtime-provider-management/renderer/ui/RuntimeProviderManagementPanelView.tsx": 3025, "src/features/terminal-workspace/renderer/ui/TerminalWorkspacePanel.tsx": 4513, "src/features/token-usage/core/domain/snapshotProjection.ts": 937, "src/features/token-usage/renderer/view-models/tokenUsageViewModel.ts": 1481, diff --git a/src/features/runtime-provider-management/renderer/hooks/useRuntimeProviderManagement.ts b/src/features/runtime-provider-management/renderer/hooks/useRuntimeProviderManagement.ts index 0be8b77b47..9bf0756eea 100644 --- a/src/features/runtime-provider-management/renderer/hooks/useRuntimeProviderManagement.ts +++ b/src/features/runtime-provider-management/renderer/hooks/useRuntimeProviderManagement.ts @@ -1783,14 +1783,11 @@ export function useRuntimeProviderManagement( const clearProjectDefault = useCallback( async (intendedProjectPath: string): Promise => { if (defaultMutationInFlightRef.current) return; - if ( - !supportsScopedDefaultModelInheritance( - providerViewRef.current, - options.bundledRuntimeVersion - ) - ) { - return; - } + const supportsScopedDefaults = supportsScopedDefaultModelInheritance( + providerViewRef.current, + options.bundledRuntimeVersion + ); + if (!supportsScopedDefaults) return; const projectContext = getProjectContextSnapshot(); const normalizedIntendedProjectPath = normalizeProjectContextPath(intendedProjectPath); if ( From 0323b613564ca28a5eadc51a78b9cd45e48e745a Mon Sep 17 00:00:00 2001 From: 777genius Date: Fri, 21 Aug 2026 00:27:27 +0300 Subject: [PATCH 08/20] fix(opencode): satisfy typed capability lint --- .../core/domain/providerManagementView.ts | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/features/runtime-provider-management/core/domain/providerManagementView.ts b/src/features/runtime-provider-management/core/domain/providerManagementView.ts index c70dc76166..ff2da0ec82 100644 --- a/src/features/runtime-provider-management/core/domain/providerManagementView.ts +++ b/src/features/runtime-provider-management/core/domain/providerManagementView.ts @@ -123,10 +123,8 @@ export function supportsScopedDefaultModelInheritance( } const match = /^(\d+)\.(\d+)\.(\d+)(?:$|[-+])/u.exec(bundledRuntimeVersion.trim()); if (!match || match[0].includes('-')) return false; - const version = match.slice(1, 4).map(Number); - const minimum = [0, 0, 75]; - for (let index = 0; index < minimum.length; index += 1) { - if (version[index]! !== minimum[index]!) return version[index]! > minimum[index]!; - } - return true; + const major = Number(match[1]); + const minor = Number(match[2]); + const patch = Number(match[3]); + return major > 0 || (major === 0 && (minor > 0 || (minor === 0 && patch >= 75))); } From 677eb528f7077d04c5a61450383f17849e2a5438 Mon Sep 17 00:00:00 2001 From: 777genius Date: Fri, 21 Aug 2026 01:15:28 +0300 Subject: [PATCH 09/20] fix(opencode): close inherited default review gaps --- scripts/ci/source-file-size-baseline.json | 2 +- .../hooks/useRuntimeProviderManagement.ts | 8 +- .../ui/LegacyConfiguredModelsPanel.tsx | 129 ++++++++++++++++++ .../OpenCodeDefaultModelInheritanceCard.tsx | 20 +-- .../ui/RuntimeProviderManagementPanelView.tsx | 108 ++------------- .../openCodeDefaultModelInheritance.test.ts | 19 +++ .../openCodeDefaultModelInheritance.ts | 5 +- ...RuntimeProviderManagementPanelView.test.ts | 98 ++++++++++++- .../useRuntimeProviderManagement.test.ts | 63 +++++++++ 9 files changed, 342 insertions(+), 110 deletions(-) create mode 100644 src/features/runtime-provider-management/renderer/ui/LegacyConfiguredModelsPanel.tsx diff --git a/scripts/ci/source-file-size-baseline.json b/scripts/ci/source-file-size-baseline.json index d47142f54a..b44c2ff355 100644 --- a/scripts/ci/source-file-size-baseline.json +++ b/scripts/ci/source-file-size-baseline.json @@ -31,7 +31,7 @@ "src/features/runtime-provider-management/renderer/hooks/useRuntimeProviderManagement.ts": 2101, "src/features/runtime-provider-management/renderer/RuntimeLocalProviderSetupDialog.tsx": 1676, "src/features/runtime-provider-management/renderer/RuntimeProviderQuickConnect.tsx": 826, - "src/features/runtime-provider-management/renderer/ui/RuntimeProviderManagementPanelView.tsx": 3025, + "src/features/runtime-provider-management/renderer/ui/RuntimeProviderManagementPanelView.tsx": 2947, "src/features/terminal-workspace/renderer/ui/TerminalWorkspacePanel.tsx": 4513, "src/features/token-usage/core/domain/snapshotProjection.ts": 937, "src/features/token-usage/renderer/view-models/tokenUsageViewModel.ts": 1481, diff --git a/src/features/runtime-provider-management/renderer/hooks/useRuntimeProviderManagement.ts b/src/features/runtime-provider-management/renderer/hooks/useRuntimeProviderManagement.ts index 9bf0756eea..90f77c5179 100644 --- a/src/features/runtime-provider-management/renderer/hooks/useRuntimeProviderManagement.ts +++ b/src/features/runtime-provider-management/renderer/hooks/useRuntimeProviderManagement.ts @@ -87,6 +87,7 @@ const DEFAULT_DIRECTORY_FILTER: RuntimeProviderDirectoryFilterDto = 'all'; const MODEL_PAGE_SIZE = 250; const MODEL_SEARCH_DEBOUNCE_MS = 300; const OAUTH_CONNECT_UI_TIMEOUT_MS = 18 * 60_000; +const CLEAR_PROJECT_DEFAULT_UI_TIMEOUT_MS = 100_000; interface ProjectContextSnapshot { path: string | null; @@ -1810,7 +1811,8 @@ export function useRuntimeProviderManagement( runtimeId: options.runtimeId, projectPath: normalizedIntendedProjectPath, }), - 'Clear project default timed out' + 'Clear project default timed out', + CLEAR_PROJECT_DEFAULT_UI_TIMEOUT_MS ); if (!isProjectContextCurrent(projectContext)) return; if (response.error) { @@ -1826,9 +1828,7 @@ export function useRuntimeProviderManagement( setModels((current) => current.map((model) => ({ ...model, default: model.modelId === nextView?.defaultModel })) ); - const translatedOpenCodeDefault = t('runtimeProvider.summary.defaultModel', { - model: '', - }) + const translatedOpenCodeDefault = t('runtimeProvider.summary.defaultModel', { model: '' }) .trim() .replace(/[::]\s*$/u, ''); const success = `${t('runtimeProvider.defaults.thisProject')}: ${t( diff --git a/src/features/runtime-provider-management/renderer/ui/LegacyConfiguredModelsPanel.tsx b/src/features/runtime-provider-management/renderer/ui/LegacyConfiguredModelsPanel.tsx new file mode 100644 index 0000000000..48f27cc21c --- /dev/null +++ b/src/features/runtime-provider-management/renderer/ui/LegacyConfiguredModelsPanel.tsx @@ -0,0 +1,129 @@ +import { useAppTranslation } from '@features/localization/renderer'; +import { Button } from '@renderer/components/ui/button'; +import { Loader2 } from 'lucide-react'; + +import { + canUseOpenCodeModelRoute, + needsOpenCodeModelExecutionProof, +} from './runtimeProviderModelAccess'; + +import type { + RuntimeProviderManagementActions, + RuntimeProviderManagementState, +} from '../hooks/useRuntimeProviderManagement'; +import type { RuntimeProviderModelDto } from '@features/runtime-provider-management/contracts'; +import type { JSX } from 'react'; + +interface LegacyConfiguredModelsPanelProps { + readonly state: RuntimeProviderManagementState; + readonly actions: RuntimeProviderManagementActions; + readonly disabled: boolean; + readonly hasProjectContext: boolean; + readonly projectPath: string | null; + readonly getUnavailableTitle: (model: RuntimeProviderModelDto) => string | undefined; +} + +export const LegacyConfiguredModelsPanel = ({ + state, + actions, + disabled, + hasProjectContext, + projectPath, + getUnavailableTitle, +}: LegacyConfiguredModelsPanelProps): JSX.Element => { + const { t } = useAppTranslation('settings'); + const models = state.view?.configuredModels ?? []; + const defaultActions = [ + { + scope: 'project', + label: t('runtimeProvider.defaults.setProjectDefault'), + variant: 'outline', + }, + { + scope: 'all_projects', + label: t('runtimeProvider.defaults.setAllProjectsDefault'), + variant: 'default', + }, + ] as const; + + if (models.length === 0) { + return ( +
+ {t('runtimeProvider.models.noneReported')} +
+ ); + } + + return ( +
+ {models.map((model) => { + const unavailableTitle = getUnavailableTitle(model); + const testing = state.testingModelIds.includes(model.modelId); + const defaultDisabled = + disabled || + Boolean(state.savingDefaultModelId) || + state.clearingProjectDefault || + (!canUseOpenCodeModelRoute(model) && !needsOpenCodeModelExecutionProof(model)); + + return ( +
+
+
+
+ {model.displayName} +
+
+ {model.modelId} +
+ {unavailableTitle ? ( +
{unavailableTitle}
+ ) : null} +
+
+ + {defaultActions.map((action) => ( + + ))} +
+
+
+ ); + })} +
+ ); +}; diff --git a/src/features/runtime-provider-management/renderer/ui/OpenCodeDefaultModelInheritanceCard.tsx b/src/features/runtime-provider-management/renderer/ui/OpenCodeDefaultModelInheritanceCard.tsx index 3d6071b445..07d363efba 100644 --- a/src/features/runtime-provider-management/renderer/ui/OpenCodeDefaultModelInheritanceCard.tsx +++ b/src/features/runtime-provider-management/renderer/ui/OpenCodeDefaultModelInheritanceCard.tsx @@ -18,7 +18,10 @@ import { presentOpenCodeDefaultModelInheritance, } from '../view-models/openCodeDefaultModelInheritance'; -import { canUseOpenCodeModelRoute } from './runtimeProviderModelAccess'; +import { + canUseOpenCodeModelRoute, + needsOpenCodeModelExecutionProof, +} from './runtimeProviderModelAccess'; import type { RuntimeProviderDefaultScopeDto } from '../../contracts'; import type { @@ -91,13 +94,14 @@ export const OpenCodeDefaultModelInheritanceCard = ({ const configuredModel = state.view?.configuredModels?.find( (entry) => entry.modelId === modelId ); - if (configuredModel && canUseOpenCodeModelRoute(configuredModel)) return null; - return ( - configuredModel?.accessReason ?? - (configuredModel - ? t('runtimeProvider.models.routeUnavailableGeneric') - : t('runtimeProvider.models.routeUnavailableUnknown')) - ); + if ( + !configuredModel || + canUseOpenCodeModelRoute(configuredModel) || + needsOpenCodeModelExecutionProof(configuredModel) + ) { + return null; + } + return configuredModel.accessReason ?? t('runtimeProvider.models.routeUnavailableGeneric'); }; const baseUnavailableReason = getUnavailableReason(model.baseModelId); const projectUnavailableReason = getUnavailableReason(model.projectEffectiveModelId); diff --git a/src/features/runtime-provider-management/renderer/ui/RuntimeProviderManagementPanelView.tsx b/src/features/runtime-provider-management/renderer/ui/RuntimeProviderManagementPanelView.tsx index 05993f19b6..2bd6daa8e1 100644 --- a/src/features/runtime-provider-management/renderer/ui/RuntimeProviderManagementPanelView.tsx +++ b/src/features/runtime-provider-management/renderer/ui/RuntimeProviderManagementPanelView.tsx @@ -54,6 +54,7 @@ import { } from '../../core/domain'; import { OPENROUTER_FREE_MODEL_ID } from '../view-models/openCodeDefaultModelInheritance'; +import { LegacyConfiguredModelsPanel } from './LegacyConfiguredModelsPanel'; import { OpenCodeDefaultModelInheritanceCard, OpenCodeDefaultTargetBanner, @@ -2139,17 +2140,19 @@ const ModelRow = ({ ? `${t('runtimeProvider.defaults.testAndUse')}: ${unavailableTitle}` : t('runtimeProvider.defaults.testAndUse') } - onClick={async (event) => { + onClick={(event) => { event.stopPropagation(); - const saved = await actions.setDefaultModel( - provider.providerId, - model.modelId, - defaultTarget, - intendedProjectPath - ); - if (saved) { - onDefaultSaved(); - } + void actions + .setDefaultModel( + provider.providerId, + model.modelId, + defaultTarget, + intendedProjectPath + ) + .then( + (saved) => saved && onDefaultSaved(), + () => undefined + ); }} > {savingDefault ? : null} @@ -2507,89 +2510,6 @@ const ProviderModelList = ({ ); }; -const LegacyConfiguredModelsPanel = ({ - state, - actions, - disabled, - hasProjectContext, -}: { - readonly state: RuntimeProviderManagementState; - readonly actions: RuntimeProviderManagementActions; - readonly disabled: boolean; - readonly hasProjectContext: boolean; -}): JSX.Element => { - const { t } = useAppTranslation('settings'); - const models = state.view?.configuredModels ?? []; - if (models.length === 0) { - return ( -
- {t('runtimeProvider.models.noneReported')} -
- ); - } - return ( -
- {models.map((model) => { - const unavailableTitle = getOpenCodeRouteUnavailableTitle(model, t); - const testing = state.testingModelIds.includes(model.modelId); - return ( -
-
-
-
- {model.displayName} -
-
- {model.modelId} -
- {unavailableTitle ? ( -
{unavailableTitle}
- ) : null} -
-
- - -
-
-
- ); - })} -
- ); -}; - export const RuntimeProviderManagementPanelView = ({ state, actions, @@ -2831,6 +2751,8 @@ export const RuntimeProviderManagementPanelView = ({ actions={actions} disabled={disabled || blockingCredentialWrite} hasProjectContext={hasProjectContext} + projectPath={normalizedProjectPath} + getUnavailableTitle={(model) => getOpenCodeRouteUnavailableTitle(model, t)} /> )}
diff --git a/src/features/runtime-provider-management/renderer/view-models/openCodeDefaultModelInheritance.test.ts b/src/features/runtime-provider-management/renderer/view-models/openCodeDefaultModelInheritance.test.ts index 01c6ba0d06..651e0cd102 100644 --- a/src/features/runtime-provider-management/renderer/view-models/openCodeDefaultModelInheritance.test.ts +++ b/src/features/runtime-provider-management/renderer/view-models/openCodeDefaultModelInheritance.test.ts @@ -74,6 +74,25 @@ describe('openCodeDefaultModelInheritance', () => { expect(presentation.baseModelId).toBe('opencode/fallback'); expect(presentation.baseDisplayName).toBe('opencode/fallback'); + expect(presentation.projectEffectiveModelId).toBe('openrouter/project'); + }); + + it('prefers the effective non-project default over the fallback when no managed base exists', () => { + const presentation = presentOpenCodeDefaultModelInheritance({ + view: { + ...view, + defaultModel: 'opencode/configured-default', + projectDefaultModel: null, + allProjectsDefaultModel: null, + defaultModelSource: 'opencode_config', + fallbackModel: 'opencode/fallback', + }, + projectPath: '/workspace/project', + projectName: 'Test project', + }); + + expect(presentation.baseModelId).toBe('opencode/configured-default'); + expect(presentation.projectEffectiveModelId).toBe('opencode/configured-default'); }); it('presents the Free Models Router with its stable product name', () => { diff --git a/src/features/runtime-provider-management/renderer/view-models/openCodeDefaultModelInheritance.ts b/src/features/runtime-provider-management/renderer/view-models/openCodeDefaultModelInheritance.ts index 0d5314b1bb..ff201b9f0f 100644 --- a/src/features/runtime-provider-management/renderer/view-models/openCodeDefaultModelInheritance.ts +++ b/src/features/runtime-provider-management/renderer/view-models/openCodeDefaultModelInheritance.ts @@ -35,8 +35,9 @@ export function presentOpenCodeDefaultModelInheritance(input: { const models = view?.configuredModels ?? []; const baseModelId = view?.allProjectsDefaultModel ?? - view?.fallbackModel ?? - (view?.defaultModelSource === 'project' ? null : (view?.defaultModel ?? null)); + (view?.defaultModelSource === 'project' + ? (view?.fallbackModel ?? null) + : (view?.defaultModel ?? view?.fallbackModel ?? null)); const projectOverrideModelId = view?.projectDefaultModel ?? null; const projectEffectiveModelId = projectOverrideModelId ?? baseModelId ?? view?.defaultModel ?? null; diff --git a/test/renderer/features/runtime-provider-management/RuntimeProviderManagementPanelView.test.ts b/test/renderer/features/runtime-provider-management/RuntimeProviderManagementPanelView.test.ts index 364c6ae02d..42f486e199 100644 --- a/test/renderer/features/runtime-provider-management/RuntimeProviderManagementPanelView.test.ts +++ b/test/renderer/features/runtime-provider-management/RuntimeProviderManagementPanelView.test.ts @@ -249,7 +249,7 @@ describe('RuntimeProviderManagementPanelView', () => { ...baseView, runtime: { ...baseView.runtime, version: '1.14.24' }, configuredModels: [legacyModel, otherLegacyModel], - projectPath: null, + projectPath: '/workspace/project-a', projectDefaultModel: null, allProjectsDefaultModel: legacyModel.modelId, defaultModelSource: 'all_projects' as const, @@ -262,6 +262,7 @@ describe('RuntimeProviderManagementPanelView', () => { state: createState({ view: legacyView }), actions, disabled: false, + projectPath: '/workspace/project-a', }) ); await Promise.resolve(); @@ -285,6 +286,20 @@ describe('RuntimeProviderManagementPanelView', () => { legacyModel.modelId, 'all_projects' ); + const setLegacyProjectDefault = Array.from(host.querySelectorAll('button')).find( + (button) => button.textContent?.trim() === 'Set project default' + ); + expect(setLegacyProjectDefault?.disabled).toBe(false); + await act(async () => { + setLegacyProjectDefault?.click(); + await Promise.resolve(); + }); + expect(actions.setDefaultModel).toHaveBeenCalledWith( + 'openrouter', + legacyModel.modelId, + 'project', + '/workspace/project-a' + ); await act(async () => { root.render( @@ -295,6 +310,7 @@ describe('RuntimeProviderManagementPanelView', () => { }), actions, disabled: false, + projectPath: '/workspace/project-a', }) ); await Promise.resolve(); @@ -304,6 +320,11 @@ describe('RuntimeProviderManagementPanelView', () => { ); expect(allLegacyDefaultButtons).toHaveLength(2); expect(allLegacyDefaultButtons.every((button) => button.disabled)).toBe(true); + const allLegacyProjectButtons = Array.from(host.querySelectorAll('button')).filter((button) => + button.textContent?.includes('Set project default') + ); + expect(allLegacyProjectButtons).toHaveLength(2); + expect(allLegacyProjectButtons.every((button) => button.disabled)).toBe(true); }); it('requests the full managed view only after the Models tab is opened', async () => { @@ -977,6 +998,14 @@ describe('RuntimeProviderManagementPanelView', () => { (button) => button.textContent?.trim() === 'Test and use' ); expect(testAndUseButton?.disabled).toBe(false); + vi.mocked(actions.setDefaultModel).mockRejectedValueOnce(new Error('write failed')); + await act(async () => { + testAndUseButton?.click(); + await Promise.resolve(); + await Promise.resolve(); + }); + expect(host.querySelector('[data-testid="opencode-default-target-banner"]')).not.toBeNull(); + await act(async () => { testAndUseButton?.click(); await Promise.resolve(); @@ -1086,7 +1115,7 @@ describe('RuntimeProviderManagementPanelView', () => { ).toBeNull(); }); - it('marks unavailable stored defaults and names their scoped actions accessibly', async () => { + it('distinguishes unavailable stored defaults from pending execution proof', async () => { const host = document.createElement('div'); document.body.appendChild(host); const root = createRoot(host); @@ -1160,6 +1189,71 @@ describe('RuntimeProviderManagementPanelView', () => { expect(actions.testModel).not.toHaveBeenCalled(); expect(actions.useModelForNewTeams).not.toHaveBeenCalled(); expect(actions.setDefaultModel).not.toHaveBeenCalled(); + + const pendingEvidenceModel = { + ...unknownDefaultModel, + displayName: 'Pending Evidence Model', + accessKind: 'configured_authless' as const, + proofState: 'needs_probe' as const, + requiresExecutionProof: true, + accessReason: 'Execution proof required', + }; + await act(async () => { + root.render( + React.createElement(RuntimeProviderManagementPanelView, { + state: createState({ + view: { + ...createState().view!, + configuredModels: [pendingEvidenceModel], + defaultModel: pendingEvidenceModel.modelId, + allProjectsDefaultModel: pendingEvidenceModel.modelId, + defaultModelSource: 'all_projects', + }, + }), + actions, + disabled: false, + bundledRuntimeVersion: '0.0.75', + }) + ); + await Promise.resolve(); + }); + expect(host.textContent).toContain('Pending Evidence Model'); + expect(host.textContent).not.toContain('Unavailable in current runtime'); + }); + + it('keeps an unmatched base default visible without treating catalog absence as unavailable', async () => { + const host = document.createElement('div'); + document.body.appendChild(host); + const root = createRoot(host); + const baseModelId = 'openrouter/provider/model-from-config'; + + await act(async () => { + root.render( + React.createElement(RuntimeProviderManagementPanelView, { + state: createState({ + view: { + ...createState().view!, + configuredModels: [], + defaultModel: baseModelId, + projectDefaultModel: null, + allProjectsDefaultModel: baseModelId, + defaultModelSource: 'all_projects', + }, + }), + actions: createActions(), + disabled: false, + bundledRuntimeVersion: '0.0.75', + }) + ); + await Promise.resolve(); + }); + + await selectOpenCodeTab(host, 'Models'); + + const inheritanceCard = host.querySelector('[data-testid="opencode-default-inheritance"]'); + expect(inheritanceCard?.textContent).toContain(baseModelId); + expect(inheritanceCard?.textContent).not.toContain('Unavailable in current runtime'); + expect(inheritanceCard?.textContent).not.toContain('Model was not found in the live catalog'); }); it('does not repeat runtime diagnostics already shown by the outer OpenCode summary', async () => { diff --git a/test/renderer/features/runtime-provider-management/useRuntimeProviderManagement.test.ts b/test/renderer/features/runtime-provider-management/useRuntimeProviderManagement.test.ts index b05bdc04f8..bd5ff3a707 100644 --- a/test/renderer/features/runtime-provider-management/useRuntimeProviderManagement.test.ts +++ b/test/renderer/features/runtime-provider-management/useRuntimeProviderManagement.test.ts @@ -3492,6 +3492,69 @@ describe('useRuntimeProviderManagement', () => { ); }); + it('keeps the clear-project watchdog beyond the 90-second main-process timeout', async () => { + vi.useFakeTimers(); + const loadView = vi.fn(() => + Promise.resolve({ + schemaVersion: 1 as const, + runtimeId: 'opencode' as const, + view: { + ...createRuntimeView(), + projectPath: '/tmp/project-a', + projectDefaultModel: 'openrouter/project-model', + allProjectsDefaultModel: 'openrouter/base-model', + defaultModel: 'openrouter/project-model', + defaultModelSource: 'project' as const, + }, + }) + ); + const clearProjectDefaultModel = vi.fn( + () => new Promise(() => undefined) + ); + Object.defineProperty(window, 'electronAPI', { + configurable: true, + value: { + runtimeProviderManagement: { loadView, clearProjectDefaultModel }, + } as unknown as ElectronAPI, + }); + const root = createRoot(host); + try { + await act(async () => { + root.render( + React.createElement(EnabledHarness, { + projectPath: '/tmp/project-a', + bundledRuntimeVersion: '0.0.75', + }) + ); + await Promise.resolve(); + }); + + let clearPromise: Promise | null = null; + await act(async () => { + clearPromise = actions?.clearProjectDefault('/tmp/project-a') ?? null; + await Promise.resolve(); + }); + + await act(async () => { + await vi.advanceTimersByTimeAsync(90_001); + }); + expect(state?.clearingProjectDefault).toBe(true); + expect(state?.error).toBeNull(); + + await act(async () => { + await vi.advanceTimersByTimeAsync(9_999); + await clearPromise; + }); + expect(state?.clearingProjectDefault).toBe(false); + expect(state?.error).toBe('Clear project default timed out'); + } finally { + vi.useRealTimers(); + await act(async () => { + root.unmount(); + }); + } + }); + it('does not clear project B while its view is still refreshing after switching from A', async () => { const clearProjectDefaultModel = vi.fn(); const projectBView = new Promise(() => undefined); From 491f2012ab461cd63cd8b681694ff3b41d6723da Mon Sep 17 00:00:00 2001 From: 777genius Date: Fri, 21 Aug 2026 01:43:08 +0300 Subject: [PATCH 10/20] fix(opencode): harden inherited default edge cases --- scripts/ci/source-file-size-baseline.json | 2 +- .../registerRuntimeProviderManagementIpc.ts | 2 +- .../ui/LegacyConfiguredModelsPanel.tsx | 18 +++- .../ui/RuntimeProviderManagementPanelView.tsx | 8 +- .../ui/runtimeProviderModelAccess.test.ts | 65 ++++++++++++ .../renderer/ui/runtimeProviderModelAccess.ts | 12 +++ ...gisterRuntimeProviderManagementIpc.test.ts | 40 +++++++ ...RuntimeProviderManagementPanelView.test.ts | 43 +++++++- .../useRuntimeProviderManagement.test.ts | 100 ++++++++++++------ 9 files changed, 244 insertions(+), 46 deletions(-) create mode 100644 src/features/runtime-provider-management/renderer/ui/runtimeProviderModelAccess.test.ts diff --git a/scripts/ci/source-file-size-baseline.json b/scripts/ci/source-file-size-baseline.json index b44c2ff355..5f97d53145 100644 --- a/scripts/ci/source-file-size-baseline.json +++ b/scripts/ci/source-file-size-baseline.json @@ -31,7 +31,7 @@ "src/features/runtime-provider-management/renderer/hooks/useRuntimeProviderManagement.ts": 2101, "src/features/runtime-provider-management/renderer/RuntimeLocalProviderSetupDialog.tsx": 1676, "src/features/runtime-provider-management/renderer/RuntimeProviderQuickConnect.tsx": 826, - "src/features/runtime-provider-management/renderer/ui/RuntimeProviderManagementPanelView.tsx": 2947, + "src/features/runtime-provider-management/renderer/ui/RuntimeProviderManagementPanelView.tsx": 2945, "src/features/terminal-workspace/renderer/ui/TerminalWorkspacePanel.tsx": 4513, "src/features/token-usage/core/domain/snapshotProjection.ts": 937, "src/features/token-usage/renderer/view-models/tokenUsageViewModel.ts": 1481, diff --git a/src/features/runtime-provider-management/main/adapters/input/registerRuntimeProviderManagementIpc.ts b/src/features/runtime-provider-management/main/adapters/input/registerRuntimeProviderManagementIpc.ts index cf019d203d..26e3089bb4 100644 --- a/src/features/runtime-provider-management/main/adapters/input/registerRuntimeProviderManagementIpc.ts +++ b/src/features/runtime-provider-management/main/adapters/input/registerRuntimeProviderManagementIpc.ts @@ -591,7 +591,7 @@ export function registerRuntimeProviderManagementIpc( _event, input: RuntimeProviderManagementClearProjectDefaultInput ): Promise => { - const projectPath = input?.projectPath?.trim(); + const projectPath = typeof input?.projectPath === 'string' ? input.projectPath.trim() : null; if (input?.runtimeId !== 'opencode' || !projectPath || projectPath.length > 4_096) { return { schemaVersion: 1, diff --git a/src/features/runtime-provider-management/renderer/ui/LegacyConfiguredModelsPanel.tsx b/src/features/runtime-provider-management/renderer/ui/LegacyConfiguredModelsPanel.tsx index 48f27cc21c..b9f8ffd970 100644 --- a/src/features/runtime-provider-management/renderer/ui/LegacyConfiguredModelsPanel.tsx +++ b/src/features/runtime-provider-management/renderer/ui/LegacyConfiguredModelsPanel.tsx @@ -59,6 +59,8 @@ export const LegacyConfiguredModelsPanel = ({ {models.map((model) => { const unavailableTitle = getUnavailableTitle(model); const testing = state.testingModelIds.includes(model.modelId); + const saving = state.savingDefaultModelId === model.modelId; + const modelTarget = model.displayName || model.modelId; const defaultDisabled = disabled || Boolean(state.savingDefaultModelId) || @@ -84,10 +86,21 @@ export const LegacyConfiguredModelsPanel = ({ ) : null}
+ {saving ? ( + + + {t('providerRuntime.actions.saving')} + + ) : null} ))} diff --git a/src/features/runtime-provider-management/renderer/ui/RuntimeProviderManagementPanelView.tsx b/src/features/runtime-provider-management/renderer/ui/RuntimeProviderManagementPanelView.tsx index 2bd6daa8e1..3640c70d6a 100644 --- a/src/features/runtime-provider-management/renderer/ui/RuntimeProviderManagementPanelView.tsx +++ b/src/features/runtime-provider-management/renderer/ui/RuntimeProviderManagementPanelView.tsx @@ -2070,6 +2070,7 @@ const ModelRow = ({ const modelDisabled = disabled || (!canUseOpenCodeModelRoute(model) && !needsOpenCodeModelExecutionProof(model)); const unavailableTitle = getOpenCodeRouteUnavailableTitle(model, t); + const modelTarget = model.displayName || model.modelId; return (
{ event.stopPropagation(); void actions diff --git a/src/features/runtime-provider-management/renderer/ui/runtimeProviderModelAccess.test.ts b/src/features/runtime-provider-management/renderer/ui/runtimeProviderModelAccess.test.ts new file mode 100644 index 0000000000..43b4152664 --- /dev/null +++ b/src/features/runtime-provider-management/renderer/ui/runtimeProviderModelAccess.test.ts @@ -0,0 +1,65 @@ +import { describe, expect, it } from 'vitest'; + +import { needsOpenCodeModelExecutionProof } from './runtimeProviderModelAccess'; + +import type { RuntimeProviderModelDto } from '@features/runtime-provider-management/contracts'; + +function model(overrides: Partial = {}): RuntimeProviderModelDto { + return { + modelId: 'local/test-model', + providerId: 'local', + displayName: 'Test Model', + sourceLabel: 'Local', + free: true, + default: false, + catalogStatus: 'active', + availability: 'untested', + accessKind: 'configured_authless', + routeKind: 'configured_local', + proofState: 'needs_probe', + requiresExecutionProof: true, + ...overrides, + }; +} + +describe('needsOpenCodeModelExecutionProof', () => { + it.each([ + ['deprecated catalog with pending proof', { catalogStatus: 'deprecated' }], + ['failed proof with a stale proof requirement', { proofState: 'failed' }], + ['unavailable model', { availability: 'unavailable' }], + ['unauthenticated availability', { availability: 'not-authenticated' }], + ['unauthenticated access', { accessKind: 'not_authenticated' }], + ['failed execution access', { accessKind: 'execution_failed' }], + ['unknown model route', { accessKind: 'unknown_model' }], + ['missing model route', { accessKind: 'no_model' }], + ] satisfies readonly [string, Partial][])( + 'returns false for hard-negative evidence: %s', + (_label, overrides) => { + expect(needsOpenCodeModelExecutionProof(model(overrides))).toBe(false); + } + ); + + it.each([ + ['verified proof', { proofState: 'verified' }], + ['available model', { availability: 'available' }], + ['verified access', { accessKind: 'verified' }], + ] satisfies readonly [string, Partial][])( + 'returns false for completed positive evidence: %s', + (_label, overrides) => { + expect(needsOpenCodeModelExecutionProof(model(overrides))).toBe(false); + } + ); + + it.each([ + ['explicit needs-probe state', { proofState: 'needs_probe', requiresExecutionProof: false }], + [ + 'explicit execution-proof requirement', + { proofState: 'not_required', requiresExecutionProof: true }, + ], + ] satisfies readonly [string, Partial][])( + 'returns true for genuine pending execution proof: %s', + (_label, overrides) => { + expect(needsOpenCodeModelExecutionProof(model(overrides))).toBe(true); + } + ); +}); diff --git a/src/features/runtime-provider-management/renderer/ui/runtimeProviderModelAccess.ts b/src/features/runtime-provider-management/renderer/ui/runtimeProviderModelAccess.ts index 5fde509429..fbd52a431f 100644 --- a/src/features/runtime-provider-management/renderer/ui/runtimeProviderModelAccess.ts +++ b/src/features/runtime-provider-management/renderer/ui/runtimeProviderModelAccess.ts @@ -9,6 +9,18 @@ export function canTestOpenCodeModelRoute(model: RuntimeProviderModelDto): boole } export function needsOpenCodeModelExecutionProof(model: RuntimeProviderModelDto): boolean { + if ( + model.catalogStatus === 'deprecated' || + model.proofState === 'failed' || + model.availability === 'unavailable' || + model.availability === 'not-authenticated' || + model.accessKind === 'not_authenticated' || + model.accessKind === 'execution_failed' || + isUnknownOpenCodeModelRoute(model) + ) { + return false; + } + return ( (model.requiresExecutionProof === true || model.proofState === 'needs_probe') && model.proofState !== 'verified' && diff --git a/test/main/features/runtime-provider-management/registerRuntimeProviderManagementIpc.test.ts b/test/main/features/runtime-provider-management/registerRuntimeProviderManagementIpc.test.ts index 8c67c5a351..e8f4f82e40 100644 --- a/test/main/features/runtime-provider-management/registerRuntimeProviderManagementIpc.test.ts +++ b/test/main/features/runtime-provider-management/registerRuntimeProviderManagementIpc.test.ts @@ -73,6 +73,46 @@ describe('registerRuntimeProviderManagementIpc', () => { ); }); + it('returns a recoverable validation error for malformed clear-project-default payloads', async () => { + const handlers = new Map Promise>(); + const ipcMain = { + handle: vi.fn((channel: string, handler: (...args: unknown[]) => Promise) => { + handlers.set(channel, handler); + }), + removeHandler: vi.fn(), + } as unknown as IpcMain; + const clearProjectDefaultModel = vi.fn(); + const feature = { + ...createCompanionFeatureStubs(), + clearProjectDefaultModel, + } as unknown as RuntimeProviderManagementFeatureFacade; + + registerRuntimeProviderManagementIpc(ipcMain, feature); + + const handler = handlers.get(RUNTIME_PROVIDER_MANAGEMENT_CLEAR_PROJECT_DEFAULT); + expect(handler).toBeDefined(); + + const malformedInputs: unknown[] = [ + null, + 42, + { runtimeId: 'opencode', projectPath: null }, + { runtimeId: 'opencode', projectPath: 42 }, + { runtimeId: 'opencode', projectPath: {} }, + ]; + for (const input of malformedInputs) { + await expect(handler?.({}, input)).resolves.toEqual({ + schemaVersion: 1, + runtimeId: 'opencode', + error: { + code: 'runtime-misconfigured', + message: 'A valid OpenCode project is required to clear its default model.', + recoverable: true, + }, + }); + } + expect(clearProjectDefaultModel).not.toHaveBeenCalled(); + }); + it('validates and routes local provider list, scan, probe, and configuration requests', async () => { const handlers = new Map Promise>(); const ipcMain = { diff --git a/test/renderer/features/runtime-provider-management/RuntimeProviderManagementPanelView.test.ts b/test/renderer/features/runtime-provider-management/RuntimeProviderManagementPanelView.test.ts index 42f486e199..486e1b4154 100644 --- a/test/renderer/features/runtime-provider-management/RuntimeProviderManagementPanelView.test.ts +++ b/test/renderer/features/runtime-provider-management/RuntimeProviderManagementPanelView.test.ts @@ -325,6 +325,24 @@ describe('RuntimeProviderManagementPanelView', () => { ); expect(allLegacyProjectButtons).toHaveLength(2); expect(allLegacyProjectButtons.every((button) => button.disabled)).toBe(true); + const legacyRows = Array.from( + host.querySelector('[data-testid="runtime-provider-legacy-models"]')?.children ?? [] + ); + const savingRow = legacyRows.find((row) => row.textContent?.includes(legacyModel.modelId)); + const idleRow = legacyRows.find((row) => row.textContent?.includes(otherLegacyModel.modelId)); + expect(savingRow?.querySelectorAll('[role="status"] .animate-spin')).toHaveLength(1); + expect(savingRow?.querySelector('button .animate-spin')).toBeNull(); + expect(idleRow?.querySelector('[role="status"]')).toBeNull(); + for (const [row, modelName] of [ + [savingRow, legacyModel.displayName], + [idleRow, otherLegacyModel.displayName], + ] as const) { + expect( + Array.from(row?.querySelectorAll('button') ?? []).every((button) => + button.getAttribute('aria-label')?.includes(modelName) + ) + ).toBe(true); + } }); it('requests the full managed view only after the Models tab is opened', async () => { @@ -887,6 +905,12 @@ describe('RuntimeProviderManagementPanelView', () => { requiresExecutionProof: false, accessReason: null, }; + const unavailableModel = { + ...configuredModel, + modelId: 'llama.cpp/unavailable:0.5b', + displayName: 'Unavailable Model', + catalogStatus: 'deprecated' as const, + }; await act(async () => { root.render( @@ -984,7 +1008,7 @@ describe('RuntimeProviderManagementPanelView', () => { selectedProviderId: 'openrouter', modelPickerProviderId: 'openrouter', modelPickerMode: 'runtime-default', - models: [configuredModel], + models: [configuredModel, unavailableModel], }), actions, disabled: false, @@ -998,6 +1022,23 @@ describe('RuntimeProviderManagementPanelView', () => { (button) => button.textContent?.trim() === 'Test and use' ); expect(testAndUseButton?.disabled).toBe(false); + expect(testAndUseButton?.getAttribute('aria-label')).toBe( + 'Test and use: qwen-test:0.5b' + ); + const modelRow = host.querySelector( + '[data-testid="runtime-provider-model-row-llama.cpp/qwen-test:0.5b"]' + ); + expect( + Array.from(modelRow?.querySelectorAll('button') ?? []) + .find((button) => button.textContent?.trim() === 'Test') + ?.getAttribute('aria-label') + ).toBe('Test: qwen-test:0.5b'); + expect( + host + .querySelector('[data-testid="runtime-provider-model-row-llama.cpp/unavailable:0.5b"]') + ?.querySelector('button[aria-label^="Test and use"]') + ?.getAttribute('aria-label') + ).toContain('Test and use: Unavailable Model: OpenCode marks this model as deprecated'); vi.mocked(actions.setDefaultModel).mockRejectedValueOnce(new Error('write failed')); await act(async () => { testAndUseButton?.click(); diff --git a/test/renderer/features/runtime-provider-management/useRuntimeProviderManagement.test.ts b/test/renderer/features/runtime-provider-management/useRuntimeProviderManagement.test.ts index bd5ff3a707..dfbba9a4d8 100644 --- a/test/renderer/features/runtime-provider-management/useRuntimeProviderManagement.test.ts +++ b/test/renderer/features/runtime-provider-management/useRuntimeProviderManagement.test.ts @@ -3464,32 +3464,51 @@ describe('useRuntimeProviderManagement', () => { runtimeProviderManagement: { loadView, clearProjectDefaultModel }, } as unknown as ElectronAPI, }); + const clearHook: { + state: RuntimeProviderManagementState | null; + actions: RuntimeProviderManagementActions | null; + } = { state: null, actions: null }; + function ClearHarness(): React.ReactElement { + const hook = useRuntimeProviderManagement({ + runtimeId: 'opencode', + enabled: true, + projectPath: '/tmp/project-a', + bundledRuntimeVersion: '0.0.75', + }); + clearHook.state = hook[0]; + clearHook.actions = hook[1]; + return React.createElement('div'); + } const root = createRoot(host); - await act(async () => { - root.render( - React.createElement(EnabledHarness, { - projectPath: '/tmp/project-a', - bundledRuntimeVersion: '0.0.75', - }) - ); - await Promise.resolve(); - }); + try { + await act(async () => { + root.render(React.createElement(ClearHarness)); + await Promise.resolve(); + }); + await act(async () => { + await vi.waitFor(() => expect(clearHook.state?.view?.projectPath).toBe('/tmp/project-a')); + }); - await act(async () => { - await actions?.clearProjectDefault('/tmp/project-a'); - await Promise.resolve(); - }); + await act(async () => { + await clearHook.actions?.clearProjectDefault('/tmp/project-a'); + await Promise.resolve(); + }); - expect(clearProjectDefaultModel).toHaveBeenCalledWith({ - runtimeId: 'opencode', - projectPath: '/tmp/project-a', - }); - expect(state?.view?.projectDefaultModel).toBeNull(); - expect(state?.view?.defaultModel).toBe('openrouter/base-model'); - expect(state?.selectedModelId).toBe('openrouter/base-model'); - expect(state?.successMessage).toBe( - 'This project: Uses default openrouter/base-model' - ); + expect(clearProjectDefaultModel).toHaveBeenCalledWith({ + runtimeId: 'opencode', + projectPath: '/tmp/project-a', + }); + expect(clearHook.state?.view?.projectDefaultModel).toBeNull(); + expect(clearHook.state?.view?.defaultModel).toBe('openrouter/base-model'); + expect(clearHook.state?.selectedModelId).toBe('openrouter/base-model'); + expect(clearHook.state?.successMessage).toBe( + 'This project: Uses default openrouter/base-model' + ); + } finally { + await act(async () => { + root.unmount(); + }); + } }); it('keeps the clear-project watchdog beyond the 90-second main-process timeout', async () => { @@ -3517,36 +3536,49 @@ describe('useRuntimeProviderManagement', () => { runtimeProviderManagement: { loadView, clearProjectDefaultModel }, } as unknown as ElectronAPI, }); + const clearHook: { + state: RuntimeProviderManagementState | null; + actions: RuntimeProviderManagementActions | null; + } = { state: null, actions: null }; + function ClearTimeoutHarness(): React.ReactElement { + const hook = useRuntimeProviderManagement({ + runtimeId: 'opencode', + enabled: true, + projectPath: '/tmp/project-a', + bundledRuntimeVersion: '0.0.75', + }); + clearHook.state = hook[0]; + clearHook.actions = hook[1]; + return React.createElement('div'); + } const root = createRoot(host); try { await act(async () => { - root.render( - React.createElement(EnabledHarness, { - projectPath: '/tmp/project-a', - bundledRuntimeVersion: '0.0.75', - }) - ); + root.render(React.createElement(ClearTimeoutHarness)); await Promise.resolve(); }); + await act(async () => { + await vi.waitFor(() => expect(clearHook.state?.view?.projectPath).toBe('/tmp/project-a')); + }); let clearPromise: Promise | null = null; await act(async () => { - clearPromise = actions?.clearProjectDefault('/tmp/project-a') ?? null; + clearPromise = clearHook.actions?.clearProjectDefault('/tmp/project-a') ?? null; await Promise.resolve(); }); await act(async () => { await vi.advanceTimersByTimeAsync(90_001); }); - expect(state?.clearingProjectDefault).toBe(true); - expect(state?.error).toBeNull(); + expect(clearHook.state?.clearingProjectDefault).toBe(true); + expect(clearHook.state?.error).toBeNull(); await act(async () => { await vi.advanceTimersByTimeAsync(9_999); await clearPromise; }); - expect(state?.clearingProjectDefault).toBe(false); - expect(state?.error).toBe('Clear project default timed out'); + expect(clearHook.state?.clearingProjectDefault).toBe(false); + expect(clearHook.state?.error).toBe('Clear project default timed out'); } finally { vi.useRealTimers(); await act(async () => { From cd24d2df63f12f2e36fa4b3dead7b26e4b8ddaee Mon Sep 17 00:00:00 2001 From: 777genius Date: Fri, 21 Aug 2026 02:05:56 +0300 Subject: [PATCH 11/20] fix(opencode): restore legacy project context UX --- scripts/ci/source-file-size-baseline.json | 2 +- .../ui/LegacyConfiguredModelsPanel.tsx | 39 ++++--- .../OpenCodeDefaultModelInheritanceCard.tsx | 107 ++++++------------ .../ui/RuntimeProviderManagementPanelView.tsx | 32 ++---- .../RuntimeProviderProjectContextSelect.tsx | 88 ++++++++++++++ .../renderer/ui/runtimeProviderModelAccess.ts | 25 ++++ ...RuntimeProviderManagementPanelView.test.ts | 103 +++++++++++++++-- 7 files changed, 271 insertions(+), 125 deletions(-) create mode 100644 src/features/runtime-provider-management/renderer/ui/RuntimeProviderProjectContextSelect.tsx diff --git a/scripts/ci/source-file-size-baseline.json b/scripts/ci/source-file-size-baseline.json index 5f97d53145..6b7f4cc310 100644 --- a/scripts/ci/source-file-size-baseline.json +++ b/scripts/ci/source-file-size-baseline.json @@ -31,7 +31,7 @@ "src/features/runtime-provider-management/renderer/hooks/useRuntimeProviderManagement.ts": 2101, "src/features/runtime-provider-management/renderer/RuntimeLocalProviderSetupDialog.tsx": 1676, "src/features/runtime-provider-management/renderer/RuntimeProviderQuickConnect.tsx": 826, - "src/features/runtime-provider-management/renderer/ui/RuntimeProviderManagementPanelView.tsx": 2945, + "src/features/runtime-provider-management/renderer/ui/RuntimeProviderManagementPanelView.tsx": 2929, "src/features/terminal-workspace/renderer/ui/TerminalWorkspacePanel.tsx": 4513, "src/features/token-usage/core/domain/snapshotProjection.ts": 937, "src/features/token-usage/renderer/view-models/tokenUsageViewModel.ts": 1481, diff --git a/src/features/runtime-provider-management/renderer/ui/LegacyConfiguredModelsPanel.tsx b/src/features/runtime-provider-management/renderer/ui/LegacyConfiguredModelsPanel.tsx index b9f8ffd970..4724f39b52 100644 --- a/src/features/runtime-provider-management/renderer/ui/LegacyConfiguredModelsPanel.tsx +++ b/src/features/runtime-provider-management/renderer/ui/LegacyConfiguredModelsPanel.tsx @@ -4,14 +4,16 @@ import { Loader2 } from 'lucide-react'; import { canUseOpenCodeModelRoute, + getOpenCodeRouteUnavailableTitle, needsOpenCodeModelExecutionProof, } from './runtimeProviderModelAccess'; +import { RuntimeProviderProjectContextSelect } from './RuntimeProviderProjectContextSelect'; import type { RuntimeProviderManagementActions, RuntimeProviderManagementState, } from '../hooks/useRuntimeProviderManagement'; -import type { RuntimeProviderModelDto } from '@features/runtime-provider-management/contracts'; +import type { ProjectPathProject } from '@renderer/components/team/dialogs/projectPathProjects'; import type { JSX } from 'react'; interface LegacyConfiguredModelsPanelProps { @@ -20,7 +22,10 @@ interface LegacyConfiguredModelsPanelProps { readonly disabled: boolean; readonly hasProjectContext: boolean; readonly projectPath: string | null; - readonly getUnavailableTitle: (model: RuntimeProviderModelDto) => string | undefined; + readonly projects: readonly ProjectPathProject[]; + readonly projectsLoading: boolean; + readonly projectError: string | null; + readonly onProjectChange?: (projectPath: string | null) => void; } export const LegacyConfiguredModelsPanel = ({ @@ -29,7 +34,10 @@ export const LegacyConfiguredModelsPanel = ({ disabled, hasProjectContext, projectPath, - getUnavailableTitle, + projects, + projectsLoading, + projectError, + onProjectChange, }: LegacyConfiguredModelsPanelProps): JSX.Element => { const { t } = useAppTranslation('settings'); const models = state.view?.configuredModels ?? []; @@ -46,18 +54,23 @@ export const LegacyConfiguredModelsPanel = ({ }, ] as const; - if (models.length === 0) { - return ( -
- {t('runtimeProvider.models.noneReported')} -
- ); - } - return (
+ + {models.length === 0 ? ( +
+ {t('runtimeProvider.models.noneReported')} +
+ ) : null} {models.map((model) => { - const unavailableTitle = getUnavailableTitle(model); + const unavailableTitle = getOpenCodeRouteUnavailableTitle(model, t); const testing = state.testingModelIds.includes(model.modelId); const saving = state.savingDefaultModelId === model.modelId; const modelTarget = model.displayName || model.modelId; @@ -85,7 +98,7 @@ export const LegacyConfiguredModelsPanel = ({
{unavailableTitle}
) : null}
-
+
{saving ? ( ; }): JSX.Element => { const { t } = useAppTranslation('settings'); - const options = useMemo(() => { - const unique = new Map(); - for (const project of projects) { - if (project.path.trim() && project.filesystemState !== 'deleted') { - unique.set(project.path.trim(), project); - } - } - if (projectPath && !unique.has(projectPath)) { - unique.set(projectPath, { - id: projectPath, - path: projectPath, - name: getProjectPathName(projectPath) ?? projectPath, - sessions: [], - totalSessions: 0, - createdAt: 0, - }); - } - return [...unique.values()]; - }, [projectPath, projects]); const activeProjectName = - options.find((project) => project.path === projectPath)?.name ?? + projects.find((project) => project.path.trim() === projectPath)?.name ?? getProjectPathName(projectPath); const model = presentOpenCodeDefaultModelInheritance({ view: state.view, @@ -232,51 +203,37 @@ export const OpenCodeDefaultModelInheritanceCard = ({
-
- - {model.projectOverrideModelId ? ( - - ) : null} +
+ projectPath && void actions.clearProjectDefault(projectPath)} + > + {state.clearingProjectDefault ? ( + + ) : ( + + )} + {t('runtimeProvider.defaults.useDefault')} + + ) : null + } + />
- {projectError ? ( -
- {projectError} -
- ) : null}
diff --git a/src/features/runtime-provider-management/renderer/ui/RuntimeProviderManagementPanelView.tsx b/src/features/runtime-provider-management/renderer/ui/RuntimeProviderManagementPanelView.tsx index 3640c70d6a..88fdec73ed 100644 --- a/src/features/runtime-provider-management/renderer/ui/RuntimeProviderManagementPanelView.tsx +++ b/src/features/runtime-provider-management/renderer/ui/RuntimeProviderManagementPanelView.tsx @@ -62,7 +62,7 @@ import { import { ProviderBrandIcon } from './providerBrandIcons'; import { canUseOpenCodeModelRoute, - isUnknownOpenCodeModelRoute, + getOpenCodeRouteUnavailableTitle, needsOpenCodeModelExecutionProof, } from './runtimeProviderModelAccess'; import { RuntimeProviderModelTestResult } from './RuntimeProviderModelTestResult'; @@ -1828,28 +1828,6 @@ function isFreeRuntimeProviderModel(model: RuntimeProviderModelDto): boolean { return isOpenCodeModelExplicitlyFree(model); } -function getOpenCodeRouteUnavailableTitle( - model: RuntimeProviderModelDto, - t: SettingsT -): string | undefined { - if (model.catalogStatus === 'deprecated') { - return 'OpenCode marks this model as deprecated. Refresh the catalog and choose an active model.'; - } - if (isUnknownOpenCodeModelRoute(model)) { - return t('runtimeProvider.models.routeUnavailableUnknown'); - } - if (model.accessKind === 'not_authenticated') { - return model.accessReason ?? t('runtimeProvider.models.routeUnavailableAuth'); - } - if (model.accessKind === 'execution_failed' || model.proofState === 'failed') { - return model.accessReason ?? t('runtimeProvider.models.routeUnavailableFailed'); - } - if (model.requiresExecutionProof === true || model.proofState === 'needs_probe') { - return model.accessReason ?? 'Test this model successfully before using it.'; - } - return undefined; -} - function getOpenCodeModelSearchText(model: RuntimeProviderModelDto): string { const recommendation = getOpenCodeTeamModelRecommendation(model.modelId); const routeStatus = getOpenCodeModelRoutePresentationStatus(model); @@ -2750,7 +2728,13 @@ export const RuntimeProviderManagementPanelView = ({ disabled={disabled || blockingCredentialWrite} hasProjectContext={hasProjectContext} projectPath={normalizedProjectPath} - getUnavailableTitle={(model) => getOpenCodeRouteUnavailableTitle(model, t)} + projects={projectContextProjects} + projectsLoading={projectContextLoading} + projectError={projectContextError} + onProjectChange={(nextProjectPath) => { + actions.closeModelPicker(); + onProjectContextChange?.(nextProjectPath); + }} /> )} diff --git a/src/features/runtime-provider-management/renderer/ui/RuntimeProviderProjectContextSelect.tsx b/src/features/runtime-provider-management/renderer/ui/RuntimeProviderProjectContextSelect.tsx new file mode 100644 index 0000000000..4ca35e331d --- /dev/null +++ b/src/features/runtime-provider-management/renderer/ui/RuntimeProviderProjectContextSelect.tsx @@ -0,0 +1,88 @@ +import { useMemo } from 'react'; + +import { useAppTranslation } from '@features/localization/renderer'; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@renderer/components/ui/select'; + +import { getProjectPathName } from '../../core/domain'; + +import type { ProjectPathProject } from '@renderer/components/team/dialogs/projectPathProjects'; +import type { JSX, ReactNode } from 'react'; + +const NO_PROJECT = '__runtime-provider-no-project__'; + +export const RuntimeProviderProjectContextSelect = ({ + projectPath, + projects, + loading, + error, + disabled, + onProjectChange, + action, +}: { + readonly projectPath: string | null; + readonly projects: readonly ProjectPathProject[]; + readonly loading: boolean; + readonly error: string | null; + readonly disabled: boolean; + readonly onProjectChange?: (projectPath: string | null) => void; + readonly action?: ReactNode; +}): JSX.Element => { + const { t } = useAppTranslation('settings'); + const options = useMemo(() => { + const unique = new Map(); + for (const project of projects) { + if (project.path.trim() && project.filesystemState !== 'deleted') { + unique.set(project.path.trim(), project); + } + } + if (projectPath && !unique.has(projectPath)) { + unique.set(projectPath, { + id: projectPath, + path: projectPath, + name: getProjectPathName(projectPath) ?? projectPath, + sessions: [], + totalSessions: 0, + createdAt: 0, + }); + } + return [...unique.values()]; + }, [projectPath, projects]); + + return ( +
+
+ + {action} +
+ {error ? ( +
+ {error} +
+ ) : null} +
+ ); +}; diff --git a/src/features/runtime-provider-management/renderer/ui/runtimeProviderModelAccess.ts b/src/features/runtime-provider-management/renderer/ui/runtimeProviderModelAccess.ts index fbd52a431f..cd2c8ccb74 100644 --- a/src/features/runtime-provider-management/renderer/ui/runtimeProviderModelAccess.ts +++ b/src/features/runtime-provider-management/renderer/ui/runtimeProviderModelAccess.ts @@ -1,5 +1,8 @@ +import type { useAppTranslation } from '@features/localization/renderer'; import type { RuntimeProviderModelDto } from '@features/runtime-provider-management/contracts'; +type SettingsT = ReturnType['t']; + export function isUnknownOpenCodeModelRoute(model: RuntimeProviderModelDto): boolean { return model.accessKind === 'unknown_model' || model.accessKind === 'no_model'; } @@ -41,3 +44,25 @@ export function canUseOpenCodeModelRoute(model: RuntimeProviderModelDto): boolea !needsOpenCodeModelExecutionProof(model) ); } + +export function getOpenCodeRouteUnavailableTitle( + model: RuntimeProviderModelDto, + t: SettingsT +): string | undefined { + if (model.catalogStatus === 'deprecated') { + return 'OpenCode marks this model as deprecated. Refresh the catalog and choose an active model.'; + } + if (isUnknownOpenCodeModelRoute(model)) { + return t('runtimeProvider.models.routeUnavailableUnknown'); + } + if (model.accessKind === 'not_authenticated') { + return model.accessReason ?? t('runtimeProvider.models.routeUnavailableAuth'); + } + if (model.accessKind === 'execution_failed' || model.proofState === 'failed') { + return model.accessReason ?? t('runtimeProvider.models.routeUnavailableFailed'); + } + if (needsOpenCodeModelExecutionProof(model)) { + return model.accessReason ?? 'Test this model successfully before using it.'; + } + return undefined; +} diff --git a/test/renderer/features/runtime-provider-management/RuntimeProviderManagementPanelView.test.ts b/test/renderer/features/runtime-provider-management/RuntimeProviderManagementPanelView.test.ts index 486e1b4154..5a1be7b326 100644 --- a/test/renderer/features/runtime-provider-management/RuntimeProviderManagementPanelView.test.ts +++ b/test/renderer/features/runtime-provider-management/RuntimeProviderManagementPanelView.test.ts @@ -315,8 +315,8 @@ describe('RuntimeProviderManagementPanelView', () => { ); await Promise.resolve(); }); - const allLegacyDefaultButtons = Array.from(host.querySelectorAll('button')).filter( - (button) => button.textContent?.includes('Set all-projects default') + const allLegacyDefaultButtons = Array.from(host.querySelectorAll('button')).filter((button) => + button.textContent?.includes('Set all-projects default') ); expect(allLegacyDefaultButtons).toHaveLength(2); expect(allLegacyDefaultButtons.every((button) => button.disabled)).toBe(true); @@ -345,6 +345,87 @@ describe('RuntimeProviderManagementPanelView', () => { } }); + it('lets bundled v0.0.74 users choose project context before setting a project default', async () => { + const host = document.createElement('div'); + document.body.appendChild(host); + const root = createRoot(host); + const actions = createActions(); + const onProjectContextChange = vi.fn(); + const legacyModel = { + providerId: 'openrouter', + modelId: 'openrouter/openai/gpt-oss-20b:free', + displayName: 'GPT OSS 20B', + sourceLabel: 'OpenRouter', + free: true, + default: true, + availability: 'available' as const, + accessKind: 'configured_authless' as const, + proofState: 'verified' as const, + requiresExecutionProof: false, + accessReason: null, + }; + + await act(async () => { + root.render( + React.createElement(RuntimeProviderManagementPanelView, { + state: createState({ + view: { + ...createState().view!, + configuredModels: [legacyModel], + }, + }), + actions, + disabled: false, + bundledRuntimeVersion: '0.0.74', + projectContextProjects: [ + { + id: 'project-a', + path: '/workspace/project-a', + name: 'Project Alpha', + sessions: [], + totalSessions: 0, + createdAt: 0, + }, + ], + onProjectContextChange, + }) + ); + await Promise.resolve(); + }); + await selectOpenCodeTab(host, 'Models'); + + const projectDefaultButton = Array.from(host.querySelectorAll('button')).find( + (button) => button.textContent?.trim() === 'Set project default' + ); + const allProjectsButton = Array.from(host.querySelectorAll('button')).find( + (button) => button.textContent?.trim() === 'Set all-projects default' + ); + const projectSelect = host.querySelector( + '[data-testid="runtime-provider-project-context-select"]' + ); + const selectTrigger = projectSelect?.querySelector('[role="combobox"]'); + + expect(projectSelect).not.toBeNull(); + expect(selectTrigger?.disabled).toBe(false); + expect(projectDefaultButton?.disabled).toBe(true); + expect(allProjectsButton?.parentElement?.classList.contains('flex-wrap')).toBe(true); + + await act(async () => { + selectTrigger?.click(); + await Promise.resolve(); + }); + const projectOption = Array.from( + document.body.querySelectorAll('[role="option"]') + ).find((option) => option.textContent?.includes('Project Alpha')); + expect(projectOption).toBeDefined(); + await act(async () => { + projectOption?.click(); + await Promise.resolve(); + }); + + expect(onProjectContextChange).toHaveBeenCalledWith('/workspace/project-a'); + }); + it('requests the full managed view only after the Models tab is opened', async () => { const host = document.createElement('div'); document.body.appendChild(host); @@ -973,8 +1054,8 @@ describe('RuntimeProviderManagementPanelView', () => { 'button[aria-label="Change: Default model"]' ); expect(document.activeElement).toBe( - Array.from(host.querySelectorAll('[role="tab"]')).find( - (tab) => tab.textContent?.includes('Models') + Array.from(host.querySelectorAll('[role="tab"]')).find((tab) => + tab.textContent?.includes('Models') ) ); @@ -1022,9 +1103,7 @@ describe('RuntimeProviderManagementPanelView', () => { (button) => button.textContent?.trim() === 'Test and use' ); expect(testAndUseButton?.disabled).toBe(false); - expect(testAndUseButton?.getAttribute('aria-label')).toBe( - 'Test and use: qwen-test:0.5b' - ); + expect(testAndUseButton?.getAttribute('aria-label')).toBe('Test and use: qwen-test:0.5b'); const modelRow = host.querySelector( '[data-testid="runtime-provider-model-row-llama.cpp/qwen-test:0.5b"]' ); @@ -1058,8 +1137,8 @@ describe('RuntimeProviderManagementPanelView', () => { null ); expect(document.activeElement).toBe( - Array.from(host.querySelectorAll('[role="tab"]')).find( - (tab) => tab.textContent?.includes('Models') + Array.from(host.querySelectorAll('[role="tab"]')).find((tab) => + tab.textContent?.includes('Models') ) ); }); @@ -1214,9 +1293,9 @@ describe('RuntimeProviderManagementPanelView', () => { expect(host.textContent).toContain('Unavailable in current runtime'); expect(host.textContent).toContain('Model was not found in the live catalog'); expect(host.querySelector('[data-testid="opencode-default-inheritance"]')).not.toBeNull(); - expect( - host.querySelector('[data-testid="opencode-default-base-model-id"]')?.textContent - ).toBe(unknownDefaultModel.modelId); + expect(host.querySelector('[data-testid="opencode-default-base-model-id"]')?.textContent).toBe( + unknownDefaultModel.modelId + ); expect( host.querySelector('[data-testid="opencode-default-project-model-id"]')?.textContent ).toBe(unknownDefaultModel.modelId); From b7b4bbf587ba2a5025f950a16862fff81f5dcedb Mon Sep 17 00:00:00 2001 From: 777genius Date: Fri, 21 Aug 2026 02:37:23 +0300 Subject: [PATCH 12/20] fix(opencode): close final default safety gaps --- scripts/ci/source-file-size-baseline.json | 4 +- ...TeamsRuntimeProviderManagementCliClient.ts | 22 ++-- .../OpenCodeGlobalDefaultContext.ts | 18 +++ .../ui/RuntimeProviderManagementPanelView.tsx | 20 ++- .../RuntimeProviderProjectContextSelect.tsx | 20 +-- .../ui/runtimeProviderProjectContext.ts | 20 +++ ...RuntimeProviderManagementCliClient.test.ts | 107 ++++++++++++---- ...RuntimeProviderManagementPanelView.test.ts | 115 ++++++++++++++++++ 8 files changed, 272 insertions(+), 54 deletions(-) create mode 100644 src/features/runtime-provider-management/main/infrastructure/OpenCodeGlobalDefaultContext.ts create mode 100644 src/features/runtime-provider-management/renderer/ui/runtimeProviderProjectContext.ts diff --git a/scripts/ci/source-file-size-baseline.json b/scripts/ci/source-file-size-baseline.json index 6b7f4cc310..d69f7af4f3 100644 --- a/scripts/ci/source-file-size-baseline.json +++ b/scripts/ci/source-file-size-baseline.json @@ -25,13 +25,13 @@ "src/features/organizations/core/domain/organizationGraph.ts": 963, "src/features/organizations/renderer/adapters/organizationGraphAdapter.ts": 1664, "src/features/organizations/renderer/ui/OrganizationEditorPanel.tsx": 942, - "src/features/runtime-provider-management/main/infrastructure/AgentTeamsRuntimeProviderManagementCliClient.ts": 2689, + "src/features/runtime-provider-management/main/infrastructure/AgentTeamsRuntimeProviderManagementCliClient.ts": 2685, "src/features/runtime-provider-management/main/infrastructure/cli-companion/RuntimeProviderCliCompanionService.ts": 838, "src/features/runtime-provider-management/main/infrastructure/OpenCodeLocalProviderConnector.ts": 1018, "src/features/runtime-provider-management/renderer/hooks/useRuntimeProviderManagement.ts": 2101, "src/features/runtime-provider-management/renderer/RuntimeLocalProviderSetupDialog.tsx": 1676, "src/features/runtime-provider-management/renderer/RuntimeProviderQuickConnect.tsx": 826, - "src/features/runtime-provider-management/renderer/ui/RuntimeProviderManagementPanelView.tsx": 2929, + "src/features/runtime-provider-management/renderer/ui/RuntimeProviderManagementPanelView.tsx": 2927, "src/features/terminal-workspace/renderer/ui/TerminalWorkspacePanel.tsx": 4513, "src/features/token-usage/core/domain/snapshotProjection.ts": 937, "src/features/token-usage/renderer/view-models/tokenUsageViewModel.ts": 1481, diff --git a/src/features/runtime-provider-management/main/infrastructure/AgentTeamsRuntimeProviderManagementCliClient.ts b/src/features/runtime-provider-management/main/infrastructure/AgentTeamsRuntimeProviderManagementCliClient.ts index ae5a7b3b96..4b8362cda0 100644 --- a/src/features/runtime-provider-management/main/infrastructure/AgentTeamsRuntimeProviderManagementCliClient.ts +++ b/src/features/runtime-provider-management/main/infrastructure/AgentTeamsRuntimeProviderManagementCliClient.ts @@ -1,12 +1,12 @@ -import { mkdtemp, rm } from 'node:fs/promises'; -import { tmpdir } from 'node:os'; -import path from 'node:path'; - import { buildProviderAwareCliEnv } from '@main/services/runtime/providerAwareCliEnv'; import { ClaudeBinaryResolver } from '@main/services/team/ClaudeBinaryResolver'; import { execCli, killProcessTree, spawnCli } from '@main/utils/childProcess'; import { resolveInteractiveShellEnvBestEffort } from '@main/utils/shellEnv'; +import { + ensureOpenCodeGlobalDefaultContextPath, + getOpenCodeGlobalDefaultContextPath, +} from './OpenCodeGlobalDefaultContext'; import { ensureOpenCodeProfileNodeModulesJunction, extractProfileIdFromSymlinkError, @@ -2494,10 +2494,8 @@ export class AgentTeamsRuntimeProviderManagementCliClient implements RuntimeProv ); } - let neutralProjectPath: string | null = null; - if (input.scope === 'all_projects') { - neutralProjectPath = await mkdtemp(path.join(tmpdir(), 'agent-teams-opencode-default-')); - } + const neutralProjectPath = + input.scope === 'all_projects' ? getOpenCodeGlobalDefaultContextPath() : null; const commandProjectPath = neutralProjectPath ?? projectPath; const args = appendProjectPathArgs( [ @@ -2523,10 +2521,11 @@ export class AgentTeamsRuntimeProviderManagementCliClient implements RuntimeProv input.runtimeId, context ); - if (misconfigured && neutralProjectPath) - await rm(neutralProjectPath, { recursive: true, force: true }).catch(() => undefined); if (misconfigured) return misconfigured; try { + if (neutralProjectPath) { + await ensureOpenCodeGlobalDefaultContextPath(); + } const { stdout, stderr } = await execCli( binaryPath, args, @@ -2552,9 +2551,6 @@ export class AgentTeamsRuntimeProviderManagementCliClient implements RuntimeProv ); } finally { this.invalidateProviderResponseCaches(); - if (neutralProjectPath) { - await rm(neutralProjectPath, { recursive: true, force: true }).catch(() => undefined); - } } } diff --git a/src/features/runtime-provider-management/main/infrastructure/OpenCodeGlobalDefaultContext.ts b/src/features/runtime-provider-management/main/infrastructure/OpenCodeGlobalDefaultContext.ts new file mode 100644 index 0000000000..2ead8f2abc --- /dev/null +++ b/src/features/runtime-provider-management/main/infrastructure/OpenCodeGlobalDefaultContext.ts @@ -0,0 +1,18 @@ +import { mkdir } from 'node:fs/promises'; +import path from 'node:path'; + +import { getAppDataPath } from '@main/utils/pathDecoder'; + +const OPEN_CODE_GLOBAL_DEFAULT_CONTEXT_DIRECTORY = 'opencode-global-default-context'; + +export function getOpenCodeGlobalDefaultContextPath(): string { + return path.join( + getAppDataPath(), + 'runtime-provider-management', + OPEN_CODE_GLOBAL_DEFAULT_CONTEXT_DIRECTORY + ); +} + +export async function ensureOpenCodeGlobalDefaultContextPath(): Promise { + await mkdir(getOpenCodeGlobalDefaultContextPath(), { recursive: true }); +} diff --git a/src/features/runtime-provider-management/renderer/ui/RuntimeProviderManagementPanelView.tsx b/src/features/runtime-provider-management/renderer/ui/RuntimeProviderManagementPanelView.tsx index 88fdec73ed..839c25ce77 100644 --- a/src/features/runtime-provider-management/renderer/ui/RuntimeProviderManagementPanelView.tsx +++ b/src/features/runtime-provider-management/renderer/ui/RuntimeProviderManagementPanelView.tsx @@ -46,7 +46,6 @@ import { import { formatProviderState, - getProjectPathName, getProviderAction, getProviderModelsLabel, getRuntimeProviderSetupPresentation, @@ -66,6 +65,7 @@ import { needsOpenCodeModelExecutionProof, } from './runtimeProviderModelAccess'; import { RuntimeProviderModelTestResult } from './RuntimeProviderModelTestResult'; +import { resolveRuntimeProviderProjectContext } from './runtimeProviderProjectContext'; import { RuntimeProviderCopilotAccessSummary, RuntimeProviderOAuthAuthorizationLink, @@ -2549,11 +2549,9 @@ export const RuntimeProviderManagementPanelView = ({ bundledRuntimeVersion ); const activeSection = selectedSection ?? 'providers'; - const hasProjectContext = Boolean(projectPath?.trim()); - const normalizedProjectPath = projectPath?.trim() || null; - const activeProjectName = - projectContextProjects.find((project) => project.path === projectPath)?.name ?? - getProjectPathName(projectPath); + const { path: effectiveProjectPath, name: activeProjectName } = + resolveRuntimeProviderProjectContext(projectPath, projectContextProjects); + const hasProjectContext = Boolean(effectiveProjectPath); const activeAuthOption = state.setupForm?.authOptions?.find( (option) => option.id === state.selectedAuthOptionId ); @@ -2563,11 +2561,11 @@ export const RuntimeProviderManagementPanelView = ({ ); useEffect(() => { - if (defaultTarget && defaultTarget.projectPath !== normalizedProjectPath) { + if (defaultTarget && defaultTarget.projectPath !== effectiveProjectPath) { setDefaultTarget(null); actions.closeModelPicker(); } - }, [actions, defaultTarget, normalizedProjectPath]); + }, [actions, defaultTarget, effectiveProjectPath]); useEffect(() => { const returnScope = returnFocusScopeRef.current; @@ -2704,7 +2702,7 @@ export const RuntimeProviderManagementPanelView = ({ state={state} actions={actions} disabled={disabled || blockingCredentialWrite} - projectPath={normalizedProjectPath} + projectPath={effectiveProjectPath} projects={projectContextProjects} projectsLoading={projectContextLoading} projectError={projectContextError} @@ -2715,7 +2713,7 @@ export const RuntimeProviderManagementPanelView = ({ }} onChooseModel={(target) => { returnFocusScopeRef.current = target; - setDefaultTarget({ scope: target, projectPath: normalizedProjectPath }); + setDefaultTarget({ scope: target, projectPath: effectiveProjectPath }); setSelectedSection('providers'); }} allProjectsActionRef={allProjectsActionRef} @@ -2727,7 +2725,7 @@ export const RuntimeProviderManagementPanelView = ({ actions={actions} disabled={disabled || blockingCredentialWrite} hasProjectContext={hasProjectContext} - projectPath={normalizedProjectPath} + projectPath={effectiveProjectPath} projects={projectContextProjects} projectsLoading={projectContextLoading} projectError={projectContextError} diff --git a/src/features/runtime-provider-management/renderer/ui/RuntimeProviderProjectContextSelect.tsx b/src/features/runtime-provider-management/renderer/ui/RuntimeProviderProjectContextSelect.tsx index 4ca35e331d..add2d33b28 100644 --- a/src/features/runtime-provider-management/renderer/ui/RuntimeProviderProjectContextSelect.tsx +++ b/src/features/runtime-provider-management/renderer/ui/RuntimeProviderProjectContextSelect.tsx @@ -11,6 +11,8 @@ import { import { getProjectPathName } from '../../core/domain'; +import { resolveRuntimeProviderProjectContext } from './runtimeProviderProjectContext'; + import type { ProjectPathProject } from '@renderer/components/team/dialogs/projectPathProjects'; import type { JSX, ReactNode } from 'react'; @@ -34,6 +36,10 @@ export const RuntimeProviderProjectContextSelect = ({ readonly action?: ReactNode; }): JSX.Element => { const { t } = useAppTranslation('settings'); + const { path: normalizedProjectPath } = resolveRuntimeProviderProjectContext( + projectPath, + projects + ); const options = useMemo(() => { const unique = new Map(); for (const project of projects) { @@ -41,24 +47,24 @@ export const RuntimeProviderProjectContextSelect = ({ unique.set(project.path.trim(), project); } } - if (projectPath && !unique.has(projectPath)) { - unique.set(projectPath, { - id: projectPath, - path: projectPath, - name: getProjectPathName(projectPath) ?? projectPath, + if (normalizedProjectPath && !unique.has(normalizedProjectPath)) { + unique.set(normalizedProjectPath, { + id: normalizedProjectPath, + path: normalizedProjectPath, + name: getProjectPathName(normalizedProjectPath) ?? normalizedProjectPath, sessions: [], totalSessions: 0, createdAt: 0, }); } return [...unique.values()]; - }, [projectPath, projects]); + }, [normalizedProjectPath, projects]); return (