diff --git a/packages/desktop/src-tauri/Info.plist b/packages/desktop/src-tauri/Info.plist index 09babc54..f6f641ce 100644 --- a/packages/desktop/src-tauri/Info.plist +++ b/packages/desktop/src-tauri/Info.plist @@ -6,5 +6,13 @@ NSSupportsSuddenTermination + NSAppleEventsUsageDescription + OpenChamber needs to run the OpenCode CLI to provide AI coding assistance. + NSDesktopFolderUsageDescription + OpenChamber needs access to work with your projects. + NSDocumentsFolderUsageDescription + OpenChamber needs access to work with your projects. + NSDownloadsFolderUsageDescription + OpenChamber needs access to work with your projects. diff --git a/packages/desktop/src-tauri/src/main.rs b/packages/desktop/src-tauri/src/main.rs index 4988c3e1..02ca7f97 100644 --- a/packages/desktop/src-tauri/src/main.rs +++ b/packages/desktop/src-tauri/src/main.rs @@ -191,17 +191,20 @@ struct ServerInfoPayload { opencode_port: Option, api_prefix: String, cli_available: bool, + has_last_directory: bool, } #[tauri::command] async fn desktop_server_info( state: tauri::State<'_, DesktopRuntime>, ) -> Result { + let has_last_directory = state.settings().last_directory().await.ok().flatten().is_some(); Ok(ServerInfoPayload { server_port: state.server_port, opencode_port: state.opencode.current_port(), api_prefix: state.opencode.api_prefix(), cli_available: state.opencode.is_cli_available(), + has_last_directory, }) } @@ -332,8 +335,14 @@ fn main() { let app_handle = app.app_handle().clone(); let runtime_clone = runtime.clone(); + let has_initial_dir = tauri::async_runtime::block_on(runtime.settings().last_directory()).ok().flatten().is_some(); tauri::async_runtime::spawn(async move { - runtime_clone.start_opencode().await; + // Only start opencode if we have a saved directory, otherwise frontend will prompt + if has_initial_dir { + runtime_clone.start_opencode().await; + } else { + info!("[desktop] No saved directory - waiting for user to select one"); + } if let Err(e) = restore_bookmarks_on_startup(app_handle.state::().clone()).await { warn!("Failed to restore bookmarks on startup: {}", e); diff --git a/packages/desktop/src/main.tsx b/packages/desktop/src/main.tsx index 4a42f258..a522febd 100644 --- a/packages/desktop/src/main.tsx +++ b/packages/desktop/src/main.tsx @@ -257,6 +257,7 @@ interface ServerInfo { opencode_port: number | null; api_prefix: string; cli_available: boolean; + has_last_directory: boolean; } const checkRuntimeReady = async (): Promise => { @@ -268,15 +269,46 @@ const checkRuntimeReady = async (): Promise => { } }; -const alreadyReady = await checkRuntimeReady(); -if (!alreadyReady) { - await new Promise((resolve) => { - listen('openchamber:runtime-ready', () => { - console.info('[main] Runtime ready event received'); - resolve(); - }); - }); -} +// Check if we need to prompt for directory selection first +const promptForDirectoryIfNeeded = async (): Promise => { + try { + const info = await invoke('desktop_server_info'); + // If CLI available but no saved directory, prompt user + if (info.cli_available && !info.has_last_directory) { + console.info('[main] No saved directory - prompting user'); + const { open } = await import('@tauri-apps/plugin-dialog'); + const selected = await open({ + directory: true, + multiple: false, + title: 'Select a project folder to get started' + }); + + if (selected && typeof selected === 'string') { + await invoke('process_directory_selection', { path: selected }); + await invoke('restart_opencode'); + } + } + } catch (error) { + console.error('[main] Directory selection failed:', error); + } +}; + +// Check if directory selection is needed, then wait for opencode +await promptForDirectoryIfNeeded(); + +// Wait for opencode to be ready (or timeout if no CLI) +const waitForOpencode = async (): Promise => { + const maxAttempts = 50; + for (let i = 0; i < maxAttempts; i++) { + const info = await invoke('desktop_server_info'); + // Ready if opencode running, or no CLI (will show onboarding) + if (!info.cli_available || info.opencode_port !== null) { + return; + } + await new Promise(r => setTimeout(r, 200)); + } +}; +await waitForOpencode(); try { await import('@openchamber/ui/main'); diff --git a/packages/ui/src/hooks/useUpdateCheck.ts b/packages/ui/src/hooks/useUpdateCheck.ts index c1f07180..00adbd1e 100644 --- a/packages/ui/src/hooks/useUpdateCheck.ts +++ b/packages/ui/src/hooks/useUpdateCheck.ts @@ -133,23 +133,22 @@ export const useUpdateCheck = (checkOnMount = true): UseUpdateCheckReturn => { const downloadUpdate = useCallback(async () => { if (mockMode) { - setMockState((prev) => ({ ...prev, downloading: true })); - // Simulate download progress + setMockState((prev) => prev ? { ...prev, downloading: true } : prev); let progress = 0; const interval = setInterval(() => { progress += 20; - setMockState((prev) => ({ + setMockState((prev) => prev ? { ...prev, progress: { downloaded: progress * 1000, total: 100000 } - })); + } : prev); if (progress >= 100) { clearInterval(interval); - setMockState((prev) => ({ + setMockState((prev) => prev ? { ...prev, downloading: false, downloaded: true, progress: null, - })); + } : prev); } }, 500); return;