From a65593f76ae68d8920055ecc2299ea59dda2f91f Mon Sep 17 00:00:00 2001 From: GCWing Date: Sun, 6 Sep 2026 16:22:05 +0800 Subject: [PATCH 1/2] fix(web): reconcile session lifecycle and surface hierarchy --- docs/architecture/session-projection.md | 23 ++ docs/architecture/theme-token-optimization.md | 12 + scripts/theme-color-governance-baseline.json | 8 +- scripts/theme-color-near-pair-decisions.json | 15 +- src/apps/desktop/src/api/system_api.rs | 32 +- src/apps/desktop/src/appearance.rs | 3 + .../startup_appearance_bootstrap.json | 22 +- .../appearance_prompt_snapshots.json | 30 +- src/web-ui/AGENTS.md | 7 + src/web-ui/index.html | 185 ++++----- .../sections/sessions/SessionsSection.tsx | 28 +- .../sessionActionConfirmation.test.ts | 54 +++ .../sections/workspaces/WorkspaceItem.tsx | 27 +- ...orkspaceItemSessionBatchManagement.test.ts | 31 ++ .../WorkspaceSessionBatchModal.scss | 359 ++++++------------ .../workspaces/WorkspaceSessionBatchModal.tsx | 280 +++++++------- .../src/app/components/SceneBar/SceneBar.scss | 2 +- .../components/SceneTopBar/SceneTopBar.scss | 14 + .../SceneTopBar/SceneTopBar.test.tsx | 191 +++++++++- .../components/SceneTopBar/SceneTopBar.tsx | 102 ++--- .../WindowControls/WindowControls.tsx | 3 +- src/web-ui/src/app/layout/AppLayout.tsx | 9 +- .../miniapps/views/MiniAppGalleryView.tsx | 10 +- .../miniapps/views/MiniAppLibraryView.tsx | 10 +- .../scenes/profile/views/NurseryGallery.tsx | 16 +- .../services/sessionSceneLifecycle.test.ts | 197 ++++++++++ .../src/app/services/sessionSceneLifecycle.ts | 31 ++ .../src/app/startup/startupPreload.test.ts | 51 ++- src/web-ui/src/app/stores/sceneStore.ts | 2 + .../ChatInputWorkspaceStrip.test.tsx | 4 +- .../components/ChatInputWorkspaceStrip.tsx | 2 +- .../ChatInputWorkspaceStripLayout.test.ts | 16 +- .../components/FileMentionPicker.scss | 8 +- .../services/sessionActivation.test.ts | 69 ++-- .../flow_chat/services/sessionActivation.ts | 35 +- .../src/flow_chat/services/storeSync.test.ts | 62 +++ .../src/flow_chat/services/storeSync.ts | 90 ++--- .../flow_chat/store/modernFlowChatStore.ts | 3 + .../builtins/appearancePresetOutput.test.ts | 10 +- .../appearance/builtins/chinaNight.ts | 37 +- .../appearance/builtins/chinaStyle.ts | 14 +- .../appearance/builtins/cyber.ts | 5 +- .../appearance/builtins/midnight.ts | 27 +- .../appearance/builtins/tokyoNight.ts | 26 +- .../src/infrastructure/runtime/index.ts | 1 + .../src/infrastructure/runtime/window.ts | 9 + src/web-ui/src/locales/en-US/common.json | 8 +- src/web-ui/src/locales/zh-CN/common.json | 8 +- src/web-ui/src/locales/zh-TW/common.json | 8 +- .../src/shared/styles/_surface-recipes.scss | 39 +- .../src/shared/styles/surfaceRecipes.test.ts | 54 +++ .../git/components/BranchQuickSwitch.scss | 6 +- 52 files changed, 1469 insertions(+), 826 deletions(-) create mode 100644 src/web-ui/src/app/components/NavPanel/sections/sessions/sessionActionConfirmation.test.ts create mode 100644 src/web-ui/src/app/components/NavPanel/sections/workspaces/WorkspaceItemSessionBatchManagement.test.ts create mode 100644 src/web-ui/src/app/services/sessionSceneLifecycle.test.ts create mode 100644 src/web-ui/src/app/services/sessionSceneLifecycle.ts create mode 100644 src/web-ui/src/infrastructure/runtime/window.ts create mode 100644 src/web-ui/src/shared/styles/surfaceRecipes.test.ts diff --git a/docs/architecture/session-projection.md b/docs/architecture/session-projection.md index 46fe9b975f..4a618c1c29 100644 --- a/docs/architecture/session-projection.md +++ b/docs/architecture/session-projection.md @@ -95,6 +95,29 @@ Any state that is per-Session is reached through its stream. A feature cannot hold Session state that is not surface-scoped, because there is nowhere to put it. +## Web UI selection and scene lifetime + +`FlowChatStore` owns the selected session on the active device surface. +`ModernFlowChatStore` is its presentation projection, not a second selection +owner. Synchronization includes an empty selection and a missing selected +record, both on subscription and after every source change. Explicit sync from +an async opener cannot select a different session. Shell and standalone chat +hosts share one source subscription. + +`app/services/sessionSceneLifecycle.ts`, installed for the `AppLayout` lifetime, +binds the Session scene to that selected record. When it no longer exists, the +shell closes the Session tab through `sceneStore`, which also removes its +navigation history. Other tabs remain available; closing the final tab exposes +the existing tabless empty surface. Delete, archive, workspace removal, and +device-surface changes use this same rule instead of mutation-specific UI +callbacks. Creating or opening a session establishes the record before opening +its scene. Loading, offline, and failed history records still exist and remain +recoverable; absence of rendered turns is never evidence of removal. + +This is frontend view reconciliation. It does not delete persisted sessions, +create replacement sessions, or change the runtime state machine and remote +wire contracts. + ## Sources are not writers Every source above becomes a way of **obtaining positioned events**, applied diff --git a/docs/architecture/theme-token-optimization.md b/docs/architecture/theme-token-optimization.md index 54186aa0e5..87b8ec5d1c 100644 --- a/docs/architecture/theme-token-optimization.md +++ b/docs/architecture/theme-token-optimization.md @@ -121,6 +121,18 @@ Web UI Appearance 的当前 schema 固定为 v2。颜色入口是 `theme-tokens` - Widget、Desktop 首屏 bootstrap 和生成式 UI 提示只消费同一 canonical 源生成的 allowlist 产物, 不能反向成为主题 owner。 +### 内置主题的工作区层级 + +侧栏与导航由 `color.surface.chrome` 承载,主内容由 `color.surface.scene` 承载。两者相邻且常驻, +不能只依靠阴影或拖拽时出现的边线区分。参照 Light 的中性灰外壳与白色内容关系,命名主题在自身色调内 +拉开明度:墨韵使用暖灰宣纸外壳、浅宣纸内容与白色浮层;墨夜和午夜使用深墨外壳、炭灰内容与更亮的面板; +Tokyo Night 使用夜靛外壳、storm 靛蓝内容与蓝紫浮层。Cyber 保留黑底与霓虹强调,其浮层比内容更亮。 + +`color.surface.tertiary` 与 `color.surface.workbench` 用于内嵌分组及工作台;hover、selection、focus +继续使用已有交互 Token。Monaco 当前行与输入面板保留独立色阶。辅助文字和边框按新的相邻底色调整, +而不通过局部组件色值修补。上述配色由 builtin palette 声明,并同步生成启动页和生成式 UI 配色清单; +不为每款主题新增 Token 名称或主题专属布局分支。 + 旧 CSS-token adapter、Token 投影层、`css-tokens` renderer 和 `--openbitfun-appearance-token-*` 运行时变量均已退休并从源码删除。不得为第三方包、旧组件或测试重新引入这些接口。 diff --git a/scripts/theme-color-governance-baseline.json b/scripts/theme-color-governance-baseline.json index 30b71cb514..dceb039bf0 100644 --- a/scripts/theme-color-governance-baseline.json +++ b/scripts/theme-color-governance-baseline.json @@ -171,13 +171,13 @@ "max": 0 }, "colorDomainNearPairs.nearTotal": { - "max": 5 + "max": 4 }, "colorDomainNearPairs.themePreset.indistinguishableTotal": { "max": 0 }, "colorDomainNearPairs.themePreset.nearTotal": { - "max": 3 + "max": 2 }, "colorDomainNearPairs.themeRuntime.indistinguishableTotal": { "max": 0 @@ -258,10 +258,10 @@ "max": 0 }, "colorDomainScopes.themePreset.occurrences": { - "max": 130 + "max": 128 }, "colorDomainScopes.themePreset.uniqueColors": { - "max": 101 + "max": 98 }, "colorDomainScopes.themeRuntime.occurrences": { "max": 0 diff --git a/scripts/theme-color-near-pair-decisions.json b/scripts/theme-color-near-pair-decisions.json index 3549ad2e75..c7c7dee945 100644 --- a/scripts/theme-color-near-pair-decisions.json +++ b/scripts/theme-color-near-pair-decisions.json @@ -8,17 +8,8 @@ "key": "#1a1b26 <-> #1c1c1f", "decision": "keep", "owner": "src/web-ui/src/infrastructure/appearance/builtins", - "reason": "Tokyo Night primary background uses the canonical Tokyo base hue; #1c1c1f is the default dark/china/slate neutral surface. Merging would flatten cross-theme identity rather than remove an app-level duplicate.", - "reevaluateWhen": "Only after a visual review proves Tokyo Night no longer needs a distinct primary background identity." - }, - { - "root": "src/web-ui", - "domain": "themePreset", - "key": "#1c1c1f <-> #212019", - "decision": "keep", - "owner": "src/web-ui/src/infrastructure/appearance/builtins", - "reason": "China Night secondary background also feeds Monaco lineHighlight through the theme projection. Merging it into the primary background removes the visible current-line cue and flattens adjacent ink surfaces.", - "reevaluateWhen": "Only if China Night gets a separate editor lineHighlight token or visual review proves the current-line cue remains visible." + "reason": "Tokyo Night structural chrome uses the canonical Tokyo base hue around storm-indigo content; #1c1c1f is the default dark/china/slate neutral surface. Merging would flatten cross-theme identity rather than remove an app-level duplicate.", + "reevaluateWhen": "Only after a visual review proves Tokyo Night no longer needs a distinct structural chrome identity." }, { "root": "src/web-ui", @@ -26,7 +17,7 @@ "key": "#2b2d30 <-> #313335", "decision": "keep", "owner": "src/web-ui/src/infrastructure/appearance/builtins", - "reason": "Midnight background, tertiary surface, and editor lineHighlight are adjacent in the same dark theme. Merging would weaken surface and editor-row separation.", + "reason": "Midnight content, raised panels, and editor lineHighlight are adjacent in the same dark theme. Merging would weaken surface and editor-row separation.", "reevaluateWhen": "Only after a Midnight surface-ramp redesign removes the adjacent role split." }, { diff --git a/src/apps/desktop/src/api/system_api.rs b/src/apps/desktop/src/api/system_api.rs index e289b78719..a4153b14a8 100644 --- a/src/apps/desktop/src/api/system_api.rs +++ b/src/apps/desktop/src/api/system_api.rs @@ -591,6 +591,7 @@ pub struct ToggleMainWindowFullscreenResponse { #[derive(Debug, Deserialize)] #[serde(rename_all = "snake_case")] pub enum StartupWindowControlAction { + GetState, Minimize, ToggleMaximize, Close, @@ -602,6 +603,12 @@ pub struct StartupWindowControlRequest { pub action: StartupWindowControlAction, } +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct StartupWindowControlResponse { + pub is_maximized: bool, +} + #[derive(Debug, Clone, Copy, PartialEq, Eq)] struct MainWindowFullscreenTransition { next_fullscreen: bool, @@ -720,19 +727,20 @@ pub async fn startup_window_control( startup_trace: State<'_, DesktopStartupTrace>, app: tauri::AppHandle, request: StartupWindowControlRequest, -) -> Result<(), String> { +) -> Result { let Some(window) = app.get_webview_window("main") else { return Err("Main window not found".to_string()); }; + let mut is_maximized = window.is_maximized().unwrap_or(false); match request.action { + StartupWindowControlAction::GetState => {} StartupWindowControlAction::Minimize => { window.minimize().map_err(|error| { format!("Failed to minimize main window during startup: {}", error) })?; } StartupWindowControlAction::ToggleMaximize => { - let is_maximized = window.is_maximized().unwrap_or(false); if is_maximized { window.unmaximize().map_err(|error| { format!("Failed to restore main window during startup: {}", error) @@ -742,6 +750,7 @@ pub async fn startup_window_control( format!("Failed to maximize main window during startup: {}", error) })?; } + is_maximized = !is_maximized; } StartupWindowControlAction::Close => { let behavior = state @@ -771,7 +780,7 @@ pub async fn startup_window_control( } } - Ok(()) + Ok(StartupWindowControlResponse { is_maximized }) } /// Toggle OS-level fullscreen for the Desktop main window. @@ -1022,6 +1031,23 @@ fn activate_main_window_from_notification(app: &tauri::AppHandle) { #[cfg(test)] mod tests { + #[test] + fn startup_window_control_contract_exposes_the_native_maximize_state() { + let request: super::StartupWindowControlRequest = + serde_json::from_value(serde_json::json!({ "action": "get_state" })) + .expect("get_state request"); + assert!(matches!( + request.action, + super::StartupWindowControlAction::GetState + )); + + let response = super::StartupWindowControlResponse { is_maximized: true }; + assert_eq!( + serde_json::to_value(response).expect("serialize response"), + serde_json::json!({ "isMaximized": true }) + ); + } + #[cfg(target_os = "windows")] #[test] fn notification_body_click_activates_main_window_but_dismissal_does_not() { diff --git a/src/apps/desktop/src/appearance.rs b/src/apps/desktop/src/appearance.rs index 223aaa69d7..8f8403d43f 100644 --- a/src/apps/desktop/src/appearance.rs +++ b/src/apps/desktop/src/appearance.rs @@ -313,6 +313,7 @@ impl AppearanceConfig { "loadingApp": "Starting OpenBitFun...", "minimize": "Minimize", "maximize": "Maximize", + "restore": "Restore", "close": "Close", "petLoading": "Loading companion..." }), @@ -320,6 +321,7 @@ impl AppearanceConfig { "loadingApp": "正在啟動 OpenBitFun...", "minimize": "最小化", "maximize": "最大化", + "restore": "還原", "close": "關閉", "petLoading": "正在載入助手..." }), @@ -327,6 +329,7 @@ impl AppearanceConfig { "loadingApp": "正在启动 OpenBitFun...", "minimize": "最小化", "maximize": "最大化", + "restore": "还原", "close": "关闭", "petLoading": "正在加载助手..." }), diff --git a/src/apps/desktop/src/generated/startup_appearance_bootstrap.json b/src/apps/desktop/src/generated/startup_appearance_bootstrap.json index a643148993..2a0594d5a4 100644 --- a/src/apps/desktop/src/generated/startup_appearance_bootstrap.json +++ b/src/apps/desktop/src/generated/startup_appearance_bootstrap.json @@ -45,18 +45,18 @@ }, { "id": "openbitfun-midnight", - "bgPrimary": "#2b2d30", - "bgSecondary": "#1c1c1f", + "bgPrimary": "#1c1c1f", + "bgSecondary": "#313335", "bgScene": "#2b2d30", "isLight": false, "textPrimary": "#c8c8c8", - "textMuted": "#6f737a", + "textMuted": "#a1a1aa", "accentColor": "#60a5fa" }, { "id": "openbitfun-china-style", - "bgPrimary": "#faf8f0", - "bgSecondary": "#f5f3e8", + "bgPrimary": "#f0ede0", + "bgSecondary": "#faf8f0", "bgScene": "#faf8f0", "isLight": true, "textPrimary": "#1c1c1f", @@ -66,11 +66,11 @@ { "id": "openbitfun-china-night", "bgPrimary": "#1c1c1f", - "bgSecondary": "#212019", - "bgScene": "#1c1c1f", + "bgSecondary": "#313335", + "bgScene": "#262626", "isLight": false, "textPrimary": "#e8e8e8", - "textMuted": "#928f89", + "textMuted": "#a1a1aa", "accentColor": "#73a5cc" }, { @@ -86,11 +86,11 @@ { "id": "openbitfun-tokyo-night", "bgPrimary": "#1a1b26", - "bgSecondary": "#1e202e", - "bgScene": "#1a1b26", + "bgSecondary": "#2f334d", + "bgScene": "#24283b", "isLight": false, "textPrimary": "#c0caf5", - "textMuted": "#787c99", + "textMuted": "#9aa5ce", "accentColor": "#7aa2f7" } ] diff --git a/src/crates/assembly/core/src/agentic/tools/implementations/generated/appearance_prompt_snapshots.json b/src/crates/assembly/core/src/agentic/tools/implementations/generated/appearance_prompt_snapshots.json index 39660a4ad9..10e0cd9e3e 100644 --- a/src/crates/assembly/core/src/agentic/tools/implementations/generated/appearance_prompt_snapshots.json +++ b/src/crates/assembly/core/src/agentic/tools/implementations/generated/appearance_prompt_snapshots.json @@ -75,13 +75,13 @@ "id": "openbitfun-midnight", "mode": "dark", "bgPrimary": "#2b2d30", - "bgSecondary": "#1c1c1f", + "bgSecondary": "#313335", "bgScene": "#2b2d30", "textPrimary": "#c8c8c8", - "textMuted": "#6f737a", + "textMuted": "#a1a1aa", "accent500": "#60a5fa", "accent600": "#3b82f6", - "borderBase": "rgba(255, 255, 255, 0.14)", + "borderBase": "rgba(255, 255, 255, 0.18)", "elementBase": "rgba(255, 255, 255, 0.09)", "radiusBase": "8px", "spacing4": "16px", @@ -92,7 +92,7 @@ "id": "openbitfun-china-style", "mode": "light", "bgPrimary": "#faf8f0", - "bgSecondary": "#f5f3e8", + "bgSecondary": "#faf8f0", "bgScene": "#faf8f0", "textPrimary": "#1c1c1f", "textMuted": "#6a6a6a", @@ -108,15 +108,15 @@ { "id": "openbitfun-china-night", "mode": "dark", - "bgPrimary": "#1c1c1f", - "bgSecondary": "#212019", - "bgScene": "#1c1c1f", + "bgPrimary": "#262626", + "bgSecondary": "#313335", + "bgScene": "#262626", "textPrimary": "#e8e8e8", - "textMuted": "#928f89", + "textMuted": "#a1a1aa", "accent500": "#73a5cc", "accent600": "#5a8bb3", - "borderBase": "rgba(232, 232, 232, 0.16)", - "elementBase": "rgba(115, 165, 204, 0.12)", + "borderBase": "rgba(232, 232, 232, 0.2)", + "elementBase": "rgba(232, 232, 232, 0.1)", "radiusBase": "6px", "spacing4": "16px", "shadowBase": "0 4px 8px rgba(0, 0, 0, 0.65)", @@ -142,14 +142,14 @@ { "id": "openbitfun-tokyo-night", "mode": "dark", - "bgPrimary": "#1a1b26", - "bgSecondary": "#1e202e", - "bgScene": "#1a1b26", + "bgPrimary": "#24283b", + "bgSecondary": "#2f334d", + "bgScene": "#24283b", "textPrimary": "#c0caf5", - "textMuted": "#787c99", + "textMuted": "#9aa5ce", "accent500": "#7aa2f7", "accent600": "#6183bb", - "borderBase": "rgba(51, 65, 85, 0.6)", + "borderBase": "rgba(169, 177, 214, 0.26)", "elementBase": "rgba(122, 162, 247, 0.11)", "radiusBase": "6px", "spacing4": "16px", diff --git a/src/web-ui/AGENTS.md b/src/web-ui/AGENTS.md index 6ce6242ec3..30003e453c 100644 --- a/src/web-ui/AGENTS.md +++ b/src/web-ui/AGENTS.md @@ -89,3 +89,10 @@ an intent-review inventory, not a pass/fail gate; do not mechanically replace deliberate layout transitions or animate virtualized content. Rely on CI for full lint, build, and broad test coverage unless the local change specifically needs it. + +For Session selection, presentation synchronization, and scene lifetime changes, +also run the focused state contracts: + +```bash +pnpm --dir src/web-ui run test:run src/app/services/sessionSceneLifecycle.test.ts src/flow_chat/services/sessionActivation.test.ts src/flow_chat/services/storeSync.test.ts src/app/stores/sceneStore.test.ts +``` diff --git a/src/web-ui/index.html b/src/web-ui/index.html index fa0e43728d..570fd1a68e 100644 --- a/src/web-ui/index.html +++ b/src/web-ui/index.html @@ -16,6 +16,7 @@ loadingApp: 'Starting OpenBitFun...', minimize: 'Minimize', maximize: 'Maximize', + restore: 'Restore', close: 'Close', petLoading: 'Loading companion...' }, @@ -23,6 +24,7 @@ loadingApp: '正在启动 OpenBitFun...', minimize: '最小化', maximize: '最大化', + restore: '还原', close: '关闭', petLoading: '正在加载助手...' }, @@ -30,6 +32,7 @@ loadingApp: '正在啟動 OpenBitFun...', minimize: '最小化', maximize: '最大化', + restore: '還原', close: '關閉', petLoading: '正在載入助手...' } @@ -70,6 +73,8 @@ })(); + +