diff --git a/Cargo.lock b/Cargo.lock index 37b6c5ccf..924da7da4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6022,6 +6022,7 @@ version = "2.0.0-rc.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ab7f01e9310a820edd31c80fde3cae445295adde21a3f9416517d7d65015b971" dependencies = [ + "serde_json", "specta-macros", "thiserror 1.0.69", ] diff --git a/apps/native/.storybook/mocks/tauri-api.ts b/apps/native/.storybook/mocks/tauri-api.ts index 37190e653..0b0e80fac 100644 --- a/apps/native/.storybook/mocks/tauri-api.ts +++ b/apps/native/.storybook/mocks/tauri-api.ts @@ -1,12 +1,7 @@ import { storybookDarwinAPI, tauriEvent } from "./tauri-runtime"; -export const DEFAULT_MAX_ITERATIONS = 25; - export const darwinAPI = storybookDarwinAPI; -export const EVOLVE_EVENT_CHANNEL = "darwin:evolve:event"; -export const CONFIG_CHANGED_CHANNEL = "config:changed"; - export const ipcRenderer = { on: tauriEvent.listen, once: tauriEvent.once, @@ -15,33 +10,40 @@ export const ipcRenderer = { // Re-export types from shared (Specta-generated) export type { BuildCheckResult, + Change, ChangeType, CliToolsState, Config as DarwinConfig, ConfigChangedEvent, + ConfigEditApplyResult, + Commit, + CommitResult, DarwinApplyDataEvent, DarwinApplyEndEvent, DarwinApplySummaryEvent, - EvolutionFailureResult, - EvolutionResult, - EvolutionState, - EvolutionTelemetry, + EvolveCancelResult, EvolveEvent, EvolveEventType, EvolveState, EvolveStep, + EvolutionFailureResult, + EvolutionResult, + EvolutionState, + EvolutionTelemetry, FeedbackAiProviderModelInfo, FeedbackFlakeInputEntry, FeedbackFlakeInputsSnapshot, + FeedbackMetadata, FeedbackMetadataRequest, FeedbackPanicDetails, FeedbackShareOptions, FeedbackSystemInfo, - FileEntry, + FeedbackUsageStats, + FinalizeApplyResult, GitFileStatus, GitStatus, - HomebrewState, HistoryItem, + HomebrewState, NixCheckResult, NixDarwinRebuildEndEvent, NixInstallEndEvent, @@ -53,44 +55,17 @@ export type { PermissionStatus, PermissionsState, PreviewIndicatorState, - RecommendedPrompt, RebuildErrorType, + RecommendedPrompt, + RollbackResult, + RustPanicEvent, SemanticChangeMap, SetDirResult, - SummarizerUpdateEvent, SummarizedChangeSet, - RustPanicEvent, + SummarizerUpdateEvent, SystemDefault, SystemDefaultsScan, UiPrefs as DarwinPrefs, UiPrefsUpdate as DarwinPrefsUpdate, WatcherEvent, - Change, - Commit, - CommitResult, - ConfigEditApplyResult, - EvolveCancelResult, - FinalizeApplyResult, - RollbackResult, } from "../../src/types/shared"; - -export interface FeedbackUsageStats { - totalEvolutions?: number; - successRate?: number; - avgIterations?: number; - lastComputedAt?: string; - extra?: Record; -} - -export interface FeedbackMetadata { - currentAppStateSnapshot?: unknown; - systemInfo?: import("../../src/types/shared").FeedbackSystemInfo; - usageStats?: FeedbackUsageStats; - evolutionLogContent?: string; - changedNixFilesDiff?: string; - aiProviderModelInfo?: import("../../src/types/shared").FeedbackAiProviderModelInfo; - buildErrorOutput?: string; - flakeInputsSnapshot?: import("../../src/types/shared").FeedbackFlakeInputsSnapshot; - appLogsContent?: string; - panicDetails?: import("../../src/types/shared").FeedbackPanicDetails; -} diff --git a/apps/native/.storybook/mocks/tauri-runtime.ts b/apps/native/.storybook/mocks/tauri-runtime.ts index a0cc1b960..ed72b52de 100644 --- a/apps/native/.storybook/mocks/tauri-runtime.ts +++ b/apps/native/.storybook/mocks/tauri-runtime.ts @@ -461,5 +461,3 @@ if (typeof window !== "undefined") { transformCallback, }; } - -export { invoke, storybookDarwinAPI, tauriEvent }; diff --git a/apps/native/src-tauri/Cargo.toml b/apps/native/src-tauri/Cargo.toml index 30f7bfe7f..62a329ae3 100644 --- a/apps/native/src-tauri/Cargo.toml +++ b/apps/native/src-tauri/Cargo.toml @@ -62,7 +62,7 @@ fancy-regex = "0.17.0" toml = "1.0.3" backtrace = "0.3" clap = { version = "4", features = ["derive"] } -specta = { version = "=2.0.0-rc.22", features = ["derive"] } +specta = { version = "=2.0.0-rc.22", features = ["derive", "serde_json"] } specta-typescript = "0.0.9" rnix = "0.14.0" serde_yaml = "0.9" diff --git a/apps/native/src-tauri/examples/specta_gen_ts.rs b/apps/native/src-tauri/examples/specta_gen_ts.rs index 7b4f741a7..298d493f1 100644 --- a/apps/native/src-tauri/examples/specta_gen_ts.rs +++ b/apps/native/src-tauri/examples/specta_gen_ts.rs @@ -39,6 +39,8 @@ fn main() { .register::() .register::() .register::() + .register::() + .register::() .register::() .register::() .register::() @@ -94,8 +96,7 @@ fn main() { .register::() .register::() .register::() - .register::() - .register::(); + .register::(); let shared_output_path = "../src/types/shared.ts"; diff --git a/apps/native/src-tauri/src/README.md b/apps/native/src-tauri/src/README.md index 4b9a3f0c2..e80b13152 100644 --- a/apps/native/src-tauri/src/README.md +++ b/apps/native/src-tauri/src/README.md @@ -43,9 +43,9 @@ Provider abstraction layer for chat completions. **Not** evolve-specific. ### `commands/` — Tauri IPC handlers Thin `#[tauri::command]` handlers that the frontend calls via `invoke()`. -Each file (config, apply, evolve, git, rollback, summarize, homebrew, feedback, ui_prefs, permissions, system_defaults, cli_tool, updater, editor, evolve_state, peek, debug) delegates to the corresponding backend module. +Each file (apply, cli_tool, config, debug, editor, evolve, evolve_state, feedback, git, homebrew, peek, permissions, rollback, summarize, system_defaults, ui_prefs, updater) delegates to the corresponding backend module. `helpers.rs` contains shared command utilities. -**Called by:** Tauri invoke handler in main.rs; primary callers are the TypeScript frontend +**Called by:** Tauri invoke handler in `main.rs`. All frontend calls go through `tauri-api.ts` in the TypeScript layer. ### `db/` — SQLite persistence diff --git a/apps/native/src-tauri/src/commands/apply.rs b/apps/native/src-tauri/src/commands/apply.rs index 09049eda6..7ce159557 100644 --- a/apps/native/src-tauri/src/commands/apply.rs +++ b/apps/native/src-tauri/src/commands/apply.rs @@ -1,9 +1,7 @@ use super::helpers::capture_err; -use crate::bootstrap::default_config; use crate::storage::store; use crate::system::nix; use crate::{rebuild, shared_types}; -use std::process::Command; use tauri::AppHandle; /// Starts a streaming darwin-rebuild switch operation. @@ -53,72 +51,6 @@ pub async fn darwin_activate_store_path( .map_err(|e| capture_err("darwin_activate_store_path", e)) } -/// Cancels an in-progress apply by stashing changes on a new branch and returning to the previous branch. -/// Does not kill the running darwin-rebuild process; process cancellation is not yet implemented. -#[tauri::command] -pub async fn darwin_apply_stream_cancel(app: AppHandle) -> Result { - let dir = store::ensure_config_dir_exists(&app) - .map_err(|e| capture_err("darwin_apply_stream_cancel", e))?; - - let output = Command::new("git") - .args(["add", "."]) - .current_dir(&dir) - .env("PATH", nix::get_nix_path()) - .output() - .map_err(|e| capture_err("darwin_apply_stream_cancel", e))?; - if !output.status.success() { - return Err(format!( - "Failed to add files to git: {}", - String::from_utf8_lossy(&output.stderr) - )); - } - - let date = chrono::Utc::now().format("%Y-%m-%d %H:%M:%S").to_string(); - let output = Command::new("git") - .args(["checkout", "-b", &format!("canceled-{}", date)]) - .current_dir(&dir) - .env("PATH", nix::get_nix_path()) - .output() - .map_err(|e| capture_err("darwin_apply_stream_cancel", e))?; - if !output.status.success() { - return Err(format!( - "Failed to checkout canceled commit: {}", - String::from_utf8_lossy(&output.stderr) - )); - } - - let commit_hash = String::from_utf8_lossy(&output.stdout).trim().to_string(); - let output = Command::new("git") - .args(["commit", "-m", &format!("Canceled commit: {}", commit_hash)]) - .current_dir(&dir) - .env("PATH", nix::get_nix_path()) - .output() - .map_err(|e| capture_err("darwin_apply_stream_cancel", e))?; - if !output.status.success() { - return Err(format!( - "Failed to commit canceled commit: {}", - String::from_utf8_lossy(&output.stderr) - )); - } - - // check out prev branch - let output = Command::new("git") - .args(["checkout", "-"]) - .current_dir(&dir) - .env("PATH", nix::get_nix_path()) - .output() - .map_err(|e| capture_err("darwin_apply_stream_cancel", e))?; - if !output.status.success() { - return Err(format!( - "Failed to checkout previous branch: {}", - String::from_utf8_lossy(&output.stderr) - )); - } - - // TODO: Implement actual cancellation by tracking the child process - Ok(shared_types::OkResult::yes()) -} - /// Records build state and changeset after a successful darwin-rebuild switch. #[tauri::command] pub async fn finalize_apply(app: AppHandle) -> Result { @@ -139,27 +71,6 @@ pub async fn finalize_rollback( .map_err(|e| capture_err("finalize_rollback", e)) } -#[tauri::command] -pub async fn flake_installed_apps(app: AppHandle) -> Result, String> { - let dir = store::ensure_config_dir_exists(&app) - .map_err(|e| capture_err("flake_installed_apps", e))?; - - let host = nix::determine_host_attr(&app) - .or_else(|| { - let hosts = nix::list_darwin_hosts(&dir).ok()?; - if hosts.len() == 1 { - Some(hosts[0].clone()) - } else { - None - } - }) - .ok_or_else(|| "Host attribute not found".to_string())?; - - let apps = nix::evaluate_installed_apps(&dir, &host) - .map_err(|e| capture_err("flake_installed_apps", e))?; - Ok(apps) -} - #[tauri::command] pub async fn nix_check() -> Result { let installed = nix::is_nix_installed(); @@ -193,12 +104,6 @@ pub async fn nix_install_start(app: AppHandle) -> Result Result { - default_config::finalize_flake_lock(&app)?; - Ok(shared_types::OkResult::yes()) -} - /// Lists all darwinConfigurations defined in the flake. #[tauri::command] pub async fn flake_list_hosts(app: AppHandle) -> Result, String> { diff --git a/apps/native/src-tauri/src/commands/editor.rs b/apps/native/src-tauri/src/commands/editor.rs index 0732b80be..3ee8e805d 100644 --- a/apps/native/src-tauri/src/commands/editor.rs +++ b/apps/native/src-tauri/src/commands/editor.rs @@ -1,4 +1,4 @@ -use crate::{editor, shared_types}; +use crate::editor; use tauri::AppHandle; /// Read a file relative to the config directory. @@ -17,12 +17,6 @@ pub async fn editor_write_file( editor::write_file(&app, &rel_path, &content).await } -/// List files in the config directory. -#[tauri::command] -pub async fn editor_list_files(app: AppHandle) -> Result, String> { - editor::list_files(&app).await -} - /// Start the nixd LSP server. #[tauri::command] pub async fn lsp_start(app: AppHandle) -> Result<(), String> { diff --git a/apps/native/src-tauri/src/commands/git.rs b/apps/native/src-tauri/src/commands/git.rs index 34b2538a0..90f525fa3 100644 --- a/apps/native/src-tauri/src/commands/git.rs +++ b/apps/native/src-tauri/src/commands/git.rs @@ -22,12 +22,6 @@ pub async fn git_status_and_cache(app: AppHandle) -> Result Result, String> { - git::cached(&app).map_err(|e| capture_err("git_cached", e)) -} - /// Stages all changes and creates a commit with the given message. #[tauri::command] pub async fn git_commit( diff --git a/apps/native/src-tauri/src/commands/permissions.rs b/apps/native/src-tauri/src/commands/permissions.rs index 5fdeb0221..6cb6638df 100644 --- a/apps/native/src-tauri/src/commands/permissions.rs +++ b/apps/native/src-tauri/src/commands/permissions.rs @@ -18,8 +18,3 @@ pub async fn permissions_request( .map_err(|e| capture_err("permissions_request", e)) } -/// Check if all required permissions are granted. -#[tauri::command] -pub async fn permissions_all_required_granted() -> Result { - Ok(permissions::all_required_permissions_granted()) -} diff --git a/apps/native/src-tauri/src/commands/updater.rs b/apps/native/src-tauri/src/commands/updater.rs index 1ca9aa89d..616b325b1 100644 --- a/apps/native/src-tauri/src/commands/updater.rs +++ b/apps/native/src-tauri/src/commands/updater.rs @@ -51,3 +51,13 @@ pub fn relaunch_after_update(app: AppHandle) -> Result<(), String> { // (it does not call std::process::exit directly), so we must return Ok here. Ok(()) } + +#[tauri::command] +pub async fn install_version(app: AppHandle, version: String) -> Result<(), String> { + crate::updater_pin::install_version(app, version).await +} + +#[tauri::command] +pub async fn clear_pinned_version(app: AppHandle) -> Result<(), String> { + crate::updater_pin::clear_pinned_version(app).await +} diff --git a/apps/native/src-tauri/src/editor/mod.rs b/apps/native/src-tauri/src/editor/mod.rs index 1857e4ad6..964794f96 100644 --- a/apps/native/src-tauri/src/editor/mod.rs +++ b/apps/native/src-tauri/src/editor/mod.rs @@ -7,7 +7,6 @@ pub mod lsp; use std::path::Path; -pub(crate) use crate::shared_types::FileEntry; use tauri::AppHandle; use crate::evolve::file_ops; @@ -46,48 +45,3 @@ pub async fn write_file(app: &AppHandle, rel_path: &str, content: &str) -> Resul std::fs::write(&full_path, content).map_err(|e| format!("Failed to write {}: {}", rel_path, e)) } -/// List files in the config directory recursively. -pub async fn list_files(app: &AppHandle) -> Result, String> { - let config_dir = store::get_config_dir(app).map_err(|e| e.to_string())?; - let base = Path::new(&config_dir); - - let mut entries = Vec::new(); - collect_entries(base, base, &mut entries).map_err(|e| e.to_string())?; - entries.sort_by(|a, b| a.path.cmp(&b.path)); - Ok(entries) -} - -fn collect_entries( - base: &Path, - dir: &Path, - entries: &mut Vec, -) -> Result<(), std::io::Error> { - for entry in std::fs::read_dir(dir)? { - let entry = entry?; - let path = entry.path(); - let name = entry.file_name().to_string_lossy().to_string(); - - // Skip hidden files/dirs and common noise - if name.starts_with('.') || name == "node_modules" || name == "result" { - continue; - } - - let rel = path - .strip_prefix(base) - .unwrap_or(&path) - .to_string_lossy() - .to_string(); - - let is_dir = path.is_dir(); - entries.push(FileEntry { - path: rel, - name, - is_dir, - }); - - if is_dir { - collect_entries(base, &path, entries)?; - } - } - Ok(()) -} diff --git a/apps/native/src-tauri/src/git/exec.rs b/apps/native/src-tauri/src/git/exec.rs index cbad01f04..7e7bd491b 100644 --- a/apps/native/src-tauri/src/git/exec.rs +++ b/apps/native/src-tauri/src/git/exec.rs @@ -315,11 +315,6 @@ pub fn cache_status(app: &AppHandle, status: &GitStatus) - crate::storage::store::set_cached_git_status(app, status) } -/// Returns cached -pub fn cached(app: &AppHandle) -> Result> { - crate::storage::store::get_cached_git_status(app) -} - /// Registers all untracked files as intent-to-add in the git index. /// Makes files visible to `git ls-files` (and therefore Nix flakes) pub fn intent_add_untracked(dir: &str) -> Result<()> { diff --git a/apps/native/src-tauri/src/git/mod.rs b/apps/native/src-tauri/src/git/mod.rs index b5bc769cd..cc80f8ce2 100644 --- a/apps/native/src-tauri/src/git/mod.rs +++ b/apps/native/src-tauri/src/git/mod.rs @@ -7,7 +7,7 @@ pub mod exec; // `crate::git::some_fn()` without change. #[allow(unused_imports)] pub use exec::{ - cache_status, cached, checkout_files_at_commit, commit_all, commit_diff, + cache_status, checkout_files_at_commit, commit_all, commit_diff, create_evolution_backup, current_branch, delete_backup_branch, get_full_diff, get_nix_diff, get_ref_sha, init_repo, intent_add_untracked, is_repo, log, read_tags, restore_all, restore_from_branch_ref, stash, status, status_and_cache, tag_commit, CommitInfo, diff --git a/apps/native/src-tauri/src/main.rs b/apps/native/src-tauri/src/main.rs index 170db6023..e7d69198f 100644 --- a/apps/native/src-tauri/src/main.rs +++ b/apps/native/src-tauri/src/main.rs @@ -490,7 +490,6 @@ fn run_gui_mode( // Git commands::git::git_status, commands::git::git_status_and_cache, - commands::git::git_cached, commands::git::git_commit, commands::git::git_stash, // Darwin/Nix @@ -499,7 +498,6 @@ fn run_gui_mode( commands::evolve::darwin_evolve_answer, commands::apply::darwin_apply_stream_start, commands::apply::darwin_activate_store_path, - commands::apply::darwin_apply_stream_cancel, commands::apply::finalize_apply, commands::apply::finalize_rollback, commands::rollback::rollback_erase, @@ -514,8 +512,6 @@ fn run_gui_mode( commands::apply::nix_check, commands::apply::nix_install_start, commands::apply::darwin_rebuild_prefetch, - commands::apply::finalize_flake_lock, - commands::apply::flake_installed_apps, commands::apply::flake_list_hosts, commands::config::flake_exists, commands::config::bootstrap_default_config, @@ -546,7 +542,6 @@ fn run_gui_mode( // Permissions commands::permissions::permissions_check_all, commands::permissions::permissions_request, - commands::permissions::permissions_all_required_granted, // System defaults scanner commands::system_defaults::get_recommended_prompt, commands::system_defaults::scan_system_defaults, @@ -556,12 +551,11 @@ fn run_gui_mode( commands::cli_tool::list_cli_models, // Updater commands::updater::relaunch_after_update, - updater_pin::install_version, - updater_pin::clear_pinned_version, + commands::updater::install_version, + commands::updater::clear_pinned_version, // Editor commands::editor::editor_read_file, commands::editor::editor_write_file, - commands::editor::editor_list_files, // LSP commands::editor::lsp_start, commands::editor::lsp_send, diff --git a/apps/native/src-tauri/src/shared_types/feedback.rs b/apps/native/src-tauri/src/shared_types/feedback.rs index 94394628b..5efa5ae45 100644 --- a/apps/native/src-tauri/src/shared_types/feedback.rs +++ b/apps/native/src-tauri/src/shared_types/feedback.rs @@ -43,8 +43,7 @@ pub struct FeedbackSystemInfo { } /// Aggregated usage stats for feedback. -#[allow(dead_code)] -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize, Type)] #[serde(rename_all = "camelCase")] pub struct FeedbackUsageStats { /// Number of evolutions recorded locally. @@ -117,8 +116,7 @@ pub struct FeedbackMetadataRequest { } /// Metadata collected for feedback submission based on user opt-in. -#[allow(dead_code)] -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize, Type)] #[serde(rename_all = "camelCase")] pub struct FeedbackMetadata { /// Current frontend/store snapshot, represented as arbitrary JSON. diff --git a/apps/native/src-tauri/src/shared_types/system.rs b/apps/native/src-tauri/src/shared_types/system.rs index f73d10b2c..c22db6f6a 100644 --- a/apps/native/src-tauri/src/shared_types/system.rs +++ b/apps/native/src-tauri/src/shared_types/system.rs @@ -120,14 +120,3 @@ pub struct RecommendedPrompt { pub prompt_text: String, } -/// File or directory entry returned by the editor tree. -#[derive(Debug, Clone, Serialize, Type)] -#[serde(rename_all = "camelCase")] -pub struct FileEntry { - /// Path relative to the selected config directory. - pub path: String, - /// File or directory basename. - pub name: String, - /// Whether this entry is a directory. - pub is_dir: bool, -} diff --git a/apps/native/src-tauri/src/system/nix.rs b/apps/native/src-tauri/src/system/nix.rs index fdb1f6d5c..10056b0ab 100644 --- a/apps/native/src-tauri/src/system/nix.rs +++ b/apps/native/src-tauri/src/system/nix.rs @@ -26,7 +26,7 @@ use anyhow::Result; use log::{error, info}; -use serde_json::Value; + use std::io::{Read as _, Write as _}; use std::process::{Command, Stdio}; use std::sync::OnceLock; @@ -97,31 +97,6 @@ pub fn determine_host_attr(app: &tauri::AppHandle) -> Opti crate::storage::store::read_host_attr_from_file() } -pub fn evaluate_installed_apps(config_dir: &str, host_attr: &str) -> Result> { - let attr = format!( - ".#darwinConfigurations.{}.config.environment.systemPackages", - host_attr - ); - - let output = Command::new("nix") - .args(["eval", "--json", &attr]) - .current_dir(config_dir) - .env("PATH", get_nix_path()) - .env("NIX_CONFIG", "experimental-features = nix-command flakes") - .output()?; - - if !output.status.success() { - anyhow::bail!( - "Failed to evaluate installed apps: {}", - String::from_utf8_lossy(&output.stderr) - ); - } - - let stdout = String::from_utf8(output.stdout)?; - let apps: Vec = serde_json::from_str(&stdout)?; - Ok(apps) -} - pub fn list_darwin_hosts(config_dir: &str) -> Result> { let output = Command::new("nix") .args([ diff --git a/apps/native/src-tauri/src/system/permissions.rs b/apps/native/src-tauri/src/system/permissions.rs index ff56b8832..515a79468 100644 --- a/apps/native/src-tauri/src/system/permissions.rs +++ b/apps/native/src-tauri/src/system/permissions.rs @@ -415,12 +415,6 @@ pub fn request_permission(permission_id: &str) -> Result { } } -/// Check if all required permissions are granted -pub fn all_required_permissions_granted() -> bool { - let state: PermissionsState = check_all_permissions(); - state.all_required_granted -} - #[cfg(test)] mod tests { use super::*; diff --git a/apps/native/src-tauri/src/updater_pin/mod.rs b/apps/native/src-tauri/src/updater_pin/mod.rs index a271bc9ec..3f47c5899 100644 --- a/apps/native/src-tauri/src/updater_pin/mod.rs +++ b/apps/native/src-tauri/src/updater_pin/mod.rs @@ -177,13 +177,11 @@ async fn install_version_impl(_app: tauri::AppHandle, _version: String) -> Resul /// Install a specific past release of nixmac for bisecting. /// /// The frontend should call `relaunch_after_update` after this returns successfully. -#[tauri::command] pub async fn install_version(app: tauri::AppHandle, version: String) -> Result<(), String> { install_version_impl(app, version).await } /// Clear the pinned-version preference so the silent update check resumes. -#[tauri::command] pub async fn clear_pinned_version(app: tauri::AppHandle) -> Result<(), String> { crate::storage::store::delete_pref(&app, crate::storage::store::PINNED_VERSION_KEY) .map_err(|e| format!("failed to clear pinned version: {e}"))?; diff --git a/apps/native/src/README.md b/apps/native/src/README.md new file mode 100644 index 000000000..354ed6849 --- /dev/null +++ b/apps/native/src/README.md @@ -0,0 +1,32 @@ +# nixmac frontend source layout + +React + TypeScript frontend served by Vite inside a Tauri webview. + +## IPC Gateway + +`tauri-api.ts` mirrors the Rust command layer: handlers registered in `main.rs` have corresponding `invoke` calls here. Hooks and occasionally components call `darwinAPI.*`; `tauri-api.ts` reflects the handlers exposed one-to-one. + +### DarwinWidget + +`components/widget/widget.tsx` is the root of the app. It initializes state on mount and handles step routing. + +`components/ui/` — shadcn/ui primitives (buttons, dialogs, inputs, etc.) + +## Domain Hooks + +Hooks in `hooks/` sit between components and `tauri-api.ts`. Each hook owns a slice of async state: loading flags, error handling, IPC subscriptions. Components call hooks; (mostly) hooks call `darwinAPI`. + +## App State + +`widget-store.ts` is a Zustand store that holds widget step routing, git status, evolve state, UI preferences, and shared flags. Most hooks read from and write to this store. + +Components read from this store directly when possible to minimize prop-drilling. + +## Preview Indicator + +A separate floating overlay window (`preview-indicator-window.tsx`) shows uncommitted-change state as a corner widget. Its components live in `components/preview-indicator/`. The Rust side (`peek.rs`, `commands/peek.rs`) manages window visibility and state. + +## Shared Types + +- `shared.ts` — Auto-generated by Specta from Rust structs in `src-tauri/src/shared_types/`. Do not edit manually. +- `sqlite.ts` — Auto-generated SQLite row types. Same process. diff --git a/apps/native/src/components/kibo-ui/nix-editor/__snapshots__/nix-editor.stories.tsx.snap b/apps/native/src/components/kibo-ui/nix-editor/__snapshots__/nix-editor.stories.tsx.snap index 72d21ae82..6fc16a877 100644 --- a/apps/native/src/components/kibo-ui/nix-editor/__snapshots__/nix-editor.stories.tsx.snap +++ b/apps/native/src/components/kibo-ui/nix-editor/__snapshots__/nix-editor.stories.tsx.snap @@ -5,4 +5,3 @@ exports[`Configuration Nix 1`] = `"
"`; exports[`Unknown File 1`] = `"
"`; - diff --git a/apps/native/src/components/widget/controls/directory-picker.test.tsx b/apps/native/src/components/widget/controls/directory-picker.test.tsx index fb4296459..0700606b3 100644 --- a/apps/native/src/components/widget/controls/directory-picker.test.tsx +++ b/apps/native/src/components/widget/controls/directory-picker.test.tsx @@ -2,6 +2,8 @@ import { act, fireEvent, render, screen, waitFor } from "@testing-library/react" import "@testing-library/jest-dom"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import type { SetDirResult } from "@/types/shared"; + import { useWidgetStore } from "@/stores/widget-store"; import { DirectoryPicker } from "@/components/widget/controls/directory-picker"; @@ -34,7 +36,7 @@ vi.mock("@/hooks/use-darwin-config", () => ({ const mockNormalize = vi.fn<(p: string) => Promise>(); const mockExists = vi.fn<(p: string) => Promise>(); -const mockSetDir = vi.fn<(p: string) => Promise>(); +const mockSetDir = vi.fn<(p: string) => Promise>(); const mockSetHostAttr = vi.fn<(h: string) => Promise>(); const mockListHosts = vi.fn<() => Promise>(); const mockFlakeExistsAt = vi.fn<(p: string) => Promise>(); @@ -51,9 +53,6 @@ vi.mock("@/tauri-api", () => ({ pickDir: () => mockPickDir(), setHostAttr: (h: string) => mockSetHostAttr(h), }, - flake: { - setHostAttr: (h: string) => mockSetHostAttr(h), - }, flake: { listHosts: () => mockListHosts(), existsAt: (p: string) => mockFlakeExistsAt(p), diff --git a/apps/native/src/components/widget/feedback/feedback-dialog.tsx b/apps/native/src/components/widget/feedback/feedback-dialog.tsx index 639e947f9..9595962b0 100644 --- a/apps/native/src/components/widget/feedback/feedback-dialog.tsx +++ b/apps/native/src/components/widget/feedback/feedback-dialog.tsx @@ -1,6 +1,6 @@ import { useEffect, useState } from "react"; import * as Sentry from "@sentry/react"; -import { invoke } from "@tauri-apps/api/core"; + import { useCurrentStep, useWidgetStore } from "@/stores/widget-store"; import { Dialog, @@ -385,15 +385,15 @@ export function FeedbackDialog() { share: shareOptions, // artifact fields left empty for now; will be populated by caller when collecting logs lastPromptText: selectedPromptText, - currentAppStateSnapshot: metadata?.currentAppStateSnapshot, - systemInfo: metadata?.systemInfo, - usageStats: metadata?.usageStats, - evolutionLogContent: metadata?.evolutionLogContent, - changedNixFilesDiff: metadata?.changedNixFilesDiff, - aiProviderModelInfo: metadata?.aiProviderModelInfo, - buildErrorOutput: metadata?.buildErrorOutput, - flakeInputsSnapshot: metadata?.flakeInputsSnapshot, - appLogsContent: metadata?.appLogsContent, + currentAppStateSnapshot: metadata?.currentAppStateSnapshot ?? undefined, + systemInfo: metadata?.systemInfo ?? undefined, + usageStats: metadata?.usageStats ?? undefined, + evolutionLogContent: metadata?.evolutionLogContent ?? undefined, + changedNixFilesDiff: metadata?.changedNixFilesDiff ?? undefined, + aiProviderModelInfo: metadata?.aiProviderModelInfo ?? undefined, + buildErrorOutput: metadata?.buildErrorOutput ?? undefined, + flakeInputsSnapshot: metadata?.flakeInputsSnapshot ?? undefined, + appLogsContent: metadata?.appLogsContent ?? undefined, panicDetails: feedbackType === FeedbackType.Error ? (panicDetails ?? undefined) : undefined, }); @@ -443,7 +443,7 @@ export function FeedbackDialog() { // Also send from Rust backend try { - await invoke("debug_sentry_event"); + await darwinAPI.debug.sentryEvent(); } catch (err) { // eslint-disable-next-line no-console console.warn("[debug_sentry_event] Failed to invoke Rust command:", err); diff --git a/apps/native/src/components/widget/history/history-item-card.tsx b/apps/native/src/components/widget/history/history-item-card.tsx index 1096b7148..37f1158b5 100644 --- a/apps/native/src/components/widget/history/history-item-card.tsx +++ b/apps/native/src/components/widget/history/history-item-card.tsx @@ -10,7 +10,7 @@ import { HistoryItemMeta } from "@/components/widget/history/history-item-meta"; import { HistoryRestoreItemButton } from "@/components/widget/history/history-restore-item-button"; import { useHistoryCard } from "@/hooks/use-history-card"; import { cn } from "@/lib/utils"; -import type { HistoryItem } from "@/tauri-api"; +import type { HistoryItem } from "@/types/shared"; import type { TimelineContext } from "@/components/widget/history/timeline-connector"; import { HistoryItemTimeline, TimeLineConnector, TimelineDot } from "@/components/widget/history/timeline-connector"; diff --git a/apps/native/src/components/widget/history/history-item-expanded-detail.tsx b/apps/native/src/components/widget/history/history-item-expanded-detail.tsx index 4662f7531..df56761ba 100644 --- a/apps/native/src/components/widget/history/history-item-expanded-detail.tsx +++ b/apps/native/src/components/widget/history/history-item-expanded-detail.tsx @@ -1,4 +1,4 @@ -import type { HistoryItem } from "@/tauri-api"; +import type { HistoryItem } from "@/types/shared"; import type { ColorMap } from "@/components/widget/utils"; import { SinglesSection } from "@/components/widget/summaries/singles-section"; import { SummaryGroup } from "@/components/widget/summaries/summary-group"; diff --git a/apps/native/src/components/widget/history/history-item-meta.tsx b/apps/native/src/components/widget/history/history-item-meta.tsx index 88c444c1f..83151c1b5 100644 --- a/apps/native/src/components/widget/history/history-item-meta.tsx +++ b/apps/native/src/components/widget/history/history-item-meta.tsx @@ -1,4 +1,4 @@ -import type { HistoryItem } from "@/tauri-api"; +import type { HistoryItem } from "@/types/shared"; import { CommitHashBadge } from "@/components/widget/badges/commit-hash-badge"; import { FileCountBadge } from "@/components/widget/badges/file-count-badge"; import { TimeBadge } from "@/components/widget/badges/time-badge"; diff --git a/apps/native/src/components/widget/overlays/__snapshots__/editor-panel.stories.tsx.snap b/apps/native/src/components/widget/overlays/__snapshots__/editor-panel.stories.tsx.snap index 2b03e97ef..11d1df83f 100644 --- a/apps/native/src/components/widget/overlays/__snapshots__/editor-panel.stories.tsx.snap +++ b/apps/native/src/components/widget/overlays/__snapshots__/editor-panel.stories.tsx.snap @@ -1,6 +1,5 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html -exports[`Editing Configuration 1`] = `"
Editingconfiguration.nix(configuration.nix)
"`; - -exports[`Editing Flake 1`] = `"
Editingflake.nix(flake.nix)
"`; +exports[`Editing Configuration 1`] = `"
Editingconfiguration.nix(configuration.nix)
Loading editor...
"`; +exports[`Editing Flake 1`] = `"
Editingflake.nix(flake.nix)
Loading editor...
"`; diff --git a/apps/native/src/components/widget/promptinput/system-defaults-cta.tsx b/apps/native/src/components/widget/promptinput/system-defaults-cta.tsx index b7c278cda..9b4bac8d6 100644 --- a/apps/native/src/components/widget/promptinput/system-defaults-cta.tsx +++ b/apps/native/src/components/widget/promptinput/system-defaults-cta.tsx @@ -7,7 +7,7 @@ import { PopoverTrigger, } from "@/components/ui/popover"; import { useWidgetStore } from "@/stores/widget-store"; -import type { SystemDefault, SystemDefaultsScan } from "@/tauri-api"; +import type { SystemDefault, SystemDefaultsScan } from "@/types/shared"; import { darwinAPI } from "@/tauri-api"; import { Monitor, X } from "lucide-react"; import { useCallback, useEffect, useMemo, useState } from "react"; diff --git a/apps/native/src/components/widget/settings/__snapshots__/ai-models-tab.stories.tsx.snap b/apps/native/src/components/widget/settings/__snapshots__/ai-models-tab.stories.tsx.snap index 2c506f8ae..130f261af 100644 --- a/apps/native/src/components/widget/settings/__snapshots__/ai-models-tab.stories.tsx.snap +++ b/apps/native/src/components/widget/settings/__snapshots__/ai-models-tab.stories.tsx.snap @@ -1,3 +1,3 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html -exports[`Cli Providers 1`] = `"

AI Models

Evolution Model

Model used to plan and apply configuration changes in Nix

Summary Model

Model used to explain and summarize changes

Evolution Limits

Control how long the AI will try before giving up

"`; +exports[`Cli Providers 1`] = `"

AI Models

OpenRouter is the supported cloud provider in the main UI. Previously saved direct OpenAI keys still work as a legacy fallback, but they are no longer shown in Settings.

Evolution Model

Model used to plan and apply configuration changes in Nix

Summary Model

Model used to explain and summarize changes

Evolution Limits

Control how long the AI will try before giving up

"`; diff --git a/apps/native/src/components/widget/settings/ai-models-tab.tsx b/apps/native/src/components/widget/settings/ai-models-tab.tsx index 74e38301b..795e14e15 100644 --- a/apps/native/src/components/widget/settings/ai-models-tab.tsx +++ b/apps/native/src/components/widget/settings/ai-models-tab.tsx @@ -9,7 +9,9 @@ import { } from "@/components/ui/select"; import { ModelCombobox } from "@/components/widget/controls/model-combobox"; import { getProviderConfigInvalidReason, isCliProvider } from "@/lib/ai-provider-validation"; -import { darwinAPI, DEFAULT_MAX_ITERATIONS, type CliToolsState } from "@/tauri-api"; +import { DEFAULT_MAX_ITERATIONS } from "@/lib/constants"; +import { darwinAPI } from "@/tauri-api"; +import type { CliToolsState } from "@/types/shared"; import type { AnyFieldApi, ReactFormExtendedApi } from "@tanstack/react-form"; import { Info } from "lucide-react"; import { useEffect, useState } from "react"; diff --git a/apps/native/src/components/widget/settings/developer-tab.tsx b/apps/native/src/components/widget/settings/developer-tab.tsx index e19808cfb..f9970b97d 100644 --- a/apps/native/src/components/widget/settings/developer-tab.tsx +++ b/apps/native/src/components/widget/settings/developer-tab.tsx @@ -2,7 +2,7 @@ import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { useWidgetStore } from "@/stores/widget-store"; import { darwinAPI } from "@/tauri-api"; -import { invoke } from "@tauri-apps/api/core"; +import { useUpdater } from "@/hooks/use-updater"; import { getVersion } from "@tauri-apps/api/app"; import { AlertTriangle, Download, History, Pin, RotateCcw } from "lucide-react"; import { useEffect, useState } from "react"; @@ -10,6 +10,7 @@ import { useEffect, useState } from "react"; const VERSION_PATTERN = /^[0-9]+(?:\.[0-9]+){0,2}(?:-[a-zA-Z0-9.-]+)?$/; export function DeveloperTab() { + const { installVersion, relaunch, clearPinnedVersion } = useUpdater(); const pinnedVersion = useWidgetStore((s) => s.pinnedVersion); const setPinnedVersion = useWidgetStore((s) => s.setPinnedVersion); const [currentVersion, setCurrentVersion] = useState(null); @@ -37,12 +38,11 @@ export function DeveloperTab() { setStatusMessage(`Fetching v${target}…`); setInstalling(true); try { - await invoke("install_version", { version: target }); + await installVersion(target); // Sync local store immediately — persistence already happened on the Rust side. setPinnedVersion(target); setStatusMessage(`Installed v${target}. Relaunching…`); - // Reuse the same relaunch path the production updater uses. - await invoke("relaunch_after_update"); + await relaunch(); } catch (err) { setStatusMessage(null); setErrorMessage(err instanceof Error ? err.message : String(err)); @@ -53,7 +53,7 @@ export function DeveloperTab() { const handleClearPin = async () => { setErrorMessage(null); try { - await invoke("clear_pinned_version"); + await clearPinnedVersion(); setPinnedVersion(null); setStatusMessage("Cleared pinned version. The auto-updater will check for the latest on next launch."); } catch (err) { diff --git a/apps/native/src/components/widget/settings/settings-dialog.tsx b/apps/native/src/components/widget/settings/settings-dialog.tsx index 808891301..41f80bdd1 100644 --- a/apps/native/src/components/widget/settings/settings-dialog.tsx +++ b/apps/native/src/components/widget/settings/settings-dialog.tsx @@ -2,7 +2,8 @@ import { Button } from "@/components/ui/button"; import { useDarwinConfig } from "@/hooks/use-darwin-config"; import { cn } from "@/lib/utils"; import { type SettingsTab, useWidgetStore } from "@/stores/widget-store"; -import { darwinAPI, DEFAULT_MAX_ITERATIONS } from "@/tauri-api"; +import { DEFAULT_MAX_ITERATIONS } from "@/lib/constants"; +import { darwinAPI } from "@/tauri-api"; import { useForm } from "@tanstack/react-form"; import { Bot, FolderOpen, Key, Settings2, SlidersHorizontal, Wrench } from "lucide-react"; import { Suspense, useEffect, useRef, useState } from "react"; diff --git a/apps/native/src/components/widget/steps/permissions-step.tsx b/apps/native/src/components/widget/steps/permissions-step.tsx index c222c7b32..f20697f74 100644 --- a/apps/native/src/components/widget/steps/permissions-step.tsx +++ b/apps/native/src/components/widget/steps/permissions-step.tsx @@ -1,7 +1,8 @@ "use client"; import { useWidgetStore } from "@/stores/widget-store"; -import { darwinAPI, type Permission, type PermissionStatus } from "@/tauri-api"; +import { darwinAPI } from "@/tauri-api"; +import type { Permission, PermissionStatus } from "@/types/shared"; import { Button } from "@/components/ui/button"; import { Card } from "@/components/ui/card"; import { Shield, Check, X, AlertCircle, Loader2 } from "lucide-react"; diff --git a/apps/native/src/components/widget/summaries/__snapshots__/unsummarized-changes-section.stories.tsx.snap b/apps/native/src/components/widget/summaries/__snapshots__/unsummarized-changes-section.stories.tsx.snap index 75ed93c5f..96bae1974 100644 --- a/apps/native/src/components/widget/summaries/__snapshots__/unsummarized-changes-section.stories.tsx.snap +++ b/apps/native/src/components/widget/summaries/__snapshots__/unsummarized-changes-section.stories.tsx.snap @@ -4,9 +4,8 @@ exports[`Also Unsummarized 1`] = `"
Manual Changes found innixpkgs:
modules/darwin/packages.nix
modules/darwin/fonts.nix
modules/home/shell.nix
modules/darwin/terminal.nix
"`; -exports[`Repeated File Hunks 1`] = `"
Manual Changes found indarwin:
flake.lockx18
hosts/common/home.nix
flake.nix
lib/mkHost.nix
files/config/zed/settings.json
"`; +exports[`Single 1`] = `"
Manual Changes found innixpkgs:
flake.nix
"`; exports[`With Rename 1`] = `"
Manual Changes found innixpkgs:
modules/darwin/packages.nix
modules/darwin/homebrew.nixmodules/darwin/brew.nix
home.nix
"`; diff --git a/apps/native/src/components/widget/widget.test.tsx b/apps/native/src/components/widget/widget.test.tsx index 9ad231462..8d5db0491 100644 --- a/apps/native/src/components/widget/widget.test.tsx +++ b/apps/native/src/components/widget/widget.test.tsx @@ -17,8 +17,6 @@ vi.mock("@/tauri-api", () => ({ ipcRenderer: { on: vi.fn().mockReturnValue(Promise.resolve(() => {})), }, - CONFIG_CHANGED_CHANNEL: "config-changed", - DEFAULT_MAX_ITERATIONS: 25, })); // Mock hooks diff --git a/apps/native/src/hooks/use-evolve.ts b/apps/native/src/hooks/use-evolve.ts index aaee525dd..4d186d44b 100644 --- a/apps/native/src/hooks/use-evolve.ts +++ b/apps/native/src/hooks/use-evolve.ts @@ -1,5 +1,7 @@ import { useWidgetStore } from "@/stores/widget-store"; -import { darwinAPI, EVOLVE_EVENT_CHANNEL, ipcRenderer, type EvolveEvent } from "@/tauri-api"; +import { EVOLVE_EVENT_CHANNEL } from "@/lib/constants"; +import { darwinAPI, ipcRenderer } from "@/tauri-api"; +import type { EvolveEvent } from "@/types/shared"; import { useCallback } from "react"; import { useGitOperations } from "./use-git-operations"; import { usePromptHistory } from "./use-prompt-history"; diff --git a/apps/native/src/hooks/use-history-card.ts b/apps/native/src/hooks/use-history-card.ts index df79e1f38..6c2bedcb2 100644 --- a/apps/native/src/hooks/use-history-card.ts +++ b/apps/native/src/hooks/use-history-card.ts @@ -1,6 +1,6 @@ import { useCallback, useState } from "react"; import type { KeyboardEvent } from "react"; -import type { HistoryItem } from "@/tauri-api"; +import type { HistoryItem } from "@/types/shared"; import { cn } from "@/lib/utils"; import { buildColorMap } from "@/components/widget/utils"; import type { ColorMap } from "@/components/widget/utils"; diff --git a/apps/native/src/hooks/use-history-restore.ts b/apps/native/src/hooks/use-history-restore.ts index 732451872..7c798b6c1 100644 --- a/apps/native/src/hooks/use-history-restore.ts +++ b/apps/native/src/hooks/use-history-restore.ts @@ -3,7 +3,7 @@ import { useWidgetStore } from "@/stores/widget-store"; import { useRebuildStream } from "@/hooks/use-rebuild-stream"; import { useHistory } from "@/hooks/use-history"; import { darwinAPI } from "@/tauri-api"; -import type { HistoryItem } from "@/tauri-api"; +import type { HistoryItem } from "@/types/shared"; // Sentinel hash used to identify the frontend-only preview item. export const PREVIEW_ITEM_HASH = "n1xm4c0"; diff --git a/apps/native/src/hooks/use-nix-install.ts b/apps/native/src/hooks/use-nix-install.ts index 17d9c1531..305981dcb 100644 --- a/apps/native/src/hooks/use-nix-install.ts +++ b/apps/native/src/hooks/use-nix-install.ts @@ -1,11 +1,6 @@ import { useWidgetStore } from "@/stores/widget-store"; -import { - darwinAPI, - ipcRenderer, - type NixDarwinRebuildEndEvent, - type NixInstallEndEvent, - type NixInstallProgressEvent, -} from "@/tauri-api"; +import { darwinAPI, ipcRenderer } from "@/tauri-api"; +import type { NixDarwinRebuildEndEvent, NixInstallEndEvent, NixInstallProgressEvent } from "@/types/shared"; import { useCallback } from "react"; export function useNixInstall() { diff --git a/apps/native/src/hooks/use-panic-handler.ts b/apps/native/src/hooks/use-panic-handler.ts index 8c6ab53c7..2b06e47bc 100644 --- a/apps/native/src/hooks/use-panic-handler.ts +++ b/apps/native/src/hooks/use-panic-handler.ts @@ -5,7 +5,7 @@ import { useEffect } from "react"; import { listen } from "@tauri-apps/api/event"; import { toast } from "sonner"; import { useWidgetStore } from "@/stores/widget-store"; -import type { RustPanicEvent } from "@/tauri-api"; +import type { RustPanicEvent } from "@/types/shared"; import { FeedbackType } from "@/types/feedback"; export function usePanicHandler() { diff --git a/apps/native/src/hooks/use-permissions.test.ts b/apps/native/src/hooks/use-permissions.test.ts index 0273df4e0..0e39e6b0f 100644 --- a/apps/native/src/hooks/use-permissions.test.ts +++ b/apps/native/src/hooks/use-permissions.test.ts @@ -1,4 +1,4 @@ -import type { PermissionsState } from "@/tauri-api"; +import type { PermissionsState } from "@/types/shared"; import { act, renderHook } from "@testing-library/react"; import { beforeEach, describe, expect, it, vi } from "vitest"; diff --git a/apps/native/src/hooks/use-permissions.ts b/apps/native/src/hooks/use-permissions.ts index ce68f3952..6f312b7d1 100644 --- a/apps/native/src/hooks/use-permissions.ts +++ b/apps/native/src/hooks/use-permissions.ts @@ -1,6 +1,6 @@ import { useWidgetStore } from "@/stores/widget-store"; import { darwinAPI } from "@/tauri-api"; -import type { PermissionStatus, PermissionsState } from "@/tauri-api"; +import type { PermissionStatus, PermissionsState } from "@/types/shared"; import { useCallback } from "react"; /** diff --git a/apps/native/src/hooks/use-queue-summarizer.ts b/apps/native/src/hooks/use-queue-summarizer.ts index 440cf5664..8aa21d1f1 100644 --- a/apps/native/src/hooks/use-queue-summarizer.ts +++ b/apps/native/src/hooks/use-queue-summarizer.ts @@ -1,6 +1,7 @@ import { useHistory } from "@/hooks/use-history"; import { useWidgetStore } from "@/stores/widget-store"; -import { ipcRenderer, type SummarizerUpdateEvent } from "@/tauri-api"; +import { ipcRenderer } from "@/tauri-api"; +import type { SummarizerUpdateEvent } from "@/types/shared"; import { useCallback, useRef } from "react"; /** diff --git a/apps/native/src/hooks/use-rebuild-stream.ts b/apps/native/src/hooks/use-rebuild-stream.ts index 088308ece..6aa2c7293 100644 --- a/apps/native/src/hooks/use-rebuild-stream.ts +++ b/apps/native/src/hooks/use-rebuild-stream.ts @@ -1,11 +1,6 @@ import { useWidgetStore, type RebuildContext } from "@/stores/widget-store"; -import { - darwinAPI, - ipcRenderer, - type DarwinApplyDataEvent, - type DarwinApplyEndEvent, - type DarwinApplySummaryEvent, -} from "@/tauri-api"; +import { darwinAPI, ipcRenderer } from "@/tauri-api"; +import type { DarwinApplyDataEvent, DarwinApplyEndEvent, DarwinApplySummaryEvent } from "@/types/shared"; import { useCallback, useRef } from "react"; import { useGitOperations } from "./use-git-operations"; diff --git a/apps/native/src/hooks/use-updater.ts b/apps/native/src/hooks/use-updater.ts index c52db9e3e..a3111bc32 100644 --- a/apps/native/src/hooks/use-updater.ts +++ b/apps/native/src/hooks/use-updater.ts @@ -1,6 +1,5 @@ import { useState, useEffect, useCallback, useRef } from "react"; import type { Update } from "@tauri-apps/plugin-updater"; -import { invoke } from "@tauri-apps/api/core"; import { darwinAPI } from "@/tauri-api"; import { useWidgetStore } from "@/stores/widget-store"; @@ -120,7 +119,7 @@ export function useUpdater() { // custom relaunch_after_update command opens the newly-installed // bundle via LaunchServices instead of re-exec-ing the cached // (potentially stale) binary path from the old bundle. - await invoke("relaunch_after_update"); + await darwinAPI.updater.relaunch(); } catch (err) { if (isDevMode) { setState((s) => ({ @@ -144,6 +143,18 @@ export function useUpdater() { } }, [isDevMode, state.available]); + const installVersion = useCallback(async (version: string): Promise => { + await darwinAPI.updater.installVersion(version); + }, []); + + const relaunch = useCallback(async (): Promise => { + await darwinAPI.updater.relaunch(); + }, []); + + const clearPinnedVersion = useCallback(async (): Promise => { + await darwinAPI.updater.clearPinnedVersion(); + }, []); + const dismiss = useCallback(() => { setState(initialState); }, []); @@ -189,6 +200,9 @@ export function useUpdater() { ...state, checkForUpdates, installUpdate, + installVersion, + relaunch, + clearPinnedVersion, dismiss, }; } diff --git a/apps/native/src/lib/ai-provider-validation.ts b/apps/native/src/lib/ai-provider-validation.ts index cbaf40aef..da771cfa7 100644 --- a/apps/native/src/lib/ai-provider-validation.ts +++ b/apps/native/src/lib/ai-provider-validation.ts @@ -1,4 +1,4 @@ -import type { CliToolsState, DarwinPrefs } from "@/tauri-api"; +import type { CliToolsState, UiPrefs as DarwinPrefs } from "@/types/shared"; const CLI_PROVIDER_VALUES = ["claude", "codex", "opencode"] as const; diff --git a/apps/native/src/lib/constants.ts b/apps/native/src/lib/constants.ts new file mode 100644 index 000000000..1b04c538c --- /dev/null +++ b/apps/native/src/lib/constants.ts @@ -0,0 +1,2 @@ +export const DEFAULT_MAX_ITERATIONS = 25; +export const EVOLVE_EVENT_CHANNEL = "darwin:evolve:event"; diff --git a/apps/native/src/lib/lsp-client.ts b/apps/native/src/lib/lsp-client.ts index c4bd4c2fd..50555cba5 100644 --- a/apps/native/src/lib/lsp-client.ts +++ b/apps/native/src/lib/lsp-client.ts @@ -1,4 +1,4 @@ -import { invoke } from "@tauri-apps/api/core"; +import { darwinAPI } from "@/tauri-api"; import { listen, type UnlistenFn } from "@tauri-apps/api/event"; interface JsonRpcRequest { @@ -40,7 +40,7 @@ export class NixdLspClient { async start(configDir: string): Promise { if (this._running) return; - await invoke("lsp_start"); + await darwinAPI.lsp.start(); // Listen for messages from nixd via Tauri events this.unlisten = await listen("lsp:message", (event) => { @@ -82,7 +82,7 @@ export class NixdLspClient { } try { - await invoke("lsp_stop"); + await darwinAPI.lsp.stop(); } catch { // Best effort } @@ -101,13 +101,13 @@ export class NixdLspClient { this.pending.set(id, { resolve, reject }); }); - await invoke("lsp_send", { message: JSON.stringify(request) }); + await darwinAPI.lsp.send(JSON.stringify(request)); return promise; } sendNotification(method: string, params: unknown): void { const notification: JsonRpcNotification = { jsonrpc: "2.0", method, params }; - invoke("lsp_send", { message: JSON.stringify(notification) }).catch((e) => { + darwinAPI.lsp.send(JSON.stringify(notification)).catch((e: unknown) => { console.warn("[lsp-client] Failed to send notification:", e); }); } diff --git a/apps/native/src/preview-indicator-window.tsx b/apps/native/src/preview-indicator-window.tsx index 8cae67db6..0211ca64d 100644 --- a/apps/native/src/preview-indicator-window.tsx +++ b/apps/native/src/preview-indicator-window.tsx @@ -3,7 +3,7 @@ import { listen } from "@tauri-apps/api/event"; import React, { useEffect, useState } from "react"; import ReactDOM from "react-dom/client"; import { PreviewIndicator } from "@/components/preview-indicator/preview-indicator"; -import type { PreviewIndicatorState } from "@/tauri-api"; +import type { PreviewIndicatorState } from "@/types/shared"; import "./index.css"; function PreviewIndicatorWindow() { diff --git a/apps/native/src/stores/widget-store.test.ts b/apps/native/src/stores/widget-store.test.ts index 658bd01b0..3c108cd90 100644 --- a/apps/native/src/stores/widget-store.test.ts +++ b/apps/native/src/stores/widget-store.test.ts @@ -1,4 +1,4 @@ -import type { EvolveEvent, GitStatus } from "@/tauri-api"; +import type { EvolveEvent, GitStatus } from "@/types/shared"; import { describe, expect, it } from "vitest"; import { createWidgetStore, diff --git a/apps/native/src/stores/widget-store.ts b/apps/native/src/stores/widget-store.ts index 6af04555f..70492d79a 100644 --- a/apps/native/src/stores/widget-store.ts +++ b/apps/native/src/stores/widget-store.ts @@ -1,4 +1,5 @@ import { computeCurrentStep } from "@/components/widget/utils"; +import { FeedbackType } from "@/types/feedback"; import type { EvolveEvent, EvolveState, @@ -6,17 +7,16 @@ import type { HistoryItem, PermissionsState, RecommendedPrompt, -} from "@/tauri-api"; -import { FeedbackType } from "@/types/feedback"; -import type { SemanticChangeMap } from "@/types/shared"; + SemanticChangeMap, +} from "@/types/shared"; import { create } from "zustand"; import { devtools } from "zustand/middleware"; export type { EvolveEvent, - EvolveEventType, EvolveState, + EvolveEventType, + EvolveState, GitStatus, - -} from "@/tauri-api"; +} from "@/types/shared"; // ============================================================================= // Types @@ -25,11 +25,31 @@ export type { /** * Widget step state - updated by useEffect based on app state. */ -export type SettingsTab = "general" | "api-keys" | "ai-models" | "preferences" | "developer"; -export type WidgetStep = "permissions" | "nix-setup" | "setup" | "begin" | "evolve" | "commit" | "manualEvolve" | "manualCommit" | "history"; +export type SettingsTab = + | "general" + | "api-keys" + | "ai-models" + | "preferences" + | "developer"; +export type WidgetStep = + | "permissions" + | "nix-setup" + | "setup" + | "begin" + | "evolve" + | "commit" + | "manualEvolve" + | "manualCommit" + | "history"; type ProcessingAction = "evolve" | "apply" | "commit" | "cancel" | null; -export type ConfirmPrefKey = "confirmBuild" | "confirmClear" | "confirmRollback"; -export type BoolPrefKey = ConfirmPrefKey | "autoSummarizeOnFocus" | "scanHomebrewOnStartup"; +export type ConfirmPrefKey = + | "confirmBuild" + | "confirmClear" + | "confirmRollback"; +export type BoolPrefKey = + | ConfirmPrefKey + | "autoSummarizeOnFocus" + | "scanHomebrewOnStartup"; // Rebuild state for showing progress inline in the widget export type RebuildErrorType = @@ -76,7 +96,11 @@ export interface WidgetState { // Nix installation nixInstalled: boolean | null; // null = not checked yet nixInstalling: boolean; - nixInstallPhase: "downloading" | "waiting-for-installer" | "prefetching" | null; + nixInstallPhase: + | "downloading" + | "waiting-for-installer" + | "prefetching" + | null; nixDownloadProgress: { downloaded: number; total: number } | null; // nix-darwin (darwin-rebuild availability) @@ -167,7 +191,9 @@ interface WidgetActions { setNixInstallPhase: ( phase: "downloading" | "waiting-for-installer" | "prefetching" | null, ) => void; - setNixDownloadProgress: (progress: { downloaded: number; total: number } | null) => void; + setNixDownloadProgress: ( + progress: { downloaded: number; total: number } | null, + ) => void; setDarwinRebuildAvailable: (available: boolean | null) => void; setDarwinRebuildPrefetching: (prefetching: boolean) => void; setEvolveState: (state: EvolveState | null) => void; @@ -182,7 +208,12 @@ interface WidgetActions { setFeedbackOpen: (open: boolean) => void; setError: (error: string | null) => void; setPanicDetails: ( - details: { message: string; location?: string; backtrace?: string; timestamp: string } | null, + details: { + message: string; + location?: string; + backtrace?: string; + timestamp: string; + } | null, ) => void; setPromptHistory: (history: string[]) => void; setRecommendedPrompt: (prompt: RecommendedPrompt | null | undefined) => void; @@ -346,138 +377,152 @@ export function createWidgetStore(initialState?: Partial) { return create()( devtools( (set, _get) => ({ - ...initialWidgetState, - ...initialState, - - // Permissions - setPermissionsState: (permissionsState) => set({ permissionsState }), - setPermissionsChecked: (permissionsChecked) => set({ permissionsChecked }), - - // Setters - setConfigDir: (configDir) => set({ configDir }), - setHosts: (hosts) => set({ hosts }), - setHost: (host) => set({ host }), - setEvolveState: (evolveState) => set({ evolveState: evolveState }), - setExternalBuildDetected: (externalBuildDetected) => set({ externalBuildDetected }), - setGitStatus: (gitStatus) => set({ gitStatus }), - setEvolvePrompt: (evolvePrompt) => set({ evolvePrompt }), - setProcessing: (isProcessing, action = null) => - set({ - isProcessing, - processingAction: isProcessing ? action : null, - }), - setChangeMap: (changeMap) => set({ changeMap }), - setBoolPref: (key: BoolPrefKey, value: boolean) => set({ [key]: value }), - initConfirmPrefs: (prefs) => - set({ - confirmBuild: prefs.confirmBuild ?? true, - confirmClear: prefs.confirmClear ?? true, - confirmRollback: prefs.confirmRollback ?? true, - }), - setAutoSummarizeOnFocus: (value) => set({ autoSummarizeOnFocus: value }), - setDeveloperMode: (value) => set({ developerMode: value }), - setPinnedVersion: (value) => set({ pinnedVersion: value }), - setHistory: (history) => set({ history }), - setHistoryLoading: (historyLoading) => set({ historyLoading }), - addAnalyzingHistoryHash: (hash) => - set((state) => ({ - analyzingHistoryForHashes: new Set([...state.analyzingHistoryForHashes, hash]), - })), - removeAnalyzingHistoryHash: (hash) => - set((state) => { - const next = new Set(state.analyzingHistoryForHashes); - next.delete(hash); - return { analyzingHistoryForHashes: next }; - }), - setSettingsOpen: (settingsOpen, tab) => - set({ settingsOpen, settingsActiveTab: tab ?? null }), - setPrefsLoaded: (prefsLoaded) => set({ prefsLoaded }), - setShowHistory: (showHistory) => set({ showHistory }), - setFeedbackOpen: (feedbackOpen) => set({ feedbackOpen }), - setFeedbackTypeOverride: (feedbackTypeOverride) => set({ feedbackTypeOverride }), - openFeedback: (type, initialText) => - set({ - feedbackOpen: true, - feedbackTypeOverride: type ?? null, - feedbackInitialText: initialText ?? null, - }), - setError: (error) => set({ error }), - setPanicDetails: (panicDetails) => set({ panicDetails }), - setPromptHistory: (promptHistory) => set({ promptHistory }), - setRecommendedPrompt: (recommendedPrompt) => set({ recommendedPrompt }), - - // Client-side UI state (NOT from server) - setBootstrapping: (isBootstrapping) => set({ isBootstrapping }), - setNixInstalled: (nixInstalled) => set({ nixInstalled }), - setNixInstalling: (nixInstalling) => set({ nixInstalling }), - setNixInstallPhase: (nixInstallPhase) => set({ nixInstallPhase }), - setNixDownloadProgress: (nixDownloadProgress) => set({ nixDownloadProgress }), - setDarwinRebuildAvailable: (darwinRebuildAvailable) => set({ darwinRebuildAvailable }), - setDarwinRebuildPrefetching: (darwinRebuildPrefetching) => set({ darwinRebuildPrefetching }), - setSummarizing: (isSummarizing) => set({ isSummarizing }), - setGenerating: (isGenerating) => set({ isGenerating }), - - // Console - appendLog: (text) => set((state) => ({ consoleLogs: state.consoleLogs + text })), - clearLogs: () => set({ consoleLogs: "" }), - - // Evolve events - appendEvolveEvent: (event) => - set((state) => ({ evolveEvents: [...state.evolveEvents, event] })), - clearEvolveEvents: () => set({ evolveEvents: [] }), - - // Conversational response - setConversationalResponse: (conversationalResponse) => set({ conversationalResponse }), - - // Commit message suggestion - setCommitMessageSuggestion: (commitMessageSuggestion) => set({ commitMessageSuggestion }), - - // Rebuild state - startRebuild: (context) => - set({ - rebuild: { - isRunning: true, - context, - lines: [{ id: 0, text: "Preparing rebuild...", type: "info" }], - rawLines: [], - exitCode: undefined, - success: undefined, - errorType: undefined, - errorMessage: undefined, - }, - }), - appendRebuildLine: (line) => - set((state) => ({ - rebuild: { - ...state.rebuild, - lines: [...state.rebuild.lines, line].slice(-50), // Keep last 50 lines - }, - })), - appendRawLine: (line) => - set((state) => ({ - rebuild: { - ...state.rebuild, - rawLines: [...state.rebuild.rawLines, line].slice(-500), // Keep last 500 raw lines - }, - })), - setRebuildError: (errorType, errorMessage) => - set((state) => ({ - rebuild: { - ...state.rebuild, - errorType, - errorMessage, - }, - })), - setRebuildComplete: (success, exitCode) => - set((state) => ({ - rebuild: { - ...state.rebuild, - isRunning: false, - success, - exitCode, - }, - })), - clearRebuild: () => set({ rebuild: initialRebuildState }), + ...initialWidgetState, + ...initialState, + + // Permissions + setPermissionsState: (permissionsState) => set({ permissionsState }), + setPermissionsChecked: (permissionsChecked) => + set({ permissionsChecked }), + + // Setters + setConfigDir: (configDir) => set({ configDir }), + setHosts: (hosts) => set({ hosts }), + setHost: (host) => set({ host }), + setEvolveState: (evolveState) => set({ evolveState: evolveState }), + setExternalBuildDetected: (externalBuildDetected) => + set({ externalBuildDetected }), + setGitStatus: (gitStatus) => set({ gitStatus }), + setEvolvePrompt: (evolvePrompt) => set({ evolvePrompt }), + setProcessing: (isProcessing, action = null) => + set({ + isProcessing, + processingAction: isProcessing ? action : null, + }), + setChangeMap: (changeMap) => set({ changeMap }), + setBoolPref: (key: BoolPrefKey, value: boolean) => + set({ [key]: value }), + initConfirmPrefs: (prefs) => + set({ + confirmBuild: prefs.confirmBuild ?? true, + confirmClear: prefs.confirmClear ?? true, + confirmRollback: prefs.confirmRollback ?? true, + }), + setAutoSummarizeOnFocus: (value) => + set({ autoSummarizeOnFocus: value }), + setDeveloperMode: (value) => set({ developerMode: value }), + setPinnedVersion: (value) => set({ pinnedVersion: value }), + setHistory: (history) => set({ history }), + setHistoryLoading: (historyLoading) => set({ historyLoading }), + addAnalyzingHistoryHash: (hash) => + set((state) => ({ + analyzingHistoryForHashes: new Set([ + ...state.analyzingHistoryForHashes, + hash, + ]), + })), + removeAnalyzingHistoryHash: (hash) => + set((state) => { + const next = new Set(state.analyzingHistoryForHashes); + next.delete(hash); + return { analyzingHistoryForHashes: next }; + }), + setSettingsOpen: (settingsOpen, tab) => + set({ settingsOpen, settingsActiveTab: tab ?? null }), + setPrefsLoaded: (prefsLoaded) => set({ prefsLoaded }), + setShowHistory: (showHistory) => set({ showHistory }), + setFeedbackOpen: (feedbackOpen) => set({ feedbackOpen }), + setFeedbackTypeOverride: (feedbackTypeOverride) => + set({ feedbackTypeOverride }), + openFeedback: (type, initialText) => + set({ + feedbackOpen: true, + feedbackTypeOverride: type ?? null, + feedbackInitialText: initialText ?? null, + }), + setError: (error) => set({ error }), + setPanicDetails: (panicDetails) => set({ panicDetails }), + setPromptHistory: (promptHistory) => set({ promptHistory }), + setRecommendedPrompt: (recommendedPrompt) => set({ recommendedPrompt }), + + // Client-side UI state (NOT from server) + setBootstrapping: (isBootstrapping) => set({ isBootstrapping }), + setNixInstalled: (nixInstalled) => set({ nixInstalled }), + setNixInstalling: (nixInstalling) => set({ nixInstalling }), + setNixInstallPhase: (nixInstallPhase) => set({ nixInstallPhase }), + setNixDownloadProgress: (nixDownloadProgress) => + set({ nixDownloadProgress }), + setDarwinRebuildAvailable: (darwinRebuildAvailable) => + set({ darwinRebuildAvailable }), + setDarwinRebuildPrefetching: (darwinRebuildPrefetching) => + set({ darwinRebuildPrefetching }), + setSummarizing: (isSummarizing) => set({ isSummarizing }), + setGenerating: (isGenerating) => set({ isGenerating }), + + // Console + appendLog: (text) => + set((state) => ({ consoleLogs: state.consoleLogs + text })), + clearLogs: () => set({ consoleLogs: "" }), + + // Evolve events + appendEvolveEvent: (event) => + set((state) => ({ evolveEvents: [...state.evolveEvents, event] })), + clearEvolveEvents: () => set({ evolveEvents: [] }), + + // Conversational response + setConversationalResponse: (conversationalResponse) => + set({ conversationalResponse }), + + // Commit message suggestion + setCommitMessageSuggestion: (commitMessageSuggestion) => + set({ commitMessageSuggestion }), + + // Rebuild state + startRebuild: (context) => + set({ + rebuild: { + isRunning: true, + context, + lines: [{ id: 0, text: "Preparing rebuild...", type: "info" }], + rawLines: [], + exitCode: undefined, + success: undefined, + errorType: undefined, + errorMessage: undefined, + }, + }), + appendRebuildLine: (line) => + set((state) => ({ + rebuild: { + ...state.rebuild, + lines: [...state.rebuild.lines, line].slice(-50), // Keep last 50 lines + }, + })), + appendRawLine: (line) => + set((state) => ({ + rebuild: { + ...state.rebuild, + rawLines: [...state.rebuild.rawLines, line].slice(-500), // Keep last 500 raw lines + }, + })), + setRebuildError: (errorType, errorMessage) => + set((state) => ({ + rebuild: { + ...state.rebuild, + errorType, + errorMessage, + }, + })), + setRebuildComplete: (success, exitCode) => + set((state) => ({ + rebuild: { + ...state.rebuild, + isRunning: false, + success, + exitCode, + }, + })), + clearRebuild: () => set({ rebuild: initialRebuildState }), }), { name: "widget-store", diff --git a/apps/native/src/tauri-api.ts b/apps/native/src/tauri-api.ts index d156f10c1..3d82742f0 100644 --- a/apps/native/src/tauri-api.ts +++ b/apps/native/src/tauri-api.ts @@ -13,12 +13,8 @@ import type { EvolveCancelResult, EvolutionResult, EvolveState, - FeedbackAiProviderModelInfo, - FeedbackFlakeInputsSnapshot, - FeedbackPanicDetails, + FeedbackMetadata, FeedbackShareOptions, - FeedbackSystemInfo, - FileEntry, FinalizeApplyResult, GitStatus, HomebrewState, @@ -38,95 +34,6 @@ import type { UiPrefsUpdate as DarwinPrefsUpdate, } from "./types/shared"; -export type { - BuildCheckResult, - ChangeType, - CliToolsState, - CommitResult, - Config as DarwinConfig, - ConfigChangedEvent, - ConfigEditApplyResult, - DarwinApplyDataEvent, - DarwinApplyEndEvent, - DarwinApplySummaryEvent, - EvolveCancelResult, - EvolveEvent, - EvolveEventType, - EvolutionFailureResult, - EvolutionResult, - EvolutionState, - EvolutionTelemetry, - EvolveState, - EvolveStep, - FeedbackAiProviderModelInfo, - FeedbackFlakeInputEntry, - FeedbackFlakeInputsSnapshot, - FeedbackMetadataRequest, - FeedbackPanicDetails, - FeedbackShareOptions, - FeedbackSystemInfo, - FileEntry, - FinalizeApplyResult, - GitFileStatus, - GitStatus, - HomebrewState, - HistoryItem, - NixCheckResult, - NixDarwinRebuildEndEvent, - NixInstallEndEvent, - NixInstallErrorType, - NixInstallPhase, - NixInstallProgressEvent, - OkResult, - Permission, - PermissionStatus, - PermissionsState, - PreviewIndicatorState, - RecommendedPrompt, - RebuildErrorType, - SemanticChangeMap, - SetDirResult, - SummarizerUpdateEvent, - SummarizedChangeSet, - RustPanicEvent, - SystemDefault, - SystemDefaultsScan, - UiPrefs as DarwinPrefs, - UiPrefsUpdate as DarwinPrefsUpdate, - WatcherEvent, -} from "./types/shared"; -export type { Change, Commit } from "./types/sqlite"; - -export const DEFAULT_MAX_ITERATIONS = 25; - -// ============================================================================= -// Feedback Types -// ============================================================================= - -export interface FeedbackUsageStats { - totalEvolutions?: number; - successRate?: number; - avgIterations?: number; - lastComputedAt?: string; - extra?: Record; -} - -export interface FeedbackMetadata { - currentAppStateSnapshot?: unknown; - systemInfo?: FeedbackSystemInfo; - usageStats?: FeedbackUsageStats; - evolutionLogContent?: string; - changedNixFilesDiff?: string; - aiProviderModelInfo?: FeedbackAiProviderModelInfo; - buildErrorOutput?: string; - flakeInputsSnapshot?: FeedbackFlakeInputsSnapshot; - appLogsContent?: string; - panicDetails?: FeedbackPanicDetails; -} - -export const EVOLVE_EVENT_CHANNEL = "darwin:evolve:event"; -export const CONFIG_CHANGED_CHANNEL = "config:changed"; - export const darwinAPI = { config: { get: () => invoke("config_get"), @@ -137,7 +44,6 @@ export const darwinAPI = { git: { status: () => invoke("git_status"), statusAndCache: () => invoke("git_status_and_cache"), - cached: () => invoke("git_cached"), commit: (message: string) => invoke("git_commit", { message }), stash: (message: string) => invoke("git_stash", { message }), }, @@ -151,7 +57,6 @@ export const darwinAPI = { invoke("darwin_apply_stream_start", { hostOverride }), activateStorePath: (storePath: string) => invoke("darwin_activate_store_path", { storePath }), - applyStreamCancel: () => invoke("darwin_apply_stream_cancel"), finalizeApply: () => invoke("finalize_apply"), finalizeRollback: (storePath: string | null, changesetId: number | null) => invoke("finalize_rollback", { storePath, changesetId }), @@ -167,11 +72,9 @@ export const darwinAPI = { }, flake: { listHosts: () => invoke("flake_list_hosts"), - installedApps: () => invoke("flake_installed_apps"), exists: () => invoke("flake_exists"), existsAt: (dir: string) => invoke("flake_exists_at", { dir }), bootstrapDefault: (hostname: string) => invoke("bootstrap_default_config", { hostname }), - finalizeFlakeLock: () => invoke("finalize_flake_lock"), }, path: { exists: (dir: string) => invoke("path_exists", { dir }), @@ -199,6 +102,7 @@ export const darwinAPI = { stage, clientTimestampUnixMs: clientTimestampUnixMs ?? null, }), + sentryEvent: () => invoke("debug_sentry_event"), }, ui: { getPrefs: () => invoke("ui_get_prefs"), @@ -237,7 +141,6 @@ export const darwinAPI = { permissions: { checkAll: () => invoke("permissions_check_all"), request: (permissionId: string) => invoke("permissions_request", { permissionId }), - allRequiredGranted: () => invoke("permissions_all_required_granted"), // macOS-specific permission checks via tauri-plugin-macos-permissions checkFullDiskAccess: () => checkFullDiskAccessPermission(), requestFullDiskAccess: () => requestFullDiskAccessPermission(), @@ -258,7 +161,6 @@ export const darwinAPI = { readFile: (relPath: string) => invoke("editor_read_file", { relPath }), writeFile: (relPath: string, content: string) => invoke("editor_write_file", { relPath, content }), - listFiles: () => invoke("editor_list_files"), }, lsp: { @@ -271,6 +173,12 @@ export const darwinAPI = { getStateDiff: () => invoke("homebrew_get_state_diff"), applyDiff: (diff: HomebrewState) => invoke("homebrew_apply_diff", { diff }), }, + + updater: { + installVersion: (version: string) => invoke("install_version", { version }), + relaunch: () => invoke("relaunch_after_update"), + clearPinnedVersion: () => invoke("clear_pinned_version"), + }, }; export const ipcRenderer = { @@ -280,14 +188,6 @@ export const ipcRenderer = { once(channel, listener), }; -// const w = new Window("lol"); -// w.once("tauri://window-created", (event) => { -// console.log(event); -// }); -// w.once("tauri://destroyed", (event) => { -// console.log(event); -// }); - declare global { interface Window { darwinAPI?: typeof darwinAPI; diff --git a/apps/native/src/types/feedback.ts b/apps/native/src/types/feedback.ts index 3a950b23d..f59144fd4 100644 --- a/apps/native/src/types/feedback.ts +++ b/apps/native/src/types/feedback.ts @@ -1,3 +1,17 @@ +import type { + FeedbackAiProviderModelInfo, + FeedbackFlakeInputsSnapshot, + FeedbackSystemInfo, + FeedbackUsageStats, +} from "./shared"; + +export type { + FeedbackAiProviderModelInfo, + FeedbackFlakeInputsSnapshot, + FeedbackSystemInfo, + FeedbackUsageStats, +}; + /** * The high-level categories of feedback the user can submit. * @@ -32,64 +46,10 @@ export interface ShareOptions { lastPrompt?: boolean; } -/** - * System information captured from the runtime. Fields are optional so - * collectors can populate what is available on the platform. - */ -interface SystemInfo { - osName?: string | null; // e.g. "macOS" - osVersion?: string | null; // e.g. "15.3" - arch?: string | null; // e.g. "aarch64-darwin" - nixVersion?: string | null; // e.g. "2.24.1" - appVersion?: string | null; // app build/version string -} - -/** - * Aggregated nixmac usage statistics from the app. - * Fields are optional and may be filled by the runtime when - * the user opts-in to sharing usage stats. - */ -interface UsageStats { - /** Total number of evolutions the user has run */ - totalEvolutions?: number; - /** Success rate as a percentage (0.0 - 100.0) of evolutions */ - successRate?: number; - /** Average number of iterations per evolution */ - avgIterations?: number; - /** ISO timestamp when these stats were last computed */ - lastComputedAt?: string; - extra?: Record; -} - -/** - * AI provider/model details and usage signals captured from the app. - * Fields are optional and may be partially populated. - */ -interface AiProviderModelInfo { - evolveProvider?: string | null; - evolveModel?: string | null; - summaryProvider?: string | null; - summaryModel?: string | null; - totalTokens?: number | null; - latencyMs?: number | null; - iterations?: number | null; - buildAttempts?: number | null; -} - -/** - * Flake.lock input metadata (subset) captured from the user's configuration. - */ -interface FlakeInputEntry { - rev?: string | null; - lastModified?: number | null; - narHash?: string | null; -} - -interface FlakeInputsSnapshot { - nixpkgs?: FlakeInputEntry | null; - "nix-darwin"?: FlakeInputEntry | null; - "home-manager"?: FlakeInputEntry | null; -} +type SystemInfo = FeedbackSystemInfo; +type UsageStats = FeedbackUsageStats; +type AiProviderModelInfo = FeedbackAiProviderModelInfo; +type FlakeInputsSnapshot = FeedbackFlakeInputsSnapshot; /** * The serializable shape of feedback that will be sent to the server. diff --git a/apps/native/src/types/shared.ts b/apps/native/src/types/shared.ts index 2a207f224..56a1769b1 100644 --- a/apps/native/src/types/shared.ts +++ b/apps/native/src/types/shared.ts @@ -406,7 +406,7 @@ export type EvolveEventType = /** * Agent is reading a file. */ -"reading" | +"reading" | "searchPackages" | /** * Agent is editing a file. */ @@ -589,6 +589,51 @@ nixpkgs: FeedbackFlakeInputEntry | null; */ "home-manager": FeedbackFlakeInputEntry | null } +/** + * Metadata collected for feedback submission based on user opt-in. + */ +export type FeedbackMetadata = { +/** + * Current frontend/store snapshot, represented as arbitrary JSON. + */ +currentAppStateSnapshot: JsonValue | null; +/** + * Runtime system information. + */ +systemInfo: FeedbackSystemInfo | null; +/** + * Aggregated local usage statistics. + */ +usageStats: FeedbackUsageStats | null; +/** + * Captured evolution log content. + */ +evolutionLogContent: string | null; +/** + * Diff for changed Nix files at submission time. + */ +changedNixFilesDiff: string | null; +/** + * AI provider/model metadata for the related run. + */ +aiProviderModelInfo: FeedbackAiProviderModelInfo | null; +/** + * Latest build error output. + */ +buildErrorOutput: string | null; +/** + * Selected locked flake input metadata. + */ +flakeInputsSnapshot: FeedbackFlakeInputsSnapshot | null; +/** + * Recent application log content. + */ +appLogsContent: string | null; +/** + * Panic details when feedback is submitted after a crash. + */ +panicDetails: FeedbackPanicDetails | null } + /** * Request payload for gathering feedback metadata. */ @@ -690,21 +735,29 @@ nixVersion: string | null; appVersion: string | null } /** - * File or directory entry returned by the editor tree. + * Aggregated usage stats for feedback. */ -export type FileEntry = { +export type FeedbackUsageStats = { /** - * Path relative to the selected config directory. + * Number of evolutions recorded locally. */ -path: string; +totalEvolutions: number | null; /** - * File or directory basename. + * Percentage of evolutions that completed successfully. */ -name: string; +successRate: number | null; +/** + * Average number of agent iterations per evolution. + */ +avgIterations: number | null; /** - * Whether this entry is a directory. + * Timestamp when the stats were computed. */ -isDir: boolean } +lastComputedAt: string | null; +/** + * Additional structured usage fields that are not part of the stable contract. + */ +extra: JsonValue | null } /** * Result of a successful `finalize_apply` or `finalize_rollback` command. @@ -863,6 +916,8 @@ source: string | null; */ lastChecked: number } +export type JsonValue = null | boolean | number | string | JsonValue[] | Partial<{ [key in string]: JsonValue }> + /** * Result of `nix_check` — reports whether Nix and darwin-rebuild are available. */