From 1dd3237da54a9a22e55b8e4713c3ec6acc0ef21e Mon Sep 17 00:00:00 2001 From: Bob Lee Date: Sat, 5 Sep 2026 15:13:13 +0800 Subject: [PATCH 01/11] style(config): format configuration compatibility fixtures --- .../assembly/core/src/service/config/types.rs | 347 +++++++++--------- 1 file changed, 166 insertions(+), 181 deletions(-) diff --git a/src/crates/assembly/core/src/service/config/types.rs b/src/crates/assembly/core/src/service/config/types.rs index 501de7eb65..305c59572c 100644 --- a/src/crates/assembly/core/src/service/config/types.rs +++ b/src/crates/assembly/core/src/service/config/types.rs @@ -1618,7 +1618,6 @@ pub trait ConfigProvider: Send + Sync { old_config: &serde_json::Value, new_config: &serde_json::Value, ) -> OpenBitFunResult<()>; - } /// Configuration change event. @@ -2199,10 +2198,9 @@ mod tests { #[test] fn plugin_config_defaults_to_empty_when_missing() { - let config: GlobalConfig = serde_json::from_value(current_global_config_with( - serde_json::json!({}), - )) - .expect("current global config should apply optional plugin defaults"); + let config: GlobalConfig = + serde_json::from_value(current_global_config_with(serde_json::json!({}))) + .expect("current global config should apply optional plugin defaults"); assert!(config.plugin.is_empty()); assert!(!config.has_configured_plugins()); @@ -2210,19 +2208,18 @@ mod tests { #[test] fn non_empty_plugin_config_requests_runtime_startup() { - let config: GlobalConfig = serde_json::from_value(current_global_config_with( - serde_json::json!({ - "plugin": [ - "file:///C:/plugins/demo.mjs", - { - "spec": "@my-org/custom-plugin", - "options": { "mode": "strict" }, - "baseDirectory": "C:/workspace" - } - ] - }), - )) - .expect("plugin config should deserialize"); + let config: GlobalConfig = + serde_json::from_value(current_global_config_with(serde_json::json!({ + "plugin": [ + "file:///C:/plugins/demo.mjs", + { + "spec": "@my-org/custom-plugin", + "options": { "mode": "strict" }, + "baseDirectory": "C:/workspace" + } + ] + }))) + .expect("plugin config should deserialize"); assert_eq!(config.plugin.len(), 2); assert!(config.has_configured_plugins()); @@ -2230,12 +2227,11 @@ mod tests { #[test] fn empty_plugin_specs_do_not_request_runtime_startup() { - let config: GlobalConfig = serde_json::from_value(current_global_config_with( - serde_json::json!({ - "plugin": ["", " ", { "spec": "" }] - }), - )) - .expect("empty plugin declarations should deserialize"); + let config: GlobalConfig = + serde_json::from_value(current_global_config_with(serde_json::json!({ + "plugin": ["", " ", { "spec": "" }] + }))) + .expect("empty plugin declarations should deserialize"); assert!(!config.has_configured_plugins()); } @@ -2305,12 +2301,11 @@ mod tests { .expect("current AI config should default an omitted optional limit"); assert_eq!(defaulted.ai.max_rounds, 0); - let limited: GlobalConfig = serde_json::from_value(current_global_config_with( - serde_json::json!({ - "ai": { "max_rounds": 37 } - }), - )) - .expect("explicit max rounds should deserialize"); + let limited: GlobalConfig = + serde_json::from_value(current_global_config_with(serde_json::json!({ + "ai": { "max_rounds": 37 } + }))) + .expect("explicit max rounds should deserialize"); assert_eq!(limited.ai.max_rounds, 37); } @@ -2331,21 +2326,20 @@ mod tests { #[test] fn user_tool_groups_preserve_the_versioned_ui_shape() { - let config: GlobalConfig = serde_json::from_value(current_global_config_with( - serde_json::json!({ - "app": { - "user_tool_groups": { - "version": 1, - "groups": [{ - "id": "daily-code", - "name": "Daily code changes", - "toolNames": ["Read", "Edit"] - }] + let config: GlobalConfig = + serde_json::from_value(current_global_config_with(serde_json::json!({ + "app": { + "user_tool_groups": { + "version": 1, + "groups": [{ + "id": "daily-code", + "name": "Daily code changes", + "toolNames": ["Read", "Edit"] + }] + } } - } - }), - )) - .expect("user tool groups should deserialize"); + }))) + .expect("user tool groups should deserialize"); assert_eq!( config.app.user_tool_groups.groups[0].tool_names, @@ -2379,21 +2373,20 @@ mod tests { #[test] fn user_skill_groups_preserve_the_versioned_ui_shape() { - let config: GlobalConfig = serde_json::from_value(current_global_config_with( - serde_json::json!({ - "app": { - "user_skill_groups": { - "version": 1, - "groups": [{ - "id": "daily-coding", - "name": "Daily coding", - "skillKeys": ["builtin::find-skills", "user::review"] - }] + let config: GlobalConfig = + serde_json::from_value(current_global_config_with(serde_json::json!({ + "app": { + "user_skill_groups": { + "version": 1, + "groups": [{ + "id": "daily-coding", + "name": "Daily coding", + "skillKeys": ["builtin::find-skills", "user::review"] + }] + } } - } - }), - )) - .expect("user skill groups should deserialize"); + }))) + .expect("user skill groups should deserialize"); assert_eq!( config.app.user_skill_groups.groups[0].skill_keys, @@ -2412,22 +2405,21 @@ mod tests { #[test] fn global_config_preserves_project_mcp_servers() { - let config: GlobalConfig = serde_json::from_value(current_global_config_with( - serde_json::json!({ - "project": { - "mcp_servers": [ - { - "id": "project-docs", - "name": "Project Docs", - "server_type": "local", - "command": "docs-mcp", - "args": [] - } - ] - } - }), - )) - .expect("project scoped MCP config should deserialize"); + let config: GlobalConfig = + serde_json::from_value(current_global_config_with(serde_json::json!({ + "project": { + "mcp_servers": [ + { + "id": "project-docs", + "name": "Project Docs", + "server_type": "local", + "command": "docs-mcp", + "args": [] + } + ] + } + }))) + .expect("project scoped MCP config should deserialize"); assert_eq!( config @@ -2448,14 +2440,13 @@ mod tests { #[test] fn global_config_preserves_terminal_panel_position() { - let config: GlobalConfig = serde_json::from_value(current_global_config_with( - serde_json::json!({ - "terminal": { - "terminal_panel_position": "bottom" - } - }), - )) - .expect("terminal panel position config should deserialize"); + let config: GlobalConfig = + serde_json::from_value(current_global_config_with(serde_json::json!({ + "terminal": { + "terminal_panel_position": "bottom" + } + }))) + .expect("terminal panel position config should deserialize"); assert_eq!(config.terminal.terminal_panel_position, "bottom"); @@ -2546,45 +2537,44 @@ mod tests { #[test] fn ai_experience_quick_actions_round_trip_through_global_config() { - let config: GlobalConfig = serde_json::from_value(current_global_config_with( - serde_json::json!({ - "app": { - "language": "en-US", - "auto_update": true, - "telemetry": true, - "startup_behavior": "default", - "confirm_on_exit": true, - "restore_windows": false, - "zoom_level": 100, - "sidebar": { "width": 260, "collapsed": false }, - "right_panel": { "width": 400, "collapsed": true }, - "notifications": { - "enabled": true, - "position": "top-right", - "duration": 4000, - "dialog_completion_notify": true, - "permission_request_notify": false, - "enable_startup_tips": true - }, - "ai_experience": { - "enable_session_title_generation": true, - "enable_welcome_panel_ai_analysis": false, - "enable_visual_mode": false, - "enable_agent_companion": true, - "enable_workspace_search": false, - "quick_actions": [ - { - "id": "custom_1", - "label": "Run tests", - "prompt": "Run the test suite", - "enabled": true - } - ] + let config: GlobalConfig = + serde_json::from_value(current_global_config_with(serde_json::json!({ + "app": { + "language": "en-US", + "auto_update": true, + "telemetry": true, + "startup_behavior": "default", + "confirm_on_exit": true, + "restore_windows": false, + "zoom_level": 100, + "sidebar": { "width": 260, "collapsed": false }, + "right_panel": { "width": 400, "collapsed": true }, + "notifications": { + "enabled": true, + "position": "top-right", + "duration": 4000, + "dialog_completion_notify": true, + "permission_request_notify": false, + "enable_startup_tips": true + }, + "ai_experience": { + "enable_session_title_generation": true, + "enable_welcome_panel_ai_analysis": false, + "enable_visual_mode": false, + "enable_agent_companion": true, + "enable_workspace_search": false, + "quick_actions": [ + { + "id": "custom_1", + "label": "Run tests", + "prompt": "Run the test suite", + "enabled": true + } + ] + } } - } - }), - )) - .expect("minimal app config with quick_actions should deserialize"); + }))) + .expect("minimal app config with quick_actions should deserialize"); let actions = &config.app.ai_experience.quick_actions; assert!(!config.app.notifications.permission_request_notify); @@ -2622,16 +2612,15 @@ mod tests { #[test] fn app_flow_chat_default_mode_id_round_trips() { - let config: GlobalConfig = serde_json::from_value(current_global_config_with( - serde_json::json!({ - "app": { - "flow_chat": { - "default_mode_id": "PlannerPlus" + let config: GlobalConfig = + serde_json::from_value(current_global_config_with(serde_json::json!({ + "app": { + "flow_chat": { + "default_mode_id": "PlannerPlus" + } } - } - }), - )) - .expect("flow chat config should deserialize"); + }))) + .expect("flow chat config should deserialize"); assert_eq!( config.app.flow_chat.default_mode_id.as_deref(), @@ -2647,14 +2636,13 @@ mod tests { #[test] fn app_flow_chat_permission_mode_control_defaults_to_visible_without_persisting_default() { - let default_config: GlobalConfig = serde_json::from_value(current_global_config_with( - serde_json::json!({ - "app": { - "flow_chat": {} - } - }), - )) - .expect("flow chat config without visibility preference should deserialize"); + let default_config: GlobalConfig = + serde_json::from_value(current_global_config_with(serde_json::json!({ + "app": { + "flow_chat": {} + } + }))) + .expect("flow chat config without visibility preference should deserialize"); assert!(default_config.app.flow_chat.show_permission_mode_control); let default_serialized = @@ -2663,16 +2651,15 @@ mod tests { .get("show_permission_mode_control") .is_none()); - let hidden_config: GlobalConfig = serde_json::from_value(current_global_config_with( - serde_json::json!({ - "app": { - "flow_chat": { - "show_permission_mode_control": false + let hidden_config: GlobalConfig = + serde_json::from_value(current_global_config_with(serde_json::json!({ + "app": { + "flow_chat": { + "show_permission_mode_control": false + } } - } - }), - )) - .expect("flow chat config with hidden permission control should deserialize"); + }))) + .expect("flow chat config with hidden permission control should deserialize"); assert!(!hidden_config.app.flow_chat.show_permission_mode_control); let hidden_serialized = @@ -2858,16 +2845,15 @@ mod tests { #[test] fn current_editor_config_defaults_new_optional_visual_fields() { - let config: GlobalConfig = serde_json::from_value(current_global_config_with( - serde_json::json!({ - "editor": { - "font_size": 16, - "font_family": "Fixture Mono", - "line_height": 1.4 - } - }), - )) - .expect("current editor config should default optional visual fields"); + let config: GlobalConfig = + serde_json::from_value(current_global_config_with(serde_json::json!({ + "editor": { + "font_size": 16, + "font_family": "Fixture Mono", + "line_height": 1.4 + } + }))) + .expect("current editor config should default optional visual fields"); assert_eq!(config.editor.font_size, 16); assert_eq!(config.editor.font_family, "Fixture Mono"); @@ -2940,31 +2926,30 @@ mod tests { #[test] fn deserializes_explicit_memories_config() { - let config: GlobalConfig = serde_json::from_value(current_global_config_with( - serde_json::json!({ - "memories": { - "generate_memories": false, - "generate_for_btw_sessions": true, - "use_memories": false, - "external_context_policy": "skip_session", - "max_raw_memories_for_consolidation": 12, - "max_unused_days": 7, - "max_rollout_age_days": 14, - "max_rollouts_per_startup": 8, - "max_rollouts_scan_limit": 200, - "min_rollout_idle_hours": 12, - "phase1_max_concurrency": 3, - "phase1_retry_backoff_minutes": 45, - "phase1_lease_seconds": 600, - "phase2_lease_seconds": 1200, - "phase2_success_cooldown_seconds": 7200, - "phase2_retry_delay_seconds": 300, - "extract_model": "extractor", - "consolidation_model": "consolidator" - } - }), - )) - .expect("global config with memories section should deserialize"); + let config: GlobalConfig = + serde_json::from_value(current_global_config_with(serde_json::json!({ + "memories": { + "generate_memories": false, + "generate_for_btw_sessions": true, + "use_memories": false, + "external_context_policy": "skip_session", + "max_raw_memories_for_consolidation": 12, + "max_unused_days": 7, + "max_rollout_age_days": 14, + "max_rollouts_per_startup": 8, + "max_rollouts_scan_limit": 200, + "min_rollout_idle_hours": 12, + "phase1_max_concurrency": 3, + "phase1_retry_backoff_minutes": 45, + "phase1_lease_seconds": 600, + "phase2_lease_seconds": 1200, + "phase2_success_cooldown_seconds": 7200, + "phase2_retry_delay_seconds": 300, + "extract_model": "extractor", + "consolidation_model": "consolidator" + } + }))) + .expect("global config with memories section should deserialize"); assert!(!config.memories.generate_memories); assert!(config.memories.generate_for_btw_sessions); From dade6a3058e28bbb0a813d0c29f48412ec143030 Mon Sep 17 00:00:00 2001 From: Bob Lee Date: Sat, 5 Sep 2026 15:13:13 +0800 Subject: [PATCH 02/11] fix(ui): keep combobox chip removal controls circular --- .../components/Combobox/Combobox.module.css | 19 +++++++++++++++---- .../ui/src/components/Combobox/Combobox.tsx | 1 + .../packages/ui/tests/combobox.test.mjs | 2 +- 3 files changed, 17 insertions(+), 5 deletions(-) diff --git a/design-system/packages/ui/src/components/Combobox/Combobox.module.css b/design-system/packages/ui/src/components/Combobox/Combobox.module.css index 819cf30342..eb3956443c 100644 --- a/design-system/packages/ui/src/components/Combobox/Combobox.module.css +++ b/design-system/packages/ui/src/components/Combobox/Combobox.module.css @@ -174,6 +174,11 @@ .tags { gap: var(--openbitfun-space-1); + font-family: var(--openbitfun-type-label-md-font-family); + font-size: var(--openbitfun-type-label-md-font-size); + font-weight: var(--openbitfun-type-label-md-font-weight); + line-height: var(--openbitfun-type-label-md-line-height); + letter-spacing: var(--openbitfun-type-label-md-letter-spacing); } .control[data-tags="true"] .tags { @@ -207,16 +212,22 @@ overflow: hidden; align-items: center; max-inline-size: 10rem; + min-block-size: var(--openbitfun-control-icon-button-xs-size); gap: var(--openbitfun-space-1); - padding-inline-start: var(--openbitfun-space-2); - border-radius: var(--openbitfun-radius-xs); - color: var(--openbitfun-color-action-neutral-content); - background: var(--openbitfun-color-action-neutral-surface); + padding-inline: var(--openbitfun-space-2); + border: var(--openbitfun-border-width-default) solid var(--openbitfun-color-border-subtle); + border-radius: var(--openbitfun-radius-pill); + color: var(--openbitfun-color-content-primary); + background: var(--openbitfun-color-surface-tertiary); pointer-events: auto; text-overflow: ellipsis; white-space: nowrap; } + .tag:has(button) { + padding-inline-end: 0; + } + .tag > span { overflow: hidden; text-overflow: ellipsis; diff --git a/design-system/packages/ui/src/components/Combobox/Combobox.tsx b/design-system/packages/ui/src/components/Combobox/Combobox.tsx index a2c876b52f..a1d0dc7388 100644 --- a/design-system/packages/ui/src/components/Combobox/Combobox.tsx +++ b/design-system/packages/ui/src/components/Combobox/Combobox.tsx @@ -621,6 +621,7 @@ const CollectionPicker = forwardRef(function Collec commitValues(selectedValues.filter((value) => value !== option.value)); }} onMouseDown={(event) => event.preventDefault()} + shape="circle" size="xs" variant="quiet" /> diff --git a/design-system/packages/ui/tests/combobox.test.mjs b/design-system/packages/ui/tests/combobox.test.mjs index ec0e092cd9..50c323a52b 100644 --- a/design-system/packages/ui/tests/combobox.test.mjs +++ b/design-system/packages/ui/tests/combobox.test.mjs @@ -74,7 +74,7 @@ test("Combobox styling uses public field, overlay, action, and motion tokens", a assert.match(styles, /--openbitfun-color-field-background/); assert.match(styles, /--openbitfun-overlay-menu-surface-radius/); - assert.match(styles, /--openbitfun-color-action-neutral-surface/); + assert.match(styles, /--openbitfun-color-surface-tertiary/); assert.match(styles, /--openbitfun-shadow-menu/); assert.match(styles, /position:\s*fixed/); assert.match(styles, /z-index:\s*var\(--openbitfun-layer-popover\)/); From 309227e88986fb16197d4d3b31e8a58d29184a25 Mon Sep 17 00:00:00 2001 From: Bob Lee Date: Sat, 5 Sep 2026 15:13:13 +0800 Subject: [PATCH 03/11] style(navigation): simplify recent workspace section heading --- src/web-ui/src/app/components/NavPanel/MainNav.tsx | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/src/web-ui/src/app/components/NavPanel/MainNav.tsx b/src/web-ui/src/app/components/NavPanel/MainNav.tsx index 65ba93d8d0..cface10339 100644 --- a/src/web-ui/src/app/components/NavPanel/MainNav.tsx +++ b/src/web-ui/src/app/components/NavPanel/MainNav.tsx @@ -28,7 +28,7 @@ import { } from '@openbitfun/ui'; import { getAppearanceOverlayHost } from '@/infrastructure/appearance/runtime/AppearanceOverlayHost'; import { isImeOwnedKeyboardEvent } from '@/shared/utils/ime'; -import { FolderOpen, FolderPlus, History, Users, Network } from 'lucide-react'; +import { FolderOpen, FolderPlus, Users, Network } from 'lucide-react'; // import { PanelsTopLeft } from 'lucide-react'; // temporarily hidden: Pages nav entry import { useSceneManager } from '../../hooks/useSceneManager'; import { useI18n } from '@/infrastructure/i18n/hooks/useI18n'; @@ -305,12 +305,7 @@ const MainNav: React.FC = ({ -