Skip to content

feat(daemon): start fresh installs on Hyperbloom with curated favorites - #442

Merged
hyperb1iss merged 6 commits into
mainfrom
nova/first-run-defaults
Oct 21, 2026
Merged

hyperb1iss merged 6 commits into
mainfrom
nova/first-run-defaults

Conversation

@hyperb1iss

@hyperb1iss hyperb1iss commented Oct 20, 2026 •

Copy link
Copy Markdown
Owner

A clean install used to open on a black scene with an empty favorites list, which on a Mac with no lights attached looks like nothing works. With this PR a genuinely fresh install starts the default scene on Hyperbloom and seeds five favorites, one per category, all of which look good with no devices attached: Hyperbloom, Borealis, Bubble Garden, Plasma Engine, and Spectral Fire. This holds on every platform, including the Linux daemon without the desktop app.

💡 How a fresh install is recognized

Both seeds use one rule, evaluated once at initialization: the start is fresh only if runtime-state.json (at the state path and the old data-dir path), scenes.json (checked after the old profile import, which writes it), and library.json were all absent. A file that exists but fails to parse counts as present, and so does a failed existence check.

A per-store rule would break upgrades: library.json only exists once someone has used favorites, presets, or playlists, so an upgrade from an install that never touched them would suddenly gain five favorites. The independent verifier caught that in the first pass, and the whole-install rule closes it.

Neither seed can fire on an existing install. Any earlier run that reached a scene change or a clean shutdown wrote the runtime session; clearing the scene saves a session; removing the last favorite rewrites the library. The scene seed also runs only when start_scene is "last" (the default) and never over a session file that appeared after initialization. Both seeds are consumed by the first start. Nothing reads the desktop app's first-run-complete marker, so the daemon does not depend on the app.

🛠️ How it works

The effect registry finishes scanning inside initialize(), before start(), and the API serves only /health until start() returns, so there is no race with effect loading. The scene seed goes through domain::effect::apply_effect, the same path a REST apply takes, with a new MutationContext::system() and empty controls (the effect's defaults), so revisions, events, and the session save behave as for any apply. Favorites go through upsert_favorite and publish the same LibraryStoreChanged event as the API.

Bundled effect ids are a hash of hypercolor:html-bundled:<lowercased file stem>, so the seed keys on the id derived from the stem, through a new bundled_html_effect_id(stem) that the loader itself now uses. A user effect with the same title or file name never matches. The curated list is one table of stems in crates/hypercolor-daemon/src/first_run.rs. Missing effects, effects this build cannot render, and display faces are skipped with one info line; without Hyperbloom the scene stays empty as before.

🎙️ No surprise microphone prompt on a Mac

Hyperbloom is audio reactive, and on macOS the default audio source (audio.device = "default", System Monitor) looked for a loopback input and, finding none, opened the default input device instead: the microphone. Starting fresh installs on Hyperbloom would have fired the macOS microphone prompt at first boot, unexplained, possibly mid welcome wizard. Screen recording and input monitoring already never prompt implicitly, so the microphone now follows the same rule.

System Monitor falls back to the default input only when that cannot prompt. A new microphone_record_permission() in hypercolor-macos-capture reads AVAudioApplication.recordPermission without requesting it, and reports NotRequired on other platforms through the crate's stubs, so core calls it with no new cfg. The rule itself is one pure function, default_input_fallback_allowed, over a neutral AudioInputPermission in hypercolor-types.

Platform and permission System Monitor with no loopback input
Linux, Windows Falls back to the default input, as before
macOS, granted Falls back to the default input, as before
macOS, not yet asked Opens nothing; effects hear silence. Diagnose warns audio_loopback_unavailable and suggests Default Microphone or a loopback input
macOS, denied Opens nothing; the existing audio_permission_denied status

Choosing Default Microphone or a named device is how a user turns audio on, and those still open, and prompt, exactly as before. A refused fallback commits a silent source that retries with backoff (250 ms up to 4 s) and logs once per transition, so live audio settings changes still apply. The audio setup, configuration, first launch, install, and troubleshooting docs say when macOS asks.

🧪 Validation

crates/hypercolor-daemon/tests/first_run_defaults_tests.rs has 14 tests with Servo (2 without): a fresh state seeds Hyperbloom and the five favorites in order; an existing runtime session, including an intentionally empty scene, gets no scene seed; an existing library, including an empty one, gets no favorites; an upgrade that never used the library gets nothing; a registry missing some seeded effects seeds only what is present; a restart after a fresh seed adds nothing.

Command Result
cargo fmt --all -- --check clean
clippy -D warnings: core --all-targets; daemon --all-targets with default features plus persistence-test-hooks; daemon --no-default-features --lib; daemon in CI's builtin-drivers,persistence-test-hooks shape clean
first_run_defaults_tests, --test-threads=1 14 passed
Daemon library tests plus 13 integration suites for library, scenes, startup, and persistence, run plain and again with real built effects through HYPERCOLOR_EFFECTS_DIR passed
html_loader_tests with effects present 31 passed
cargo check --workspace ok
Core audio_pipeline_tests and --lib input::audio 52 and 45 passed
hypercolor-macos-capture tests 17, 1, and 5 passed
On a Mac (macOS 26.6, arm64): cargo check and clippy -D warnings --all-targets for core and macos-capture, the fallback rule and audio tests, and a probe of the permission read pass; the probe reported Undetermined without prompting

No test here watched a real prompt appear or stay away, and the Device list in the UI does not show the new audio status yet. CI runs daemon integration tests without Servo, so only the negative seeding tests run there today. Adding cargo test --locked -p hypercolor-daemon --test first_run_defaults_tests -- --test-threads=1 to the daemon Servo test step would cover the positive path; that CI edit is not in this PR. The Playwright e2e suite starts its daemon on fresh directories, so it will now begin on Hyperbloom with five favorites; the specs checked tolerate that, but the Studio specs are worth a run.

🤖 Generated with Claude Code

hyperb1iss and others added 3 commits October 19, 2026 22:44
A bundled HTML effect's id hashes only its lowercased file stem, which
is why the same effect carries the same id on every install and
platform. That derivation was private to the loader, so anything that
needed to name a bundled effect had to hardcode a UUID or copy the
hash.

bundled_html_effect_id(stem) is the loader's own derivation made
public, and stable_bundled_html_effect_id now calls it. The new test
pins it against a loaded bundled file, against a user copy of the same
stem (which keeps a path-derived id and must not match), and against
the id real installs report for Hyperbloom.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A clean install opened on a black canvas and an empty favorites list.
A genuinely fresh install now starts the default scene with Hyperbloom
and seeds the favorites with Hyperbloom, Borealis, Bubble Garden,
Plasma Engine, and Spectral Fire, listed in that order. The picks live
in one table in first_run.rs as bundled file stems, so swapping one is
a one-line change.

Fresh means initialization found no runtime session at either its
state or legacy path, no scenes.json, and no library.json. Those are
the daemon's own stores, so the seeds never depend on the desktop
app's first-run marker. Any earlier run that reached a scene change or
a clean shutdown left the session behind, so an upgrade never gets a
seed, even one that never touched its favorites. Clearing the scene
saves a session and removing the last favorite rewrites the library,
so a user's choice is never undone. The scene seed also runs only on
the start_scene "last" path and never over a session file that
appeared after initialization.

The registry finishes its scan inside initialize(), and no client
reaches the API until start() returns, so the seeds cannot race effect
registration. The scene seed applies through apply_effect with system
provenance, the same path a client apply takes, so the revision,
events, and runtime session save happen as usual. Picks this build
lacks or cannot render, and display faces, are skipped with one info
line; without Hyperbloom the scene stays empty exactly as before.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
First launch, configuration, Studio scenes, and the REST library
section now say what a fresh install starts with, that it happens once,
and that an existing install never gets it.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Oct 20, 2026 •

Copy link
Copy Markdown

Warning

Review limit reached

  • Run on-demand review

This review includes 27 billable files and costs up to $6.75.

Or wait 14 minutes for your next included review.

Check out review usage here.

View limit details

Limit details: You’ve used the included review currently available.

Learn how review limits work.

Review configuration:

⚙️ Run configuration
  • Configuration used: Organization UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 7306d468-9614-4c6a-baae-a878c3a04270
📥 Commits

Reviewing files that changed from the base of the PR and between ce477c8 and f2169c5.

⛔ Files ignored due to path filters: 1
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing: 27
  • Cargo.toml
  • crates/hypercolor-core/src/effect/loader.rs
  • crates/hypercolor-core/src/effect/mod.rs
  • crates/hypercolor-core/src/input/audio/mod.rs
  • crates/hypercolor-core/tests/audio_pipeline_tests.rs
  • crates/hypercolor-core/tests/html_loader_tests.rs
  • crates/hypercolor-daemon/src/domain/mod.rs
  • crates/hypercolor-daemon/src/first_run.rs
  • crates/hypercolor-daemon/src/lib.rs
  • crates/hypercolor-daemon/src/startup/lifecycle.rs
  • crates/hypercolor-daemon/src/startup/mod.rs
  • crates/hypercolor-daemon/src/startup/services.rs
  • crates/hypercolor-daemon/tests/first_run_defaults_tests.rs
  • crates/hypercolor-macos-capture/Cargo.toml
  • crates/hypercolor-macos-capture/src/lib.rs
  • crates/hypercolor-macos-capture/src/microphone.rs
  • crates/hypercolor-macos-capture/src/stubs.rs
  • crates/hypercolor-macos-capture/tests/microphone_permission_tests.rs
  • crates/hypercolor-types/src/audio.rs
  • docs/content/api/rest.md
  • docs/content/guide/audio-setup.md
  • docs/content/guide/choose-your-install.md
  • docs/content/guide/configuration.md
  • docs/content/guide/first-launch.md
  • docs/content/guide/installation.md
  • docs/content/studio/scenes.md
  • docs/content/troubleshooting/audio-not-reacting.md
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Comment @coderabbitai help to get the list of available commands.

hyperb1iss and others added 3 commits October 21, 2026 09:26
macOS gates every audio input, loopback drivers included, behind the
Microphone privacy permission, and opening an input before the user
has answered shows the system prompt. Callers that open inputs on
their own initiative need the current answer first, the same way
screen recording and input monitoring are preflighted.

microphone_record_permission reads AVAudioApplication's
recordPermission and never requests access. It returns the new neutral
AudioInputPermission from hypercolor-types (Granted, Denied,
Undetermined, NotRequired), so shared code never sees an AVFAudio
type. Off macOS the stub reports NotRequired, since no Microphone TCC
service exists there. objc2-avf-audio joins the workspace objc2
family; it was already in the lockfile through cpal's iOS path.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
With audio.device = "default" and no loopback device, system-monitor
capture fell back to the default input, which on a Mac is the
microphone. A fresh install now starts with Hyperbloom, so first boot
opened the mic and fired the macOS permission prompt with no
explanation, possibly in the middle of the welcome wizard.

The fallback now goes through default_input_fallback_allowed, a pure
rule over AudioInputPermission: open the default input only when the
platform has no prompt or the user already granted access. Linux and
Windows report NotRequired and keep falling back exactly as before.
On macOS an unanswered permission refuses the fallback and opens no
device; choosing Default Microphone or a named input is the user's
explicit request and still opens, and prompts, normally.

The refusal is a typed error rather than a string match. An unanswered
permission becomes audio_loopback_unavailable, a recoverable status
with a remediation that says to choose Default Microphone or a
loopback input, retried on the device-reappearance cadence and logged
once at info per transition. A denied permission reports the existing
audio_permission_denied. Status publication now builds every audio
issue through one helper so remediations travel with the code.

Live reconfiguration builds capture synchronously, so a refusal there
would have aborted the source swap: toggling audio off and on, or
changing any audio key while Hyperbloom ran, left the old source live
and dropped the status. A prepared source now stages the refusal in
place of a capture runtime and commits as a silent source with the
waiting status and a recovery worker, the same state a running source
reaches. Any other capture error still fails the swap.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The audio setup guide claimed system-audio capture was simply
unavailable without a loopback device, and that "default" spots
BlackHole by name. Neither held: Linux and Windows fall back to the
default input, and BlackHole's name matches no loopback keyword.

Audio setup, configuration, installation, and the macOS install notes
now say that "default" falls back to the default input on Linux and
Windows, and on macOS only once Microphone permission is granted, so
the prompt appears only when the daemon opens an input the user set
up. The first-launch guide says Hyperbloom on a fresh Mac glows
without reacting until an input is chosen, and both troubleshooting
paths name the waiting status and how to resolve it.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@hyperb1iss
hyperb1iss merged commit c4afbbc into main Oct 21, 2026
42 of 43 checks passed
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.

1 participant