diff --git a/src/apps/desktop/src/api/app_state.rs b/src/apps/desktop/src/api/app_state.rs index 244e16a9f..3e88ce4c0 100644 --- a/src/apps/desktop/src/api/app_state.rs +++ b/src/apps/desktop/src/api/app_state.rs @@ -1,3 +1,6 @@ + + + //! Application state management use bitfun_core::agentic::side_question::SideQuestionRuntime; @@ -168,7 +171,7 @@ impl AppState { "worker_host.js not found in any candidate location; \ MiniApp Workers will not start" ); - std::path::PathBuf::from("worker_host.js") + std::path::PathBuf::from("/data/storage/el2/base/files").join("woker_host.js") } }; let js_worker_pool = JsWorkerPool::new(path_manager, worker_host_path) diff --git a/src/apps/desktop/src/api/mod.rs b/src/apps/desktop/src/api/mod.rs index 093d3227b..692f27076 100644 --- a/src/apps/desktop/src/api/mod.rs +++ b/src/apps/desktop/src/api/mod.rs @@ -40,5 +40,6 @@ pub mod subagent_api; pub mod system_api; pub mod terminal_api; pub mod tool_api; +pub mod ohos; pub use app_state::{AppState, AppStatistics, HealthStatus, RemoteWorkspace}; diff --git a/src/apps/desktop/src/api/ohos/ohos_file_system.rs b/src/apps/desktop/src/api/ohos/ohos_file_system.rs index 622a027f6..64d7d33e7 100644 --- a/src/apps/desktop/src/api/ohos/ohos_file_system.rs +++ b/src/apps/desktop/src/api/ohos/ohos_file_system.rs @@ -1,4 +1,3 @@ -use crate::Appstate; use bitfun_core::util::open_dialog_file; #[tauri::command] pub async fn open_oh_file_dialog() -> Result { diff --git a/src/apps/desktop/src/api/ohos/window.rs b/src/apps/desktop/src/api/ohos/window.rs new file mode 100644 index 000000000..1a52e0563 --- /dev/null +++ b/src/apps/desktop/src/api/ohos/window.rs @@ -0,0 +1,116 @@ +use crate::AppState; +use bitfun_core::util::JS_THREADSAFE_FUNCTION; +use log::error; +use napi_ohos::threadsafe_function::ThreadsafeFunctionCallMode; +use std::sync::mpsc::channel; +use tauri::State; + +#[tauri::command] +pub fn handle_min_window() -> Result<(), String> { + let function = { + let lock = JS_THREADSAFE_FUNCTION.read(); + lock.get("handle_min_window").cloned() + }; + let Some(function) = function else { + return Err("The Arkts has not register the function".to_owned()); + }; + function.call(Ok("".to_string()),ThreadsafeFunctionCallMode::NonBlocking); + Ok(()) +} +#[tauri::command] +pub fn handle_max_window() -> Result<(),String> { + let function = { + let lock = JS_THREADSAFE_FUNCTION.read(); + + lock.get("handle_max_window").cloned() + }; + let Some(function) = function else { + return Err("The Arkts has not register the function".to_owned()); + }; + function.call(Ok("".to_string()), ThreadsafeFunctionCallMode::NonBlocking); + Ok(()) +} +#[tauri::command] +pub fn handle_restore_window() -> Result<(),String> { + let function = { + let lock = JS_THREADSAFE_FUNCTION.read(); + lock.get("handle_restore_window").cloned() + }; + let Some(function) = function else { + return Err("The Arkts has not register the function".to_owned()); + }; + function.call(Ok("".to_string()), ThreadsafeFunctionCallMode::NonBlocking); + Ok(()) +} +#[tauri::command] +pub async fn window_is_minimized() -> Result { + let function = { + let lock = JS_THREADSAFE_FUNCTION.read(); + lock.get("window_is_minimized").cloned() + }; + let Some(function) = function else { + return Err("The Arkts has not register the function".to_owned()); + }; + let res = function.call_async(Ok("str".to_string())).await; + match res { + Ok(err) => match err.await{ + Ok(result) => { + if result.eq("true") { + Ok(true) + } else { + Ok(false) + } + }, + Err(err) => Err(err.to_string()), + } + Err(err) => Err(err.to_string()), + } +} +#[tauri::command] +pub fn window_start_dragging() -> Result<(),String> { + let function = { + let lock = JS_THREADSAFE_FUNCTION.read(); + lock.get("window_start_dragging").cloned() + }; + let Some(function) = function else { + return Err("The Arkts has not register the function".to_owned()); + }; + function.call(Ok("".to_string()), ThreadsafeFunctionCallMode::NonBlocking); + Ok(()) +} +#[tauri::command] +pub fn close_window() -> Result<(),String> { + let function = { + let lock = JS_THREADSAFE_FUNCTION.read(); + lock.get("close_window").cloned() + }; + let Some(function) = function else { + return Err("The Arkts has not register the function".to_owned()); + }; + function.call(Ok("".to_string()), ThreadsafeFunctionCallMode::NonBlocking); + Ok(()) +} +#[tauri::command] +pub async fn window_is_maximized() -> Result { + let function = { + let lock = JS_THREADSAFE_FUNCTION.read(); + lock.get("window_is_maximized").cloned() + }; + let Some(function) = function else { + return Err("The Arkts has not register the function".to_owned()); + }; + let res = function.call_async(Ok("str".to_string())).await; + match res { + Ok(err) => match err.await { + Ok(result) => { + if result.eq("true") { + Ok(true) + } else { + Ok(false) + } + }, + Err(err) => Err(err.to_string()), + } + Err(err) => Err(err.to_string()), + } +} \ No newline at end of file diff --git a/src/apps/desktop/src/lib.rs b/src/apps/desktop/src/lib.rs index b721d1e3a..2eae44307 100644 --- a/src/apps/desktop/src/lib.rs +++ b/src/apps/desktop/src/lib.rs @@ -23,6 +23,12 @@ use tauri::Manager; // Re-export API pub use api::*; + +use crate::ohos::ohos_file_system::open_oh_file_dialog; +use crate::ohos::window::{ + close_window,handle_max_window,handle_min_window,handle_restore_window,window_is_maximized, + window_is_minimized, window_start_dragging +}; use std::path::PathBuf; use api::ai_rules_api::*; use api::clipboard_file_api::*; @@ -46,6 +52,8 @@ use api::storage_commands::*; use api::subagent_api::*; use api::system_api::*; use api::tool_api::*; +use std::ffi::CString; +use std::ptr; /// Agentic Coordinator state #[derive(Clone)] @@ -792,6 +800,16 @@ pub async fn _run() { api::announcement_api::never_show_announcement, api::announcement_api::trigger_announcement, api::announcement_api::get_announcement_tips, + // ohos adater + open_oh_file_dialog, + handle_min_window, + handle_max_window, + handle_restore_window, + window_is_maximized, + window_is_minimized, + window_start_dragging, + close_window, + ]) .run(tauri::generate_context!()); if let Err(e) = run_result { diff --git a/src/crates/core/src/agentic/execution/execution_engine.rs b/src/crates/core/src/agentic/execution/execution_engine.rs index 528895ff8..e9a5c50bc 100644 --- a/src/crates/core/src/agentic/execution/execution_engine.rs +++ b/src/crates/core/src/agentic/execution/execution_engine.rs @@ -1787,7 +1787,6 @@ impl ExecutionEngine { } let tool_name = tool.name().to_string(); - if mode_allowed_tools.contains(&tool_name) { let description = tool .description_with_context(Some(&description_context)) .await @@ -1802,7 +1801,7 @@ impl ExecutionEngine { description, parameters, }); - } + } // Order tools for the model API: terminal → file-ish tools → **`ControlHub`** diff --git a/src/crates/core/src/agentic/tools/implementations/calendar_tool.rs b/src/crates/core/src/agentic/tools/implementations/calendar_tool.rs new file mode 100644 index 000000000..d93efcd6c --- /dev/null +++ b/src/crates/core/src/agentic/tools/implementations/calendar_tool.rs @@ -0,0 +1,151 @@ +use crate::agentic::tools::framework::{Tool, ToolResult, ToolUseContext}; +use crate::util::errors::BitFunResult; +use crate::util::JS_THREADSAFE_FUNCTION; +use async_trait::async_trait; +use serde_json::{ Value,json}; + +pub struct CalendarTool; + +impl CalendarTool { + pub fn new() -> CalendarTool { + Self + } +} + +#[async_trait] +impl Tool for CalendarTool { + fn name(&self) -> &str { + "Calendar" + } + + async fn description(&self) -> BitFunResult { + Ok(r#"Manages all types of calendar schedules, including events, reminders, deadlines, and all-day entries. + + Usage Guidelines: + - Supported actions: 'create' (new entry) + - You MUST extract the specific city or venue into the 'location' field (e.g, 'Beijing'). + - DO NOT leave the primary location only inside the 'description' or 'title'. + - Time Format: Always use 'YYYY-MM-DD HH:mm'. + - Participants can include names or email address, Leave empty for personal tasks. + "#.to_string()) + } + + fn input_schema(&self) -> Value { + json!({ + "type": "object", + "properties": { + "title": { + "type": "string", + "description": "Short title of the schedule (e.g., 'Flight to Tokyo'. 'Dentist Appointment')", + }, + "description": { + "type": "string", + "description": "Detailed notes or additional information" + }, + "start_time": { + "type": "string", + "description": "YYYY-MM-DD HH:mm format" + }, + "end_time": { + "type": "string", + "description": "YYYY-MM-DD HH:mm" + }, + "location": { + "type": "string", + "description": "The specific physical location, city, or address. E.G., 'Beijing' or 'Forbidden City'", + } + }, + "required": ["action"], + "additionalProperties": false + }) + } + + fn is_readonly(&self) -> bool { + false + } + + fn is_concurrency_safe(&self, _input: Option<&Value>) -> bool { + false + } + + async fn call_impl( + &self, + input: &Value, + _context: &ToolUseContext + )-> BitFunResult> { + let title = input.get("title").and_then(|v| v.as_str()).unwrap_or_default(); + let description = input.get("description").and_then(|v| v.as_str()).unwrap_or_default(); + let start_time = input.get("start_time").and_then(|v| v.as_str()).unwrap_or_default(); + let end_time = input.get("end_time").and_then(|v| v.as_str()).unwrap_or_default(); + let info = CalendarInfo::new(title.to_string(), description.to_string(), start_time.to_string(), end_time.to_string()); + + let res = call_calender(serde_json::to_string(&info).unwrap_or_default()); + let action = "创建日程"; + + let result = ToolResult::Result { + data: json!({ + "action": action, + "success": true + }), + result_for_assistant: Some(format!( + "Calendar {} operation executed successfully", + action + )), + image_attachments: None, + }; + Ok(vec![result]) + } +} + +#[napi(object)] +#[derive(Debug,Clone,Serialize)] +#[serde(rename_all = "camelCase")] +pub struct CalendarInfo { + pub title: String, + pub start_time: String, + pub end_time: String, + pub description: String, +} + +impl CalendarInfo { + pub fn new(title: String, start_time: String, end_time: String, description: String) -> Self { + Self { + title, + start_time, + end_time, + description + } + } +} + +use napi_derive_ohos::napi; +use serde::Serialize; + +use napi_ohos::threadsafe_function::ThreadsafeFunctionCallMode; + +pub fn call_calender(args: String) -> Result{ + let result = Ok(args); + match JS_THREADSAFE_FUNCTION.write().get("call_calender") { + None => { + return Err("The Arkts has not register the functions".to_string()); + } + Some(functions) => { + functions.call_with_return_value( + result, + ThreadsafeFunctionCallMode::Blocking, + move |result,_| { + match result { + Ok(_) => { + log::info!("Successfully called Arkts"); + } + Err(err) => { + log::error!("call calender with error {:?}", err); + } + } + Ok(()) + } + ); + } + } + Ok("".to_string()) +} \ No newline at end of file diff --git a/src/crates/core/src/agentic/tools/implementations/harmonyos_project.rs b/src/crates/core/src/agentic/tools/implementations/harmonyos_project.rs new file mode 100644 index 000000000..ee9db6de8 --- /dev/null +++ b/src/crates/core/src/agentic/tools/implementations/harmonyos_project.rs @@ -0,0 +1,100 @@ +use crate::agentic::tools::framework::{Tool, ToolResult, ToolUseContext}; +use crate::util::errors::BitFunResult; +use crate::util::JS_THREADSAFE_FUNCTION; +use async_trait::async_trait; +use napi_ohos::threadsafe_function::ThreadsafeFunctionCallMode; +use serde_json::{json, Value}; + +pub struct HarmonyProjectGenTool; + +impl HarmonyProjectGenTool { + pub fn new() -> Self { + Self + } +} + +#[async_trait] +impl Tool for HarmonyProjectGenTool { + fn name(&self) -> &str { + "HarmonyGenerate" + } + + async fn description(&self) -> BitFunResult { + Ok(r#"Generates or create a new HarmonyOS project. + Usages: + - Use this to scaffold a new HarmonyOS/OpenHarmony project using ArkTs."# + .to_string()) + } + + fn input_schema(&self) -> Value { + json!({ + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "HarmonyOS/OpenHarmony Project Name" + }, + }, + "required": ["name"], + "additionalProperties": false + }) + } + + fn is_readonly(&self) -> bool { + false + } + + fn is_concurrency_safe(&self, _input: Option<&Value>) -> bool { + false + } + + async fn call_impl( + &self, + input: &Value, + context: &ToolUseContext, + ) -> BitFunResult> { + let title = input + .get("name") + .and_then(|v| v.as_str()) + .unwrap_or_default(); + let _ = harmonyos_create(title.to_string()); + let result = ToolResult::Result { + data: json!({ + "project_name": title, + "bundle_name": "com.example.myapplication", + "status": "created", + }), + result_for_assistant: Some("Successfully generated HarmonyOS project".to_string()), + image_attachments: None, + }; + Ok(vec![result]) + } +} + +pub fn harmonyos_create(title: String) -> Result { + let result = Ok(title); + + match JS_THREADSAFE_FUNCTION.write().get("harmonyos_create") { + None => { + return Err(String::from("harmonyos_create is not defined")); + } + Some(functions) => { + functions.call_with_return_value( + result, + ThreadsafeFunctionCallMode::Blocking, + move |result, _| { + match result { + Ok(_) => { + log::info!("harmonyos_create is created"); + } + Err(error) => { + log::error!("harmonyos_create error: {:?}", error); + } + } + Ok(()) + }, + ); + } + } + Ok("".to_string()) +} diff --git a/src/crates/core/src/agentic/tools/implementations/mod.rs b/src/crates/core/src/agentic/tools/implementations/mod.rs index 4b58dc643..d01e3243d 100644 --- a/src/crates/core/src/agentic/tools/implementations/mod.rs +++ b/src/crates/core/src/agentic/tools/implementations/mod.rs @@ -2,6 +2,8 @@ pub mod ask_user_question_tool; pub mod bash_tool; +pub mod calendar_tool; +pub mod harmonyos_project; pub mod code_review_tool; pub mod computer_use_actions; pub mod computer_use_input; diff --git a/src/web-ui/src/app/components/NavPanel/MainNav.tsx b/src/web-ui/src/app/components/NavPanel/MainNav.tsx index fa19a2043..fddbf89a8 100644 --- a/src/web-ui/src/app/components/NavPanel/MainNav.tsx +++ b/src/web-ui/src/app/components/NavPanel/MainNav.tsx @@ -46,6 +46,7 @@ import { useShortcut } from '@/infrastructure/hooks/useShortcut'; import { ALL_SHORTCUTS } from '@/shared/constants/shortcuts'; import './NavPanel.scss'; +import {workspaceAPI} from "@/infrastructure"; const NAV_TOGGLE_SEARCH_DEF = ALL_SHORTCUTS.find((d) => d.id === 'nav.toggleSearch')!; @@ -239,12 +240,10 @@ const MainNav: React.FC = ({ const handleOpenProject = useCallback(async () => { try { - // const { open } = await import('@tauri-apps/plugin-dialog'); - // const selected = await open({ directory: true, multiple: false, title: t('header.selectProjectDirectory') }); - // if (selected && typeof selected === 'string') { - let path_manager = "/data/storage/el2/base/files/test"; - await workspaceManager.openWorkspace(path_manager); - // } + const selected = await workspaceAPI.open_oh_file_dialog(); + if(selected && typeof selected === 'string'){ + await workspaceManager.openWorkspace(selected); + } } catch (err) { log.error('Failed to open project', err); } diff --git a/src/web-ui/src/app/components/NewProjectDialog/NewProjectDialog.tsx b/src/web-ui/src/app/components/NewProjectDialog/NewProjectDialog.tsx index 73852c63c..eba586370 100644 --- a/src/web-ui/src/app/components/NewProjectDialog/NewProjectDialog.tsx +++ b/src/web-ui/src/app/components/NewProjectDialog/NewProjectDialog.tsx @@ -17,6 +17,7 @@ import { useTranslation } from 'react-i18next'; import { createLogger } from '@/shared/utils/logger'; import { Modal, Button, Input } from '@/component-library'; import './NewProjectDialog.scss'; +import {workspaceAPI} from "@/infrastructure"; const log = createLogger('NewProjectDialog'); @@ -49,7 +50,8 @@ export const NewProjectDialog: React.FC = ({ // Open directory picker dialog const handleSelectParentPath = useCallback(async () => { try { - let selected = "/data/storage/el2/base/files"; + const selected = await workspaceAPI.open_oh_file_dialog(); + if (selected && typeof selected === 'string') { setParentPath(selected); setError(''); diff --git a/src/web-ui/src/app/hooks/useWindowControls.ts b/src/web-ui/src/app/hooks/useWindowControls.ts index 4ef975e18..9a538667f 100644 --- a/src/web-ui/src/app/hooks/useWindowControls.ts +++ b/src/web-ui/src/app/hooks/useWindowControls.ts @@ -6,6 +6,7 @@ import { createLogger } from '@/shared/utils/logger'; import { sendDebugProbe } from '@/shared/utils/debugProbe'; import { nowMs } from '@/shared/utils/timing'; import { useI18n } from '@/infrastructure/i18n'; +import {workspaceAPI} from "@/infrastructure"; import { isMacOSDesktopRuntime, supportsNativeWindowControls } from '@/infrastructure/runtime'; const log = createLogger('useWindowControls'); @@ -200,8 +201,7 @@ export const useWindowControls = (options?: { isToolbarMode?: boolean }) => { ); try { - const appWindow = getCurrentWindow(); - await appWindow.minimize(); + await workspaceAPI.handle_min_window(); // Ensure input is usable after restore // Listen for restore @@ -261,15 +261,13 @@ export const useWindowControls = (options?: { isToolbarMode?: boolean }) => { isMaximizeInProgress.current = true; // Skip auto updates to avoid duplicate state changes shouldSkipStateUpdate.current = true; - - const appWindow = getCurrentWindow(); - + // Optimization: skip isVisible check; query maximized directly. // If minimized, user restores via taskbar instead of double-clicking header. // Check current state to avoid duplicate toggles. let currentMaximized = false; try { - currentMaximized = await appWindow.isMaximized(); + currentMaximized = await workspaceAPI.window_is_maximized(); } catch (error) { log.warn('Failed to get maximized state, assuming not maximized', error); currentMaximized = false; @@ -283,10 +281,10 @@ export const useWindowControls = (options?: { isToolbarMode?: boolean }) => { // Toggle maximize/restore if (currentMaximized) { - await appWindow.unmaximize(); + await workspaceAPI.handle_restore_window(); updateState(false); } else { - await appWindow.maximize(); + await workspaceAPI.handle_max_window(); updateState(true); } @@ -333,8 +331,7 @@ export const useWindowControls = (options?: { isToolbarMode?: boolean }) => { if (!canUseNativeWindowControls) return; try { - const appWindow = getCurrentWindow(); - await appWindow.close(); + await workspaceAPI.close_window() } catch (error) { log.error('Failed to close window', error); notificationService.error(t('window.closeFailed', { error: formatErrorMessage(error) })); diff --git a/src/web-ui/src/app/layout/AppLayout.tsx b/src/web-ui/src/app/layout/AppLayout.tsx index 2242a2e38..95a1388f1 100644 --- a/src/web-ui/src/app/layout/AppLayout.tsx +++ b/src/web-ui/src/app/layout/AppLayout.tsx @@ -9,14 +9,13 @@ */ import React, { useState, useCallback, useEffect, useMemo, useRef, useContext } from 'react'; -import { open } from '@tauri-apps/plugin-dialog'; import { useWorkspaceContext } from '../../infrastructure/contexts/WorkspaceContext'; import { useWindowControls } from '../hooks/useWindowControls'; import { useAssistantBootstrap } from '../hooks/useAssistantBootstrap'; import { useApp } from '../hooks/useApp'; import { useSceneStore } from '../stores/sceneStore'; import { useShortcut } from '@/infrastructure/hooks/useShortcut'; -import { configManager } from '@/infrastructure/config'; +import { configManager } from '@/infrastructure'; import { sessionStorageAdapter } from '@/shared'; type TransitionDirection = 'entering' | 'returning' | null; @@ -118,11 +117,7 @@ const AppLayout: React.FC = ({ className = '' }) => { const [showWorkspaceStatus, setShowWorkspaceStatus] = useState(false); const handleOpenProject = useCallback(async () => { try { - const selected = await open({ - directory: true, - multiple: false, - title: t('header.selectProjectDirectory'), - }); + const selected = await workspaceAPI.open_oh_file_dialog(); if (selected && typeof selected === 'string') { await openWorkspace(selected); @@ -169,10 +164,9 @@ const AppLayout: React.FC = ({ className = '' }) => { void (async () => { try { const { listen } = await import('@tauri-apps/api/event'); - const { open } = await import('@tauri-apps/plugin-dialog'); unlistenFns.push(await listen('bitfun_menu_open_project', async () => { try { - const selected = await open({ directory: true, multiple: false }) as string; + const selected = await workspaceAPI.open_oh_file_dialog(); if (selected) await openWorkspace(selected); } catch {} })); diff --git a/src/web-ui/src/app/scenes/miniapps/hooks/useMiniAppBridge.ts b/src/web-ui/src/app/scenes/miniapps/hooks/useMiniAppBridge.ts index daca705b2..b5ddf9588 100644 --- a/src/web-ui/src/app/scenes/miniapps/hooks/useMiniAppBridge.ts +++ b/src/web-ui/src/app/scenes/miniapps/hooks/useMiniAppBridge.ts @@ -6,13 +6,14 @@ */ import { useLayoutEffect, useRef, useEffect, RefObject } from 'react'; import { miniAppAPI } from '@/infrastructure/api/service-api/MiniAppAPI'; -import { open as dialogOpen, save as dialogSave, message as dialogMessage } from '@tauri-apps/plugin-dialog'; +import { save as dialogSave, message as dialogMessage } from '@tauri-apps/plugin-dialog'; import type { MiniApp } from '@/infrastructure/api/service-api/MiniAppAPI'; import { useCurrentWorkspace } from '@/infrastructure/contexts/WorkspaceContext'; import { useTheme } from '@/infrastructure/theme/hooks/useTheme'; import { buildMiniAppThemeVars } from '../utils/buildMiniAppThemeVars'; import { api } from '@/infrastructure/api/service-api/ApiClient'; import { useI18n } from '@/infrastructure/i18n'; +import {workspaceAPI} from "@/infrastructure"; interface JSONRPC { jsonrpc?: string; @@ -157,7 +158,7 @@ export function useMiniAppBridge( return; } if (method === 'dialog.open') { - reply(await dialogOpen(params as unknown as Parameters[0])); + reply(await workspaceAPI.open_oh_file_dialog()); return; } if (method === 'dialog.save') { diff --git a/src/web-ui/src/app/scenes/miniapps/views/MiniAppGalleryView.tsx b/src/web-ui/src/app/scenes/miniapps/views/MiniAppGalleryView.tsx index d0a5a676c..cd9028e2e 100644 --- a/src/web-ui/src/app/scenes/miniapps/views/MiniAppGalleryView.tsx +++ b/src/web-ui/src/app/scenes/miniapps/views/MiniAppGalleryView.tsx @@ -9,7 +9,6 @@ import { Tag, Trash2, } from 'lucide-react'; -import { open } from '@tauri-apps/plugin-dialog'; import { useSceneManager } from '@/app/hooks/useSceneManager'; import MiniAppCard from '../components/MiniAppCard'; import type { MiniAppMeta } from '@/infrastructure/api/service-api/MiniAppAPI'; @@ -33,6 +32,7 @@ import { useMiniAppStore } from '../miniAppStore'; import { useI18n } from '@/infrastructure/i18n'; import { useGallerySceneAutoRefresh } from '@/app/hooks/useGallerySceneAutoRefresh'; import './MiniAppGalleryView.scss'; +import {workspaceAPI} from "@/infrastructure"; const log = createLogger('MiniAppGalleryView'); @@ -167,12 +167,7 @@ const MiniAppGalleryView: React.FC = () => { const handleAddFromFolder = async () => { try { - const selected = await open({ - directory: true, - multiple: false, - title: t('selectFolderTitle'), - }); - const path = Array.isArray(selected) ? selected[0] : selected; + const path = await workspaceAPI.open_oh_file_dialog(); if (!path) return; setLoading(true); diff --git a/src/web-ui/src/app/scenes/skills/hooks/useInstalledSkills.ts b/src/web-ui/src/app/scenes/skills/hooks/useInstalledSkills.ts index 2623b0697..dccf6e780 100644 --- a/src/web-ui/src/app/scenes/skills/hooks/useInstalledSkills.ts +++ b/src/web-ui/src/app/scenes/skills/hooks/useInstalledSkills.ts @@ -1,7 +1,6 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; -import { open } from '@tauri-apps/plugin-dialog'; import { useTranslation } from 'react-i18next'; -import { configAPI } from '@/infrastructure/api'; +import { configAPI,workspaceAPI } from '@/infrastructure/api'; import type { SkillInfo, SkillLevel, SkillValidationResult } from '@/infrastructure/config/types'; import { useWorkspaceManagerSync } from '@/infrastructure/hooks/useWorkspaceManagerSync'; import { useNotification } from '@/shared/notification-system'; @@ -90,11 +89,7 @@ export function useInstalledSkills({ searchQuery, activeFilter }: UseInstalledSk const handleBrowse = useCallback(async () => { try { - const selected = await open({ - directory: true, - multiple: false, - title: t('form.path.label'), - }); + const selected = await workspaceAPI.open_oh_file_dialog(); if (selected) { setFormPath(selected as string); } diff --git a/src/web-ui/src/app/scenes/welcome/WelcomeScene.tsx b/src/web-ui/src/app/scenes/welcome/WelcomeScene.tsx index 2f42d1ac8..393440064 100644 --- a/src/web-ui/src/app/scenes/welcome/WelcomeScene.tsx +++ b/src/web-ui/src/app/scenes/welcome/WelcomeScene.tsx @@ -19,6 +19,7 @@ import type { SceneTabId } from '@/app/components/SceneBar/types'; import type { WorkspaceInfo } from '@/shared/types'; import { getRecentWorkspaceLineParts } from '@/shared/utils/recentWorkspaceDisplay'; import './WelcomeScene.scss'; +import {workspaceAPI} from "@/infrastructure"; const log = createLogger('WelcomeScene'); @@ -55,7 +56,7 @@ const WelcomeScene: React.FC = () => { const handleOpenFolder = useCallback(async () => { try { setIsSelecting(true); - let selected = "/data/storage/el2/base/files/test"; + const selected = await workspaceAPI.open_oh_file_dialog(); if (selected && typeof selected === 'string') { await openWorkspace(selected); openScene('session' as SceneTabId); diff --git a/src/web-ui/src/features/ssh-remote/RemoteFileBrowser.tsx b/src/web-ui/src/features/ssh-remote/RemoteFileBrowser.tsx index d879106bb..2c36c2422 100644 --- a/src/web-ui/src/features/ssh-remote/RemoteFileBrowser.tsx +++ b/src/web-ui/src/features/ssh-remote/RemoteFileBrowser.tsx @@ -23,6 +23,7 @@ import { Download, } from 'lucide-react'; import './RemoteFileBrowser.scss'; +import {workspaceAPI} from "@/infrastructure"; interface RemoteFileBrowserProps { connectionId: string; @@ -311,12 +312,7 @@ export const RemoteFileBrowser: React.FC = ({ setError(t('ssh.remote.transferNeedsDesktop')); return; } - const { open } = await import('@tauri-apps/plugin-dialog'); - const selected = await open({ - title: t('ssh.remote.uploadDialogTitle'), - multiple: true, - directory: false, - }); + const selected = await workspaceAPI.open_oh_file_dialog(); if (selected === null) return; const paths = Array.isArray(selected) ? selected : [selected]; if (paths.length === 0) return; diff --git a/src/web-ui/src/features/ssh-remote/pickSshPrivateKeyPath.ts b/src/web-ui/src/features/ssh-remote/pickSshPrivateKeyPath.ts index 242004bbc..dd7fe2bee 100644 --- a/src/web-ui/src/features/ssh-remote/pickSshPrivateKeyPath.ts +++ b/src/web-ui/src/features/ssh-remote/pickSshPrivateKeyPath.ts @@ -2,22 +2,14 @@ * Native file picker for SSH private keys; default folder is ~/.ssh (via Tauri homeDir + join). */ -import { open } from '@tauri-apps/plugin-dialog'; -import { homeDir, join } from '@tauri-apps/api/path'; +import {workspaceAPI} from "@/infrastructure"; import { createLogger } from '@/shared/utils/logger'; const log = createLogger('pickSshPrivateKeyPath'); -export async function pickSshPrivateKeyPath(options: { title?: string } = {}): Promise { +export async function pickSshPrivateKeyPath(_options: { title?: string } = {}): Promise { try { - const home = await homeDir(); - const defaultPath = await join(home, '.ssh'); - const selected = await open({ - multiple: false, - directory: false, - defaultPath, - title: options.title, - }); + const selected = await workspaceAPI.open_oh_file_dialog(); return selected ?? null; } catch (e) { log.error('SSH private key file picker failed', e); diff --git a/src/web-ui/src/flow_chat/components/WelcomePanel.tsx b/src/web-ui/src/flow_chat/components/WelcomePanel.tsx index a3071db85..4cc473539 100644 --- a/src/web-ui/src/flow_chat/components/WelcomePanel.tsx +++ b/src/web-ui/src/flow_chat/components/WelcomePanel.tsx @@ -6,7 +6,7 @@ import React, { useEffect, useState, useCallback, useRef, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { FolderOpen, ChevronDown, Check, GitBranch } from 'lucide-react'; -import { gitAPI } from '../../infrastructure/api'; +import { gitAPI ,workspaceAPI} from '../../infrastructure/api'; import type { GitWorkState } from '../../infrastructure/api/service-api/StartchatAgentAPI'; import { useApp } from '../../app/hooks/useApp'; import { createLogger } from '@/shared/utils/logger'; @@ -151,8 +151,9 @@ export const WelcomePanel: React.FC = ({ try { setWorkspaceDropdownOpen(false); setIsSelectingWorkspace(true); - let path_manager = "/data/storage/el2/base/files/test"; - await openWorkspace(path_manager); + + const selected = await workspaceAPI.open_oh_file_dialog(); + if (selected && typeof selected === 'string') await openWorkspace(selected); } catch (err) { log.warn('Failed to open workspace folder', err); } finally { diff --git a/src/web-ui/src/infrastructure/api/adapters/tauri-adapter.ts b/src/web-ui/src/infrastructure/api/adapters/tauri-adapter.ts index a086b708a..498ecd63f 100644 --- a/src/web-ui/src/infrastructure/api/adapters/tauri-adapter.ts +++ b/src/web-ui/src/infrastructure/api/adapters/tauri-adapter.ts @@ -28,13 +28,13 @@ export class TauriTransportAdapter implements ITransportAdapter { private async doInitialize() { try { // Check if Tauri API is available - if (typeof window !== 'undefined' && !('__TAURI__' in window)) { - log.warn('Tauri API not available, running in non-Tauri environment'); - this.invokeFn = async () => { - throw new Error('Tauri API is not available. Make sure you are running in a Tauri environment.'); - }; - return; - } + // if (typeof window !== 'undefined' && !('__TAURI__' in window)) { + // log.warn('Tauri API not available, running in non-Tauri environment'); + // this.invokeFn = async () => { + // throw new Error('Tauri API is not available. Make sure you are running in a Tauri environment.'); + // }; + // return; + // } const tauriApi = await import('@tauri-apps/api/core'); this.invokeFn = tauriApi.invoke; diff --git a/src/web-ui/src/infrastructure/api/service-api/WorkspaceAPI.ts b/src/web-ui/src/infrastructure/api/service-api/WorkspaceAPI.ts index b3c142a40..91cafc584 100644 --- a/src/web-ui/src/infrastructure/api/service-api/WorkspaceAPI.ts +++ b/src/web-ui/src/infrastructure/api/service-api/WorkspaceAPI.ts @@ -786,11 +786,74 @@ export class WorkspaceAPI { } } - + async open_oh_file_dialog(): Promise { + try { + return await api.invoke("open_oh_file_dialog") + }catch (error){ + throw createTauriCommandError('open_oh_file_dialog',error) + } + } + + async window_is_minimized(): Promise { + try { + return await api.invoke("window_is_minimized") + }catch (error){ + throw createTauriCommandError('window_is_minimized',error) + } + } + + async window_start_dragging(): Promise { + try { + return await api.invoke("window_start_dragging") + }catch (error){ + throw createTauriCommandError('window_start_dragging',error) + } + } + + async close_window(): Promise { + try { + return await api.invoke("close_window") + }catch (error){ + throw createTauriCommandError('close_window',error) + } + } + + async window_is_maximized(): Promise { + try { + return await api.invoke("window_is_maximized") + }catch (error){ + throw createTauriCommandError('window_is_maximized',error) + } + } + + async handle_min_window(): Promise { + try { + return await api.invoke("handle_min_window") + }catch (error){ + throw createTauriCommandError('handle_min_window',error) + } + } + + async handle_max_window(): Promise { + try { + return await api.invoke("handle_max_window") + }catch (error){ + throw createTauriCommandError('handle_max_window',error) + } + } + + async handle_restore_window(): Promise { + try { + return await api.invoke("handle_restore_window") + }catch (error){ + throw createTauriCommandError('handle_restore_window',error) + } + } + async revealInExplorer(path: string): Promise { try { await api.invoke('reveal_in_explorer', { - request: { path } + request: { path } }); } catch (error) { throw createTauriCommandError('reveal_in_explorer', error, { path }); diff --git a/src/web-ui/src/infrastructure/api/service-api/tauri-commands.ts b/src/web-ui/src/infrastructure/api/service-api/tauri-commands.ts index 2daca9e8e..b53154fe8 100644 --- a/src/web-ui/src/infrastructure/api/service-api/tauri-commands.ts +++ b/src/web-ui/src/infrastructure/api/service-api/tauri-commands.ts @@ -64,7 +64,9 @@ export interface ImportConfigRequest { configData: any; } - +export interface OpenOhosPath { + path: string; +} export interface GetModelInfoRequest { modelId: string; diff --git a/src/web-ui/src/infrastructure/config/components/LspConfig.tsx b/src/web-ui/src/infrastructure/config/components/LspConfig.tsx index 5cf718132..3ce480fdf 100644 --- a/src/web-ui/src/infrastructure/config/components/LspConfig.tsx +++ b/src/web-ui/src/infrastructure/config/components/LspConfig.tsx @@ -5,9 +5,9 @@ import { Save, X, RefreshCw, Upload } from 'lucide-react'; import { ConfigPageHeader, ConfigPageLayout, ConfigPageContent, ConfigPageSection, ConfigPageRow } from './common'; import { LspPluginList } from '@/tools/lsp'; import { lspService } from '@/tools/lsp/services/LspService'; -import { open } from '@tauri-apps/plugin-dialog'; import { createLogger } from '@/shared/utils/logger'; import './LspConfig.scss'; +import {workspaceAPI} from "@/infrastructure"; import { storage } from '@/shared'; const log = createLogger('LspConfig'); @@ -71,10 +71,7 @@ const LspConfig: React.FC = () => { const handleInstallPlugin = async () => { try { - const selected = await open({ - multiple: false, - filters: [{ name: t('fileDialog.pluginPackage'), extensions: ['vcpkg'] }] - }); + const selected = await workspaceAPI.open_oh_file_dialog(); if (!selected) return; setIsInstalling(true); diff --git a/src/web-ui/src/infrastructure/config/components/SessionConfig.tsx b/src/web-ui/src/infrastructure/config/components/SessionConfig.tsx index 7e7a46040..952313331 100644 --- a/src/web-ui/src/infrastructure/config/components/SessionConfig.tsx +++ b/src/web-ui/src/infrastructure/config/components/SessionConfig.tsx @@ -26,7 +26,7 @@ import { DEFAULT_LANGUAGE_TEMPLATES, } from '../types'; import { ModelSelectionRadio } from './ModelSelectionRadio'; -import { open } from '@tauri-apps/plugin-dialog'; +import {workspaceAPI} from "@/infrastructure"; import { createLogger } from '@/shared/utils/logger'; import './AIFeaturesConfig.scss'; import './DebugConfig.scss'; @@ -469,11 +469,7 @@ const SessionConfig: React.FC = () => { const handleSelectLogPath = async () => { try { - const selected = await open({ - multiple: false, - directory: false, - filters: [{ name: tDebug('fileDialog.logFile'), extensions: ['log', 'txt', 'ndjson'] }], - }); + const selected = await workspaceAPI.open_oh_file_dialog(); if (selected) { updateDebugConfig({ log_path: selected }); notificationService.success(tDebug('messages.logPathUpdated'), { duration: 2000 }); diff --git a/src/web-ui/src/infrastructure/config/components/SkillsConfig.tsx b/src/web-ui/src/infrastructure/config/components/SkillsConfig.tsx index 23e6d7281..24561f4cb 100644 --- a/src/web-ui/src/infrastructure/config/components/SkillsConfig.tsx +++ b/src/web-ui/src/infrastructure/config/components/SkillsConfig.tsx @@ -8,9 +8,9 @@ import { useCurrentWorkspace } from '@/infrastructure/contexts/WorkspaceContext' import { useNotification } from '@/shared/notification-system'; import { configAPI } from '../../api/service-api/ConfigAPI'; import type { SkillInfo, SkillLevel, SkillMarketItem, SkillValidationResult } from '../types'; -import { open } from '@tauri-apps/plugin-dialog'; import { createLogger } from '@/shared/utils/logger'; import './SkillsConfig.scss'; +import {workspaceAPI} from "@/infrastructure"; const log = createLogger('SkillsConfig'); @@ -176,7 +176,7 @@ const SkillsConfig: React.FC = () => { const handleBrowse = async () => { try { - const selected = await open({ directory: true, multiple: false, title: t('form.path.label') }); + const selected = await workspaceAPI.open_oh_file_dialog(); if (selected) setFormPath(selected as string); } catch (err) { log.error('Failed to open file dialog', err);