mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-05-05 07:10:55 +00:00
feat(coding-plan): implement Coding Plan configuration management and update prompts
This commit is contained in:
parent
76d31d50c4
commit
a8a05188cb
15 changed files with 639 additions and 18 deletions
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
@ -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
|
||||
// ============================================================================
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
// ============================================================================
|
||||
|
|
|
|||
|
|
@ -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
|
||||
// ============================================================================
|
||||
|
|
|
|||
|
|
@ -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
|
||||
// ============================================================================
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
288
packages/cli/src/ui/hooks/useCodingPlanUpdates.test.ts
Normal file
288
packages/cli/src/ui/hooks/useCodingPlanUpdates.test.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
201
packages/cli/src/ui/hooks/useCodingPlanUpdates.ts
Normal file
201
packages/cli/src/ui/hooks/useCodingPlanUpdates.ts
Normal 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,
|
||||
};
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue