refactor(whispering): make Epicenter the native host#2449
Open
braden-w wants to merge 51 commits into
Open
Conversation
Define the greenfield ownership and availability contracts before migrating the remaining platform graph. The build now selects the Epicenter product condition, manual recording enters through one complete environment, and type tests keep workspace and account lifecycles distinct.
Use one exact 39131 development origin in Rust and every capability scope. Removing the override and wildcard closes the packaged-debug ACL mismatch instead of preserving two sources of port authority.
Select authentication through the environment, then construct one always-available workspace per application boot. Signed-out local state remains a complete product runtime while account-only resources stay behind the signed-in gate.
Route recording, artifacts, downloads, notifications, OS context, and text through the complete environment. Delete the native analytics promise and its entire browser/native setting, runtime, dependency, and event family instead of preserving a broken optional branch.
Launch-at-login controls the lifetime of the Epicenter host, so an embedded app must not own or expose that policy. Remove the Whispering UI, bridge commands, permissions, and Tauri plugin instead of preserving an app-level compatibility capability.
Route shared code through complete build-selected product operations instead of a nullable native capability bag. Recipe presentation now has host-selected implementations, delivery and recorder configuration describe their own capabilities, and Epicenter-only auth reaches the generated command boundary directly.
Vite already owns the selected browser or Epicenter base URL. Normalize that value once instead of maintaining a second build-selected constant pair and tests for the duplicated configuration.
Browser code no longer receives a null system-shortcut backend. Each build now constructs one complete routed shortcut surface, which owns its global-reach capability and serves settings, runtime synchronization, and recording hints through the same API.
The browser build now resolves the overlay route to a redirect instead of receiving no-op window event transports. Epicenter alone owns the auxiliary-window surface and its typed frontend event channels.
The remaining earned host selections now use semantic imports for OS identity and command contributions. Replace active guidance that taught the deleted platform service-locator architecture with the complete runtime and focused-surface model.
Epicenter now creates the secured overlay window on every desktop platform. Whispering's Epicenter shell attaches the missing lifecycle driver that projects recording status, synchronizes visibility and position, and routes overlay actions without regaining window creation ownership.
…eenfield-collapse # Conflicts: # apps/whispering/docs/recording-overlay.md # apps/whispering/src/routes/(app)/(config)/settings/recording/+page.svelte # apps/whispering/src/routes/(app)/_runtime/attach-recording-overlay.browser.ts # apps/whispering/src/routes/(app)/_runtime/attach-recording-overlay.tauri.svelte.ts # apps/whispering/src/routes/(app)/_runtime/runtime-owners.ts
Whispering now sends semantic visibility and reveal requests instead of looking up, positioning, showing, or hiding native windows. Epicenter owns monitor selection and presentation, allowing the embedded surface to drop five window permissions.
Supersede the four accepted playback-control decisions now that Whispering no longer pauses or resumes other applications. The clean break removes a disproportionate cross-platform implementation family from the core dictation promise.
The native recorder already sends microphone levels from Rust directly to the overlay, so its frontend callback path could never run. Delete that duplicate emitter and sweep documentation that still named deleted runtime and transcription paths.
Recipe presentation and the recording overlay now enter through one host-owned reveal command. Delete the overlay-to-main event relay and the final direct window permissions from Whispering's native surface.
The canonical brand pipeline never shipped, and the always-on doc worker was replaced by the client-owned loop. Mark both proposals superseded and delete their spent implementation scaffolds instead of aging them as active plans.
Contributor
Preview Deployment
Previews update automatically with new commits to this PR. No cleanup needed: previews are version aliases on the existing workers. Commit 426914b |
Epicenter now owns recording-scoped suppression and guarded macOS Core Audio restoration. The narrow lease API keeps volume controls and platform state out of Whispering while native recorder teardown prevents orphaned effects.
Keep exact Windows GSMTC session objects and Linux MPRIS bus names in the Rust suppression epoch. Resume only retained sessions that still report paused, and deliberately forget the epoch when Epicenter exits.
Grant the trusted Whispering surface access to the lease commands, retain exact Linux D-Bus ownership across ambiguous pause outcomes, and restore every guarded macOS channel independently.
Centralize final-lease restoration in one Rust state transition and name the TypeScript port for suppression rather than general playback control.
Let manual recordings opt into lower-volume, mute, or pause behavior through one recording-scoped lease. Keep platform restoration state native and treat unsupported or failed suppression as a no-op.
The native recorder already releases suppression on every lifecycle edge (supplant, stop, cancel, exit), so the opaque lease crossing the IPC boundary made TypeScript a second bookkeeper of an invariant Rust already owned. Passing recording ids lets the webview stay stateless: the lease type, the lease registry, and the generation-tracking coordinator all collapse.
"Background audio" read as noise suppression of the mic capture, and the control rendered in browser hosts that cannot act on it. The setting is now "Other apps' audio" with a "Keep playing" off state, renders only when the host reports canSuppressPlayback, and defaults to lowering volume so speaker bleed does not degrade first-run transcriptions. The persisted key becomes recording.playbackSuppression while the branch is unshipped and the rename is free.
WhisperingBaseEnvironment encoded construction order (workspace needs auth and downloads before transcription exists), not a product boundary. Constructing the workspace first lets each root state the complete environment as one literal, so the contract collapses to a single WhisperingEnvironment carrying the membership doctrine in its JSDoc.
The #manual-playback package condition duplicated the environment's own membership rule: an operation complete in both hosts with a build-selected implementation. It also hid a settings read inside the Epicenter variant, against the settings-as-parameters rule. Playback is now environment.playback with the suppression mode passed by the caller, and the canSuppress fact rides the capability it describes (the delivery.supportsCursor idiom) instead of floating as a top-level flag. The diagnostic log splits into $lib/report/log so the composition roots can warn on suppression failure without importing the toast reporter, which reads #runtime and would cycle back into the root.
os lived in the environment and behind #os at once, because state composed by the runtime roots (device-config) sits upstream of #runtime and cannot read the environment. Two doors to one static fact is a dual representation; the leaf seam is the one every altitude can reach, so the environment sheds os and its three downstream readers import #os directly. The environment now holds only operations complete in both hosts; host facts ride #os or the capability they describe.
The workspace singleton moved into the runtime roots long ago, so the active.ts sentence pointed at a deleted file. The host-composition section now names the rule the environment collapse follows: operations complete in both hosts live in the environment, host facts enter through #os at any altitude, and surfaces above the roots keep their own seams because they compose code that already reads #runtime.
…ssion Everywhere else in the app, playback means playing a recording back (rpc/audio playbackUrl, recording detail playback), so environment.playback.begin(recordingId) read as starting audio. playbackSuppression matches the desktop capability, the Rust commands, and the persisted setting key; the flag becomes supported to drop the canSuppress stutter inside the renamed member.
presentRecipePicker's build-selected pair differed only by whether the native window is revealed first. Reveal becomes one environment operation (desktop adapter on Epicenter, no-op in the browser) and the picker presentation collapses to one shared function.
Completion follows the captureSurfaces and transcription.providers pattern: one shared implementation consults supportsCompletion instead of a build-selected module pair whose Epicenter side only refused. The refusal is now one literal in the Epicenter composition root.
The build-selected state pair becomes one factory that receives the desktop dictation port; the Epicenter root constructs it and the browser root supplies the constant page value. Callers read environment.dictation instead of a dedicated alias.
Adds the pinning tests ADR-0118 promised: window labels must come from the compiled surface catalog, each development/production capability pair must grant identical authority, and no generic plugin family may appear. Deletes the dialog:allow-open/save grants (no frontend caller; the Rust failure dialog needs no ACL) and routes Whispering's asset handler through the catalog pattern instead of a literal.
The platform seam family is gone from the architecture; the directory was its last vocabulary. Auth composition moves beside the runtime roots, host-OS identity gets its own #os home, the shortcut family gets one shortcuts/ directory, and the grab-bag types.ts splits into the contracts its consumers actually own (WhisperingAuth now lives on the environment contract).
The V1 completion condition holds: both builds pass, the native workflow tests pass, no feature module imports Tauri primitives, and the old platform graph is deleted. Durable ownership is already recorded by ADR-0118; done means deletion.
Review follow-up: the catalog validation only proved granted windows were known; it now also proves no catalog surface is silently missing from the shared-apps capability. Also repoints the keyring spec at the moved auth composition file.
The code renamed recording.backgroundAudioSuppression to recording.playbackSuppression and made duck the default while the branch is unshipped; ADR-0120 now states the landed promise instead of the superseded one.
Playback suppression has the same lifetime as native recording, so exposing independent begin and end commands created a second lifecycle the SPA had to coordinate. Pass the selected policy into recording startup and keep application, restoration, and platform state private to Rust.
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.
Whispering's browser and Epicenter builds had accumulated nullable platform bags, runtime Tauri checks, and app-owned native policy. That made the embedded app, rather than Epicenter, the effective owner of desktop lifecycle.
This change makes each build select one complete runtime, moves native policy into the Epicenter host, and then collapses the selection machinery itself until every remaining seam is structurally earned.
Ownership
The environment contract is now the host-variance surface:
dictation,playbackSuppression,reveal, andsupportsCompletionare environment operations and facts rather than standalone build-selected module pairs. The import map shrank from twelve aliases to eight; each survivor either is a composition root (#runtime,#desktop,#app-shell), sits upstream of the runtime by dependency order (#os,#shortcuts,#command-contributions), or selects a whole window surface (#recording-overlay-surface,#desktop-promotion). The legacysrc/lib/platform/directory is dissolved intoruntime/,os/, andshortcuts/.On the host side, the compiled surface catalog now validates the Tauri ACL the way ADR-0118 promised: every window label must come from the catalog, every catalog surface must receive the shared capability, each development/production capability pair must grant identical authority, and no generic plugin family may appear. Dead
dialog:*grants are deleted.Playback suppression
Whispering does not control system media playback. Epicenter owns a narrower promise: suppress other apps' audio for the life of a recording, keyed by recording id, best effort, restored when the recording ends or the process exits. The
recording.playbackSuppressionsetting offersoff,duck,mute, orpauseand defaults to duck so speaker bleed does not degrade first-run transcriptions; it renders only in hosts that can act on it. ADR-0119 records the refusal of general media control; ADR-0120 (amended) records the lease and the default.Deliberate refusals
Browser boundary
The browser remains a first-class product target. It keeps browser recording, IndexedDB artifacts, browser auth, focused shortcuts, cloud and self-hosted remote transcription, completion (Polish and Recipes), and in-page recording feedback.
Native recording, local GGUF transcription, cursor delivery, global shortcuts, keyring storage, playback suppression, and auxiliary windows exist only in the Epicenter-selected implementation. Browser bundles do not resolve those native paths.