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;