Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 0 additions & 4 deletions apps/native/.storybook/mocks/tauri-runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -307,10 +307,6 @@ export const storybookTauriAPI = {
emit("nix:install:end", { ok: true, code: 0, darwin_rebuild_available: true });
return okResult();
},
prefetchDarwinRebuild: async () => {
emit("nix:darwin-rebuild:end", { ok: true });
return okResult();
},
},
flake: {
listHosts: async () => [...defaultHosts],
Expand Down
7 changes: 4 additions & 3 deletions apps/native/.storybook/preview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,14 @@ import { useEffect } from "react";
import "./mocks/tauri-runtime";
import "../src/index.css";

// Replace the widget-store module wholesale with a clamped variant that
// can never drift the nix-setup / permissions / feedback-dialog
// bypasses. The redirect target is `apps/native/src/stores/__mocks__/widget-store.ts`.
// Replace store modules with clamped variants that can never drift the
// nix-setup / permissions bypasses (widget-store) or trigger the feedback
// dialog (feedback-store). Redirect targets live in `src/stores/__mocks__/`.
//
// Storybook's mocker resolves the path via Node's `require.resolve`, which
// doesn't know about `.ts` extensions — so we spell it out.
sb.mock(import("../src/stores/widget-store.ts"));
sb.mock(import("../src/stores/feedback-store.ts"));

/**
* Decorator that applies the dark theme class to the document.
Expand Down
1 change: 0 additions & 1 deletion apps/native/src-tauri/examples/specta_gen_ts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,6 @@ fn main() {
.register::<shared_types::NixInstallErrorType>()
.register::<shared_types::NixInstallProgressEvent>()
.register::<shared_types::NixInstallEndEvent>()
.register::<shared_types::NixDarwinRebuildEndEvent>()
.register::<shared_types::RebuildErrorType>()
.register::<shared_types::DarwinApplyDataEvent>()
.register::<shared_types::DarwinApplySummaryEvent>()
Expand Down
7 changes: 0 additions & 7 deletions apps/native/src-tauri/src/commands/apply.rs
Original file line number Diff line number Diff line change
Expand Up @@ -91,13 +91,6 @@ pub async fn nix_check() -> Result<shared_types::NixCheckResult, String> {
})
}

#[tauri::command]
pub async fn darwin_rebuild_prefetch(app: AppHandle) -> Result<shared_types::OkResult, String> {
nix::prefetch_darwin_rebuild_stream(&app)
.map_err(|e| capture_err("darwin_rebuild_prefetch", e))?;
Ok(shared_types::OkResult::yes())
}

#[tauri::command]
pub async fn nix_install_start(app: AppHandle) -> Result<shared_types::OkResult, String> {
nix::install_nix_stream(&app).map_err(|e| capture_err("nix_install_start", e))?;
Expand Down
1 change: 0 additions & 1 deletion apps/native/src-tauri/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -514,7 +514,6 @@ fn run_gui_mode(
commands::evolve_state::routing_state_clear,
commands::apply::nix_check,
commands::apply::nix_install_start,
commands::apply::darwin_rebuild_prefetch,
commands::apply::flake_list_hosts,
commands::config::flake_exists,
commands::config::bootstrap_default_config,
Expand Down
10 changes: 0 additions & 10 deletions apps/native/src-tauri/src/shared_types/events.rs
Original file line number Diff line number Diff line change
Expand Up @@ -63,16 +63,6 @@ pub struct NixInstallEndEvent {
pub error: Option<String>,
}

/// Payload for `nix:darwin-rebuild:end`.
#[derive(Debug, Clone, Serialize, Deserialize, Type)]
#[serde(rename_all = "camelCase")]
pub struct NixDarwinRebuildEndEvent {
/// Whether nix-darwin setup completed successfully.
pub ok: bool,
/// Human-readable failure message.
pub error: Option<String>,
}

/// Known rebuild/activation failure categories.
#[derive(Debug, Clone, Serialize, Deserialize, Type)]
#[serde(rename_all = "snake_case")]
Expand Down
47 changes: 0 additions & 47 deletions apps/native/src-tauri/src/system/nix.rs
Original file line number Diff line number Diff line change
Expand Up @@ -162,53 +162,6 @@ pub fn get_nix_version() -> Option<String> {
}
}

/// Prefetches darwin-rebuild by running `nix build --no-link nix-darwin/master#darwin-rebuild`.
/// This caches the derivation in the nix store so the `nix run` fallback in darwin.rs is fast.
/// Emits `nix:darwin-rebuild:end` with `{ ok: bool, error?: string }` on completion.
pub fn prefetch_darwin_rebuild_stream(app: &AppHandle) -> Result<()> {
info!("[nix] prefetch_darwin_rebuild_stream called");

let app_handle = app.clone();

// All emit calls below are fire-and-forget: background thread; window may not be
// listening. Tauri emit returns Err only when no listeners are registered.
std::thread::spawn(move || {
let result = Command::new("nix")
.args(["build", "--no-link", "nix-darwin/master#darwin-rebuild"])
.env("PATH", get_nix_path_with_login_shell())
.env("NIX_CONFIG", "experimental-features = nix-command flakes")
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.output();

match result {
Ok(output) if output.status.success() => {
info!("[nix] darwin-rebuild prefetch succeeded");
let _ =
app_handle.emit("nix:darwin-rebuild:end", serde_json::json!({ "ok": true }));
}
Ok(output) => {
let stderr = String::from_utf8_lossy(&output.stderr).to_string();
error!("[nix] darwin-rebuild prefetch failed: {}", stderr);
let _ = app_handle.emit(
"nix:darwin-rebuild:end",
serde_json::json!({ "ok": false, "error": stderr }),
);
}
Err(e) => {
error!("[nix] darwin-rebuild prefetch error: {}", e);
let _ = app_handle.emit(
"nix:darwin-rebuild:end",
serde_json::json!({ "ok": false, "error": e.to_string() }),
);
}
}
});

info!("[nix] prefetch_darwin_rebuild_stream started background thread");
Ok(())
}

pub fn install_nix_stream(app: &AppHandle) -> Result<()> {
info!("[nix] install_nix_stream called");

Expand Down
9 changes: 7 additions & 2 deletions apps/native/src/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,14 @@ Hooks in `hooks/` sit between components and `ipc/api.ts`. Each hook owns a slic

## 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.
`stores/` holds four Zustand stores, each scoped to a single concern:

Components read from this store directly when possible to minimize prop-drilling.
- `widget-store.ts` (composed from slices in `stores/slices/`) — backend-mirrored ViewModel state: setup, config, evolve state, git status, rebuild progress, history, summary, console logs.
- `ui-store.ts` — UI navigation and ephemeral interaction state (settings panel, processing flags, evolve prompt draft, prompt history, filesystem/editor view toggles).
- `feedback-store.ts` — error banner + feedback dialog state + captured panic payload.
- `pref-store.ts` — persisted user preferences (confirmation prompts, developer mode, update channel, etc.) — hydrated from / written through to the Tauri prefs store on the Rust side.

Hooks read from and write to whichever store(s) they need. Components read directly to minimize prop-drilling.

## Preview Indicator

Expand Down
27 changes: 19 additions & 8 deletions apps/native/src/components/widget/controls/bootstrap-config.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { useDarwinConfig } from "@/hooks/use-darwin-config";
import { useWidgetStore } from "@/stores/widget-store";
import { tauriAPI } from "@/ipc/api";
import { useFeedbackStore } from "@/stores/feedback-store";
import { useWidgetStore } from "@/stores/widget-store";
import { AlertCircle, GitCommit, Sparkles } from "lucide-react";
import { useEffect, useState } from "react";

Expand All @@ -27,24 +28,32 @@ export function BootstrapConfig({ label, onSuccess }: BootstrapConfigProps) {
setFlakeExists(false);
return;
}
tauriAPI.flake.existsAt(configDir).then(setFlakeExists).catch(() => setFlakeExists(false));
tauriAPI.flake
.existsAt(configDir)
.then(setFlakeExists)
.catch(() => setFlakeExists(false));
}, [configDir]);

const needsInitialCommit = flakeExists && (gitStatus === null || gitStatus.headCommitHash === "");
const needsInitialCommit =
flakeExists && (gitStatus === null || gitStatus.headCommitHash === "");

const message = needsInitialCommit
? "flake.nix found but not committed — Nix needs a git commit to evaluate your flake"
: "No nix-darwin configuration found in this directory";
const buttonLabel = needsInitialCommit ? "Make initial commit" : "Create Default Configuration";
const loadingLabel = needsInitialCommit ? "Committing..." : "Creating Configuration...";
const buttonLabel = needsInitialCommit
? "Make initial commit"
: "Create Default Configuration";
const loadingLabel = needsInitialCommit
? "Committing..."
: "Creating Configuration...";
const helpText = needsInitialCommit
? "Stages all files and creates the first commit"
: "This will create a basic nix-darwin flake in the directory";

const handleBootstrap = async (): Promise<void> => {
setLocalError(null);
await bootstrap(needsInitialCommit ? "" : hostname);
const storeError = useWidgetStore.getState().error;
const storeError = useFeedbackStore.getState().error;
if (storeError) {
setLocalError(storeError);
} else {
Expand Down Expand Up @@ -86,7 +95,9 @@ export function BootstrapConfig({ label, onSuccess }: BootstrapConfigProps) {
onClick={handleBootstrap}
className="w-full"
data-testid="create-default-config-button"
disabled={(!needsInitialCommit && !hostname.trim()) || isBootstrapping}
disabled={
(!needsInitialCommit && !hostname.trim()) || isBootstrapping
}
>
{isBootstrapping ? (
<>
Expand All @@ -111,4 +122,4 @@ export function BootstrapConfig({ label, onSuccess }: BootstrapConfigProps) {
</div>
</div>
);
}
}
4 changes: 2 additions & 2 deletions apps/native/src/components/widget/controls/confirm-button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { Button } from "@/components/ui/button";
import { CheckConfirmationOff } from "@/components/widget/controls/check-confirmation-off";
import { ConfirmationDialog } from "@/components/widget/controls/confirmation-dialog";
import { usePrefs } from "@/hooks/use-prefs";
import { useWidgetStore, type ConfirmPrefKey } from "@/stores/widget-store";
import { usePrefStore, type ConfirmPrefKey } from "@/stores/pref-store";
import type { ComponentProps } from "react";
import { useState } from "react";

Expand All @@ -23,7 +23,7 @@ export function ConfirmButton({
children,
...buttonProps
}: ConfirmButtonProps) {
const confirm = useWidgetStore((s) => s[confirmPrefKey]);
const confirm = usePrefStore((s) => s[confirmPrefKey]);
const { setPref } = usePrefs();

const [open, setOpen] = useState(false);
Expand Down
16 changes: 13 additions & 3 deletions apps/native/src/components/widget/evolve-flow.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
// @ts-nocheck - Storybook 10 alpha types have inference issues (resolves to `never`)
import preview from "#storybook/preview";
import { useUiStore } from "@/stores/ui-store";
import { useWidgetStore } from "@/stores/widget-store";
import type { EvolveEvent } from "@/stores/widget-store";
import type { SemanticChangeMap, EvolveState, GitStatus, Change } from "@/ipc/types";
import type {
Change,
EvolveEvent,
EvolveState,
GitStatus,
SemanticChangeMap,
} from "@/ipc/types";
import { useEffect, useRef } from "react";
import { DarwinWidget } from "./widget";

Expand Down Expand Up @@ -158,7 +164,9 @@ const mockEvolveEvents: EvolveEvent[] = [

function WidgetWithState({ storeState }: { storeState: Record<string, unknown> }) {
useEffect(() => {
useWidgetStore.setState(storeState);
const { evolvePrompt, ...rest } = storeState;
useWidgetStore.setState(rest);
if (evolvePrompt !== undefined) useUiStore.setState({ evolvePrompt });
}, [storeState]);

return <DarwinWidget />;
Expand All @@ -170,6 +178,8 @@ function AnimatedEvolveFlow() {
useEffect(() => {
useWidgetStore.setState({
evolveState: evolveStateBegin,
});
useUiStore.setState({
evolvePrompt: "Add system monitoring tools like htop and btop",
});

Expand Down
Loading
Loading