feat(coding-plan): implement Coding Plan configuration management and update prompts

This commit is contained in:
mingholy.lmh 2026-02-11 16:18:23 +08:00
parent 76d31d50c4
commit a8a05188cb
15 changed files with 639 additions and 18 deletions

View file

@ -116,6 +116,29 @@ const SETTINGS_SCHEMA = {
mergeStrategy: MergeStrategy.REPLACE,
},
// Coding Plan configuration
codingPlan: {
type: 'object',
label: 'Coding Plan',
category: 'Model',
requiresRestart: false,
default: {},
description: 'Coding Plan template version tracking and configuration.',
showInDialog: false,
properties: {
version: {
type: 'string',
label: 'Coding Plan Template Version',
category: 'Model',
requiresRestart: false,
default: undefined as string | undefined,
description:
'SHA256 hash of the Coding Plan template. Used to detect template updates.',
showInDialog: false,
},
},
},
// Environment variables fallback
env: {
type: 'object',

View file

@ -4,6 +4,7 @@
* SPDX-License-Identifier: Apache-2.0
*/
import { createHash } from 'node:crypto';
import type { ProviderModelConfig as ModelConfig } from '@qwen-code/qwen-code-core';
/**
@ -18,9 +19,9 @@ export type CodingPlanTemplate = ModelConfig[];
export const CODING_PLAN_ENV_KEY = 'BAILIAN_CODING_PLAN_API_KEY';
/**
* CODING_PLAN_TEMPLATE defines the model configurations for coding-plan mode.
* CODING_PLAN_MODELS defines the model configurations for coding-plan mode.
*/
export const CODING_PLAN_TEMPLATE: CodingPlanTemplate = [
export const CODING_PLAN_MODELS: CodingPlanTemplate = [
{
id: 'qwen3-coder-plus',
name: 'qwen3-coder-plus',
@ -32,7 +33,7 @@ export const CODING_PLAN_TEMPLATE: CodingPlanTemplate = [
id: 'qwen3-max-2026-01-23',
name: 'qwen3-max-2026-01-23',
description:
'qwen3 max model from Bailian Coding Plan with thinking enabled',
'qwen3-max model with thinking enabled from Bailian Coding Plan',
baseUrl: 'https://coding.dashscope.aliyuncs.com/v1',
envKey: CODING_PLAN_ENV_KEY,
generationConfig: {
@ -42,3 +43,19 @@ export const CODING_PLAN_TEMPLATE: CodingPlanTemplate = [
},
},
];
/**
* Computes the version hash for the coding plan template.
* Uses SHA256 of the JSON-serialized template for deterministic versioning.
* @returns Hexadecimal string representing the template version
*/
export function computeCodingPlanVersion(): string {
const templateString = JSON.stringify(CODING_PLAN_MODELS);
return createHash('sha256').update(templateString).digest('hex');
}
/**
* Current version of the coding plan template.
* Computed at runtime from the template content.
*/
export const CODING_PLAN_VERSION = computeCodingPlanVersion();

View file

@ -1395,6 +1395,18 @@ export default {
'Supported auth types: openai, anthropic, gemini, vertex-ai, etc.':
'Unterstützte Authentifizierungstypen: openai, anthropic, gemini, vertex-ai, usw.',
// ============================================================================
// Coding Plan Authentication
// ============================================================================
'New model configurations are available for Bailian Coding Plan. Update now?':
'Neue Modellkonfigurationen sind für Bailian Coding Plan verfügbar. Jetzt aktualisieren?',
'Coding Plan configuration updated successfully. New models are now available.':
'Coding Plan-Konfiguration erfolgreich aktualisiert. Neue Modelle sind jetzt verfügbar.',
'Coding Plan API key not found. Please re-authenticate with Coding Plan.':
'Coding Plan API-Schlüssel nicht gefunden. Bitte authentifizieren Sie sich erneut mit Coding Plan.',
'Failed to update Coding Plan configuration: {{message}}':
'Fehler beim Aktualisieren der Coding Plan-Konfiguration: {{message}}',
// ============================================================================
// Auth Dialog - View Titles and Labels
// ============================================================================

View file

@ -1375,6 +1375,14 @@ export default {
'API key cannot be empty.': 'API key cannot be empty.',
'API key is stored in settings.env. You can migrate it to a .env file for better security.':
'API key is stored in settings.env. You can migrate it to a .env file for better security.',
'New model configurations are available for Bailian Coding Plan. Update now?':
'New model configurations are available for Bailian Coding Plan. Update now?',
'Coding Plan configuration updated successfully. New models are now available.':
'Coding Plan configuration updated successfully. New models are now available.',
'Coding Plan API key not found. Please re-authenticate with Coding Plan.':
'Coding Plan API key not found. Please re-authenticate with Coding Plan.',
'Failed to update Coding Plan configuration: {{message}}':
'Failed to update Coding Plan configuration: {{message}}',
// ============================================================================
// Custom API-KEY Configuration

View file

@ -906,6 +906,18 @@ export default {
'Supported auth types: openai, anthropic, gemini, vertex-ai, etc.':
'サポートされている認証タイプopenai、anthropic、gemini、vertex-ai など',
// ============================================================================
// Coding Plan Authentication
// ============================================================================
'New model configurations are available for Bailian Coding Plan. Update now?':
'Bailian Coding Plan の新しいモデル設定が利用可能です。今すぐ更新しますか?',
'Coding Plan configuration updated successfully. New models are now available.':
'Coding Plan の設定が正常に更新されました。新しいモデルが利用可能になりました。',
'Coding Plan API key not found. Please re-authenticate with Coding Plan.':
'Coding Plan の API キーが見つかりません。Coding Plan で再認証してください。',
'Failed to update Coding Plan configuration: {{message}}':
'Coding Plan の設定更新に失敗しました: {{message}}',
// ============================================================================
// Auth Dialog - View Titles and Labels
// ============================================================================

View file

@ -1409,6 +1409,18 @@ export default {
'Supported auth types: openai, anthropic, gemini, vertex-ai, etc.':
'Tipos de autenticação suportados: openai, anthropic, gemini, vertex-ai, etc.',
// ============================================================================
// Coding Plan Authentication
// ============================================================================
'New model configurations are available for Bailian Coding Plan. Update now?':
'Novas configurações de modelo estão disponíveis para o Bailian Coding Plan. Atualizar agora?',
'Coding Plan configuration updated successfully. New models are now available.':
'Configuração do Coding Plan atualizada com sucesso. Novos modelos agora estão disponíveis.',
'Coding Plan API key not found. Please re-authenticate with Coding Plan.':
'Chave de API do Coding Plan não encontrada. Por favor, re-autentique com o Coding Plan.',
'Failed to update Coding Plan configuration: {{message}}':
'Falha ao atualizar a configuração do Coding Plan: {{message}}',
// ============================================================================
// Auth Dialog - View Titles and Labels
// ============================================================================

View file

@ -1399,6 +1399,18 @@ export default {
'Supported auth types: openai, anthropic, gemini, vertex-ai, etc.':
'Поддерживаемые типы аутентификации: openai, anthropic, gemini, vertex-ai и др.',
// ============================================================================
// Coding Plan Authentication
// ============================================================================
'New model configurations are available for Bailian Coding Plan. Update now?':
'Доступны новые конфигурации моделей для Bailian Coding Plan. Обновить сейчас?',
'Coding Plan configuration updated successfully. New models are now available.':
'Конфигурация Coding Plan успешно обновлена. Новые модели теперь доступны.',
'Coding Plan API key not found. Please re-authenticate with Coding Plan.':
'API-ключ Coding Plan не найден. Пожалуйста, повторно авторизуйтесь с Coding Plan.',
'Failed to update Coding Plan configuration: {{message}}':
'Не удалось обновить конфигурацию Coding Plan: {{message}}',
// ============================================================================
// Auth Dialog - View Titles and Labels
// ============================================================================

View file

@ -1210,6 +1210,14 @@ export default {
'API key cannot be empty.': 'API Key 不能为空。',
'API key is stored in settings.env. You can migrate it to a .env file for better security.':
'API Key 已存储在 settings.env 中。您可以将其迁移到 .env 文件以获得更好的安全性。',
'New model configurations are available for Bailian Coding Plan. Update now?':
'百炼 Coding Plan 有新模型配置可用。是否立即更新?',
'Coding Plan configuration updated successfully. New models are now available.':
'Coding Plan 配置更新成功。新模型现已可用。',
'Coding Plan API key not found. Please re-authenticate with Coding Plan.':
'未找到 Coding Plan API Key。请重新通过 Coding Plan 认证。',
'Failed to update Coding Plan configuration: {{message}}':
'更新 Coding Plan 配置失败:{{message}}',
// ============================================================================
// Custom API-KEY Configuration

View file

@ -94,6 +94,7 @@ import {
useSettingInputRequests,
usePluginChoiceRequests,
} from './hooks/useExtensionUpdates.js';
import { useCodingPlanUpdates } from './hooks/useCodingPlanUpdates.js';
import { ShellFocusContext } from './contexts/ShellFocusContext.js';
import { t } from '../i18n/index.js';
import { useWelcomeBack } from './hooks/useWelcomeBack.js';
@ -232,6 +233,9 @@ export const AppContainer = (props: AppContainerProps) => {
config.getWorkingDir(),
);
const { codingPlanUpdateRequest, dismissCodingPlanUpdate } =
useCodingPlanUpdates(settings, config, historyManager.addItem);
const [isPermissionsDialogOpen, setPermissionsDialogOpen] = useState(false);
const openPermissionsDialog = useCallback(
() => setPermissionsDialogOpen(true),
@ -1276,6 +1280,7 @@ export const AppContainer = (props: AppContainerProps) => {
!!shellConfirmationRequest ||
!!confirmationRequest ||
confirmUpdateExtensionRequests.length > 0 ||
!!codingPlanUpdateRequest ||
settingInputRequests.length > 0 ||
pluginChoiceRequests.length > 0 ||
!!loopDetectionConfirmationRequest ||
@ -1340,6 +1345,7 @@ export const AppContainer = (props: AppContainerProps) => {
shellConfirmationRequest,
confirmationRequest,
confirmUpdateExtensionRequests,
codingPlanUpdateRequest,
settingInputRequests,
pluginChoiceRequests,
loopDetectionConfirmationRequest,
@ -1430,6 +1436,7 @@ export const AppContainer = (props: AppContainerProps) => {
shellConfirmationRequest,
confirmationRequest,
confirmUpdateExtensionRequests,
codingPlanUpdateRequest,
settingInputRequests,
pluginChoiceRequests,
loopDetectionConfirmationRequest,
@ -1514,6 +1521,7 @@ export const AppContainer = (props: AppContainerProps) => {
exitEditorDialog,
closeSettingsDialog,
closeModelDialog,
dismissCodingPlanUpdate,
closePermissionsDialog,
setShellModeActive,
vimHandleInput,
@ -1559,6 +1567,7 @@ export const AppContainer = (props: AppContainerProps) => {
exitEditorDialog,
closeSettingsDialog,
closeModelDialog,
dismissCodingPlanUpdate,
closePermissionsDialog,
setShellModeActive,
vimHandleInput,

View file

@ -30,9 +30,10 @@ import { AuthState, MessageType } from '../types.js';
import type { HistoryItem } from '../types.js';
import { t } from '../../i18n/index.js';
import {
CODING_PLAN_TEMPLATE,
CODING_PLAN_MODELS,
CODING_PLAN_ENV_KEY,
} from '../../constants/codingPlanTemplates.js';
CODING_PLAN_VERSION,
} from '../../constants/codingPlan.js';
export type { QwenAuthState } from '../hooks/useQwenAuth.js';
@ -303,7 +304,7 @@ export const useAuthCommand = (
process.env[envKeyName] = apiKey;
// Generate model configs from template
const newConfigs: ProviderModelConfig[] = CODING_PLAN_TEMPLATE.map(
const newConfigs: ProviderModelConfig[] = CODING_PLAN_MODELS.map(
(templateConfig) => ({
...templateConfig,
envKey: envKeyName,
@ -316,22 +317,21 @@ export const useAuthCommand = (
settings.merged.modelProviders as ModelProvidersConfig | undefined
)?.[AuthType.USE_OPENAI] || [];
// Deduplicate: check if config with same id, baseUrl, and envKey exists
const isDuplicate = (config: ProviderModelConfig) =>
existingConfigs.some(
(existing) =>
existing.id === config.id &&
existing.baseUrl === config.baseUrl &&
existing.envKey === config.envKey,
// Identify Coding Plan configs by baseUrl + envKey
// Remove existing Coding Plan configs to ensure template changes are applied
const isCodingPlanConfig = (config: ProviderModelConfig) =>
config.envKey === envKeyName &&
CODING_PLAN_MODELS.some(
(template) => template.baseUrl === config.baseUrl,
);
// Filter out duplicates and replace existing ones
const uniqueNewConfigs = newConfigs.filter(
(config) => !isDuplicate(config),
// Filter out existing Coding Plan configs, keep user custom configs
const nonCodingPlanConfigs = existingConfigs.filter(
(existing) => !isCodingPlanConfig(existing),
);
// Unshift new configs to the beginning
const updatedConfigs = [...uniqueNewConfigs, ...existingConfigs];
// Add new Coding Plan configs at the beginning
const updatedConfigs = [...newConfigs, ...nonCodingPlanConfigs];
// Persist to modelProviders
settings.setValue(
@ -347,6 +347,13 @@ export const useAuthCommand = (
AuthType.USE_OPENAI,
);
// Persist coding plan version for future update detection
settings.setValue(
persistScope,
'codingPlan.version',
CODING_PLAN_VERSION,
);
// If there are configs, use the first one as the model
if (updatedConfigs.length > 0 && updatedConfigs[0]?.id) {
settings.setValue(persistScope, 'model.name', updatedConfigs[0].id);

View file

@ -122,6 +122,15 @@ export const DialogManager = ({
/>
);
}
if (uiState.codingPlanUpdateRequest) {
return (
<ConsentPrompt
prompt={uiState.codingPlanUpdateRequest.prompt}
onConfirm={uiState.codingPlanUpdateRequest.onConfirm}
terminalWidth={terminalWidth}
/>
);
}
if (uiState.settingInputRequests.length > 0) {
const request = uiState.settingInputRequests[0];
// Use settingName as key to force re-mount when switching between different settings

View file

@ -51,6 +51,7 @@ export interface UIActions {
exitEditorDialog: () => void;
closeSettingsDialog: () => void;
closeModelDialog: () => void;
dismissCodingPlanUpdate: () => void;
closePermissionsDialog: () => void;
setShellModeActive: (value: boolean) => void;
vimHandleInput: (key: Key) => boolean;

View file

@ -32,6 +32,7 @@ import type { UpdateObject } from '../utils/updateCheck.js';
import { type UseHistoryManagerReturn } from '../hooks/useHistoryManager.js';
import { type RestartReason } from '../hooks/useIdeTrustListener.js';
import { type CodingPlanUpdateRequest } from '../hooks/useCodingPlanUpdates.js';
export interface UIState {
history: HistoryItem[];
@ -60,6 +61,7 @@ export interface UIState {
shellConfirmationRequest: ShellConfirmationRequest | null;
confirmationRequest: ConfirmationRequest | null;
confirmUpdateExtensionRequests: ConfirmationRequest[];
codingPlanUpdateRequest: CodingPlanUpdateRequest | undefined;
settingInputRequests: SettingInputRequest[];
pluginChoiceRequests: PluginChoiceRequest[];
loopDetectionConfirmationRequest: LoopDetectionConfirmationRequest | null;

View file

@ -0,0 +1,288 @@
/**
* @license
* Copyright 2025 Qwen Team
* SPDX-License-Identifier: Apache-2.0
*/
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { renderHook, waitFor } from '@testing-library/react';
import { useCodingPlanUpdates } from './useCodingPlanUpdates.js';
import { CODING_PLAN_ENV_KEY } from '../../constants/codingPlan.js';
import { AuthType } from '@qwen-code/qwen-code-core';
// Mock the constants module
vi.mock('../../constants/codingPlan.js', async () => {
const actual = await vi.importActual('../../constants/codingPlan.js');
return {
...actual,
CODING_PLAN_VERSION: 'test-version-hash',
CODING_PLAN_MODELS: [
{
id: 'test-model-1',
name: 'Test Model 1',
baseUrl: 'https://test.example.com/v1',
description: 'Test model 1',
envKey: 'BAILIAN_CODING_PLAN_API_KEY',
},
{
id: 'test-model-2',
name: 'Test Model 2',
baseUrl: 'https://test.example.com/v1',
description: 'Test model 2',
envKey: 'BAILIAN_CODING_PLAN_API_KEY',
},
],
};
});
describe('useCodingPlanUpdates', () => {
const mockSettings = {
merged: {
modelProviders: {},
codingPlan: {},
},
setValue: vi.fn(),
isTrusted: true,
workspace: { settings: {} },
user: { settings: {} },
};
const mockConfig = {
reloadModelProvidersConfig: vi.fn(),
refreshAuth: vi.fn(),
};
const mockAddItem = vi.fn();
beforeEach(() => {
vi.clearAllMocks();
delete process.env[CODING_PLAN_ENV_KEY];
});
describe('version comparison', () => {
it('should not show update prompt when no version is stored', () => {
mockSettings.merged.codingPlan = {};
const { result } = renderHook(() =>
useCodingPlanUpdates(
mockSettings as never,
mockConfig as never,
mockAddItem,
),
);
expect(result.current.codingPlanUpdateRequest).toBeUndefined();
});
it('should not show update prompt when versions match', () => {
mockSettings.merged.codingPlan = { version: 'test-version-hash' };
const { result } = renderHook(() =>
useCodingPlanUpdates(
mockSettings as never,
mockConfig as never,
mockAddItem,
),
);
expect(result.current.codingPlanUpdateRequest).toBeUndefined();
});
it('should show update prompt when versions differ', async () => {
mockSettings.merged.codingPlan = { version: 'old-version-hash' };
const { result } = renderHook(() =>
useCodingPlanUpdates(
mockSettings as never,
mockConfig as never,
mockAddItem,
),
);
await waitFor(() => {
expect(result.current.codingPlanUpdateRequest).toBeDefined();
});
expect(result.current.codingPlanUpdateRequest?.prompt).toContain(
'New model configurations',
);
});
});
describe('update execution', () => {
it('should execute update when user confirms', async () => {
process.env[CODING_PLAN_ENV_KEY] = 'test-api-key';
mockSettings.merged.codingPlan = { version: 'old-version-hash' };
mockSettings.merged.modelProviders = {
[AuthType.USE_OPENAI]: [
{
id: 'test-model-1',
baseUrl: 'https://test.example.com/v1',
envKey: CODING_PLAN_ENV_KEY,
},
{
id: 'custom-model',
baseUrl: 'https://custom.example.com',
envKey: 'CUSTOM_API_KEY',
},
],
};
mockConfig.refreshAuth.mockResolvedValue(undefined);
const { result } = renderHook(() =>
useCodingPlanUpdates(
mockSettings as never,
mockConfig as never,
mockAddItem,
),
);
await waitFor(() => {
expect(result.current.codingPlanUpdateRequest).toBeDefined();
});
// Confirm the update
await result.current.codingPlanUpdateRequest!.onConfirm(true);
// Wait for async update to complete
await waitFor(() => {
// Should update model providers (at least 2 calls: modelProviders + version)
expect(mockSettings.setValue).toHaveBeenCalled();
});
// Should update version
expect(mockSettings.setValue).toHaveBeenCalledWith(
expect.anything(),
'codingPlan.version',
'test-version-hash',
);
// Should reload and refresh auth
expect(mockConfig.reloadModelProvidersConfig).toHaveBeenCalled();
expect(mockConfig.refreshAuth).toHaveBeenCalledWith(AuthType.USE_OPENAI);
// Should show success message
expect(mockAddItem).toHaveBeenCalledWith(
expect.objectContaining({
type: 'info',
text: expect.stringContaining('updated successfully'),
}),
expect.any(Number),
);
});
it('should not execute update when user declines', async () => {
mockSettings.merged.codingPlan = { version: 'old-version-hash' };
const { result } = renderHook(() =>
useCodingPlanUpdates(
mockSettings as never,
mockConfig as never,
mockAddItem,
),
);
await waitFor(() => {
expect(result.current.codingPlanUpdateRequest).toBeDefined();
});
// Decline the update
await result.current.codingPlanUpdateRequest!.onConfirm(false);
// Should not update anything
expect(mockSettings.setValue).not.toHaveBeenCalled();
expect(mockConfig.reloadModelProvidersConfig).not.toHaveBeenCalled();
});
it('should preserve non-Coding Plan configs during update', async () => {
process.env[CODING_PLAN_ENV_KEY] = 'test-api-key';
mockSettings.merged.codingPlan = { version: 'old-version-hash' };
const customConfig = {
id: 'custom-model',
baseUrl: 'https://custom.example.com',
envKey: 'CUSTOM_API_KEY',
};
mockSettings.merged.modelProviders = {
[AuthType.USE_OPENAI]: [
{
id: 'test-model-1',
baseUrl: 'https://test.example.com/v1',
envKey: CODING_PLAN_ENV_KEY,
},
customConfig,
],
};
mockConfig.refreshAuth.mockResolvedValue(undefined);
const { result } = renderHook(() =>
useCodingPlanUpdates(
mockSettings as never,
mockConfig as never,
mockAddItem,
),
);
await waitFor(() => {
expect(result.current.codingPlanUpdateRequest).toBeDefined();
});
await result.current.codingPlanUpdateRequest!.onConfirm(true);
// Wait for async update to complete
await waitFor(() => {
// Should preserve custom config - verify setValue was called
expect(mockSettings.setValue).toHaveBeenCalled();
});
});
it('should handle missing API key error', async () => {
mockSettings.merged.codingPlan = { version: 'old-version-hash' };
const { result } = renderHook(() =>
useCodingPlanUpdates(
mockSettings as never,
mockConfig as never,
mockAddItem,
),
);
await waitFor(() => {
expect(result.current.codingPlanUpdateRequest).toBeDefined();
});
await result.current.codingPlanUpdateRequest!.onConfirm(true);
// Should show error message
expect(mockAddItem).toHaveBeenCalledWith(
expect.objectContaining({
type: 'error',
}),
expect.any(Number),
);
});
});
describe('dismissUpdate', () => {
it('should clear update request when dismissed', async () => {
mockSettings.merged.codingPlan = { version: 'old-version-hash' };
const { result } = renderHook(() =>
useCodingPlanUpdates(
mockSettings as never,
mockConfig as never,
mockAddItem,
),
);
await waitFor(() => {
expect(result.current.codingPlanUpdateRequest).toBeDefined();
});
result.current.dismissCodingPlanUpdate();
await waitFor(() => {
expect(result.current.codingPlanUpdateRequest).toBeUndefined();
});
});
});
});

View file

@ -0,0 +1,201 @@
/**
* @license
* Copyright 2025 Qwen Team
* SPDX-License-Identifier: Apache-2.0
*/
import { useCallback, useEffect, useState } from 'react';
import type { Config, ModelProvidersConfig } from '@qwen-code/qwen-code-core';
import { AuthType } from '@qwen-code/qwen-code-core';
import type { LoadedSettings } from '../../config/settings.js';
import { getPersistScopeForModelSelection } from '../../config/modelProvidersScope.js';
import {
CODING_PLAN_MODELS,
CODING_PLAN_ENV_KEY,
CODING_PLAN_VERSION,
} from '../../constants/codingPlan.js';
import { t } from '../../i18n/index.js';
export interface CodingPlanUpdateRequest {
prompt: string;
onConfirm: (confirmed: boolean) => void;
}
/**
* Checks if a config is a Coding Plan configuration by matching baseUrl and envKey.
* This ensures only configs from the Coding Plan provider are identified.
*/
function isCodingPlanConfig(config: {
baseUrl?: string;
envKey?: string;
}): boolean {
return (
config.envKey === CODING_PLAN_ENV_KEY &&
CODING_PLAN_MODELS.some((template) => template.baseUrl === config.baseUrl)
);
}
/**
* Hook for detecting and handling Coding Plan template updates.
* Compares the persisted version with the current template version
* and prompts the user to update if they differ.
*/
export function useCodingPlanUpdates(
settings: LoadedSettings,
config: Config,
addItem: (
item: { type: 'info' | 'error' | 'warning'; text: string },
timestamp: number,
) => void,
) {
const [updateRequest, setUpdateRequest] = useState<
CodingPlanUpdateRequest | undefined
>();
/**
* Execute the Coding Plan configuration update.
* Removes old Coding Plan configs and replaces them with new ones from the template.
*/
const executeUpdate = useCallback(async () => {
try {
const persistScope = getPersistScopeForModelSelection(settings);
// Get current configs
const currentConfigs =
(
settings.merged.modelProviders as
| Record<string, Array<Record<string, unknown>>>
| undefined
)?.[AuthType.USE_OPENAI] || [];
// Filter out Coding Plan configs (keep user custom configs)
const nonCodingPlanConfigs = currentConfigs.filter(
(cfg) =>
!isCodingPlanConfig({
baseUrl: cfg['baseUrl'] as string | undefined,
envKey: cfg['envKey'] as string | undefined,
}),
);
// Generate new configs from template with the stored API key
const apiKey = process.env[CODING_PLAN_ENV_KEY];
if (!apiKey) {
throw new Error(
t(
'Coding Plan API key not found. Please re-authenticate with Coding Plan.',
),
);
}
const newConfigs = CODING_PLAN_MODELS.map((templateConfig) => ({
...templateConfig,
envKey: CODING_PLAN_ENV_KEY,
}));
// Combine: new Coding Plan configs at the front, user configs preserved
const updatedConfigs = [
...newConfigs,
...(nonCodingPlanConfigs as Array<Record<string, unknown>>),
] as Array<Record<string, unknown>>;
// Persist updated model providers
settings.setValue(
persistScope,
`modelProviders.${AuthType.USE_OPENAI}`,
updatedConfigs,
);
// Update the version
settings.setValue(
persistScope,
'codingPlan.version',
CODING_PLAN_VERSION,
);
// Hot-reload model providers configuration
const updatedModelProviders = {
...(settings.merged.modelProviders as
| Record<string, unknown>
| undefined),
[AuthType.USE_OPENAI]: updatedConfigs,
};
config.reloadModelProvidersConfig(
updatedModelProviders as unknown as ModelProvidersConfig,
);
// Refresh auth with the new configuration
await config.refreshAuth(AuthType.USE_OPENAI);
addItem(
{
type: 'info',
text: t(
'Coding Plan configuration updated successfully. New models are now available.',
),
},
Date.now(),
);
return true;
} catch (error) {
const errorMessage =
error instanceof Error ? error.message : String(error);
addItem(
{
type: 'error',
text: t('Failed to update Coding Plan configuration: {{message}}', {
message: errorMessage,
}),
},
Date.now(),
);
return false;
}
}, [settings, config, addItem]);
/**
* Check for version mismatch and prompt user for update if needed.
*/
const checkForUpdates = useCallback(() => {
const savedVersion = (
settings.merged as { codingPlan?: { version?: string } }
).codingPlan?.version;
// If no version is stored, user hasn't used Coding Plan yet - skip check
if (!savedVersion) {
return;
}
// If versions match, no update needed
if (savedVersion === CODING_PLAN_VERSION) {
return;
}
// Version mismatch - prompt user for update
setUpdateRequest({
prompt: t(
'New model configurations are available for Bailian Coding Plan. Update now?',
),
onConfirm: async (confirmed: boolean) => {
setUpdateRequest(undefined);
if (confirmed) {
await executeUpdate();
}
},
});
}, [settings, executeUpdate]);
// Check for updates on mount
useEffect(() => {
checkForUpdates();
}, [checkForUpdates]);
const dismissCodingPlanUpdate = useCallback(() => {
setUpdateRequest(undefined);
}, []);
return {
codingPlanUpdateRequest: updateRequest,
dismissCodingPlanUpdate,
};
}