diff --git a/Cargo.toml b/Cargo.toml index 5f6785bb88..9960803457 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -84,7 +84,7 @@ toml = "0.8" git2 = { version = "0.18", default-features = false, features = ["https", "vendored-libgit2"] } # Terminal -portable-pty = "0.8" +portable-pty = "0.9.0" vte = "0.15.0" # Grep (search) @@ -101,14 +101,16 @@ similar = "2.5" urlencoding = "2.1" # Tauri (desktop only) - tauri = { version = "2", features = ["unstable"] } + tauri = { git = "https://github.com/richerfu/tauri", branch = "feat/open-harmony", features = [] } +tauri-plugin-dialog = "2" tauri-plugin-opener = "2" -tauri-plugin-dialog = "2.6" tauri-plugin-fs = "2" tauri-plugin-log = "2" tauri-plugin-autostart = "2" tauri-plugin-notification = "2" -tauri-build = { version = "2", features = [] } +tauri-build = { git = "https://github.com/richerfu/tauri", branch = "feat/open-harmony", features = [] } +napi-ohos = { version = "1.1" } +napi-derive-ohos = { version = "1.1" } # Windows-specific dependencies win32job = "2.0" @@ -150,3 +152,13 @@ lto = false codegen-units = 16 strip = false incremental = true + +[patch.crates-io] +wry = { git = "https://github.com/richerfu/wry"} +tao = { git = "https://github.com/richerfu/tao", branch = "feat-ohos-webview"} +openharmony-ability = {git = "https://github.com/harmony-contrib/openharmony-ability.git"} +openharmony-ability-derive = {git = "https://github.com/harmony-contrib/openharmony-ability.git"} +tauri = { git = "https://github.com/richerfu/tauri", branch = "feat/open-harmony"} +tauri-runtime = { git = "https://github.com/richerfu/tauri", branch = "feat/open-harmony"} +tauri-macros = { git = "https://github.com/richerfu/tauri", branch = "feat/open-harmony"} +tauri-runtime-wry = { git = "https://github.com/richerfu/tauri", branch = "feat/open-harmony"} \ No newline at end of file diff --git a/src/apps/desktop/Cargo.toml b/src/apps/desktop/Cargo.toml index e42f69dbf5..0213818c83 100644 --- a/src/apps/desktop/Cargo.toml +++ b/src/apps/desktop/Cargo.toml @@ -29,8 +29,8 @@ tauri-plugin-opener = { workspace = true } tauri-plugin-dialog = { workspace = true } tauri-plugin-fs = { workspace = true } tauri-plugin-log = { workspace = true } -tauri-plugin-autostart = { workspace = true } -tauri-plugin-notification = { workspace = true } +napi-ohos = { workspace = true } +napi-derive-ohos = { workspace = true } # Inherited from workspace tokio = { workspace = true } @@ -41,7 +41,6 @@ log = { workspace = true } chrono = { workspace = true } regex = { workspace = true } dirs = { workspace = true } -dark-light = { workspace = true } similar = { workspace = true } ignore = { workspace = true } urlencoding = { workspace = true } @@ -50,8 +49,6 @@ thiserror = "1.0" futures = { workspace = true } async-trait = { workspace = true } sha1 = { workspace = true } -screenshots = "0.8" -enigo = "0.2" image = { version = "0.24", default-features = false, features = ["png", "jpeg"] } resvg = { version = "0.47.0", default-features = false } @@ -79,6 +76,5 @@ windows = { version = "0.61.3", features = [ ] } windows-core = "0.61.2" -[target.'cfg(target_os = "linux")'.dependencies] -atspi = "0.29" +[target.'cfg(all(target_os = "linux", not(target_env = "ohos")))'.dependencies] leptess = "0.14.0" diff --git a/src/apps/desktop/capabilities/browser-webview.json b/src/apps/desktop/capabilities/browser-webview.json deleted file mode 100644 index 5fcbaf8260..0000000000 --- a/src/apps/desktop/capabilities/browser-webview.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "$schema": "../gen/schemas/desktop-schema.json", - "identifier": "browser-webview", - "description": "Minimal permissions for embedded browser webviews to emit events back to the host", - "webviews": ["embedded-browser-*"], - "local": true, - "remote": { - "urls": ["https://*", "https://*:*", "http://*", "http://*:*"] - }, - "permissions": [ - "core:event:allow-emit" - ] -} diff --git a/src/apps/desktop/capabilities/default.json b/src/apps/desktop/capabilities/default.json index 9455809033..0f22e9753c 100644 --- a/src/apps/desktop/capabilities/default.json +++ b/src/apps/desktop/capabilities/default.json @@ -1,109 +1,108 @@ { - "$schema": "../gen/schemas/desktop-schema.json", - "identifier": "default", - "description": "BitFun default capabilities", - "windows": ["main"], - "permissions": [ - "log:default", - "autostart:default", - "core:default", - "core:path:default", - "core:event:default", - "core:event:allow-listen", - "core:event:allow-emit", - "core:window:default", - "core:webview:default", - "core:webview:allow-create-webview", - "core:webview:allow-set-webview-position", - "core:webview:allow-set-webview-size", - "core:webview:allow-set-webview-focus", - "core:webview:allow-reparent", - "core:webview:allow-webview-show", - "core:webview:allow-webview-hide", - "core:webview:allow-webview-close", - "core:window:allow-create", - "core:window:allow-set-focus", - "core:window:allow-set-always-on-top", - "core:window:allow-set-position", - "core:window:allow-set-size", - "core:window:allow-set-decorations", - "core:window:allow-set-title-bar-style", - "core:window:allow-set-skip-taskbar", - "core:window:allow-set-resizable", - "core:window:allow-current-monitor", - "core:window:allow-outer-position", - "core:window:allow-outer-size", - "core:window:allow-is-maximized", - "core:window:allow-center", - "core:window:allow-close", - "core:window:allow-hide", - "core:window:allow-maximize", - "core:window:allow-minimize", - "core:window:allow-show", - "core:window:allow-start-dragging", - "core:window:allow-unmaximize", - "core:window:allow-unminimize", - "core:window:allow-set-min-size", - "dialog:default", - "dialog:allow-open", - "dialog:allow-save", - "dialog:allow-ask", - "dialog:allow-confirm", - "dialog:allow-message", - "opener:default", - "opener:allow-open-url", - { - "identifier": "opener:allow-open-path", - "allow": [ - { "path": "$APPDATA/**" }, - { "path": "$HOME/**" } - ] - }, - "opener:allow-reveal-item-in-dir", - "fs:default", - "fs:allow-read-file", - "fs:allow-write-file", - "fs:allow-read-dir", - "fs:allow-copy-file", - "fs:allow-create", - "fs:allow-mkdir", - "fs:allow-remove", - "fs:allow-rename", - "fs:allow-exists", - "fs:allow-read-text-file", - "fs:allow-write-text-file", - "fs:allow-stat", - "fs:allow-lstat", - "fs:allow-fstat", - "fs:allow-truncate", - "fs:allow-ftruncate", - "fs:allow-open", - "fs:allow-read", - "fs:allow-write", - "fs:allow-seek", - "fs:allow-unwatch", - "fs:allow-watch", - "fs:read-all", - "fs:write-all", - "fs:scope-app-recursive", - { - "identifier": "fs:allow-home-read-recursive", - "allow": [ - { "path": "$HOME/**" } - ] - }, - { - "identifier": "fs:allow-home-write-recursive", - "allow": [ - { "path": "$HOME/**" } - ] - }, - "notification:default", - "notification:allow-notify", - "notification:allow-show", - "notification:allow-request-permission", - "notification:allow-check-permissions", - "notification:allow-permission-state", - "notification:allow-is-permission-granted" - ] -} + "$schema": "../gen/schemas/desktop-schema.json", + "identifier": "default", + "description": "BitFun default capabilities", + "windows": ["main"], + "permissions": [ + "log:default", + "core:default", + "core:path:default", + "core:event:default", + "core:event:allow-listen", + "core:event:allow-emit", + "core:window:default", + "core:webview:default", + "core:webview:allow-create-webview", + "core:webview:allow-set-webview-position", + "core:webview:allow-set-webview-size", + "core:webview:allow-set-webview-focus", + "core:webview:allow-reparent", + "core:webview:allow-webview-show", + "core:webview:allow-webview-hide", + "core:webview:allow-webview-close", + "core:window:allow-create", + "core:window:allow-set-focus", + "core:window:allow-set-always-on-top", + "core:window:allow-set-position", + "core:window:allow-set-size", + "core:window:allow-set-decorations", + "core:window:allow-set-title-bar-style", + "core:window:allow-set-skip-taskbar", + "core:window:allow-set-resizable", + "core:window:allow-current-monitor", + "core:window:allow-outer-position", + "core:window:allow-outer-size", + "core:window:allow-is-maximized", + "core:window:allow-center", + "core:window:allow-close", + "core:window:allow-hide", + "core:window:allow-maximize", + "core:window:allow-minimize", + "core:window:allow-show", + "core:window:allow-start-dragging", + "core:window:allow-unmaximize", + "core:window:allow-unminimize", + "core:window:allow-set-min-size", + "dialog:default", + "dialog:allow-open", + "dialog:allow-save", + "dialog:allow-ask", + "dialog:allow-confirm", + "dialog:allow-message", + "opener:default", + "opener:allow-open-url", + { + "identifier": "opener:allow-open-path", + "allow": [ + { "path": "$APPDATA/**" }, + { "path": "$HOME/**" } + ] + }, + "opener:allow-reveal-item-in-dir", + "fs:default", + "fs:allow-read-file", + "fs:allow-write-file", + "fs:allow-read-dir", + "fs:allow-copy-file", + "fs:allow-create", + "fs:allow-mkdir", + "fs:allow-remove", + "fs:allow-rename", + "fs:allow-exists", + "fs:allow-read-text-file", + "fs:allow-write-text-file", + "fs:allow-stat", + "fs:allow-lstat", + "fs:allow-fstat", + "fs:allow-truncate", + "fs:allow-ftruncate", + "fs:allow-open", + "fs:allow-read", + "fs:allow-write", + "fs:allow-seek", + "fs:allow-unwatch", + "fs:allow-watch", + "fs:read-all", + "fs:write-all", + "fs:scope-app-recursive", + { + "identifier": "fs:allow-home-read-recursive", + "allow": [ + { "path": "$HOME/**" } + ] + }, + { + "identifier": "fs:allow-home-write-recursive", + "allow": [ + { "path": "$HOME/**" } + ] + }, + "notification:default", + "notification:allow-notify", + "notification:allow-show", + "notification:allow-request-permission", + "notification:allow-check-permissions", + "notification:allow-permission-state", + "notification:allow-is-permission-granted" + ] +} \ No newline at end of file diff --git a/src/apps/desktop/src/api/browser_api.rs b/src/apps/desktop/src/api/browser_api.rs index b0189e156c..b204f7f78a 100644 --- a/src/apps/desktop/src/api/browser_api.rs +++ b/src/apps/desktop/src/api/browser_api.rs @@ -20,13 +20,7 @@ pub async fn browser_webview_eval( app: tauri::AppHandle, request: WebviewEvalRequest, ) -> Result<(), String> { - let webview = app - .get_webview(&request.label) - .ok_or_else(|| format!("Webview not found: {}", request.label))?; - - webview - .eval(&request.script) - .map_err(|e| format!("eval failed: {e}")) + Err("Webview not found".to_string()) } #[derive(Debug, Deserialize)] @@ -45,15 +39,5 @@ pub async fn browser_get_url( app: tauri::AppHandle, request: WebviewLabelRequest, ) -> Result { - let webview = app - .get_webview(&request.label) - .ok_or_else(|| format!("Webview not found: {}", request.label))?; - - let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| webview.url())); - - match result { - Ok(Ok(url)) => Ok(url.to_string()), - Ok(Err(e)) => Err(format!("url failed: {e}")), - Err(_) => Err("url unavailable (webview URL is nil)".to_string()), - } + Err("Url is unavailable (webview URL is nil)".to_string()) } diff --git a/src/apps/desktop/src/api/remote_connect_api.rs b/src/apps/desktop/src/api/remote_connect_api.rs index 4634d11c4a..83cb1aeabb 100644 --- a/src/apps/desktop/src/api/remote_connect_api.rs +++ b/src/apps/desktop/src/api/remote_connect_api.rs @@ -33,7 +33,7 @@ pub fn set_mobile_web_resource_path(path: PathBuf) { /// and restore any previously paired bot connections. Without this, bots /// only start listening after the user first opens the Remote Connect dialog. pub fn init_on_startup() { - tokio::spawn(async { + tauri::async_runtime::spawn(async { if let Err(e) = ensure_service().await { log::warn!("Remote connect startup init failed: {e}"); } diff --git a/src/apps/desktop/src/api/system_api.rs b/src/apps/desktop/src/api/system_api.rs index 975840019d..6281b606fe 100644 --- a/src/apps/desktop/src/api/system_api.rs +++ b/src/apps/desktop/src/api/system_api.rs @@ -180,11 +180,5 @@ pub async fn send_system_notification( app: tauri::AppHandle, request: SendNotificationRequest, ) -> Result<(), String> { - use tauri_plugin_notification::NotificationExt; - - let mut builder = app.notification().builder().title(&request.title); - if let Some(body) = &request.body { - builder = builder.body(body); - } - builder.show().map_err(|e| e.to_string()) + Err("No notification provided".to_string()) } diff --git a/src/apps/desktop/src/api/terminal_api.rs b/src/apps/desktop/src/api/terminal_api.rs index abd0b8e634..4995952d98 100644 --- a/src/apps/desktop/src/api/terminal_api.rs +++ b/src/apps/desktop/src/api/terminal_api.rs @@ -828,7 +828,7 @@ pub async fn terminal_get_history( } pub fn start_terminal_event_loop(terminal_state: TerminalState, app_handle: AppHandle) { - tokio::spawn(async move { + tauri::async_runtime::spawn(async move { let api = match terminal_state.get_or_init_api().await { Ok(api) => api, Err(e) => { diff --git a/src/apps/desktop/src/computer_use/desktop_host.rs b/src/apps/desktop/src/computer_use/desktop_host.rs index 1a84db911c..7477f6d1aa 100644 --- a/src/apps/desktop/src/computer_use/desktop_host.rs +++ b/src/apps/desktop/src/computer_use/desktop_host.rs @@ -1,5 +1,6 @@ //! Cross-platform `ComputerUseHost` via `screenshots` + `enigo`. +use anyhow::anyhow; use async_trait::async_trait; use bitfun_core::agentic::tools::computer_use_host::{ clamp_point_crop_half_extent, ActionRecord, AppClickParams, AppInfo, AppSelector, @@ -21,7 +22,6 @@ use bitfun_core::agentic::tools::computer_use_host::{ }; use bitfun_core::agentic::tools::computer_use_optimizer::ComputerUseOptimizer; use bitfun_core::util::errors::{BitFunError, BitFunResult}; -use enigo::{Axis, Button, Coordinate, Direction, Enigo, Key, Keyboard, Mouse, Settings}; use image::codecs::jpeg::JpegEncoder; use image::{DynamicImage, Rgb, RgbImage}; use log::{debug, info, warn}; @@ -49,7 +49,6 @@ const VISION_PIXEL_NUDGE_AFTER_SCREENSHOT_MSG: &str = "Computer use refused: do #[derive(Debug, Clone)] struct ScreenshotCacheEntry { rgba: image::RgbaImage, - screen: Screen, capture_time: Instant, } @@ -255,43 +254,6 @@ fn implicit_confirmation_should_apply( true } -fn global_to_native_full_pixel_center( - gx: f64, - gy: f64, - native_w: u32, - native_h: u32, - d: &DisplayInfo, -) -> (u32, u32) { - #[cfg(target_os = "macos")] - { - let geo = MacPointerGeo::from_display(native_w, native_h, d); - let lx = gx - geo.disp_ox; - let ly = gy - geo.disp_oy; - if lx < 0.0 || lx >= geo.disp_w || ly < 0.0 || ly >= geo.disp_h { - return clamp_center_to_native(native_w / 2, native_h / 2, native_w, native_h); - } - let full_ix = ((lx / geo.disp_w) * geo.full_px_w as f64).floor() as u32; - let full_iy = ((ly / geo.disp_h) * geo.full_px_h as f64).floor() as u32; - clamp_center_to_native(full_ix, full_iy, native_w, native_h) - } - #[cfg(not(target_os = "macos"))] - { - let disp_w = d.width as f64; - let disp_h = d.height as f64; - if disp_w <= 0.0 || disp_h <= 0.0 || native_w == 0 || native_h == 0 { - return (0, 0); - } - let lx = gx - d.x as f64; - let ly = gy - d.y as f64; - if lx < 0.0 || lx >= disp_w || ly < 0.0 || ly >= disp_h { - return clamp_center_to_native(native_w / 2, native_h / 2, native_w, native_h); - } - let full_ix = ((lx / disp_w) * native_w as f64).floor() as u32; - let full_iy = ((ly / disp_h) * native_h as f64).floor() as u32; - clamp_center_to_native(full_ix, full_iy, native_w, native_h) - } -} - #[cfg(target_os = "macos")] #[allow(dead_code)] fn implicit_global_center_for_confirmation( @@ -1036,41 +998,6 @@ end tell"#]) } } - fn with_enigo(f: F) -> BitFunResult - where - F: FnOnce(&mut Enigo) -> BitFunResult, - { - Self::ensure_input_automation_allowed()?; - let settings = Settings::default(); - let mut enigo = - Enigo::new(&settings).map_err(|e| BitFunError::tool(format!("enigo init: {}", e)))?; - f(&mut enigo) - } - - /// Enigo on macOS uses Text Input Source / AppKit paths that must run on the main queue. - /// Tokio `spawn_blocking` threads are not main; dispatch there hits `dispatch_assert_queue_fail`. - /// - /// On macOS, the main-queue dispatch is also wrapped in an Objective-C - /// `@try/@catch` (via `objc2::exception::catch`) so that an `NSException` - /// thrown by TSM / HIToolbox / AppKit during keyboard or text input is - /// converted into a Rust error instead of propagating across the FFI - /// boundary as a "foreign exception" — which would otherwise cause Rust's - /// `catch_unwind` to abort the whole process (`SIGABRT`). - fn run_enigo_job(job: F) -> BitFunResult - where - F: FnOnce(&mut Enigo) -> BitFunResult + Send, - T: Send, - { - #[cfg(target_os = "macos")] - { - macos::run_on_main_for_enigo(move || Self::with_enigo(job)) - } - #[cfg(not(target_os = "macos"))] - { - Self::with_enigo(job) - } - } - /// Absolute pointer move in Quartz global **points** with full float precision (avoids enigo integer truncation). #[cfg(target_os = "macos")] fn post_mouse_moved_cg_global(x: f64, y: f64) -> BitFunResult<()> { @@ -1135,90 +1062,15 @@ end tell"#]) const MAX_STEPS: usize = 85; const MAX_DURATION_MS: u64 = 400; - Self::run_enigo_job(|e| { - let (cx, cy) = e.location().map_err(|err| { - BitFunError::tool(format!("smooth_mouse_move: pointer location: {}", err)) - })?; - let x0 = cx as f64; - let y0 = cy as f64; - let dx = x1 - x0; - let dy = y1 - y0; - let dist = (dx * dx + dy * dy).sqrt(); - if dist < MIN_DIST { - return e - .move_mouse(x1.round() as i32, y1.round() as i32, Coordinate::Abs) - .map_err(|err| BitFunError::tool(format!("mouse_move: {}", err))); - } - let duration_ms = (70.0 + dist * 0.28).min(MAX_DURATION_MS as f64) as u64; - let steps = ((dist / 5.5).ceil() as usize).clamp(MIN_STEPS, MAX_STEPS); - let step_delay = Duration::from_millis((duration_ms / steps as u64).max(1)); - - for i in 1..=steps { - let t = i as f64 / steps as f64; - let te = Self::smoothstep01(t); - let x = x0 + dx * te; - let y = y0 + dy * te; - e.move_mouse(x.round() as i32, y.round() as i32, Coordinate::Abs) - .map_err(|err| BitFunError::tool(format!("mouse_move: {}", err)))?; - if i < steps { - std::thread::sleep(step_delay); - } - } - Ok(()) - }) + Ok(()) } - fn map_button(s: &str) -> BitFunResult + ); })} diff --git a/src/web-ui/src/app/components/NewProjectDialog/NewProjectDialog.tsx b/src/web-ui/src/app/components/NewProjectDialog/NewProjectDialog.tsx index 316954193b..73852c63c4 100644 --- a/src/web-ui/src/app/components/NewProjectDialog/NewProjectDialog.tsx +++ b/src/web-ui/src/app/components/NewProjectDialog/NewProjectDialog.tsx @@ -12,7 +12,7 @@ import { Check, X } from 'lucide-react'; -import { open } from '@tauri-apps/plugin-dialog'; +// import { open } from '@tauri-apps/plugin-dialog'; import { useTranslation } from 'react-i18next'; import { createLogger } from '@/shared/utils/logger'; import { Modal, Button, Input } from '@/component-library'; @@ -49,13 +49,7 @@ export const NewProjectDialog: React.FC = ({ // Open directory picker dialog const handleSelectParentPath = useCallback(async () => { try { - const selected = await open({ - directory: true, - multiple: false, - title: t('newProject.selectParentDirectory'), - defaultPath: parentPath || defaultParentPath - }) as string; - + let selected = "/data/storage/el2/base/files"; if (selected && typeof selected === 'string') { setParentPath(selected); setError(''); diff --git a/src/web-ui/src/app/components/RemoteConnectDialog/remoteConnectDisclaimerStorage.ts b/src/web-ui/src/app/components/RemoteConnectDialog/remoteConnectDisclaimerStorage.ts index 49cd6fdc4e..9c0a0b2f73 100644 --- a/src/web-ui/src/app/components/RemoteConnectDialog/remoteConnectDisclaimerStorage.ts +++ b/src/web-ui/src/app/components/RemoteConnectDialog/remoteConnectDisclaimerStorage.ts @@ -1,8 +1,10 @@ +import { storage } from "@/shared"; + export const REMOTE_CONNECT_DISCLAIMER_KEY = 'bitfun:remote-connect:disclaimer-agreed:v1'; export const getRemoteConnectDisclaimerAgreed = (): boolean => { try { - return localStorage.getItem(REMOTE_CONNECT_DISCLAIMER_KEY) === 'true'; + return storage.getItem(REMOTE_CONNECT_DISCLAIMER_KEY) === 'true'; } catch { return false; } @@ -10,7 +12,7 @@ export const getRemoteConnectDisclaimerAgreed = (): boolean => { export const setRemoteConnectDisclaimerAgreed = (): void => { try { - localStorage.setItem(REMOTE_CONNECT_DISCLAIMER_KEY, 'true'); + storage.setItem(REMOTE_CONNECT_DISCLAIMER_KEY, 'true'); } catch { // Ignore storage failures and fall back to in-memory state. } diff --git a/src/web-ui/src/app/layout/AppLayout.tsx b/src/web-ui/src/app/layout/AppLayout.tsx index e1a1081190..2242a2e38a 100644 --- a/src/web-ui/src/app/layout/AppLayout.tsx +++ b/src/web-ui/src/app/layout/AppLayout.tsx @@ -17,6 +17,7 @@ import { useApp } from '../hooks/useApp'; import { useSceneStore } from '../stores/sceneStore'; import { useShortcut } from '@/infrastructure/hooks/useShortcut'; import { configManager } from '@/infrastructure/config'; +import { sessionStorageAdapter } from '@/shared'; type TransitionDirection = 'entering' | 'returning' | null; import { FlowChatManager } from '../../flow_chat/services/FlowChatManager'; @@ -191,10 +192,10 @@ const AppLayout: React.FC = ({ className = '' }) => { // Always initialize FlowChat so historical sessions list even when SSH is not connected yet. try { const explicitPreferredMode = - sessionStorage.getItem('bitfun:flowchat:preferredMode') || + sessionStorageAdapter.getItem('bitfun:flowchat:preferredMode') || undefined; if (explicitPreferredMode) { - sessionStorage.removeItem('bitfun:flowchat:preferredMode'); + sessionStorageAdapter.removeItem('bitfun:flowchat:preferredMode'); } const initializationPreferredMode = @@ -229,9 +230,9 @@ const AppLayout: React.FC = ({ className = '' }) => { ensureAssistantBootstrapForWorkspace(currentWorkspace, activeSessionId); } - const pendingDescription = sessionStorage.getItem('pendingProjectDescription'); + const pendingDescription = sessionStorageAdapter.getItem('pendingProjectDescription'); if (pendingDescription && pendingDescription.trim()) { - sessionStorage.removeItem('pendingProjectDescription'); + sessionStorageAdapter.removeItem('pendingProjectDescription'); setTimeout(async () => { try { @@ -257,9 +258,9 @@ const AppLayout: React.FC = ({ className = '' }) => { }, 500); } - const pendingSettings = sessionStorage.getItem('pendingOpenSettings'); + const pendingSettings = sessionStorageAdapter.getItem('pendingOpenSettings'); if (pendingSettings) { - sessionStorage.removeItem('pendingOpenSettings'); + sessionStorageAdapter.removeItem('pendingOpenSettings'); setTimeout(async () => { try { const { quickActions } = await import('@/shared/services/ide-control'); diff --git a/src/web-ui/src/app/layout/panelConfig.ts b/src/web-ui/src/app/layout/panelConfig.ts index 3c2f074879..40c730f407 100644 --- a/src/web-ui/src/app/layout/panelConfig.ts +++ b/src/web-ui/src/app/layout/panelConfig.ts @@ -13,6 +13,7 @@ * - expanded: expanded mode (wide content, more information) */ +import { storage } from '@/shared'; import { createLogger } from '@/shared/utils/logger'; const log = createLogger('PanelConfig'); @@ -194,7 +195,7 @@ export const STORAGE_KEYS = { */ export function savePanelWidth(key: string, width: number): void { try { - localStorage.setItem(key, String(width)); + storage.setItem(key, String(width)); } catch (e) { log.warn('Failed to save panel width', { key, width, error: e }); } @@ -205,7 +206,7 @@ export function savePanelWidth(key: string, width: number): void { */ export function loadPanelWidth(key: string, defaultValue: number): number { try { - const stored = localStorage.getItem(key); + const stored = storage.getItem(key); if (stored) { const parsed = parseInt(stored, 10); if (!isNaN(parsed) && parsed > 0) { diff --git a/src/web-ui/src/app/scenes/welcome/WelcomeScene.tsx b/src/web-ui/src/app/scenes/welcome/WelcomeScene.tsx index ca5ac675f5..2f42d1ac85 100644 --- a/src/web-ui/src/app/scenes/welcome/WelcomeScene.tsx +++ b/src/web-ui/src/app/scenes/welcome/WelcomeScene.tsx @@ -55,12 +55,7 @@ const WelcomeScene: React.FC = () => { const handleOpenFolder = useCallback(async () => { try { setIsSelecting(true); - const { open } = await import('@tauri-apps/plugin-dialog'); - const selected = await open({ - directory: true, - multiple: false, - title: t('startup.selectWorkspaceDirectory'), - }); + let selected = "/data/storage/el2/base/files/test"; if (selected && typeof selected === 'string') { await openWorkspace(selected); openScene('session' as SceneTabId); diff --git a/src/web-ui/src/app/services/AppManager.ts b/src/web-ui/src/app/services/AppManager.ts index 430e511eec..e72726408f 100644 --- a/src/web-ui/src/app/services/AppManager.ts +++ b/src/web-ui/src/app/services/AppManager.ts @@ -18,6 +18,7 @@ import { import { globalEventBus } from '../../infrastructure/event-bus'; import { createLogger } from '@/shared/utils/logger'; import { i18nService } from '@/infrastructure/i18n'; +import { storage } from '@/shared'; const log = createLogger('AppManager'); @@ -351,13 +352,13 @@ export class AppManager implements IAppManager { private clearPersistedPanelState(): void { try { // Clear AppManager persisted state - localStorage.removeItem('bitfun-app-state'); + storage.removeItem('bitfun-app-state'); // Clear other potential panel state keys - localStorage.removeItem('BitFun-left-panel-width'); - localStorage.removeItem('BitFun-left-panel-collapsed'); - localStorage.removeItem('BitFun-right-panel-collapsed'); - localStorage.removeItem('right-panel-collapsed'); + storage.removeItem('BitFun-left-panel-width'); + storage.removeItem('BitFun-left-panel-collapsed'); + storage.removeItem('BitFun-right-panel-collapsed'); + storage.removeItem('right-panel-collapsed'); } catch (error) { log.warn('Failed to clear persisted panel state', error); } diff --git a/src/web-ui/src/flow_chat/components/ChatInput.tsx b/src/web-ui/src/flow_chat/components/ChatInput.tsx index 818770cc83..74780f51cd 100644 --- a/src/web-ui/src/flow_chat/components/ChatInput.tsx +++ b/src/web-ui/src/flow_chat/components/ChatInput.tsx @@ -58,6 +58,7 @@ import { useDeepReviewConsent } from './DeepReviewConsentDialog'; import { useSessionReviewActivity } from '../hooks/useSessionReviewActivity'; import { shouldBlockDeepReviewCommand } from '../utils/deepReviewCommandGuard'; import './ChatInput.scss'; +import { sessionStorageAdapter } from '@/shared'; const log = createLogger('ChatInput'); @@ -782,7 +783,7 @@ export const ChatInput: React.FC = ({ log.debug('Session switched, syncing mode', { sessionId, mode }); dispatchMode({ type: 'SET_CURRENT_MODE', payload: mode }); try { - sessionStorage.setItem('bitfun:flowchat:lastMode', mode); + sessionStorageAdapter.setItem('bitfun:flowchat:lastMode', mode); } catch { // ignore } @@ -812,7 +813,7 @@ export const ChatInput: React.FC = ({ }); dispatchMode({ type: 'SET_CURRENT_MODE', payload: nextMode }); try { - sessionStorage.setItem('bitfun:flowchat:lastMode', nextMode); + sessionStorageAdapter.setItem('bitfun:flowchat:lastMode', session.mode); } catch { // ignore } @@ -1700,7 +1701,7 @@ export const ChatInput: React.FC = ({ }); try { - sessionStorage.setItem('bitfun:flowchat:lastMode', modeId); + sessionStorageAdapter.setItem('bitfun:flowchat:lastMode', modeId); } catch { // ignore } diff --git a/src/web-ui/src/flow_chat/components/WelcomePanel.tsx b/src/web-ui/src/flow_chat/components/WelcomePanel.tsx index 2283a6ab86..a3071db858 100644 --- a/src/web-ui/src/flow_chat/components/WelcomePanel.tsx +++ b/src/web-ui/src/flow_chat/components/WelcomePanel.tsx @@ -151,9 +151,8 @@ export const WelcomePanel: React.FC = ({ try { setWorkspaceDropdownOpen(false); setIsSelectingWorkspace(true); - const { open } = await import('@tauri-apps/plugin-dialog'); - const selected = await open({ directory: true, multiple: false }); - if (selected && typeof selected === 'string') await openWorkspace(selected); + let path_manager = "/data/storage/el2/base/files/test"; + await openWorkspace(path_manager); } catch (err) { log.warn('Failed to open workspace folder', err); } finally { diff --git a/src/web-ui/src/flow_chat/store/FlowChatStore.ts b/src/web-ui/src/flow_chat/store/FlowChatStore.ts index 7545a07757..48cff3bba0 100644 --- a/src/web-ui/src/flow_chat/store/FlowChatStore.ts +++ b/src/web-ui/src/flow_chat/store/FlowChatStore.ts @@ -40,6 +40,7 @@ import { import type { WorkspaceInfo } from '@/shared/types'; import { sessionBelongsToWorkspaceNavRow } from '../utils/sessionOrdering'; import { sessionMatchesWorkspace } from '../utils/workspaceScope'; +import { storage } from '@/shared'; const log = createLogger('FlowChatStore'); @@ -47,7 +48,7 @@ export class FlowChatStore { private static instance: FlowChatStore; private state: FlowChatState; private listeners: Set<(state: FlowChatState) => void> = new Set(); - + private silentMode = false; private constructor() { @@ -65,18 +66,23 @@ export class FlowChatStore { 'bitfun-flow-chat-global', 'bitfun-session-ids' ]; - + keysToRemove.forEach(key => { - if (localStorage.getItem(key)) { - localStorage.removeItem(key); + if (storage.getItem(key)) { + storage.removeItem(key); } }); + try { + const keys = storage.getKeys(); + keys.forEach(key => { + if (key.startsWith('bitfun-session-')) { + storage.removeItem(key); + } + }) + } catch (e) { + log.warn("Failed to clear session key"); + } - Object.keys(localStorage).forEach(key => { - if (key.startsWith('bitfun-session-')) { - localStorage.removeItem(key); - } - }); } catch (error) { log.warn('Failed to clear old storage data', error); } @@ -97,7 +103,7 @@ export class FlowChatStore { public setState(updater: (prevState: FlowChatState) => FlowChatState): void { const newState = updater(this.state); this.state = newState; - + if (!this.silentMode) { this.listeners.forEach(listener => { try { @@ -108,7 +114,7 @@ export class FlowChatStore { }); } } - + /** * Silent state update (does not trigger listeners) * Used for batch updates, call notifyListeners() after completion @@ -122,7 +128,7 @@ export class FlowChatStore { this.silentMode = prevSilentMode; } } - + /** * Manually notify all listeners (call after batch updates complete) */ @@ -135,11 +141,11 @@ export class FlowChatStore { } }); } - + public beginSilentMode(): void { this.silentMode = true; } - + public endSilentMode(): void { this.silentMode = false; this.notifyListeners(); @@ -211,7 +217,7 @@ export class FlowChatStore { import('../state-machine').then(({ stateMachineManager }) => { stateMachineManager.getOrCreate(sessionId); }); - + this.setState(prev => { const relationship = normalizeSessionRelationship({ sessionKind: 'normal' }); const titleState = deriveSessionTitleState(titleDescriptor); @@ -316,18 +322,18 @@ export class FlowChatStore { public switchSession(sessionId: string): void { let sessionMode: string | undefined; - + this.setState(prev => { if (!prev.sessions.has(sessionId)) return prev; - + const session = prev.sessions.get(sessionId)!; sessionMode = session.mode; - + const updatedSession = { ...session, lastActiveAt: Date.now() }; - + const newSessions = new Map(prev.sessions); newSessions.set(sessionId, updatedSession); @@ -337,7 +343,7 @@ export class FlowChatStore { activeSessionId: sessionId }; }); - + window.dispatchEvent(new CustomEvent('bitfun:session-switched', { detail: { sessionId, mode: sessionMode || 'agentic' } })); @@ -832,7 +838,7 @@ export class FlowChatStore { const session = prev.sessions.get(sessionId); if (!session) return prev; - const updatedDialogTurns = session.dialogTurns.map(turn => + const updatedDialogTurns = session.dialogTurns.map(turn => turn.id === dialogTurnId ? updater(turn) : turn ); @@ -856,8 +862,8 @@ export class FlowChatStore { * Add image analysis phase to dialog turn */ public addImageAnalysisPhase( - sessionId: string, - dialogTurnId: string, + sessionId: string, + dialogTurnId: string, imageContexts: import('@/shared/types/context').ImageContext[] ): void { this.updateDialogTurn(sessionId, dialogTurnId, turn => { @@ -890,11 +896,11 @@ export class FlowChatStore { dialogTurnId: string, results: ImageAnalysisResult[] ): void { - this.updateDialogTurn(sessionId, dialogTurnId, turn => { - if (!turn.imageAnalysisPhase) { - log.warn('Attempting to update non-existent image analysis phase', { sessionId, dialogTurnId }); - return turn; - } + this.updateDialogTurn(sessionId, dialogTurnId, turn => { + if (!turn.imageAnalysisPhase) { + log.warn('Attempting to update non-existent image analysis phase', { sessionId, dialogTurnId }); + return turn; + } const updatedItems: FlowImageAnalysisItem[] = turn.imageAnalysisPhase.items.map(item => { const result = results.find(r => r.image_id === item.imageContext.id); @@ -963,7 +969,7 @@ export class FlowChatStore { public updateModelRound(sessionId: string, dialogTurnId: string, modelRoundId: string, updater: (round: ModelRound) => ModelRound): void { this.updateDialogTurn(sessionId, dialogTurnId, turn => ({ ...turn, - modelRounds: turn.modelRounds.map(round => + modelRounds: turn.modelRounds.map(round => round.id === modelRoundId ? updater(round) : round ) })); @@ -973,12 +979,12 @@ export class FlowChatStore { * Batch update multiple model round items (reduces store update frequency) */ public batchUpdateModelRoundItems( - sessionId: string, - dialogTurnId: string, + sessionId: string, + dialogTurnId: string, updates: Array<{ itemId: string; changes: Partial }> ): void { if (updates.length === 0) return; - + this.updateDialogTurn(sessionId, dialogTurnId, turn => { const updatedModelRounds = turn.modelRounds.map(round => ({ ...round, @@ -987,7 +993,7 @@ export class FlowChatStore { return update ? ({ ...item, ...update.changes } as AnyFlowItem) : item; }) })); - + return { ...turn, modelRounds: updatedModelRounds @@ -998,18 +1004,18 @@ export class FlowChatStore { public addModelRoundItem(sessionId: string, dialogTurnId: string, item: AnyFlowItem, modelRoundId?: string): void { this.updateDialogTurn(sessionId, dialogTurnId, turn => { let targetModelRoundIndex = turn.modelRounds.length - 1; - if (modelRoundId) { - targetModelRoundIndex = turn.modelRounds.findIndex(round => round.id === modelRoundId); - if (targetModelRoundIndex === -1) { - log.warn('Model round not found', { sessionId, dialogTurnId, modelRoundId }); - return turn; - } - } - + if (modelRoundId) { + targetModelRoundIndex = turn.modelRounds.findIndex(round => round.id === modelRoundId); if (targetModelRoundIndex === -1) { - log.warn('No available model rounds', { sessionId, dialogTurnId }); + log.warn('Model round not found', { sessionId, dialogTurnId, modelRoundId }); return turn; } + } + + if (targetModelRoundIndex === -1) { + log.warn('No available model rounds', { sessionId, dialogTurnId }); + return turn; + } const targetModelRound = turn.modelRounds[targetModelRoundIndex]; @@ -1019,7 +1025,7 @@ export class FlowChatStore { } const updatedModelRounds = [...turn.modelRounds]; - + updatedModelRounds[targetModelRoundIndex] = { ...targetModelRound, items: [...targetModelRound.items, item] @@ -1057,7 +1063,7 @@ export class FlowChatStore { this.updateDialogTurn(sessionId, dialogTurnId, turn => { let parentRoundIndex = -1; let parentItemIndex = -1; - + for (let i = 0; i < turn.modelRounds.length; i++) { const itemIndex = turn.modelRounds[i].items.findIndex((item: any) => item.id === parentToolId); if (itemIndex !== -1) { @@ -1066,21 +1072,21 @@ export class FlowChatStore { break; } } - + if (parentRoundIndex === -1 || parentItemIndex === -1) { log.warn('Parent tool item not found', { sessionId, dialogTurnId, parentToolId }); return turn; } - + const targetModelRound = turn.modelRounds[parentRoundIndex]; - + const existingItem = targetModelRound.items.find((item: any) => item.id === newItem.id); if (existingItem) { return turn; } - + let insertIndex = parentItemIndex + 1; - + while (insertIndex < targetModelRound.items.length) { const currentItem = targetModelRound.items[insertIndex] as any; if (currentItem.parentTaskToolId === parentToolId && currentItem.isSubagentItem) { @@ -1089,19 +1095,19 @@ export class FlowChatStore { break; } } - + const updatedItems = [ ...targetModelRound.items.slice(0, insertIndex), newItem, ...targetModelRound.items.slice(insertIndex) ]; - + const updatedModelRounds = [...turn.modelRounds]; updatedModelRounds[parentRoundIndex] = { ...targetModelRound, items: updatedItems }; - + return { ...turn, modelRounds: updatedModelRounds @@ -1112,10 +1118,10 @@ export class FlowChatStore { public updateModelRoundItem(sessionId: string, dialogTurnId: string, itemId: string, updates: Partial): void { this.updateDialogTurn(sessionId, dialogTurnId, turn => { let updated = false; - + const updatedModelRounds = turn.modelRounds.map(modelRound => { if (updated) return modelRound; - + const updatedItems = modelRound.items.map((item: any) => { if (item.id === itemId) { const updatedItem = { ...item, ...updates }; @@ -1123,15 +1129,15 @@ export class FlowChatStore { } return item; }); - + if (updatedItems.some((item: any) => item.id === itemId)) { updated = true; return { ...modelRound, items: updatedItems }; } - + return modelRound; }); - + if (!updated) { log.warn('Item not found for update', { sessionId, dialogTurnId, itemId }); return turn; @@ -1179,7 +1185,7 @@ export class FlowChatStore { } public updateTokenUsage( - sessionId: string, + sessionId: string, tokenUsage: { inputTokens: number; outputTokens?: number; totalTokens: number } ): void { this.setState(prev => { @@ -1421,7 +1427,7 @@ export class FlowChatStore { } const turnIndex = session.dialogTurns.findIndex(t => t.id === turnId); - + const turnData = { turnId, turnIndex, @@ -1444,7 +1450,7 @@ export class FlowChatStore { timestamp: item.timestamp, status: item.status, })); - + const toolItems = round.items .filter(item => item.type === 'tool') .map(item => ({ @@ -1457,11 +1463,11 @@ export class FlowChatStore { startTime: (item as any).startTime || item.timestamp, endTime: (item as any).endTime, status: item.status, - durationMs: (item as any).endTime - ? (item as any).endTime - (item as any).startTime + durationMs: (item as any).endTime + ? (item as any).endTime - (item as any).startTime : undefined })); - + const thinkingItems = round.items .filter(item => item.type === 'thinking') .map(item => ({ @@ -1472,7 +1478,7 @@ export class FlowChatStore { timestamp: item.timestamp, status: item.status, })); - + return { id: round.id, turnId, @@ -1521,29 +1527,29 @@ export class FlowChatStore { sessions.forEach(metadata => { stateMachineManager.getOrCreate(metadata.sessionId); }); - + const processSession = async (metadata: any) => { const existingSession = this.state.sessions.get(metadata.sessionId); if (existingSession) { return; } - + let maxContextTokens = 128128; try { const { configManager } = await import('@/infrastructure/config/services/ConfigManager'); const models = await configManager.getConfig('ai.models') || []; - + if (metadata.modelName) { const model = models.find((m: any) => m.name === metadata.modelName || m.id === metadata.modelName); if (model?.context_window) { maxContextTokens = model.context_window; } } - + if (maxContextTokens === 128128) { const defaultModels = await configManager.getConfig>('ai.default_models'); const primaryModelId = defaultModels?.primary; - + if (primaryModelId) { const primaryModel = models.find((m: any) => m.id === primaryModelId); if (primaryModel?.context_window) { @@ -1554,7 +1560,7 @@ export class FlowChatStore { } catch (error) { log.warn('Failed to get model context window size, using default', { sessionId: metadata.sessionId, error }); } - + const relationship = deriveSessionRelationshipFromMetadata(metadata); const lastFinishedAt = deriveLastFinishedAtFromMetadata(metadata); const titleState = deriveSessionTitleStateFromMetadata(metadata); @@ -1564,15 +1570,15 @@ export class FlowChatStore { if (prev.sessions.has(metadata.sessionId)) { return prev; } - + const VALID_AGENT_TYPES = ['agentic', 'debug', 'Plan', 'Cowork', 'Claw', 'Team', 'DeepResearch']; const rawAgentType = metadata.agentType || 'agentic'; const validatedAgentType = VALID_AGENT_TYPES.includes(rawAgentType) ? rawAgentType : 'agentic'; - + if (rawAgentType !== validatedAgentType) { log.warn('Invalid agentType, falling back to agentic', { sessionId: metadata.sessionId, rawAgentType, validatedAgentType }); } - + const session: Session = { sessionId: metadata.sessionId, title: titleState.title, @@ -1603,17 +1609,17 @@ export class FlowChatStore { btwThreads: [], btwOrigin: relationship.btwOrigin, }; - + const newSessions = new Map(prev.sessions); newSessions.set(metadata.sessionId, session); - + return { ...prev, sessions: newSessions, }; }); }; - + await Promise.all(sessions.map(processSession)); } catch (error) { log.error('Failed to load persisted sessions', error); @@ -1633,14 +1639,14 @@ export class FlowChatStore { try { const { stateMachineManager } = await import('../state-machine'); stateMachineManager.getOrCreate(sessionId); - + try { const { agentAPI } = await import('@/infrastructure/api'); await agentAPI.restoreSession(sessionId, workspacePath, remoteConnectionId, remoteSshHost); } catch (error) { log.warn('Backend session restore failed (may be new session)', { sessionId, error }); } - + const { sessionAPI } = await import('@/infrastructure/api'); const turns = await sessionAPI.loadSessionTurns( sessionId, @@ -1649,22 +1655,22 @@ export class FlowChatStore { remoteConnectionId, remoteSshHost ); - + const dialogTurns = this.convertToDialogTurns(turns); - + this.setState(prev => { const session = prev.sessions.get(sessionId); if (!session) return prev; - + const updatedSession = { ...session, dialogTurns, isHistorical: false, }; - + const newSessions = new Map(prev.sessions); newSessions.set(sessionId, updatedSession); - + return { ...prev, sessions: newSessions, @@ -1701,12 +1707,12 @@ export class FlowChatStore { const hasImages = Array.isArray(metaImages) && metaImages.length > 0; const images = hasImages ? metaImages.map((img: any) => ({ - id: img.id || img.name || `img-${Date.now()}`, - name: img.name || 'image', - dataUrl: img.data_url, - imagePath: img.image_path, - mimeType: img.mime_type, - })) + id: img.id || img.name || `img-${Date.now()}`, + name: img.name || 'image', + dataUrl: img.data_url, + imagePath: img.image_path, + mimeType: img.mime_type, + })) : undefined; const displayContent = @@ -1714,99 +1720,99 @@ export class FlowChatStore { const normalizedTurnStatus = normalizeRecoveredTurnStatus(turn.status, { error: undefined }); return { - id: turn.turnId, - sessionId: turn.sessionId, - kind: turn.kind || 'user_dialog', - userMessage: { - id: turn.userMessage.id, - type: 'user' as const, - content: displayContent, - timestamp: turn.userMessage.timestamp, - hasImages, - metadata, - images, - }, - modelRounds: turn.modelRounds.map((round: any) => { - const normalizedRoundStatus = normalizeRecoveredRoundStatus(round.status, normalizedTurnStatus); + id: turn.turnId, + sessionId: turn.sessionId, + kind: turn.kind || 'user_dialog', + userMessage: { + id: turn.userMessage.id, + type: 'user' as const, + content: displayContent, + timestamp: turn.userMessage.timestamp, + hasImages, + metadata, + images, + }, + modelRounds: turn.modelRounds.map((round: any) => { + const normalizedRoundStatus = normalizeRecoveredRoundStatus(round.status, normalizedTurnStatus); - return { - id: round.id, - turnId: round.turnId, - index: round.roundIndex ?? 0, - items: [ - ...round.textItems.map((text: any) => ({ - id: text.id, - type: 'text' as const, - content: text.content, - isStreaming: false, - isMarkdown: text.isMarkdown !== undefined ? text.isMarkdown : true, - timestamp: text.timestamp, - status: normalizeRecoveredTextStatus(text.status, normalizedTurnStatus), - orderIndex: text.orderIndex, - isSubagentItem: text.isSubagentItem, - parentTaskToolId: text.parentTaskToolId, - subagentSessionId: text.subagentSessionId, - })), - ...round.toolItems.map((tool: any) => ({ - id: tool.id, - type: 'tool' as const, - toolName: tool.toolName, - interruptionReason: - tool.interruptionReason === 'app_restart' - ? 'app_restart' - : isTransientToolStatus(tool.status) + return { + id: round.id, + turnId: round.turnId, + index: round.roundIndex ?? 0, + items: [ + ...round.textItems.map((text: any) => ({ + id: text.id, + type: 'text' as const, + content: text.content, + isStreaming: false, + isMarkdown: text.isMarkdown !== undefined ? text.isMarkdown : true, + timestamp: text.timestamp, + status: normalizeRecoveredTextStatus(text.status, normalizedTurnStatus), + orderIndex: text.orderIndex, + isSubagentItem: text.isSubagentItem, + parentTaskToolId: text.parentTaskToolId, + subagentSessionId: text.subagentSessionId, + })), + ...round.toolItems.map((tool: any) => ({ + id: tool.id, + type: 'tool' as const, + toolName: tool.toolName, + interruptionReason: + tool.interruptionReason === 'app_restart' ? 'app_restart' - : undefined, - toolCall: tool.toolCall, - toolResult: tool.toolResult, - aiIntent: tool.aiIntent, - startTime: tool.startTime, - endTime: tool.endTime, - timestamp: tool.startTime, - status: normalizeRecoveredToolStatus( - tool.status, - normalizedTurnStatus, - tool.toolResult, - { preservePendingConfirmation: true }, - ), - orderIndex: tool.orderIndex, - isSubagentItem: tool.isSubagentItem, - parentTaskToolId: tool.parentTaskToolId, - subagentSessionId: tool.subagentSessionId, - })), - ...(round.thinkingItems || []).map((thinking: any) => ({ - id: thinking.id, - type: 'thinking' as const, - content: thinking.content, - isStreaming: false, - isCollapsed: thinking.isCollapsed ?? true, - timestamp: thinking.timestamp, - status: normalizeRecoveredThinkingStatus(thinking.status, normalizedTurnStatus), - orderIndex: thinking.orderIndex, - isSubagentItem: thinking.isSubagentItem, - parentTaskToolId: thinking.parentTaskToolId, - subagentSessionId: thinking.subagentSessionId, - })), - ].sort((a: any, b: any) => { - const aIndex = a.orderIndex !== undefined ? a.orderIndex : a.timestamp || 0; - const bIndex = b.orderIndex !== undefined ? b.orderIndex : b.timestamp || 0; - - return aIndex - bIndex; - }), - isStreaming: false, - isComplete: normalizedRoundStatus !== 'pending' && normalizedRoundStatus !== 'streaming', - status: normalizedRoundStatus, - startTime: round.startTime ?? round.timestamp, - endTime: round.endTime, - timestamp: round.timestamp, - }; - }), - timestamp: turn.timestamp, - status: normalizedTurnStatus, - startTime: turn.startTime, - endTime: turn.endTime, - backendTurnIndex: turn.turnIndex, - }; + : isTransientToolStatus(tool.status) + ? 'app_restart' + : undefined, + toolCall: tool.toolCall, + toolResult: tool.toolResult, + aiIntent: tool.aiIntent, + startTime: tool.startTime, + endTime: tool.endTime, + timestamp: tool.startTime, + status: normalizeRecoveredToolStatus( + tool.status, + normalizedTurnStatus, + tool.toolResult, + { preservePendingConfirmation: true }, + ), + orderIndex: tool.orderIndex, + isSubagentItem: tool.isSubagentItem, + parentTaskToolId: tool.parentTaskToolId, + subagentSessionId: tool.subagentSessionId, + })), + ...(round.thinkingItems || []).map((thinking: any) => ({ + id: thinking.id, + type: 'thinking' as const, + content: thinking.content, + isStreaming: false, + isCollapsed: thinking.isCollapsed ?? true, + timestamp: thinking.timestamp, + status: normalizeRecoveredThinkingStatus(thinking.status, normalizedTurnStatus), + orderIndex: thinking.orderIndex, + isSubagentItem: thinking.isSubagentItem, + parentTaskToolId: thinking.parentTaskToolId, + subagentSessionId: thinking.subagentSessionId, + })), + ].sort((a: any, b: any) => { + const aIndex = a.orderIndex !== undefined ? a.orderIndex : a.timestamp || 0; + const bIndex = b.orderIndex !== undefined ? b.orderIndex : b.timestamp || 0; + + return aIndex - bIndex; + }), + isStreaming: false, + isComplete: normalizedRoundStatus !== 'pending' && normalizedRoundStatus !== 'streaming', + status: normalizedRoundStatus, + startTime: round.startTime ?? round.timestamp, + endTime: round.endTime, + timestamp: round.timestamp, + }; + }), + timestamp: turn.timestamp, + status: normalizedTurnStatus, + startTime: turn.startTime, + endTime: turn.endTime, + backendTurnIndex: turn.turnIndex, + }; }); } @@ -1853,7 +1859,7 @@ export class FlowChatStore { const turn = session.dialogTurns.find(t => t.id === turnId); return turn?.todos || []; } - + public deleteTodo(sessionId: string, todoId: string): void { this.setState(prev => { const session = prev.sessions.get(sessionId); @@ -1888,18 +1894,18 @@ export class FlowChatStore { public getTodos(sessionId: string): import('../types/flow-chat').TodoItem[] { const session = this.state.sessions.get(sessionId); if (!session) return []; - + const allTodos: import('../types/flow-chat').TodoItem[] = []; session.dialogTurns.forEach(turn => { if (turn.todos && turn.todos.length > 0) { allTodos.push(...turn.todos); } }); - + if (session.todos && session.todos.length > 0) { allTodos.push(...session.todos); } - + return allTodos; } diff --git a/src/web-ui/src/flow_chat/store/inputHistoryStore.ts b/src/web-ui/src/flow_chat/store/inputHistoryStore.ts index c581c5b50a..28e0012f29 100644 --- a/src/web-ui/src/flow_chat/store/inputHistoryStore.ts +++ b/src/web-ui/src/flow_chat/store/inputHistoryStore.ts @@ -4,8 +4,9 @@ * History is now session-scoped - each session maintains its own input history. */ +import { storage } from '@/shared'; import { create } from 'zustand'; -import { persist } from 'zustand/middleware'; +import { createJSONStorage, persist } from 'zustand/middleware'; export interface InputHistoryState { /** Map of sessionId to list of previously sent messages (most recent first) */ @@ -92,6 +93,7 @@ export const useInputHistoryStore = create()( { name: 'bitfun-input-history', version: 2, // Bump version to migrate from old format + storage: createJSONStorage(() => storage), migrate: (persistedState: any, version: number) => { if (version < 2) { // Migrate from old global format to new session-scoped format diff --git a/src/web-ui/src/hooks/useModelConfigs.ts b/src/web-ui/src/hooks/useModelConfigs.ts index 960cb1bc43..35cc22a8f5 100644 --- a/src/web-ui/src/hooks/useModelConfigs.ts +++ b/src/web-ui/src/hooks/useModelConfigs.ts @@ -1,6 +1,7 @@ import { useState, useEffect } from 'react'; import { ModelConfig } from '../shared/types'; import { modelConfigManager } from '../infrastructure/config/services/modelConfigs'; +import { storage } from '@/shared'; export const useModelConfigs = () => { const [configs, setConfigs] = useState([]); const [loading, setLoading] = useState(true); @@ -37,9 +38,9 @@ export const useCurrentModelConfig = (initialConfigId?: string) => { const setCurrentConfigWithPersistence = (config: ModelConfig | null) => { setCurrentConfig(config); if (config) { - localStorage.setItem(CURRENT_CONFIG_KEY, config.id); + storage.setItem(CURRENT_CONFIG_KEY, config.id); } else { - localStorage.removeItem(CURRENT_CONFIG_KEY); + storage.removeItem(CURRENT_CONFIG_KEY); } // Notify other components via storage event @@ -75,14 +76,14 @@ export const useCurrentModelConfig = (initialConfigId?: string) => { } if (!currentConfig) { - const savedConfigId = localStorage.getItem(CURRENT_CONFIG_KEY); + const savedConfigId = storage.getItem(CURRENT_CONFIG_KEY); const targetConfigId = initialConfigId || savedConfigId; if (targetConfigId) { const foundConfig = configs.find(c => c.id === targetConfigId); if (foundConfig) { setCurrentConfig(foundConfig); - localStorage.setItem(CURRENT_CONFIG_KEY, foundConfig.id); + storage.setItem(CURRENT_CONFIG_KEY, foundConfig.id); return; } } @@ -91,7 +92,7 @@ export const useCurrentModelConfig = (initialConfigId?: string) => { const firstConfig = configs[0]; if (firstConfig) { setCurrentConfig(firstConfig); - localStorage.setItem(CURRENT_CONFIG_KEY, firstConfig.id); + storage.setItem(CURRENT_CONFIG_KEY, firstConfig.id); } return; } @@ -102,10 +103,10 @@ export const useCurrentModelConfig = (initialConfigId?: string) => { const firstConfig = configs[0]; if (firstConfig) { setCurrentConfig(firstConfig); - localStorage.setItem(CURRENT_CONFIG_KEY, firstConfig.id); + storage.setItem(CURRENT_CONFIG_KEY, firstConfig.id); } else { setCurrentConfig(null); - localStorage.removeItem(CURRENT_CONFIG_KEY); + storage.removeItem(CURRENT_CONFIG_KEY); } } else { // Sync with latest version (config may have been edited) diff --git a/src/web-ui/src/infrastructure/config/components/LspConfig.tsx b/src/web-ui/src/infrastructure/config/components/LspConfig.tsx index b1eb987e67..5cf7181322 100644 --- a/src/web-ui/src/infrastructure/config/components/LspConfig.tsx +++ b/src/web-ui/src/infrastructure/config/components/LspConfig.tsx @@ -8,6 +8,7 @@ import { lspService } from '@/tools/lsp/services/LspService'; import { open } from '@tauri-apps/plugin-dialog'; import { createLogger } from '@/shared/utils/logger'; import './LspConfig.scss'; +import { storage } from '@/shared'; const log = createLogger('LspConfig'); @@ -29,7 +30,7 @@ const LspConfig: React.FC = () => { function loadSettings() { try { - const saved = localStorage.getItem(LSP_SETTINGS_KEY); + const saved = storage.getItem(LSP_SETTINGS_KEY); if (saved) setSettings({ ...DEFAULT_LSP_SETTINGS, ...JSON.parse(saved) }); } catch (error) { log.error('Failed to load settings', error); @@ -40,7 +41,7 @@ const LspConfig: React.FC = () => { const saveSettings = () => { try { - localStorage.setItem(LSP_SETTINGS_KEY, JSON.stringify(settings)); + storage.setItem(LSP_SETTINGS_KEY, JSON.stringify(settings)); setHasSettingsChanges(false); setInstallMessage({ type: 'success', text: t('messages.settingsSaved') }); setTimeout(() => setInstallMessage(null), 2000); diff --git a/src/web-ui/src/infrastructure/i18n/store/i18nStore.ts b/src/web-ui/src/infrastructure/i18n/store/i18nStore.ts index df26b6a9fb..133fab9817 100644 --- a/src/web-ui/src/infrastructure/i18n/store/i18nStore.ts +++ b/src/web-ui/src/infrastructure/i18n/store/i18nStore.ts @@ -1,9 +1,10 @@ import { create } from 'zustand'; -import { persist } from 'zustand/middleware'; +import { persist, createJSONStorage } from 'zustand/middleware'; import type { LocaleId, I18nNamespace, I18nState, I18nActions } from '../types'; import { DEFAULT_LOCALE, DEFAULT_FALLBACK_LOCALE } from '../presets'; +import { storage } from '@/shared'; const initialState: I18nState = { @@ -59,6 +60,7 @@ export const useI18nStore = create()( }), { name: 'bitfun-i18n-state', + storage: createJSONStorage(() => storage), partialize: (state) => ({ currentLanguage: state.currentLanguage, fallbackLanguage: state.fallbackLanguage, diff --git a/src/web-ui/src/shared/stores/contextStore.ts b/src/web-ui/src/shared/stores/contextStore.ts index 05925dcb80..215c64afd5 100644 --- a/src/web-ui/src/shared/stores/contextStore.ts +++ b/src/web-ui/src/shared/stores/contextStore.ts @@ -1,24 +1,25 @@ - + import { create } from 'zustand'; -import { devtools, persist } from 'zustand/middleware'; +import { devtools, persist, StorageValue } from 'zustand/middleware'; import { ContextItem, ValidationResult } from '../types/context'; import { createLogger } from '@/shared/utils/logger'; +import { storage } from '@/shared/utils/storageAdapter'; const log = createLogger('ContextStore'); interface ContextState { - + contexts: ContextItem[]; - - + + validationStates: Map; - - + + validatingIds: Set; - + // Actions addContext: (item: ContextItem) => void; removeContext: (id: string) => void; @@ -35,35 +36,35 @@ export const useContextStore = create()( devtools( persist( (set, _get) => ({ - + contexts: [], validationStates: new Map(), validatingIds: new Set(), - - + + addContext: (item: ContextItem) => { set((state) => { - + if (state.contexts.some(c => c.id === item.id)) { log.warn('Context already exists', { id: item.id }); return state; } - + return { contexts: [...state.contexts, item] }; }, false, 'addContext'); }, - - + + removeContext: (id: string) => { set((state) => { const newValidationStates = new Map(state.validationStates); newValidationStates.delete(id); - + const newValidatingIds = new Set(state.validatingIds); newValidatingIds.delete(id); - + return { contexts: state.contexts.filter(c => c.id !== id), validationStates: newValidationStates, @@ -71,8 +72,8 @@ export const useContextStore = create()( }; }, false, 'removeContext'); }, - - + + clearContexts: () => { set({ contexts: [], @@ -80,24 +81,24 @@ export const useContextStore = create()( validatingIds: new Set() }, false, 'clearContexts'); }, - - + + updateValidation: (id: string, result: ValidationResult) => { set((state) => { const newValidationStates = new Map(state.validationStates); newValidationStates.set(id, result); - + const newValidatingIds = new Set(state.validatingIds); newValidatingIds.delete(id); - + return { validationStates: newValidationStates, validatingIds: newValidatingIds }; }, false, 'updateValidation'); }, - - + + setValidating: (id: string, validating: boolean) => { set((state) => { const newValidatingIds = new Set(state.validatingIds); @@ -106,36 +107,58 @@ export const useContextStore = create()( } else { newValidatingIds.delete(id); } - + return { validatingIds: newValidatingIds }; }, false, 'setValidating'); }, - - + + reorderContexts: (startIndex: number, endIndex: number) => { set((state) => { const newContexts = [...state.contexts]; const [removed] = newContexts.splice(startIndex, 1); newContexts.splice(endIndex, 0, removed); - + return { contexts: newContexts }; }, false, 'reorderContexts'); }, - - + + updateContext: (id: string, updates: Partial) => { set((state) => { - const contexts = state.contexts.map(c => + const contexts = state.contexts.map(c => c.id === id ? { ...c, ...updates } as ContextItem : c ); - + return { contexts }; }, false, 'updateContext'); } }), { name: 'bitfun-context-storage', - + storage: { + getItem(name: string) { + try { + return storage.getItem(name); + } catch (e) { + console.error(`Failed to get item from storage ${e}`); + } + }, + setItem(name: string, value: StorageValue) { + try { + return storage.setItem(name, JSON.stringify(value)); + } catch (e) { + console.error(`Failed to set item from storage ${e}`); + } + }, + removeItem(name: string) { + try { + return storage.removeItem(name); + } catch (e) { + console.error(`Failed to remove item from storage ${e}`); + } + } + }, serialize: (state: any) => { return JSON.stringify({ ...state.state, @@ -143,7 +166,7 @@ export const useContextStore = create()( validatingIds: Array.from(state.state.validatingIds) }); }, - + deserialize: (str: string) => { const parsed = JSON.parse(str); return { @@ -155,8 +178,8 @@ export const useContextStore = create()( } }; }, - - partialize: (state: any) => ({ + + partialize: (state: any) => ({ contexts: state.contexts.filter((ctx: any) => ctx.type !== 'image') }) } as any @@ -172,35 +195,35 @@ export const useContextStore = create()( export const selectContexts = (state: ContextState) => state.contexts; export const selectContextCount = (state: ContextState) => state.contexts.length; -export const selectContextById = (id: string) => (state: ContextState) => +export const selectContextById = (id: string) => (state: ContextState) => state.contexts.find(c => c.id === id); -export const selectValidationState = (id: string) => (state: ContextState) => +export const selectValidationState = (id: string) => (state: ContextState) => state.validationStates.get(id); -export const selectIsValidating = (id: string) => (state: ContextState) => +export const selectIsValidating = (id: string) => (state: ContextState) => state.validatingIds.has(id); -export const selectHasInvalidContexts = (state: ContextState) => +export const selectHasInvalidContexts = (state: ContextState) => Array.from(state.validationStates.values()).some(v => !v.valid); - + export const cleanupImageContextsFromStorage = () => { try { const storageKey = 'bitfun-context-storage'; - const stored = localStorage.getItem(storageKey); - + const stored = storage.getItem(storageKey); + if (stored) { const parsed = JSON.parse(stored); - + if (parsed.state && Array.isArray(parsed.state.contexts)) { const imageCount = parsed.state.contexts.filter((ctx: any) => ctx.type === 'image').length; - + if (imageCount > 0) { - + parsed.state.contexts = parsed.state.contexts.filter((ctx: any) => ctx.type !== 'image'); - - - localStorage.setItem(storageKey, JSON.stringify(parsed)); + + + storage.setItem(storageKey, JSON.stringify(parsed)); } } } diff --git a/src/web-ui/src/shared/utils/index.ts b/src/web-ui/src/shared/utils/index.ts index ca4d9cef4f..53ebc1d800 100644 --- a/src/web-ui/src/shared/utils/index.ts +++ b/src/web-ui/src/shared/utils/index.ts @@ -13,3 +13,5 @@ export * from './debugProbe'; export * from './configConverter'; export * from './contextGenerator'; export * from './eventManager'; +export * from './storageAdapter'; +export * from './sessionStorageAdapter'; diff --git a/src/web-ui/src/shared/utils/sessionStorageAdapter.ts b/src/web-ui/src/shared/utils/sessionStorageAdapter.ts new file mode 100644 index 0000000000..89e35d36c6 --- /dev/null +++ b/src/web-ui/src/shared/utils/sessionStorageAdapter.ts @@ -0,0 +1,87 @@ +class SessionStorageAdapter { + private isAvailable: boolean; + private memoryStorage: Map = new Map(); + + constructor() { + this.isAvailable = this.checkAvailability(); + } + + private checkAvailability(): boolean { + try { + const testkey = 'testkey'; + sessionStorage.setItem(testkey, testkey); + sessionStorage.removeItem(testkey); + return true; + } catch (e) { + console.error("SessionStorageAdapter check failed for session storage", e); + return false; + } + } + + getItem(key: string): string | null { + if (this.isAvailable) { + try { + return sessionStorage.getItem(key); + } catch (e) { + console.error(`Failed to get ${key} for `, e); + } + } + return this.memoryStorage.get(key) || null; + } + + setItem(key: string, value: string): void { + if (this.isAvailable) { + try { + sessionStorage.setItem(key, value); + this.memoryStorage.set(key, value); + return; + } catch (e) { + console.warn(`Failed to set ${key} Local storage not available`, e); + } + } + this.memoryStorage.set(key, value); + } + + removeItem(key: string): void { + if (this.isAvailable) { + try { + sessionStorage.removeItem(key); + this.memoryStorage.delete(key); + return; + } catch (e) { + console.warn(`Failed to delete ${key} Local storage not available`, e); + } + } + this.memoryStorage.delete(key); + } + + clear(): void { + if (this.isAvailable) { + try { + sessionStorage.clear(); + this.memoryStorage.clear(); + return; + } catch (e) { + console.warn(`Failed to clear storage. Local storage not available`, e); + } + } + this.memoryStorage.clear(); + } + + getKeys(): string[] { + if (this.isAvailable) { + try { + return Object.keys(sessionStorage); + } catch (e) { + console.warn(`Failed to get keys. Local storage not available`, e); + } + } + return Array.from(this.memoryStorage.keys()); + } + + hasKey(key: string): boolean { + return this.getItem(key) !== null; + } +} + +export const sessionStorageAdapter = new SessionStorageAdapter(); \ No newline at end of file diff --git a/src/web-ui/src/shared/utils/storageAdapter.ts b/src/web-ui/src/shared/utils/storageAdapter.ts new file mode 100644 index 0000000000..9bb06adf59 --- /dev/null +++ b/src/web-ui/src/shared/utils/storageAdapter.ts @@ -0,0 +1,88 @@ +class StorageAdapter { + private isAvailable: boolean; + private memoryStorage: Map = new Map(); + + constructor() { + this.isAvailable = this.checkAvailability(); + } + + private checkAvailability(): boolean { + try { + const storageKey = '__storageKey__'; + localStorage.setItem(storageKey, storageKey); + localStorage.removeItem(storageKey); + return true; + } catch (e) { + console.error("SessionStorageAdapter check failed for session storage", e); + return false; + } + } + + + getItem(key: string): string | null { + if (this.isAvailable) { + try { + return localStorage.getItem(key); + } catch (e) { + console.error(`Failed to get ${key} for `, e); + } + } + return this.memoryStorage.get(key) || null; + } + + setItem(key: string, value: string): void { + if (this.isAvailable) { + try { + localStorage.setItem(key, value); + this.memoryStorage.set(key, value); + return; + } catch (e) { + console.warn(`Failed to set ${key} Local storage not available`, e); + } + } + this.memoryStorage.set(key, value); + } + + removeItem(key: string): void { + if (this.isAvailable) { + try { + localStorage.removeItem(key); + this.memoryStorage.delete(key); + return; + } catch (e) { + console.warn(`Failed to delete ${key} Local storage not available`, e); + } + } + this.memoryStorage.delete(key); + } + + clear(): void { + if (this.isAvailable) { + try { + localStorage.clear(); + this.memoryStorage.clear(); + return; + } catch (e) { + console.warn(`Failed to clear storage. Local storage not available`, e); + } + } + this.memoryStorage.clear(); + } + + getKeys(): string[] { + if (this.isAvailable) { + try { + return Object.keys(localStorage); + } catch (e) { + console.warn(`Failed to get keys. Local storage not available`, e); + } + } + return Array.from(this.memoryStorage.keys()); + } + + hasKey(key: string): boolean { + return this.getItem(key) !== null; + } +} + +export const storage = new StorageAdapter(); \ No newline at end of file diff --git a/src/web-ui/src/tools/editor/services/EditorManager.ts b/src/web-ui/src/tools/editor/services/EditorManager.ts index 98ec47198d..a955f0c3fa 100644 --- a/src/web-ui/src/tools/editor/services/EditorManager.ts +++ b/src/web-ui/src/tools/editor/services/EditorManager.ts @@ -13,6 +13,7 @@ import { import { globalEventBus } from '../../../infrastructure/event-bus'; import { getMonacoLanguage } from '@/infrastructure/language-detection'; import { createLogger } from '@/shared/utils/logger'; +import { storage } from '@/shared'; const log = createLogger('EditorManager'); @@ -416,7 +417,7 @@ export class EditorManager implements IEditorManager { private loadConfig(): void { try { - const savedConfig = localStorage.getItem('editor-config'); + const savedConfig = storage.getItem('editor-config'); if (savedConfig) { const parsed = JSON.parse(savedConfig); this.config = { ...DEFAULT_CONFIG, ...parsed }; @@ -428,7 +429,7 @@ export class EditorManager implements IEditorManager { private saveConfig(): void { try { - localStorage.setItem('editor-config', JSON.stringify(this.config)); + storage.setItem('editor-config', JSON.stringify(this.config)); } catch (error) { log.warn('Failed to save config', error); } diff --git a/src/web-ui/src/tools/lsp/services/LspConfigService.ts b/src/web-ui/src/tools/lsp/services/LspConfigService.ts index 87ff426a83..520b269114 100644 --- a/src/web-ui/src/tools/lsp/services/LspConfigService.ts +++ b/src/web-ui/src/tools/lsp/services/LspConfigService.ts @@ -2,6 +2,7 @@ * LSP config service (user-facing settings). */ +import { storage } from '@/shared'; import { createLogger } from '@/shared/utils/logger'; const log = createLogger('LspConfigService'); @@ -30,7 +31,7 @@ class LspConfigService { getSettings(): LspSettings { try { - const saved = localStorage.getItem(LSP_SETTINGS_KEY); + const saved = storage.getItem(LSP_SETTINGS_KEY); if (saved) { const parsed = JSON.parse(saved); return { ...DEFAULT_LSP_SETTINGS, ...parsed }; @@ -43,7 +44,7 @@ class LspConfigService { saveSettings(settings: LspSettings): void { try { - localStorage.setItem(LSP_SETTINGS_KEY, JSON.stringify(settings)); + storage.setItem(LSP_SETTINGS_KEY, JSON.stringify(settings)); } catch (error) { log.error('Failed to save settings', { error }); throw error; diff --git a/src/web-ui/src/tools/terminal/services/manualTerminalProfileService.ts b/src/web-ui/src/tools/terminal/services/manualTerminalProfileService.ts index 331d54b22c..69d6bb5345 100644 --- a/src/web-ui/src/tools/terminal/services/manualTerminalProfileService.ts +++ b/src/web-ui/src/tools/terminal/services/manualTerminalProfileService.ts @@ -1,3 +1,4 @@ +import { storage } from '@/shared'; import { STORAGE_KEYS } from '@/shared/constants/app'; import { createLogger } from '@/shared/utils/logger'; @@ -73,7 +74,7 @@ function normalizeState(raw: unknown): ManualTerminalProfilesState { export function loadManualTerminalProfiles(workspacePath: string): ManualTerminalProfilesState { try { - const raw = localStorage.getItem(getStorageKey(workspacePath)); + const raw = storage.getItem(getStorageKey(workspacePath)); if (raw) { return normalizeState(JSON.parse(raw)); } @@ -89,7 +90,7 @@ export function saveManualTerminalProfiles( state: ManualTerminalProfilesState, ): void { try { - localStorage.setItem(getStorageKey(workspacePath), JSON.stringify(normalizeState(state))); + storage.setItem(getStorageKey(workspacePath), JSON.stringify(normalizeState(state))); } catch (error) { logger.error('Failed to save manual terminal profiles', { workspacePath, error }); } diff --git a/src/web-ui/vite.config.ts b/src/web-ui/vite.config.ts index 96015dee55..073312887d 100644 --- a/src/web-ui/vite.config.ts +++ b/src/web-ui/vite.config.ts @@ -99,6 +99,7 @@ export default defineConfig(({ mode, command }) => { outDir: '../../dist', // Empty the output directory emptyOutDir: true, + minify: false, } }; });