Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions docs/architecture/session-projection.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
12 changes: 12 additions & 0 deletions docs/architecture/theme-token-optimization.md
Original file line number Diff line number Diff line change
Expand Up @@ -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-*`
运行时变量均已退休并从源码删除。不得为第三方包、旧组件或测试重新引入这些接口。

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9765,7 +9765,7 @@
"visibility": "implementation",
"rustPath": "api::system_api::startup_window_control",
"sourceFile": "src/apps/desktop/src/api/system_api.rs",
"signature": "fn startup_window_control( state: State<'_, AppState>, startup_trace: State<'_, DesktopStartupTrace>, app: tauri::AppHandle, request: StartupWindowControlRequest, ) -> Result<(), String>",
"signature": "fn startup_window_control( state: State<'_, AppState>, startup_trace: State<'_, DesktopStartupTrace>, app: tauri::AppHandle, request: StartupWindowControlRequest, ) -> Result<StartupWindowControlResponse, String>",
"remoteWorkspacePolicy": "LocalOnly"
},
{
Expand Down
8 changes: 4 additions & 4 deletions scripts/theme-color-governance-baseline.json
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
15 changes: 3 additions & 12 deletions scripts/theme-color-near-pair-decisions.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,25 +8,16 @@
"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",
"domain": "themePreset",
"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."
},
{
Expand Down
32 changes: 29 additions & 3 deletions src/apps/desktop/src/api/system_api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -591,6 +591,7 @@ pub struct ToggleMainWindowFullscreenResponse {
#[derive(Debug, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum StartupWindowControlAction {
GetState,
Minimize,
ToggleMaximize,
Close,
Expand All @@ -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,
Expand Down Expand Up @@ -720,19 +727,20 @@ pub async fn startup_window_control(
startup_trace: State<'_, DesktopStartupTrace>,
app: tauri::AppHandle,
request: StartupWindowControlRequest,
) -> Result<(), String> {
) -> Result<StartupWindowControlResponse, String> {
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)
Expand All @@ -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
Expand Down Expand Up @@ -771,7 +780,7 @@ pub async fn startup_window_control(
}
}

Ok(())
Ok(StartupWindowControlResponse { is_maximized })
}

/// Toggle OS-level fullscreen for the Desktop main window.
Expand Down Expand Up @@ -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() {
Expand Down
3 changes: 3 additions & 0 deletions src/apps/desktop/src/appearance.rs
Original file line number Diff line number Diff line change
Expand Up @@ -313,20 +313,23 @@ impl AppearanceConfig {
"loadingApp": "Starting OpenBitFun...",
"minimize": "Minimize",
"maximize": "Maximize",
"restore": "Restore",
"close": "Close",
"petLoading": "Loading companion..."
}),
"zh-TW" | "zh-Hant-TW" => serde_json::json!({
"loadingApp": "正在啟動 OpenBitFun...",
"minimize": "最小化",
"maximize": "最大化",
"restore": "還原",
"close": "關閉",
"petLoading": "正在載入助手..."
}),
_ => serde_json::json!({
"loadingApp": "正在启动 OpenBitFun...",
"minimize": "最小化",
"maximize": "最大化",
"restore": "还原",
"close": "关闭",
"petLoading": "正在加载助手..."
}),
Expand Down
22 changes: 11 additions & 11 deletions src/apps/desktop/src/generated/startup_appearance_bootstrap.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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"
},
{
Expand All @@ -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"
}
]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -92,7 +92,7 @@
"id": "openbitfun-china-style",
"mode": "light",
"bgPrimary": "#faf8f0",
"bgSecondary": "#f5f3e8",
"bgSecondary": "#faf8f0",
"bgScene": "#faf8f0",
"textPrimary": "#1c1c1f",
"textMuted": "#6a6a6a",
Expand All @@ -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)",
Expand All @@ -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",
Expand Down
7 changes: 7 additions & 0 deletions src/web-ui/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
```
Loading
Loading