diff --git a/docs/en/wegent/user-guide/coding/managing-code-tasks.md b/docs/en/wegent/user-guide/coding/managing-code-tasks.md index ad77ddedb1..d704f94269 100644 --- a/docs/en/wegent/user-guide/coding/managing-code-tasks.md +++ b/docs/en/wegent/user-guide/coding/managing-code-tasks.md @@ -77,6 +77,15 @@ Click the model selector to override the agent's default model: When you switch to a different model in an existing Wework conversation, Wework asks for confirmation first. Different models may interpret existing context differently and may vary in tool support, response style, and task continuity. After confirmation, the new model is used for the next message; a response already in progress continues with the previous model. No warning is shown when selecting a model for a new conversation or reselecting the model that is already chosen. +#### Friendly Titles + +You can enable **Use friendly titles** under **Settings > General > Runtime**. It is off by default, and enabling it requires a title-generation model: + +- **Same as task**: The default option. Each new task uses the model actually selected for that task to generate its title. +- **A specific model**: Used only to generate the title asynchronously; it does not change the model used by the task itself. + +Title generation never blocks task submission. If a selected title model is no longer available, Wework skips title generation and still creates the task normally. + #### Knowledge Base Context Click the context button to add knowledge bases: diff --git a/docs/zh/wegent/user-guide/coding/managing-code-tasks.md b/docs/zh/wegent/user-guide/coding/managing-code-tasks.md index e014541ca7..6494035cd8 100644 --- a/docs/zh/wegent/user-guide/coding/managing-code-tasks.md +++ b/docs/zh/wegent/user-guide/coding/managing-code-tasks.md @@ -77,6 +77,15 @@ sidebar_position: 2 在 Wework 的已有对话中切换到不同模型时,系统会先提示确认。不同模型对已有上下文的理解、工具支持、回复风格和任务连续性可能存在差异。确认后,新模型从下一条消息开始使用;如果当前回复仍在进行,它会继续使用原模型。新对话首次选择模型,或重复选择已经选中的模型时,不会显示该提示。 +#### 友好标题 + +在 **设置 > 通用 > 运行** 中可以打开“使用友好标题”。该功能默认关闭;打开时必须指定标题生成模型: + +- **与任务相同**:默认选项。每次创建任务时,使用该任务实际选择的模型生成标题。 +- **指定模型**:只用于异步生成标题,不会改变任务本身使用的模型。 + +标题生成不会阻止任务发送。若指定的标题模型已经不可用,Wework 会跳过标题生成,任务仍会正常创建。 + #### 知识库上下文 点击上下文按钮添加知识库: diff --git a/wework/e2e/desktop/task-flow.e2e.mjs b/wework/e2e/desktop/task-flow.e2e.mjs index a13e2fcd18..8a1790f4ed 100644 --- a/wework/e2e/desktop/task-flow.e2e.mjs +++ b/wework/e2e/desktop/task-flow.e2e.mjs @@ -238,20 +238,20 @@ const DEFAULT_MODEL_LABEL = 'GPT 5.6 Luna' const LOCAL_MODEL_CASES = [ { protocol: 'responses', - optionId: 'local-model:desktop-e2e-responses', - label: 'Desktop E2E Responses', + optionIds: ['wework-custom-desktop-e2e-responses', 'local-model:desktop-e2e-responses'], + labels: ['wework-custom-desktop-e2e-responses', 'Desktop E2E Responses'], modelId: 'desktop-e2e-responses-model', }, { protocol: 'chat', - optionId: 'local-model:desktop-e2e-chat', - label: 'Desktop E2E Chat', + optionIds: ['wework-custom-desktop-e2e-chat', 'local-model:desktop-e2e-chat'], + labels: ['wework-custom-desktop-e2e-chat', 'Desktop E2E Chat'], modelId: 'desktop-e2e-chat-model', }, { protocol: 'anthropic', - optionId: 'local-model:desktop-e2e-anthropic', - label: 'Desktop E2E Anthropic', + optionIds: ['wework-custom-desktop-e2e-anthropic', 'local-model:desktop-e2e-anthropic'], + labels: ['wework-custom-desktop-e2e-anthropic', 'Desktop E2E Anthropic'], modelId: 'desktop-e2e-anthropic-model', }, ] @@ -259,8 +259,8 @@ const MODEL_PROTOCOLS = ['responses', 'chat', 'anthropic'] const CLOUD_MODEL_CASES = MODEL_PROTOCOLS.map(protocol => ({ source: 'cloud', protocol, - optionId: `desktop-e2e-cloud-${protocol}`, - label: protocol === 'chat' ? 'moonshot-kimi-k3' : `desktop-e2e-cloud-${protocol}`, + optionIds: [`desktop-e2e-cloud-${protocol}`], + labels: [protocol === 'chat' ? 'moonshot-kimi-k3' : `desktop-e2e-cloud-${protocol}`], modelId: protocol === 'chat' ? 'moonshot-kimi-k3' : `desktop-e2e-cloud-${protocol}-upstream`, })) const MODEL_PROTOCOL_MATRIX_CASES = [ @@ -268,8 +268,8 @@ const MODEL_PROTOCOL_MATRIX_CASES = [ ...MODEL_PROTOCOLS.map(protocol => ({ source: 'codex', protocol, - optionId: DEFAULT_MODEL_ID, - label: DEFAULT_MODEL_LABEL, + optionIds: [DEFAULT_MODEL_ID], + labels: [DEFAULT_MODEL_LABEL], modelId: DEFAULT_MODEL_ID, })), ...CLOUD_MODEL_CASES, @@ -311,8 +311,14 @@ const LOCAL_MODEL_SWITCH_COMPLETE = 'WEWORK_LOCAL_MODEL_SWITCH_COMPLETE' const LOCAL_MODEL_SWITCH_INVALID_CALL_ID = 'functions.exec_command:0' const LOCAL_MODEL_SWITCH_ARTIFACT = 'wework-model-switch-protocol.txt' const LOCAL_MODEL_SWITCH_ARTIFACT_CONTENT = 'WEWORK_MODEL_SWITCH_PROTOCOL_EXEC_COMMAND' -const PROVIDER_SWITCH_LUNA_OPTION_ID = 'local-model:desktop-e2e-luna-overseas' -const PROVIDER_SWITCH_LUNA_LABEL = 'GPT 5.6 Luna (海外)' +const PROVIDER_SWITCH_LUNA_OPTION_IDS = [ + 'wework-custom-desktop-e2e-luna-overseas', + 'local-model:desktop-e2e-luna-overseas', +] +const PROVIDER_SWITCH_LUNA_LABELS = [ + 'wework-custom-desktop-e2e-luna-overseas', + 'GPT 5.6 Luna (海外)', +] const PROVIDER_SWITCH_LUNA_MODEL_ID = 'gpt-5.6-luna' // The local E2E Codex catalog is classified as third-party (custom provider), so // the official option is served from the cloud model catalog with a canonical @@ -3308,6 +3314,10 @@ async function verifyExpandedToolDetail( } async function ensureToggleExpanded(control, selector) { + await control.command('waitFor', selector, { + visible: true, + timeoutMs: DEFAULT_STEP_TIMEOUT_MS, + }) const expandedCount = Number( await control.command('getElementCount', `${selector}[aria-expanded="true"]`) ) @@ -5548,7 +5558,10 @@ async function verifyAutomationLifecycle(control, workspacePath) { ), 'The existing-task selector did not list the pinned local task' ) - await control.command('click', '[data-testid="automation-target-task-select"]') + await control.command( + 'click', + `[data-testid="automation-target-task-select-option-local-device:${manualTaskId}"]` + ) await control.command('click', '[data-testid="automation-repeat-menu"]') await control.command('click', '[data-testid="automation-repeat-menu-option-one_time"]') const scheduledFor = new Date(Date.now() + 5_000) @@ -5999,10 +6012,21 @@ async function revealGroupedModelOption(control, targetOptionId) { return false } -async function ensureModelOptionVisible(control, targetOptionId) { +function modelOptionIdCandidates(modelIds) { + return (Array.isArray(modelIds) ? modelIds : [modelIds]).map(modelId => + modelId.startsWith('model-option-') ? modelId : `model-option-${modelId}` + ) +} + +function hasModelOption(menu, targetOptionIds) { + return targetOptionIds.some(targetOptionId => menu.testIds.includes(targetOptionId)) +} + +async function ensureModelOptionVisible(control, modelIds) { + const targetOptionIds = modelOptionIdCandidates(modelIds) for (let attempt = 0; attempt < 8; attempt += 1) { let menu = JSON.parse(await control.command('snapshot', 'body')) - if (menu.testIds.includes(targetOptionId)) return menu + if (hasModelOption(menu, targetOptionIds)) return menu if (menu.testIds.includes('model-control-menu-model')) { await control .command('hover', '[data-testid="model-control-menu-model"]', { @@ -6025,13 +6049,15 @@ async function ensureModelOptionVisible(control, targetOptionId) { } await new Promise(resolvePromise => setTimeout(resolvePromise, 150)) menu = JSON.parse(await control.command('snapshot', 'body')) - if (menu.testIds.includes(targetOptionId)) return menu - if (await revealGroupedModelOption(control, targetOptionId)) { - return JSON.parse(await control.command('snapshot', 'body')) + if (hasModelOption(menu, targetOptionIds)) return menu + for (const targetOptionId of targetOptionIds) { + if (await revealGroupedModelOption(control, targetOptionId)) { + return JSON.parse(await control.command('snapshot', 'body')) + } } } - throw new Error(`Model option ${targetOptionId} did not become visible`) + throw new Error(`Model options ${targetOptionIds.join(', ')} did not become visible`) } async function confirmLocalProjectName(control, name) { @@ -6072,9 +6098,10 @@ async function createSingleRootLocalProject(control, workspacePath, name) { async function selectE2EModel( control, - modelId = DEFAULT_MODEL_ID, - modelLabel = DEFAULT_MODEL_LABEL + modelIds = DEFAULT_MODEL_ID, + modelLabels = DEFAULT_MODEL_LABEL ) { + const labels = Array.isArray(modelLabels) ? modelLabels : [modelLabels] await control.command('waitFor', '[data-testid="model-selector-button"]', { timeoutMs: WORKBENCH_READY_TIMEOUT_MS, }) @@ -6082,14 +6109,17 @@ async function selectE2EModel( 'getText', '[data-testid="model-selector-button"]' ) - if (selectedModelLabel.includes(modelLabel)) return + if (labels.some(label => selectedModelLabel.includes(label))) return - const targetOptionId = `model-option-${modelId}` - await ensureModelOptionVisible(control, targetOptionId) - await control.command('waitFor', `[data-testid="model-option-${modelId}"]`, { + const selectionMenu = await ensureModelOptionVisible(control, modelIds) + const targetOptionId = modelOptionIdCandidates(modelIds).find(optionId => + selectionMenu.testIds.includes(optionId) + ) + assert.ok(targetOptionId, `No visible model option matched ${modelOptionIdCandidates(modelIds)}`) + await control.command('waitFor', `[data-testid="${targetOptionId}"]`, { timeoutMs: DEFAULT_STEP_TIMEOUT_MS, }) - await control.command('click', `[data-testid="model-option-${modelId}"]`) + await control.command('click', `[data-testid="${targetOptionId}"]`) const selectionSnapshot = JSON.parse(await control.command('snapshot', 'body')) if (selectionSnapshot.testIds.includes('model-switch-warning-dialog')) { await control.command( @@ -6100,10 +6130,7 @@ async function selectE2EModel( } ) } - await control.command('waitFor', '[data-testid="model-selector-button"]', { - text: modelLabel, - timeoutMs: DEFAULT_STEP_TIMEOUT_MS, - }) + await waitForE2EModelLabel(control, labels) await control.command('press', 'body', { key: 'Escape' }) await waitForSnapshot( control, @@ -6112,13 +6139,26 @@ async function selectE2EModel( ) } +async function waitForE2EModelLabel(control, labels) { + const startedAt = Date.now() + while (Date.now() - startedAt < DEFAULT_STEP_TIMEOUT_MS) { + const selectedModelLabel = await control.command( + 'getText', + '[data-testid="model-selector-button"]' + ) + if (labels.some(label => selectedModelLabel.includes(label))) return + await new Promise(resolvePromise => setTimeout(resolvePromise, 100)) + } + throw new Error(`Model selector did not display one of: ${labels.join(', ')}`) +} + async function verifyCrossProviderSwitchRetry(control, composerSelector) { control.setScenario('provider_switch_retry') await control.command('click', '[data-testid="new-chat-button"]') await control.command('waitFor', composerSelector, { timeoutMs: WORKBENCH_READY_TIMEOUT_MS, }) - await selectE2EModel(control, PROVIDER_SWITCH_LUNA_OPTION_ID, PROVIDER_SWITCH_LUNA_LABEL) + await selectE2EModel(control, PROVIDER_SWITCH_LUNA_OPTION_IDS, PROVIDER_SWITCH_LUNA_LABELS) await sendPrompt(control, composerSelector, PROVIDER_SWITCH_PROMPT) await control.command('waitFor', ACTIVE_SWITCH_MODEL_RETRY_SELECTOR, { visible: true, @@ -7278,7 +7318,7 @@ async function verifyAnthropicEmptyResponseRecovery({ composerSelector, control await control.command('waitFor', composerSelector, { timeoutMs: WORKBENCH_READY_TIMEOUT_MS, }) - await selectE2EModel(control, anthropicModel.optionId, anthropicModel.label) + await selectE2EModel(control, anthropicModel.optionIds, anthropicModel.labels) await sendPromptUntilScenarioRequest( control, composerSelector, @@ -8415,7 +8455,7 @@ class RealCloudEnvironment { async seedCloudProtocolModels() { const items = CLOUD_MODEL_CASES.map(model => ({ - name: model.optionId, + name: model.optionIds[0], env: { model: model.protocol === 'anthropic' ? 'claude' : 'openai', model_id: model.modelId, @@ -13167,7 +13207,7 @@ async function verifyModelProtocolMatrix({ stableMs: COMPOSER_READY_STABILITY_MS, timeoutMs: WORKBENCH_READY_TIMEOUT_MS, }) - await selectE2EModel(control, model.optionId, model.label) + await selectE2EModel(control, model.optionIds, model.labels) const confirmCloudModelCatalogSync = !hasConfirmedCatalogSync && model.execution === 'cloud' && model.source === 'local' @@ -14662,7 +14702,7 @@ last_updated = "2026-07-30T00:00:00Z"` await control.command('waitFor', composerSelector, { timeoutMs: WORKBENCH_READY_TIMEOUT_MS, }) - await selectE2EModel(control, sourceModel.optionId, sourceModel.label) + await selectE2EModel(control, sourceModel.optionIds, sourceModel.labels) await sendPrompt(control, composerSelector, LOCAL_MODEL_SWITCH_INITIAL_PROMPT) await control.command('waitFor', '[data-testid="message-assistant"]', { text: LOCAL_MODEL_SWITCH_INITIAL_COMPLETE, @@ -14698,7 +14738,7 @@ last_updated = "2026-07-30T00:00:00Z"` '[data-testid="model-selector-menu"]' ) } - await selectE2EModel(control, targetModel.optionId, targetModel.label) + await selectE2EModel(control, targetModel.optionIds, targetModel.labels) if (switchIndex === 0) { await captureVerificationScreenshot( control, @@ -14726,10 +14766,7 @@ last_updated = "2026-07-30T00:00:00Z"` 'model_switch_target_complete', `${switchCase.id} did not complete the automatic same-conversation retry` ) - await control.command('waitFor', '[data-testid="model-selector-button"]', { - text: targetModel.label, - timeoutMs: DEFAULT_STEP_TIMEOUT_MS, - }) + await waitForE2EModelLabel(control, targetModel.labels) await waitForSnapshot( control, snapshot => !/下一轮|Next/.test(snapshot.text), diff --git a/wework/src/api/local/localServices.test.ts b/wework/src/api/local/localServices.test.ts index 5611fb53c0..2694e30d2d 100644 --- a/wework/src/api/local/localServices.test.ts +++ b/wework/src/api/local/localServices.test.ts @@ -1105,7 +1105,7 @@ describe('createLocalAppServices', () => { }) test('uses local model settings for create and continue execution requests', async () => { - saveLocalModelConfig({ + const ollama = saveLocalModelConfig({ id: 'ollama', displayName: 'Ollama GPT', modelId: 'gpt-oss:20b', @@ -1113,7 +1113,7 @@ describe('createLocalAppServices', () => { contextWindow: 128000, catalogReady: true, }) - saveLocalModelConfig({ + const lmstudio = saveLocalModelConfig({ id: 'lmstudio', displayName: 'LM Studio', modelId: 'qwen3-coder', @@ -1123,7 +1123,7 @@ describe('createLocalAppServices', () => { imageGenerationEnabled: true, catalogReady: true, }) - saveLocalModelConfig({ + const custom = saveLocalModelConfig({ id: 'custom', displayName: 'Custom Gateway', modelId: 'custom-model', @@ -1146,7 +1146,7 @@ describe('createLocalAppServices', () => { runtime: 'codex', message: 'hello', title: 'Hello', - modelId: 'local-model:ollama', + modelId: ollama.codexCatalogModelId, }) await services.runtimeWorkApi?.sendRuntimeMessage({ address: { @@ -1155,7 +1155,7 @@ describe('createLocalAppServices', () => { taskId: 'task-1', }, message: 'continue', - modelId: 'local-model:ollama', + modelId: ollama.codexCatalogModelId, }) await services.runtimeWorkApi?.sendRuntimeMessage({ address: { @@ -1164,7 +1164,7 @@ describe('createLocalAppServices', () => { taskId: 'task-1', }, message: 'secure continue', - modelId: 'local-model:lmstudio', + modelId: lmstudio.codexCatalogModelId, }) await services.runtimeWorkApi?.sendRuntimeMessage({ address: { @@ -1173,7 +1173,7 @@ describe('createLocalAppServices', () => { taskId: 'task-1', }, message: 'custom continue', - modelId: 'local-model:custom', + modelId: custom.codexCatalogModelId, }) const createPayload = request.mock.calls.find( @@ -1189,9 +1189,9 @@ describe('createLocalAppServices', () => { expect(continueModelConfig).toEqual(createModelConfig) expect(sendPayloads.map(payload => payload.modelSelection)).toEqual([ - { modelName: 'local-model:ollama', modelType: null, options: {} }, - { modelName: 'local-model:lmstudio', modelType: null, options: {} }, - { modelName: 'local-model:custom', modelType: null, options: {} }, + { modelName: ollama.codexCatalogModelId, modelType: null, options: {} }, + { modelName: lmstudio.codexCatalogModelId, modelType: null, options: {} }, + { modelName: custom.codexCatalogModelId, modelType: null, options: {} }, ]) expect(createModelConfig).toEqual( expect.objectContaining({ diff --git a/wework/src/components/layout/useWorkbenchPaneSession.ts b/wework/src/components/layout/useWorkbenchPaneSession.ts index 0736be9b52..3f2aab1b4b 100644 --- a/wework/src/components/layout/useWorkbenchPaneSession.ts +++ b/wework/src/components/layout/useWorkbenchPaneSession.ts @@ -469,6 +469,7 @@ export function useWorkbenchPaneSession({ currentRuntimeTask }: WorkbenchPaneSes .then(response => { if (!cancelled) { const loadedGoal = response.accepted ? response.goal : null + const resolvedGoal = loadedGoal ?? seededGoal?.goal ?? null if (import.meta.env.VITE_WEWORK_RUNTIME_DEBUG === '1') { console.info('[Wework] Runtime goal hydration resolved', { address: runtimeAddressDebug(runtimeTaskLoadTarget.address), @@ -476,10 +477,10 @@ export function useWorkbenchPaneSession({ currentRuntimeTask }: WorkbenchPaneSes goalStatus: loadedGoal?.status ?? null, }) } - setRuntimeConversationGoal(runtimeTaskLoadTarget.address, loadedGoal) + setRuntimeConversationGoal(runtimeTaskLoadTarget.address, resolvedGoal) lifecycleStore.goalStatusReceived( runtimeTaskLoadTarget.address, - loadedGoal?.status ?? seededGoal?.goal.status ?? null + resolvedGoal?.status ?? null ) if (loadedGoal?.status === 'active') { void refreshWorkListsRef.current().catch(() => undefined) diff --git a/wework/src/components/settings/GeneralSettingsPage.test.tsx b/wework/src/components/settings/GeneralSettingsPage.test.tsx index e85d25353b..34ac31aaf5 100644 --- a/wework/src/components/settings/GeneralSettingsPage.test.tsx +++ b/wework/src/components/settings/GeneralSettingsPage.test.tsx @@ -2,6 +2,8 @@ import { fireEvent, render, screen, waitFor, within } from '@testing-library/rea import userEvent from '@testing-library/user-event' import { beforeEach, describe, expect, test, vi } from 'vitest' import type { AppPreferences } from '@/tauri/appPreferences' +import { WorkbenchContext } from '@/features/workbench/useWorkbench' +import type { WorkbenchContextValue } from '@/features/workbench/workbenchContextTypes' import { GeneralSettingsPage } from './GeneralSettingsPage' const defaultPreferences: AppPreferences = { @@ -28,13 +30,22 @@ const defaultPreferences: AppPreferences = { appshotsPlaySound: true, popoutWindowShortcut: 'Alt+Shift+Space', popoutWindowProjectlessDefaultEnabled: false, + friendlyTaskTitlesEnabled: false, + friendlyTaskTitleModel: null, quickPhrases: [], } const getAppPreferencesMock = vi.hoisted(() => vi.fn()) const updateAppPreferencesMock = vi.hoisted(() => vi.fn()) const applyLanguagePreferenceMock = vi.hoisted(() => vi.fn()) -const translateMock = vi.hoisted(() => (key: string, fallback?: string) => fallback ?? key) +const translateMock = vi.hoisted( + () => (key: string, fallback?: string, options?: { modelName?: string }) => { + if (key === 'workbench.friendly_task_titles_model_unavailable') { + return `${options?.modelName ?? ''}(不可用)` + } + return fallback ?? key + } +) const importExternalContentMock = vi.hoisted(() => vi.fn()) const getWegentUsageDisplayMock = vi.hoisted(() => vi.fn()) @@ -68,6 +79,8 @@ vi.mock('@/tauri/appPreferences', () => ({ appshotsPlaySound: true, popoutWindowShortcut: 'Alt+Shift+Space', popoutWindowProjectlessDefaultEnabled: false, + friendlyTaskTitlesEnabled: false, + friendlyTaskTitleModel: null, quickPhrases: [], }, getAppPreferences: getAppPreferencesMock, @@ -185,6 +198,90 @@ describe('GeneralSettingsPage', () => { }) }) + test('keeps friendly titles off by default and uses the task model when enabled', async () => { + const taskModel = { + name: 'gpt-5', + type: 'runtime', + displayName: 'GPT-5', + } + const workbench = { + projectChat: { + models: [taskModel], + selectedModel: taskModel, + }, + } as unknown as WorkbenchContextValue + + render( + + + + ) + + const toggle = await screen.findByTestId('friendly-task-titles-toggle') + await waitFor(() => expect(toggle).toBeEnabled()) + expect(toggle).toHaveAttribute('aria-checked', 'false') + + await userEvent.click(toggle) + + await waitFor(() => { + expect(updateAppPreferencesMock).toHaveBeenCalledWith({ + friendlyTaskTitlesEnabled: true, + friendlyTaskTitleModel: null, + }) + }) + + const modelSelect = screen.getByTestId('friendly-task-title-model-select') + expect( + (within(modelSelect).getByRole('option', { name: '与任务相同' }) as HTMLOptionElement) + .selected + ).toBe(true) + expect( + within(modelSelect).queryByRole('option', { name: '请选择模型' }) + ).not.toBeInTheDocument() + }) + + test('shows an unavailable saved friendly title model for replacement', async () => { + const taskModel = { + name: 'gpt-5', + type: 'runtime', + displayName: 'GPT-5', + } + getAppPreferencesMock.mockResolvedValue({ + ...defaultPreferences, + friendlyTaskTitlesEnabled: true, + friendlyTaskTitleModel: { + modelName: 'removed-model', + modelType: 'runtime', + executionModelId: 'removed-model', + executionModelType: 'runtime', + }, + }) + + render( + + + + ) + + const modelSelect = await screen.findByTestId('friendly-task-title-model-select') + expect( + ( + within(modelSelect).getByRole('option', { + name: 'removed-model(不可用)', + }) as HTMLOptionElement + ).selected + ).toBe(true) + }) + test('enables the system drag panel by default and persists disabling it', async () => { render() diff --git a/wework/src/components/settings/GeneralSettingsPage.tsx b/wework/src/components/settings/GeneralSettingsPage.tsx index 60067b9db8..0cb5e07a87 100644 --- a/wework/src/components/settings/GeneralSettingsPage.tsx +++ b/wework/src/components/settings/GeneralSettingsPage.tsx @@ -34,7 +34,6 @@ import { getWegentUsageDisplay } from '@/api/wegentUsage' import { useOptionalCloudConnection } from '@/features/cloud-connection/useCloudConnection' import { WorkbenchContext } from '@/features/workbench/useWorkbench' import { selectedModelExecutionFields } from '@/features/workbench/runtimeModelSelection' -import type { UnifiedModel } from '@/types/api' type BooleanPreferenceKey = { [Key in keyof AppPreferencesPatch]-?: AppPreferencesPatch[Key] extends boolean | undefined @@ -51,6 +50,7 @@ interface SwitchRowProps { const GENERAL_ROW_CLASS_NAME = 'py-4' const GENERAL_ROW_LABEL_CLASS_NAME = 'font-normal' +const FRIENDLY_TITLE_TASK_MODEL_VALUE = 'task-model' interface TrayDisplayOption { preferenceKey: BooleanPreferenceKey @@ -264,24 +264,28 @@ export function GeneralSettingsPage() { const saveFriendlyTaskTitles = async ( enabled: boolean, - modelName = preferences.friendlyTaskTitleModel?.modelName ?? '', - modelType = preferences.friendlyTaskTitleModel?.modelType ?? null + modelKey = FRIENDLY_TITLE_TASK_MODEL_VALUE ) => { + const useTaskModel = modelKey === FRIENDLY_TITLE_TASK_MODEL_VALUE + const [modelType, ...nameParts] = modelKey.split(':') + const modelName = nameParts.join(':') const selectedModel = workbench?.projectChat.models.find( model => `${model.type}:${model.name}` === `${modelType ?? ''}:${modelName}` ) const execution = selectedModel ? selectedModelExecutionFields(selectedModel, {}) : null const patch: AppPreferencesPatch = { - friendlyTaskTitlesEnabled: enabled && Boolean(selectedModel), - friendlyTaskTitleModel: selectedModel - ? { - modelName: selectedModel.name, - modelType: selectedModel.type, - executionModelId: execution?.modelId ?? '', - executionModelType: execution?.modelType ?? null, - options: execution?.modelOptions, - } - : null, + friendlyTaskTitlesEnabled: enabled && (useTaskModel || Boolean(selectedModel)), + friendlyTaskTitleModel: useTaskModel + ? null + : selectedModel + ? { + modelName: selectedModel.name, + modelType: selectedModel.type, + executionModelId: execution?.modelId ?? '', + executionModelType: execution?.modelType ?? null, + options: execution?.modelOptions, + } + : null, } const previousPreferences = preferences setSaving(true) @@ -299,8 +303,10 @@ export function GeneralSettingsPage() { } const friendlyTitleModel = preferences.friendlyTaskTitleModel - const friendlyTitleModelKey = `${friendlyTitleModel?.modelType ?? ''}:${friendlyTitleModel?.modelName ?? ''}` const friendlyTitleModels = workbench?.projectChat.models ?? [] + const friendlyTitleModelKey = friendlyTitleModel + ? `${friendlyTitleModel.modelType ?? ''}:${friendlyTitleModel.modelName}` + : FRIENDLY_TITLE_TASK_MODEL_VALUE return ( @@ -422,15 +428,7 @@ export function GeneralSettingsPage() { (!friendlyTitleModel?.modelName && !workbench?.projectChat.selectedModel) } onCheckedChange={checked => { - void saveFriendlyTaskTitles( - checked, - friendlyTitleModel?.modelName ?? - workbench?.projectChat.selectedModel?.name ?? - '', - friendlyTitleModel?.modelType ?? - workbench?.projectChat.selectedModel?.type ?? - null - ) + void saveFriendlyTaskTitles(checked, friendlyTitleModelKey) }} aria-label={t('workbench.friendly_task_titles_title', '使用友好标题')} /> @@ -455,19 +453,30 @@ export function GeneralSettingsPage() { value={friendlyTitleModelKey} disabled={loading || saving} onChange={event => { - const [modelType, ...nameParts] = event.target.value.split(':') - const modelName = nameParts.join(':') void saveFriendlyTaskTitles( - preferences.friendlyTaskTitlesEnabled && Boolean(modelName), - modelName, - modelName ? (modelType as UnifiedModel['type']) : null + preferences.friendlyTaskTitlesEnabled, + event.target.value ) }} className="h-8 w-full rounded-md border border-border bg-background px-2 text-sm text-text-primary md:w-[220px]" > - + {friendlyTitleModel && + !friendlyTitleModels.some( + model => + model.name === friendlyTitleModel.modelName && + model.type === friendlyTitleModel.modelType + ) && ( + + )} {friendlyTitleModels.map(model => (