Skip to content

Make "Restart setup" button work + make choosing a configuration idempotent#478

Merged
Juanpe Bolívar (arximboldi) merged 13 commits into
mainfrom
jp/restart-setup
Jul 6, 2026
Merged

Make "Restart setup" button work + make choosing a configuration idempotent#478
Juanpe Bolívar (arximboldi) merged 13 commits into
mainfrom
jp/restart-setup

Conversation

@arximboldi

Copy link
Copy Markdown
Contributor

Summary

This addresses a couple of issues with the onboarding process:

  1. Choosing a configuration is not forgiving. If, for example, you go back to a previous step to choose a different flake to import, it will fail to import the new flake because the target destination is already occupied. This is solved by marking the paths as "pending" until the workflow is done.

  2. The "reset" button in the onboarding workflow was doing nothing. This makes it work again by wiring it up with the backend appropriately. This moves the user back to the first step where they actually need to make a choice ("chooose a configuration", which is why (1) was important at this stage). This is also marked in the UI with a slight separator.

Test Plan

  • No test plan needed

Docs

  • Docs updated (companion PR in darkmatter/nixmac-web: #___)
  • No docs update needed

@arximboldi Juanpe Bolívar (arximboldi) changed the title Make "Restart setup" button and make choosing a configuration idempotent Make "Restart setup" button work + make choosing a configuration idempotent Jul 5, 2026
@darkmatter

darkmatter Bot commented Jul 5, 2026

Copy link
Copy Markdown
Contributor

🎨 Storybook preview

Open Storybook preview

Updated for 2a4dbd9


🧭 Story changes

Compared to main (snapshot diff at story level):

✏️ Changed stories (51)

💡 Update snapshots ↗ to regenerate baselines and open a PR.


⚠️ Detected UI changes (5)

These stories' HTML snapshots changed. I've added screenshots + links to the changed stories below. Review them carefully then accept the changes to regenerate baselines and include them in this PR:

Widget/Drift/DriftReview › Unsummarized

Widget/Drift/DriftReview › Unsummarized

Widget/Drift/DriftReview › Summarized

Widget/Drift/DriftReview › Summarized

Widget/Drift/DriftReview › Ai Session

Widget/Drift/DriftReview › Ai Session

Widget/Drift/DriftReview › Single Change

Widget/Drift/DriftReview › Single Change

Widget/Controls/FlakeDirChooser › Multiple Candidates

Widget/Controls/FlakeDirChooser › Multiple Candidates


Accept UI changes

  • Click here to accept these changes

Alternatively, you can run bun run test:update-snapshots locally to re-generate the baselines and then push the changes to this PR.

What does this do?

The screenshots above show UI changes detected by the Storybook
snapshot tests run on this PR. Each image is the rendered output of
a Storybook story from the code in this PR branch; the snapshot
test compared it against the committed baseline in
__snapshots__/ and flagged the difference.

Checking the box tells the darkmatter[bot] to regenerate the
baselines from this PR's current code and commit them directly to
this branch. The new baselines become the source of truth for
future runs — only accept after confirming the visual changes are
intentional.

Comparison baseline: the committed __snapshots__/ files on this
PR branch (carried forward from develop). Accept updates them in
place on this branch.

@github-actions

github-actions Bot commented Jul 5, 2026

Copy link
Copy Markdown
Contributor
Warnings
⚠️

No Linear issue ID found in this PR's title, description, or branch name (expected something like ENG-123). Add one so this work is traceable in Linear, or add #no-linear to the PR description to acknowledge it's intentionally untracked.

⚠️

New Rust modules were added without tests. Consider adding a #[cfg(test)] mod tests { ... } block or a file under apps/native/src-tauri/tests/:

  • apps/native/src-tauri/src/orpc/onboarding.rs

📋 PR Overview

Lines changed 834 (+762 / -72)
Files 3 added, 22 modified, 0 deleted
Draft / WIP no
Has Test Plan no
Linear issue no
No Test Plan Needed yes
New UI components no
New Storybook stories no
New Rust modules yes (2)
New TS source files no
New tests yes (1)
package.json touched no
Cargo.toml touched no
Infra / CI touched no

🔬 Coverage

Report Lines Statements Functions Branches
apps/native/coverage/coverage-summary.json 34.2% 33.8% 30.4% 28.6%

Generated by 🚫 dangerJS against 2a4dbd9

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if it would be simpler if Onboarding had its very own in-memory data model and didn't start touching app state until you hit the big button at the end.

@arximboldi

Copy link
Copy Markdown
Contributor Author

Scott McMaster (@scottmcmaster) I like your approach and was thinking in similar lines, but didn't want to affect too much the current structure. I'll try that out in a later PR.

@arximboldi Juanpe Bolívar (arximboldi) added this pull request to the merge queue Jul 6, 2026
@github-merge-queue github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Jul 6, 2026
…dirs

Record the clone/scaffold ROOT in a new GlobalPreferences field,
onboarding_provisional_config_dir, whenever the app materializes the
config directory (GitHub/zip import, new-dir scaffold). User-selected
pre-existing directories never get the marker, and the first successful
apply clears it: from that point the directory is the user's and restart
must never delete it.

The marker intentionally stores the materialization root rather than the
(possibly nested) flake subdirectory, so releasing ownership can remove
the whole clone. It is not exposed through UiPrefsUpdate — backend code
paths only, like onboarding_last_build_at.

Groundwork for an idempotent "Restart setup" and safe re-imports.
Resetting clears every durable fact the step machine derives progress
from — config_dir, repo_root, host_attr, the three onboarding stamps —
plus the legacy KV configDir key, which get_config_dir_if_set would
otherwise fall back to and silently resurrect the old selection.

When the active config directory is still owned by onboarding
(provisional marker matches and no build has been applied from it), its
contents are deleted too, so the next import finds an empty target.
Guard rails: resets are refused while a rebuild stream is running, a
stale marker that doesn't contain the active config dir never deletes
anything, and the canonical /etc/nix-darwin is emptied in place rather
than removed.

Also adds watcher::stop_watching() — with no config dir there is
nothing to poll, and polling a deleted one would emit git_state_error
every tick — and clears the git/evolve cells the way a fresh launch
starts out.
…hange

Two mid-onboarding invariants when the config dir is replaced (before
any successful apply):

- The "scan this Mac" stamp described the outgoing repo, so it is
  cleared and the step machine re-surfaces the customizations step.
  A previously materialized provisional tree that the new selection
  does not live in is deleted rather than orphaned.

- Import and scaffold targets must be empty, which made retrying an
  import fail on its own leftovers — the original non-idempotence.
  When the blocking tree is onboarding's own provisional
  materialization it is now released for reuse, so re-importing the
  same repo (after a restart or "Change source") just works.

Post-onboarding config-dir changes are untouched: once a build has
been applied, neither path rewinds onboarding or deletes anything.
Dead since e6a6b40 moved step progression into derived durable facts:
the button only reset the transient UI store, which the step machine
does not read. It now confirms with a dialog and calls the new
onboarding.reset command; the preferences-changed event re-derives the
flow back to the config-dir step with no navigation code.

An import parked on the flake-dir chooser exists only on disk with no
backend record, so its clone dir is mirrored into the transient store
and discarded before the reset. The backend's refusal to reset while a
build is running surfaces as a toast, and the restart telemetry event
fires only after the reset succeeds instead of before a no-op.
…_completed

computeOnboardingStep is what every reset/cascade behavior in this
branch relies on, and it was untested; the new cases pin the gate
order, the strictly-first-unsatisfied rule, the deferred-inference
build gate, and the exact post-reset derivation.

Also captures the onboarding_completed telemetry event when the
celebration raises — it was declared in the event union but never
emitted anywhere.
The dialog content carried button-style classes: bg-transparent made it
translucent over whatever was behind it, font-bold applied to all the
copy, and hover:-translate-y-1 overrode the centering translate-y so
the whole dialog jumped on hover. Drop them and let the base
DialogContent's solid background, border, and shadow apply; footer
buttons use standard variants (the hardcoded text-white cancel label
would have been invisible on the light background).

Adds an optional bolded title so the question reads bold and the
explanation doesn't; the restart-setup dialog splits its copy that way.
The gates above it (permissions, Nix install) are machine state a reset
can't undo; "Restart setup" rewinds to config-dir. A light rule above
that step hints at the boundary. The step is named by a
RESTART_TARGET_STEP constant in the step registry so the semantics live
next to the step definitions.
evolve-flow and drift-review catch up with the ConfirmationDialog
restyle (solid background, standard button variants, normal-weight
copy). rebuild-overlay-panel was already stale against the committed
tree before this branch.

Not included: the SaveContinueEditing interaction test in evolve-flow
was already failing before this branch — it clicks a /continue
editing/i button that no longer exists anywhere in product code.
A NeedsFlakeDirChoice clone is real on-disk state, but its only record
lived in frontend component state plus a manually-synced copy in the
transient onboarding store — two frontend copies, zero backend record.
A remount or app restart mid-choice orphaned the tree, and
onboarding_reset could not be self-contained: the frontend had to
remember to discard the clone before calling it.

Record the parked tree in GlobalPreferences.pending_import_dir when an
import returns NeedsFlakeDirChoice; clear it on finalize/discard;
discard it (with config_discard_import's safety rules) at the start of
any new import and inside onboarding_reset. Import retries after an
abandoned choice now find a free target instead of failing on the
leftover clone.
The backend now records and discards parked NeedsFlakeDirChoice trees
itself, so the transient store field, the manual mirroring in
use-import-config, and the two-phase discard in the restart handler all
disappear. The flake-dir chooser keeps its component-local view state,
which is caller-owned query-result data per the state taxonomy.
…ites

The in-memory cache in ipc/preferences.ts only invalidated on frontend
setPrefs, but the backend also writes preferences (apply bookkeeping,
onboarding reset, settings import) — those writes left the cache
serving stale provider/toggle values. The preferences sync module now
folds every global_preferences_changed payload into the cache,
overwriting only keys the payloads actually carry: the keychain-backed
fields the cache exists to protect are untouched, and merging instead
of dropping avoids re-hitting the keychain on the next read.
… rule

Main (#473) relaxed validate_new_dir_location to allow nested config
dirs under home, which the PR merge build combines with this branch —
asserting that a nested home path is not wipeable now fails there. The
guard test keeps only the invariants that are its own: paths outside
home are never wipeable, and the active config is never deleted from
under itself. How deep an import target may nest is
validate_new_dir_location's contract, tested where it lives.
PR #479 (208271e) accidentally removed the export keyword from
OnboardingStepInputs, breaking the tsc build:

  error TS2459: Module './onboarding' declares 'OnboardingStepInputs'
  locally, but it is not exported.

Restored so onboarding.test.ts can import the type.
@arximboldi

Copy link
Copy Markdown
Contributor Author

There was a rebase issue that prevented it from merging. I fixed it I hope it works now.

Merged via the queue into main with commit b19aae9 Jul 6, 2026
10 checks passed
@arximboldi Juanpe Bolívar (arximboldi) deleted the jp/restart-setup branch July 6, 2026 20:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants