Repository navigation
feat(daemon): start fresh installs on Hyperbloom with curated favorites - #442
Merged
Merged
Conversation
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>
|
Warning Review limit reached
This review includes 27 billable files and costs up to $6.75. Or wait 14 minutes for your next included review. View limit details
Comment |
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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), andlibrary.jsonwere 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.jsononly 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_sceneis"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'sfirst-run-completemarker, so the daemon does not depend on the app.🛠️ How it works
The effect registry finishes scanning inside
initialize(), beforestart(), and the API serves only/healthuntilstart()returns, so there is no race with effect loading. The scene seed goes throughdomain::effect::apply_effect, the same path a REST apply takes, with a newMutationContext::system()and empty controls (the effect's defaults), so revisions, events, and the session save behave as for any apply. Favorites go throughupsert_favoriteand publish the sameLibraryStoreChangedevent 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 newbundled_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 incrates/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()inhypercolor-macos-capturereadsAVAudioApplication.recordPermissionwithout requesting it, and reportsNotRequiredon other platforms through the crate's stubs, so core calls it with no newcfg. The rule itself is one pure function,default_input_fallback_allowed, over a neutralAudioInputPermissioninhypercolor-types.audio_loopback_unavailableand suggests Default Microphone or a loopback inputaudio_permission_deniedstatusChoosing 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.rshas 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.cargo fmt --all -- --check-D warnings: core--all-targets; daemon--all-targetswith default features pluspersistence-test-hooks; daemon--no-default-features --lib; daemon in CI'sbuiltin-drivers,persistence-test-hooksshapefirst_run_defaults_tests,--test-threads=1HYPERCOLOR_EFFECTS_DIRhtml_loader_testswith effects presentcargo check --workspaceaudio_pipeline_testsand--lib input::audiohypercolor-macos-capturetestscargo checkand clippy-D warnings --all-targetsfor core and macos-capture, the fallback rule and audio tests, and a probe of the permission readUndeterminedwithout promptingNo 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=1to 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