From 689dc70e7bba58f40efabeb999619da9b39b05b6 Mon Sep 17 00:00:00 2001 From: GCWing Date: Wed, 9 Sep 2026 12:44:49 +0800 Subject: [PATCH] feat(web): refine session defaults and chat affordances --- .../primitives/OverflowText/OverflowText.tsx | 13 +- .../packages/ui/tests/overflow-text.test.mjs | 1 + docs/interactive-capabilities/README.md | 12 +- .../capabilities.json | 94 ++++++- .../capabilities/setting.workspace.session.md | 14 +- .../technical/product-control-open-audit.json | 23 +- .../technical/tauri-command-map.json | 2 +- src/apps/desktop/src/api/custom_agent_api.rs | 4 +- .../service/remote_connect/settings_sync.rs | 36 +++ .../contracts/config-contracts/src/types.rs | 123 ++++++++- .../generated/product-control-catalog.json | 94 ++++++- .../interactive-capabilities/catalog.json | 33 ++- .../src/app/components/NavPanel/MainNav.tsx | 21 +- .../src/app/components/NavPanel/NavPanel.scss | 38 --- .../sections/workspaces/WorkspaceItem.tsx | 30 ++- .../workspaces/WorkspaceListSection.scss | 4 +- .../WorkspaceListSectionLayout.test.ts | 8 +- .../NavPanel/unifiedSessionCreation.test.ts | 30 ++- .../generated/interactive-capabilities.json | 94 ++++++- src/web-ui/src/app/layout/AppLayout.tsx | 7 +- .../src/flow_chat/components/ChatInput.tsx | 66 ++--- .../modern/FLOWCHAT_VIRTUALIZATION.md | 8 +- .../modern/ModernFlowChatContainer.tsx | 1 - .../components/modern/UserMessageItem.scss | 30 +-- .../modern/UserMessageItem.test.tsx | 2 +- .../components/modern/UserMessageItem.tsx | 1 + .../modern/UserMessageItemActions.test.ts | 22 +- .../components/voice/voiceTaskBridge.ts | 3 +- .../hooks/useSessionModeSelection.test.tsx | 6 + .../hooks/useSessionModeSelection.ts | 5 + .../ChatInputModePreferenceService.test.ts | 127 +++++++++ .../ChatInputModePreferenceService.ts | 160 +++++++++++ .../flow-chat-manager/SessionModule.test.ts | 44 +++- .../flow-chat-manager/SessionModule.ts | 27 +- .../src/flow_chat/utils/chatInputMode.test.ts | 2 + .../src/flow_chat/utils/chatInputMode.ts | 25 +- .../api/generated/productControl.ts | 2 +- .../components/DefaultHarnessConfig.test.tsx | 223 ++++++++++++++++ .../components/DefaultHarnessConfig.tsx | 248 ++++++++++++++++++ .../components/RuntimeSettingsPages.test.tsx | 2 + .../components/RuntimeSettingsPages.tsx | 3 + .../src/infrastructure/config/types/index.ts | 4 + src/web-ui/src/locales/en-US/flow-chat.json | 1 + src/web-ui/src/locales/en-US/settings.json | 2 +- .../src/locales/en-US/settings/runtime.json | 14 + src/web-ui/src/locales/zh-CN/common.json | 2 +- src/web-ui/src/locales/zh-CN/flow-chat.json | 1 + src/web-ui/src/locales/zh-CN/settings.json | 2 +- .../src/locales/zh-CN/settings/runtime.json | 14 + src/web-ui/src/locales/zh-TW/flow-chat.json | 1 + src/web-ui/src/locales/zh-TW/settings.json | 2 +- .../src/locales/zh-TW/settings/runtime.json | 14 + .../src/shared/ui/OverflowText.test.tsx | 8 + 53 files changed, 1529 insertions(+), 224 deletions(-) create mode 100644 src/web-ui/src/flow_chat/services/ChatInputModePreferenceService.test.ts create mode 100644 src/web-ui/src/flow_chat/services/ChatInputModePreferenceService.ts create mode 100644 src/web-ui/src/infrastructure/config/components/DefaultHarnessConfig.test.tsx create mode 100644 src/web-ui/src/infrastructure/config/components/DefaultHarnessConfig.tsx diff --git a/design-system/packages/ui/src/primitives/OverflowText/OverflowText.tsx b/design-system/packages/ui/src/primitives/OverflowText/OverflowText.tsx index 6e7e9c37f3..5240ac7472 100644 --- a/design-system/packages/ui/src/primitives/OverflowText/OverflowText.tsx +++ b/design-system/packages/ui/src/primitives/OverflowText/OverflowText.tsx @@ -86,15 +86,22 @@ export const OverflowText = forwardRef( if (!element || !content) return; const distance = Math.max(0, content.scrollWidth - element.clientWidth); - const isOverflowing = element.clientWidth > 0 && (distance > 0 - || (element.clientHeight > 0 && element.scrollHeight > element.clientHeight)); + // A compact single-line label can have glyph ink extend slightly beyond + // its line box without any text being clipped. Only multiline clamps use + // vertical overflow as a truncation signal; single-line slots are clipped + // exclusively on the inline axis. + const hasVerticalClampOverflow = lines !== undefined + && element.clientHeight > 0 + && element.scrollHeight > element.clientHeight; + const isOverflowing = element.clientWidth > 0 + && (distance > 0 || hasVerticalClampOverflow); const current = measurementRef.current; if (current.distance === distance && current.isOverflowing === isOverflowing) return; const next = { distance, isOverflowing }; measurementRef.current = next; setMeasurement(next); - }, []); + }, [lines]); const prepareTooltip = useCallback(() => { const element = elementRef.current; diff --git a/design-system/packages/ui/tests/overflow-text.test.mjs b/design-system/packages/ui/tests/overflow-text.test.mjs index 116da0a9fd..66e15acbe7 100644 --- a/design-system/packages/ui/tests/overflow-text.test.mjs +++ b/design-system/packages/ui/tests/overflow-text.test.mjs @@ -60,6 +60,7 @@ test("OverflowText measures real clipping for fade and marquee treatments", asyn ]); assert.match(source, /content\.scrollWidth - element\.clientWidth/); + assert.match(source, /lines !== undefined\s*&& element\.clientHeight > 0\s*&& element\.scrollHeight > element\.clientHeight/s); assert.match(source, /measurementRef\.current/); assert.match( source, diff --git a/docs/interactive-capabilities/README.md b/docs/interactive-capabilities/README.md index 09eac55a30..a54bf7855e 100644 --- a/docs/interactive-capabilities/README.md +++ b/docs/interactive-capabilities/README.md @@ -1,9 +1,9 @@ # OpenBitFun 功能与设置目录 / OpenBitFun Features & Settings -OpenBitFun Playbook 当前包含 **22 个功能**和 **21 个设置页**,共 **43 个**用户可理解的条目、**321 项**有源码证据的子能力。每个条目有独立 Markdown,并直接服务于说明书网站、OpenBitFun 全局搜索和 `OpenBitFunControl` Agent 工具。 +OpenBitFun Playbook 当前包含 **22 个功能**和 **21 个设置页**,共 **43 个**用户可理解的条目、**322 项**有源码证据的子能力。每个条目有独立 Markdown,并直接服务于说明书网站、OpenBitFun 全局搜索和 `OpenBitFunControl` Agent 工具。 -OpenBitFun Playbook currently contains **22 features**, **21 settings pages**, and **321** source-backed sub-capabilities across **43** user-facing entries. Every entry has its own Markdown page and directly powers the website, in-app global search, and the `OpenBitFunControl` agent tool. +OpenBitFun Playbook currently contains **22 features**, **21 settings pages**, and **322** source-backed sub-capabilities across **43** user-facing entries. Every entry has its own Markdown page and directly powers the website, in-app global search, and the `OpenBitFunControl` agent tool. ## 唯一事实源 / Single source of truth @@ -33,14 +33,14 @@ Docs, website, search, and agents see only features, settings, and documented su ## 控制边界 / Control boundary -- 每个子能力都明确标记为直接控制、委托给专用 Agent 工具、需交互打开或不支持;“打开页面”不会再被统计成“Agent 已控制”。当前覆盖:直接 **48**、委托 **61**、需交互 **212**、不支持 **0**。 +- 每个子能力都明确标记为直接控制、委托给专用 Agent 工具、需交互打开或不支持;“打开页面”不会再被统计成“Agent 已控制”。当前覆盖:直接 **48**、委托 **61**、需交互 **213**、不支持 **0**。 - 稳定行为声明为带 JSON 输入契约的 `operations` 或 `options`,并绑定原生产品控制 Provider;Agent 不接触原始 Tauri Command。 -- `OpenBitFunControl list` 和 `search` 都返回带 `nextCursor` 的精简分页结果;目录可持续增长,不靠固定总量上限。完整目录和 321 项子能力都不会写入 system prompt。 +- `OpenBitFunControl list` 和 `search` 都返回带 `nextCursor` 的精简分页结果;目录可持续增长,不靠固定总量上限。完整目录和 322 项子能力都不会写入 system prompt。 - 目录发现与契约读取不依赖 React 或可见窗口。普通配置型 option 统一由 Product Assembly 的共享 ConfigService 执行器读、写并回读,因此 Desktop、CLI 与 Headless 表面走同一份实现;只有宿主原生 operation/provider option 和界面导航按表面注册适配器,缺失时必须明确返回不可用,禁止静默回退本机。只读 Agent 只能发现和读取目录。 -- Every documented item is classified as direct control, delegated Agent control, interactive opening, or unsupported; opening a page is never counted as direct control. Current coverage is **48 direct**, **61 delegated**, **212 interactive**, and **0 unsupported**. +- Every documented item is classified as direct control, delegated Agent control, interactive opening, or unsupported; opening a page is never counted as direct control. Current coverage is **48 direct**, **61 delegated**, **213 interactive**, and **0 unsupported**. - Stable behavior becomes a typed `operation` or `option` with a JSON input contract and a native product-control provider. Agents never receive raw Tauri commands. -- `OpenBitFunControl list` and `search` return compact pages with a `nextCursor`; the catalog can grow without a fixed total-size ceiling. Neither the full catalog nor its 321 documented items enters the system prompt. +- `OpenBitFunControl list` and `search` return compact pages with a `nextCursor`; the catalog can grow without a fixed total-size ceiling. Neither the full catalog nor its 322 documented items enters the system prompt. - Discovery and contract lookup do not depend on React or a visible window. Ordinary config-backed options are read, written, and read back by one Product Assembly ConfigService executor shared by Desktop, CLI, and headless surfaces. Only host-native operations/provider options and presentation routes install surface adapters; missing adapters return explicit unavailability without local fallback. Read-only agents may only discover and inspect entries. ## 防腐化门禁 / Anti-drift gates diff --git a/docs/interactive-capabilities/capabilities.json b/docs/interactive-capabilities/capabilities.json index a8c4149de3..267f045143 100644 --- a/docs/interactive-capabilities/capabilities.json +++ b/docs/interactive-capabilities/capabilities.json @@ -4,7 +4,7 @@ "title": "OpenBitFun Playbook", "origin": "https://playbook.openbitfun.com", "source": "src/shared/interactive-capabilities/catalog.json", - "digest": "d7a7419ddd673eb733ae8bd33dbb3dcd3b4d1ce0acfc953067a3403fe26d6699", + "digest": "409e6dbef7ebceafccc11606e2855227b9a33fa1ed71f5b4608da04e8a23de7d", "ownerDigest": "c0e5c187cf62bc6ed06196ce8520b3eb427bf268cf24659b72d2552fb1d99c54", "searchAcceptance": [ { @@ -138,11 +138,11 @@ "features": 22, "settings": 21, "userFacing": 43, - "documentedItems": 321, + "documentedItems": 322, "controlCoverage": { "direct": 48, "delegated": 61, - "interactive": 212, + "interactive": 213, "unsupported": 0 } }, @@ -14816,6 +14816,7 @@ "id": "setting.workspace.session:query", "capabilityId": "setting.workspace.session", "itemIds": [ + "default-agent-harness", "automatic-titles", "title-model", "accelerated-search", @@ -14949,6 +14950,52 @@ "pageId": "workspace.session" } }, + { + "id": "setting.workspace.session:open:default-agent-harness", + "capabilityId": "setting.workspace.session", + "itemIds": [ + "default-agent-harness" + ], + "kind": "open", + "risk": "ui", + "executionHost": "presentationSurface", + "availability": { + "desktop": { + "available": true + }, + "cli": { + "available": false, + "reason": "This delivery profile has no live presentation surface" + }, + "peer": { + "available": true, + "requiredCapabilities": [ + "product_control_v1", + "product_control_presentation_v1" + ] + }, + "remoteControl": { + "available": true + }, + "detachedDispatch": { + "available": false, + "reason": "This delivery profile has no live presentation surface" + } + }, + "inputSchema": { + "type": "object", + "additionalProperties": false + }, + "outputSchema": { + "type": "object", + "additionalProperties": true + }, + "openReason": "unstructuredInteraction", + "presentationTarget": { + "kind": "settings", + "pageId": "workspace.session" + } + }, { "id": "setting.workspace.session:open:title-model", "capabilityId": "setting.workspace.session", @@ -27132,11 +27179,13 @@ "categoryId": "assistant", "titleZh": "会话与工作区设置", "titleEn": "Session & workspace settings", - "summaryZh": "控制会话标题生成、工作区加速搜索和与项目上下文相关的行为。", - "summaryEn": "Control session-title generation, accelerated workspace search, and project-context behavior.", + "summaryZh": "控制默认 Agent Harness、会话标题生成、工作区加速搜索和与项目上下文相关的行为。", + "summaryEn": "Control the default Agent Harness, session-title generation, accelerated workspace search, and project-context behavior.", "keywordsZh": [ "会话设置", "工作区设置", + "默认 Harness", + "上次选择", "标题生成", "项目搜索", "索引" @@ -27144,21 +27193,36 @@ "keywordsEn": [ "session settings", "workspace settings", + "default Harness", + "last selection", "title generation", "project search", "index" ], "highlightsZh": [ + "跟随上次选择或固定默认 Harness", "自动生成会话标题", "本地工作区加速搜索", "会话使用的标题模型" ], "highlightsEn": [ + "Follow the last selection or fix a default Harness", "Automatic session titles", "Accelerated local-workspace search", "Model used for session titles" ], "items": [ + { + "id": "default-agent-harness", + "titleZh": "让普通新会话跟随 ChatInput 上次选择,或固定使用指定 Harness", + "titleEn": "Make regular new sessions follow the last ChatInput selection or use a fixed Harness", + "control": { + "kind": "open", + "reasonCode": "unstructuredInteraction", + "reasonZh": "“让普通新会话跟随 ChatInput 上次选择,或固定使用指定 Harness”由会话设置页中的关联控件共同管理,目前没有一个能确定完成整个流程的单一结构化 Command;Agent 会打开精确入口,并把选择保留在用户可见界面。", + "reasonEn": "“Make regular new sessions follow the last ChatInput selection or use a fixed Harness” is managed by related controls on the session settings page and currently has no single structured Command that can deterministically complete the whole workflow; the Agent opens the exact entry and keeps the selection visible to the user." + } + }, { "id": "automatic-titles", "titleZh": "启用或停用根据会话内容自动生成标题", @@ -27224,18 +27288,22 @@ "stepsZh": [ "打开设置", "进入“工作区 > 会话与工作区”", - "调整标题或搜索选项" + "调整默认 Harness、标题或搜索选项" ], "stepsEn": [ "Open Settings", "Go to Workspace > Session & Workspace", - "Tune title or search controls" + "Tune the default Harness, title, or search controls" ], "agentExamplesZh": [ + "让新会话跟随 ChatInput 上一次选择的 Harness", + "把新会话固定为 Ultimate Harness", "关闭自动生成会话标题", "启用工作区加速搜索" ], "agentExamplesEn": [ + "Make new sessions follow the last Harness selected in ChatInput", + "Fix Ultimate as the Harness for new sessions", "Turn off automatic session titles", "Enable accelerated workspace search" ], @@ -27274,20 +27342,28 @@ "AI & collaboration", "会话设置", "工作区设置", + "默认 Harness", + "上次选择", "标题生成", "项目搜索", "索引", "session settings", "workspace settings", + "default Harness", + "last selection", "title generation", "project search", "index", + "跟随上次选择或固定默认 Harness", "自动生成会话标题", "本地工作区加速搜索", "会话使用的标题模型", + "Follow the last selection or fix a default Harness", "Automatic session titles", "Accelerated local-workspace search", "Model used for session titles", + "让普通新会话跟随 ChatInput 上次选择,或固定使用指定 Harness", + "Make regular new sessions follow the last ChatInput selection or use a fixed Harness", "启用或停用根据会话内容自动生成标题", "Enable or disable automatic titles generated from session content", "让标题模型继承主模型、快速模型或使用指定模型", @@ -27298,8 +27374,12 @@ "Inspect index status and rebuild the workspace index when needed", "远程工作区明确降级搜索实现,不把远程路径回退到本机", "Use an explicit remote-workspace search path without silently falling back to local files", + "让新会话跟随 ChatInput 上一次选择的 Harness", + "把新会话固定为 Ultimate Harness", "关闭自动生成会话标题", "启用工作区加速搜索", + "Make new sessions follow the last Harness selected in ChatInput", + "Fix Ultimate as the Harness for new sessions", "Turn off automatic session titles", "Enable accelerated workspace search" ], diff --git a/docs/interactive-capabilities/capabilities/setting.workspace.session.md b/docs/interactive-capabilities/capabilities/setting.workspace.session.md index a8a3710c63..4039dc2ede 100644 --- a/docs/interactive-capabilities/capabilities/setting.workspace.session.md +++ b/docs/interactive-capabilities/capabilities/setting.workspace.session.md @@ -11,12 +11,14 @@ title_en: "Session & workspace settings" > 设置 / Setting -控制会话标题生成、工作区加速搜索和与项目上下文相关的行为。 +控制默认 Agent Harness、会话标题生成、工作区加速搜索和与项目上下文相关的行为。 -Control session-title generation, accelerated workspace search, and project-context behavior. +Control the default Agent Harness, session-title generation, accelerated workspace search, and project-context behavior. ## 完整功能清单 / Everything included +- **Agent 可定位入口,需交互完成 / Agent opens; interaction required** · 让普通新会话跟随 ChatInput 上次选择,或固定使用指定 Harness + - Make regular new sessions follow the last ChatInput selection or use a fixed Harness - **Agent 可直接控制 / Direct Agent control** · 启用或停用根据会话内容自动生成标题 - Enable or disable automatic titles generated from session content - **Agent 可定位入口,需交互完成 / Agent opens; interaction required** · 让标题模型继承主模型、快速模型或使用指定模型 @@ -34,8 +36,8 @@ Control session-title generation, accelerated workspace search, and project-cont Open Settings 2. 进入“工作区 > 会话与工作区” Go to Workspace > Session & Workspace -3. 调整标题或搜索选项 - Tune title or search controls +3. 调整默认 Harness、标题或搜索选项 + Tune the default Harness, title, or search controls 入口 / Entry: OpenBitFun 设置 @@ -54,6 +56,10 @@ Control session-title generation, accelerated workspace search, and project-cont ## 可以直接对 Agent 说 / Try saying +- “让新会话跟随 ChatInput 上一次选择的 Harness” + - “Make new sessions follow the last Harness selected in ChatInput” +- “把新会话固定为 Ultimate Harness” + - “Fix Ultimate as the Harness for new sessions” - “关闭自动生成会话标题” - “Turn off automatic session titles” - “启用工作区加速搜索” diff --git a/docs/interactive-capabilities/technical/product-control-open-audit.json b/docs/interactive-capabilities/technical/product-control-open-audit.json index b2f434a198..cf6eecf716 100644 --- a/docs/interactive-capabilities/technical/product-control-open-audit.json +++ b/docs/interactive-capabilities/technical/product-control-open-audit.json @@ -1,12 +1,12 @@ { "schemaVersion": 1, "generatedFrom": "src/shared/interactive-capabilities/catalog.json", - "catalogDigest": "d7a7419ddd673eb733ae8bd33dbb3dcd3b4d1ce0acfc953067a3403fe26d6699", - "count": 212, + "catalogDigest": "409e6dbef7ebceafccc11606e2855227b9a33fa1ed71f5b4608da04e8a23de7d", + "count": 213, "reasonCounts": { "externalAuth": 4, "secretEntry": 5, - "unstructuredInteraction": 185, + "unstructuredInteraction": 186, "visualSelection": 18 }, "entries": [ @@ -2891,6 +2891,23 @@ "command:reset_config" ] }, + { + "capabilityId": "setting.workspace.session", + "itemId": "default-agent-harness", + "titleZh": "让普通新会话跟随 ChatInput 上次选择,或固定使用指定 Harness", + "titleEn": "Make regular new sessions follow the last ChatInput selection or use a fixed Harness", + "reasonCode": "unstructuredInteraction", + "reasonZh": "“让普通新会话跟随 ChatInput 上次选择,或固定使用指定 Harness”由会话设置页中的关联控件共同管理,目前没有一个能确定完成整个流程的单一结构化 Command;Agent 会打开精确入口,并把选择保留在用户可见界面。", + "reasonEn": "“Make regular new sessions follow the last ChatInput selection or use a fixed Harness” is managed by related controls on the session settings page and currently has no single structured Command that can deterministically complete the whole workflow; the Agent opens the exact entry and keeps the selection visible to the user.", + "presentationTarget": { + "kind": "settings", + "pageId": "workspace.session" + }, + "evidence": [ + "source:src/web-ui/src/infrastructure/config/components/DefaultHarnessConfig.tsx#DefaultHarnessConfig", + "source:src/web-ui/src/locales/zh-CN/settings/runtime.json#defaultHarness.title" + ] + }, { "capabilityId": "setting.workspace.session", "itemId": "title-model", diff --git a/docs/interactive-capabilities/technical/tauri-command-map.json b/docs/interactive-capabilities/technical/tauri-command-map.json index ba54fcf874..437a242365 100644 --- a/docs/interactive-capabilities/technical/tauri-command-map.json +++ b/docs/interactive-capabilities/technical/tauri-command-map.json @@ -1,7 +1,7 @@ { "schemaVersion": 2, "generatedFrom": "src/shared/interactive-capabilities/catalog.json", - "catalogDigest": "d7a7419ddd673eb733ae8bd33dbb3dcd3b4d1ce0acfc953067a3403fe26d6699", + "catalogDigest": "409e6dbef7ebceafccc11606e2855227b9a33fa1ed71f5b4608da04e8a23de7d", "commandCount": 666, "coverage": { "commandCount": 666, diff --git a/src/apps/desktop/src/api/custom_agent_api.rs b/src/apps/desktop/src/api/custom_agent_api.rs index 5bf4de3db5..21b72fee28 100644 --- a/src/apps/desktop/src/api/custom_agent_api.rs +++ b/src/apps/desktop/src/api/custom_agent_api.rs @@ -399,9 +399,7 @@ pub async fn delete_custom_agent( "", |config: &mut openbitfun_core::service::config::GlobalConfig| { config.ai.agent_profiles.remove(&agent_id); - if config.app.flow_chat.default_mode_id.as_deref() == Some(agent_id.as_str()) { - config.app.flow_chat.default_mode_id = None; - } + config.app.flow_chat.remove_mode_reference(&agent_id); Ok(()) }, ) diff --git a/src/crates/assembly/core/src/service/remote_connect/settings_sync.rs b/src/crates/assembly/core/src/service/remote_connect/settings_sync.rs index 2bdc1eb8fb..d9a783af9f 100644 --- a/src/crates/assembly/core/src/service/remote_connect/settings_sync.rs +++ b/src/crates/assembly/core/src/service/remote_connect/settings_sync.rs @@ -597,6 +597,39 @@ mod tests { ); } + #[test] + fn account_settings_payload_preserves_chat_input_default_preference() { + let mut config = crate::service::config::GlobalConfig::default(); + config.app.flow_chat.default_mode_strategy = + Some(crate::service::config::types::ChatInputDefaultModeStrategy::FollowLast); + config.app.flow_chat.default_mode_id = Some("Ultra".to_string()); + config.app.flow_chat.last_mode_id = Some("Creative".to_string()); + + let payload = settings_payload(config, "2026-01-01T00:00:00Z", "1.0.0"); + let export = config_export_value(&payload).unwrap(); + + assert_eq!( + export.config.app.flow_chat.default_mode_strategy, + Some(crate::service::config::types::ChatInputDefaultModeStrategy::FollowLast) + ); + assert_eq!( + export.config.app.flow_chat.default_mode_id.as_deref(), + Some("Ultra") + ); + assert_eq!( + export.config.app.flow_chat.last_mode_id.as_deref(), + Some("Creative") + ); + assert_eq!( + serde_json::to_value(export).unwrap()["config"]["app"]["flow_chat"], + serde_json::json!({ + "default_mode_strategy": "follow_last", + "default_mode_id": "Ultra", + "last_mode_id": "Creative" + }) + ); + } + #[test] fn content_hash_rejects_bare_config_payload() { let bare = serde_json::to_string(&crate::service::config::GlobalConfig::default()).unwrap(); @@ -640,6 +673,9 @@ mod tests { assert!(export.config.app.user_tool_groups.groups.is_empty()); assert!(export.config.app.user_skill_groups.groups.is_empty()); assert!(!export.config.app.prevent_sleep); + assert_eq!(export.config.app.flow_chat.default_mode_strategy, None); + assert_eq!(export.config.app.flow_chat.default_mode_id, None); + assert_eq!(export.config.app.flow_chat.last_mode_id, None); assert!(export.config.font.is_none()); assert!(export.config.app.ai_experience.quick_actions.is_empty()); let reexported = serde_json::to_string(&export).unwrap(); diff --git a/src/crates/contracts/config-contracts/src/types.rs b/src/crates/contracts/config-contracts/src/types.rs index dfeca0bc6c..78e4fc2a6f 100644 --- a/src/crates/contracts/config-contracts/src/types.rs +++ b/src/crates/contracts/config-contracts/src/types.rs @@ -314,9 +314,19 @@ pub struct ModelExchangeTracingConfig { #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(default)] pub struct AppFlowChatConfig { - /// Optional user override for the default ChatInput mode id. + /// Explicit policy for choosing the default ChatInput mode. + /// + /// `None` is retained as a compatibility state: an older persisted + /// `default_mode_id` remains fixed, while an untouched configuration + /// follows the user's most recent explicit selection. + #[serde(skip_serializing_if = "Option::is_none")] + pub default_mode_strategy: Option, + /// Optional fixed ChatInput mode id. #[serde(skip_serializing_if = "Option::is_none")] pub default_mode_id: Option, + /// Most recent mode explicitly selected from the ChatInput Harness control. + #[serde(skip_serializing_if = "Option::is_none")] + pub last_mode_id: Option, /// Whether the chat input exposes the global permission-mode shortcut. /// /// The default is visible, but that value is omitted from persisted config @@ -328,6 +338,13 @@ pub struct AppFlowChatConfig { pub show_permission_mode_control: bool, } +#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "snake_case")] +pub enum ChatInputDefaultModeStrategy { + FollowLast, + Fixed, +} + fn default_show_permission_mode_control() -> bool { true } @@ -339,12 +356,32 @@ fn is_permission_mode_control_visible(value: &bool) -> bool { impl Default for AppFlowChatConfig { fn default() -> Self { Self { + default_mode_strategy: None, default_mode_id: None, + last_mode_id: None, show_permission_mode_control: default_show_permission_mode_control(), } } } +impl AppFlowChatConfig { + /// Remove persisted references to an Agent mode that no longer exists. + /// + /// A missing strategy plus a fixed id is the legacy fixed-default shape. + /// Keep that behavior after removing the id instead of silently changing + /// the user's policy to follow-last. + pub fn remove_mode_reference(&mut self, mode_id: &str) { + if self.default_mode_id.as_deref() == Some(mode_id) { + self.default_mode_id = None; + self.default_mode_strategy + .get_or_insert(ChatInputDefaultModeStrategy::Fixed); + } + if self.last_mode_id.as_deref() == Some(mode_id) { + self.last_mode_id = None; + } + } +} + /// A user-defined quick action for the FlowChat post-coding actions menu. #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] pub struct AiExperienceQuickAction { @@ -2039,10 +2076,10 @@ impl AIModelConfig { mod tests { use super::{ AIConfig, AIExperienceConfig, AIModelConfig, AgentModelDefaultsConfig, AgentProfileConfig, - AgentProfileView, AppConfig, AppLoggingConfig, AuthConfig, EditorConfig, GlobalConfig, - MemoryExternalContextPolicy, ModelExchangeTracingMode, NotificationConfig, OpenCodePlan, - SubagentBatchExecutionPolicy, SubagentModelSelection, SubscriptionProvider, - UserSkillGroupsConfig, UserToolGroupsConfig, WebSearchConfig, + AgentProfileView, AppConfig, AppLoggingConfig, AuthConfig, ChatInputDefaultModeStrategy, + EditorConfig, GlobalConfig, MemoryExternalContextPolicy, ModelExchangeTracingMode, + NotificationConfig, OpenCodePlan, SubagentBatchExecutionPolicy, SubagentModelSelection, + SubscriptionProvider, UserSkillGroupsConfig, UserToolGroupsConfig, WebSearchConfig, }; use openbitfun_product_domains::tool_permissions::ToolPermissionConfig; @@ -2617,6 +2654,8 @@ mod tests { config.app.flow_chat.default_mode_id.as_deref(), Some("PlannerPlus") ); + assert_eq!(config.app.flow_chat.default_mode_strategy, None); + assert_eq!(config.app.flow_chat.last_mode_id, None); let serialized = serde_json::to_value(&config).expect("config should serialize"); assert_eq!( @@ -2625,6 +2664,80 @@ mod tests { ); } + #[test] + fn app_flow_chat_default_mode_preference_is_additive_and_round_trips() { + let untouched: GlobalConfig = + serde_json::from_value(current_global_config_with(serde_json::json!({ + "app": { "flow_chat": {} } + }))) + .expect("flow chat config without a preference should deserialize"); + assert_eq!(untouched.app.flow_chat.default_mode_strategy, None); + assert_eq!(untouched.app.flow_chat.default_mode_id, None); + assert_eq!(untouched.app.flow_chat.last_mode_id, None); + + let configured: GlobalConfig = + serde_json::from_value(current_global_config_with(serde_json::json!({ + "app": { + "flow_chat": { + "default_mode_strategy": "follow_last", + "default_mode_id": "Ultra", + "last_mode_id": "Creative" + } + } + }))) + .expect("flow chat mode preference should deserialize"); + assert_eq!( + configured.app.flow_chat.default_mode_strategy, + Some(ChatInputDefaultModeStrategy::FollowLast) + ); + assert_eq!( + configured.app.flow_chat.default_mode_id.as_deref(), + Some("Ultra") + ); + assert_eq!( + configured.app.flow_chat.last_mode_id.as_deref(), + Some("Creative") + ); + + let serialized = serde_json::to_value(configured).expect("config should serialize"); + assert_eq!( + serialized["app"]["flow_chat"]["default_mode_strategy"], + "follow_last" + ); + assert_eq!(serialized["app"]["flow_chat"]["last_mode_id"], "Creative"); + } + + #[test] + fn removing_agent_mode_references_preserves_the_configured_default_policy() { + let mut legacy_fixed = super::AppFlowChatConfig { + default_mode_strategy: None, + default_mode_id: Some("PlannerPlus".to_string()), + last_mode_id: Some("PlannerPlus".to_string()), + ..Default::default() + }; + legacy_fixed.remove_mode_reference("PlannerPlus"); + assert_eq!( + legacy_fixed.default_mode_strategy, + Some(ChatInputDefaultModeStrategy::Fixed) + ); + assert_eq!(legacy_fixed.default_mode_id, None); + assert_eq!(legacy_fixed.last_mode_id, None); + + let mut follow_last = super::AppFlowChatConfig { + default_mode_strategy: Some(ChatInputDefaultModeStrategy::FollowLast), + default_mode_id: Some("PlannerPlus".to_string()), + last_mode_id: Some("Creative".to_string()), + ..Default::default() + }; + follow_last.remove_mode_reference("PlannerPlus"); + assert_eq!( + follow_last.default_mode_strategy, + Some(ChatInputDefaultModeStrategy::FollowLast) + ); + assert_eq!(follow_last.default_mode_id, None); + assert_eq!(follow_last.last_mode_id.as_deref(), Some("Creative")); + } + #[test] fn app_flow_chat_permission_mode_control_defaults_to_visible_without_persisting_default() { let default_config: GlobalConfig = diff --git a/src/crates/contracts/product-domains/src/generated/product-control-catalog.json b/src/crates/contracts/product-domains/src/generated/product-control-catalog.json index 1a16b0b74a..aaea11d048 100644 --- a/src/crates/contracts/product-domains/src/generated/product-control-catalog.json +++ b/src/crates/contracts/product-domains/src/generated/product-control-catalog.json @@ -4,7 +4,7 @@ "title": "OpenBitFun Playbook", "origin": "https://playbook.openbitfun.com", "source": "src/shared/interactive-capabilities/catalog.json", - "digest": "d7a7419ddd673eb733ae8bd33dbb3dcd3b4d1ce0acfc953067a3403fe26d6699", + "digest": "409e6dbef7ebceafccc11606e2855227b9a33fa1ed71f5b4608da04e8a23de7d", "ownerDigest": "c0e5c187cf62bc6ed06196ce8520b3eb427bf268cf24659b72d2552fb1d99c54", "searchAcceptance": [ { @@ -138,11 +138,11 @@ "features": 22, "settings": 21, "userFacing": 43, - "documentedItems": 321, + "documentedItems": 322, "controlCoverage": { "direct": 48, "delegated": 61, - "interactive": 212, + "interactive": 213, "unsupported": 0 } }, @@ -14816,6 +14816,7 @@ "id": "setting.workspace.session:query", "capabilityId": "setting.workspace.session", "itemIds": [ + "default-agent-harness", "automatic-titles", "title-model", "accelerated-search", @@ -14949,6 +14950,52 @@ "pageId": "workspace.session" } }, + { + "id": "setting.workspace.session:open:default-agent-harness", + "capabilityId": "setting.workspace.session", + "itemIds": [ + "default-agent-harness" + ], + "kind": "open", + "risk": "ui", + "executionHost": "presentationSurface", + "availability": { + "desktop": { + "available": true + }, + "cli": { + "available": false, + "reason": "This delivery profile has no live presentation surface" + }, + "peer": { + "available": true, + "requiredCapabilities": [ + "product_control_v1", + "product_control_presentation_v1" + ] + }, + "remoteControl": { + "available": true + }, + "detachedDispatch": { + "available": false, + "reason": "This delivery profile has no live presentation surface" + } + }, + "inputSchema": { + "type": "object", + "additionalProperties": false + }, + "outputSchema": { + "type": "object", + "additionalProperties": true + }, + "openReason": "unstructuredInteraction", + "presentationTarget": { + "kind": "settings", + "pageId": "workspace.session" + } + }, { "id": "setting.workspace.session:open:title-model", "capabilityId": "setting.workspace.session", @@ -27431,11 +27478,13 @@ "categoryId": "assistant", "titleZh": "会话与工作区设置", "titleEn": "Session & workspace settings", - "summaryZh": "控制会话标题生成、工作区加速搜索和与项目上下文相关的行为。", - "summaryEn": "Control session-title generation, accelerated workspace search, and project-context behavior.", + "summaryZh": "控制默认 Agent Harness、会话标题生成、工作区加速搜索和与项目上下文相关的行为。", + "summaryEn": "Control the default Agent Harness, session-title generation, accelerated workspace search, and project-context behavior.", "keywordsZh": [ "会话设置", "工作区设置", + "默认 Harness", + "上次选择", "标题生成", "项目搜索", "索引" @@ -27443,21 +27492,36 @@ "keywordsEn": [ "session settings", "workspace settings", + "default Harness", + "last selection", "title generation", "project search", "index" ], "highlightsZh": [ + "跟随上次选择或固定默认 Harness", "自动生成会话标题", "本地工作区加速搜索", "会话使用的标题模型" ], "highlightsEn": [ + "Follow the last selection or fix a default Harness", "Automatic session titles", "Accelerated local-workspace search", "Model used for session titles" ], "items": [ + { + "id": "default-agent-harness", + "titleZh": "让普通新会话跟随 ChatInput 上次选择,或固定使用指定 Harness", + "titleEn": "Make regular new sessions follow the last ChatInput selection or use a fixed Harness", + "control": { + "kind": "open", + "reasonCode": "unstructuredInteraction", + "reasonZh": "“让普通新会话跟随 ChatInput 上次选择,或固定使用指定 Harness”由会话设置页中的关联控件共同管理,目前没有一个能确定完成整个流程的单一结构化 Command;Agent 会打开精确入口,并把选择保留在用户可见界面。", + "reasonEn": "“Make regular new sessions follow the last ChatInput selection or use a fixed Harness” is managed by related controls on the session settings page and currently has no single structured Command that can deterministically complete the whole workflow; the Agent opens the exact entry and keeps the selection visible to the user." + } + }, { "id": "automatic-titles", "titleZh": "启用或停用根据会话内容自动生成标题", @@ -27523,18 +27587,22 @@ "stepsZh": [ "打开设置", "进入“工作区 > 会话与工作区”", - "调整标题或搜索选项" + "调整默认 Harness、标题或搜索选项" ], "stepsEn": [ "Open Settings", "Go to Workspace > Session & Workspace", - "Tune title or search controls" + "Tune the default Harness, title, or search controls" ], "agentExamplesZh": [ + "让新会话跟随 ChatInput 上一次选择的 Harness", + "把新会话固定为 Ultimate Harness", "关闭自动生成会话标题", "启用工作区加速搜索" ], "agentExamplesEn": [ + "Make new sessions follow the last Harness selected in ChatInput", + "Fix Ultimate as the Harness for new sessions", "Turn off automatic session titles", "Enable accelerated workspace search" ], @@ -27587,20 +27655,28 @@ "AI & collaboration", "会话设置", "工作区设置", + "默认 Harness", + "上次选择", "标题生成", "项目搜索", "索引", "session settings", "workspace settings", + "default Harness", + "last selection", "title generation", "project search", "index", + "跟随上次选择或固定默认 Harness", "自动生成会话标题", "本地工作区加速搜索", "会话使用的标题模型", + "Follow the last selection or fix a default Harness", "Automatic session titles", "Accelerated local-workspace search", "Model used for session titles", + "让普通新会话跟随 ChatInput 上次选择,或固定使用指定 Harness", + "Make regular new sessions follow the last ChatInput selection or use a fixed Harness", "启用或停用根据会话内容自动生成标题", "Enable or disable automatic titles generated from session content", "让标题模型继承主模型、快速模型或使用指定模型", @@ -27611,8 +27687,12 @@ "Inspect index status and rebuild the workspace index when needed", "远程工作区明确降级搜索实现,不把远程路径回退到本机", "Use an explicit remote-workspace search path without silently falling back to local files", + "让新会话跟随 ChatInput 上一次选择的 Harness", + "把新会话固定为 Ultimate Harness", "关闭自动生成会话标题", "启用工作区加速搜索", + "Make new sessions follow the last Harness selected in ChatInput", + "Fix Ultimate as the Harness for new sessions", "Turn off automatic session titles", "Enable accelerated workspace search" ], diff --git a/src/shared/interactive-capabilities/catalog.json b/src/shared/interactive-capabilities/catalog.json index 1e24bb24d2..6d74414c56 100644 --- a/src/shared/interactive-capabilities/catalog.json +++ b/src/shared/interactive-capabilities/catalog.json @@ -6996,11 +6996,13 @@ "categoryId": "assistant", "titleZh": "会话与工作区设置", "titleEn": "Session & workspace settings", - "summaryZh": "控制会话标题生成、工作区加速搜索和与项目上下文相关的行为。", - "summaryEn": "Control session-title generation, accelerated workspace search, and project-context behavior.", + "summaryZh": "控制默认 Agent Harness、会话标题生成、工作区加速搜索和与项目上下文相关的行为。", + "summaryEn": "Control the default Agent Harness, session-title generation, accelerated workspace search, and project-context behavior.", "keywordsZh": [ "会话设置", "工作区设置", + "默认 Harness", + "上次选择", "标题生成", "项目搜索", "索引" @@ -7008,21 +7010,40 @@ "keywordsEn": [ "session settings", "workspace settings", + "default Harness", + "last selection", "title generation", "project search", "index" ], "highlightsZh": [ + "跟随上次选择或固定默认 Harness", "自动生成会话标题", "本地工作区加速搜索", "会话使用的标题模型" ], "highlightsEn": [ + "Follow the last selection or fix a default Harness", "Automatic session titles", "Accelerated local-workspace search", "Model used for session titles" ], "items": [ + { + "id": "default-agent-harness", + "titleZh": "让普通新会话跟随 ChatInput 上次选择,或固定使用指定 Harness", + "titleEn": "Make regular new sessions follow the last ChatInput selection or use a fixed Harness", + "control": { + "kind": "open", + "reasonCode": "unstructuredInteraction", + "reasonZh": "“让普通新会话跟随 ChatInput 上次选择,或固定使用指定 Harness”由会话设置页中的关联控件共同管理,目前没有一个能确定完成整个流程的单一结构化 Command;Agent 会打开精确入口,并把选择保留在用户可见界面。", + "reasonEn": "“Make regular new sessions follow the last ChatInput selection or use a fixed Harness” is managed by related controls on the session settings page and currently has no single structured Command that can deterministically complete the whole workflow; the Agent opens the exact entry and keeps the selection visible to the user." + }, + "evidence": [ + "source:src/web-ui/src/infrastructure/config/components/DefaultHarnessConfig.tsx#DefaultHarnessConfig", + "source:src/web-ui/src/locales/zh-CN/settings/runtime.json#defaultHarness.title" + ] + }, { "id": "automatic-titles", "titleZh": "启用或停用根据会话内容自动生成标题", @@ -7107,18 +7128,22 @@ "stepsZh": [ "打开设置", "进入“工作区 > 会话与工作区”", - "调整标题或搜索选项" + "调整默认 Harness、标题或搜索选项" ], "stepsEn": [ "Open Settings", "Go to Workspace > Session & Workspace", - "Tune title or search controls" + "Tune the default Harness, title, or search controls" ], "agentExamplesZh": [ + "让新会话跟随 ChatInput 上一次选择的 Harness", + "把新会话固定为 Ultimate Harness", "关闭自动生成会话标题", "启用工作区加速搜索" ], "agentExamplesEn": [ + "Make new sessions follow the last Harness selected in ChatInput", + "Fix Ultimate as the Harness for new sessions", "Turn off automatic session titles", "Enable accelerated workspace search" ], diff --git a/src/web-ui/src/app/components/NavPanel/MainNav.tsx b/src/web-ui/src/app/components/NavPanel/MainNav.tsx index 66f93c239b..76aa2308dd 100644 --- a/src/web-ui/src/app/components/NavPanel/MainNav.tsx +++ b/src/web-ui/src/app/components/NavPanel/MainNav.tsx @@ -2,7 +2,7 @@ * MainNav — primary product navigation sidebar. * * Layout (top to bottom): - * 1. Search and New Session + * 1. Search * 2. AI Assistant, Task Board, Mini Apps, then Extensions & Compatibility * 3. Unified Sessions (all or grouped by project / assistant) * @@ -235,10 +235,6 @@ const MainNav: React.FC = ({ }; }, [workspaceMenuOpen, updateWorkspaceMenuPos]); - const handleCreateSession = useCallback(() => { - void activateProductAction('session.new'); - }, []); - const handleOpenAgents = useCallback(() => { void activateProductAction('surface.agents.open'); }, []); @@ -345,7 +341,6 @@ const MainNav: React.FC = ({ getAppearanceOverlayHost() ) : null; - const createSessionLabel = t('nav.sessions.newSession'); const addSessionGroupTooltip = t('nav.tooltips.addSessionGroup'); const agentsTooltip = t('nav.tooltips.agents'); const skillsTooltip = t('nav.tooltips.skills'); @@ -394,20 +389,6 @@ const MainNav: React.FC = ({ - - - diff --git a/src/web-ui/src/app/components/NavPanel/NavPanel.scss b/src/web-ui/src/app/components/NavPanel/NavPanel.scss index 1e04fbe966..07a679e579 100644 --- a/src/web-ui/src/app/components/NavPanel/NavPanel.scss +++ b/src/web-ui/src/app/components/NavPanel/NavPanel.scss @@ -2151,43 +2151,6 @@ $_section-header-height: 22px; min-width: 0; } -.openbitfun-nav-panel__utility-action { - flex: 0 0 auto; - width: 34px; - min-width: 34px; - height: 34px; - display: inline-flex; - align-items: center; - justify-content: center; - padding: 0; - border: 1px solid; - @include shell-surfaces.sidebar-hairline-color; - border-radius: var(--openbitfun-radius-pill); - background: var(--openbitfun-color-surface-scene); - color: var(--openbitfun-color-content-secondary); - cursor: pointer; - transition: background var(--openbitfun-motion-duration-fast) var(--openbitfun-motion-easing-standard), - border-color var(--openbitfun-motion-duration-fast) var(--openbitfun-motion-easing-standard), - color var(--openbitfun-motion-duration-fast) var(--openbitfun-motion-easing-standard); - - &:hover, - &:focus-visible { - background: var(--openbitfun-color-content-primary); - border-color: var(--openbitfun-color-content-primary); - color: var(--openbitfun-color-surface-scene); - - > svg { - color: var(--openbitfun-color-surface-scene); - stroke: currentColor; - } - } - - &:focus-visible { - outline: 1px solid var(--openbitfun-color-accent-default); - outline-offset: 1px; - } -} - .openbitfun-nav-panel__count-badge { flex: 0 0 auto; min-width: 20px; @@ -2871,7 +2834,6 @@ $_section-header-height: 22px; } &__search-trigger__icon, - &__utility-action, &__section-action, &__item-action, &__footer-btn--icon { diff --git a/src/web-ui/src/app/components/NavPanel/sections/workspaces/WorkspaceItem.tsx b/src/web-ui/src/app/components/NavPanel/sections/workspaces/WorkspaceItem.tsx index d437b3ee14..bbb3aee964 100644 --- a/src/web-ui/src/app/components/NavPanel/sections/workspaces/WorkspaceItem.tsx +++ b/src/web-ui/src/app/components/NavPanel/sections/workspaces/WorkspaceItem.tsx @@ -722,7 +722,7 @@ const WorkspaceItem: React.FC = ({ ]); const handleCreateProjectSession = useCallback(() => { - void handleCreateSession('agentic'); + void handleCreateSession(); }, [handleCreateSession]); const handleCreateAcpSession = useCallback(async (client: AcpClientInfo) => { @@ -877,6 +877,20 @@ const WorkspaceItem: React.FC = ({
e.stopPropagation()}> + + + + ); hover(host.querySelector('button')!);