Skip to content

refactor(whispering): make Epicenter the native host#2449

Open
braden-w wants to merge 51 commits into
mainfrom
codex/desktop-host-greenfield-collapse
Open

refactor(whispering): make Epicenter the native host#2449
braden-w wants to merge 51 commits into
mainfrom
codex/desktop-host-greenfield-collapse

Conversation

@braden-w

@braden-w braden-w commented Jul 11, 2026

Copy link
Copy Markdown
Member

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

before
shared Whispering callers
  -> inspect platform/Tauri
  -> choose adapters
  -> manage native windows and policy

after
#runtime (one complete environment per host, plus the always-on workspace)
  |-- browser: browser recording, IndexedDB, remote transcription, completion
  `-- Epicenter: native recording, local transcription, host capabilities

#desktop   Epicenter-only operations behind one adapter over generated bindings
#app-shell one build-selected UI composition root

Epicenter host
  -> native windows, permissions, launch policy, overlay presentation,
     recording-scoped playback suppression (ADR-0120)

The environment contract is now the host-variance surface: dictation, playbackSuppression, reveal, and supportsCompletion are 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 legacy src/lib/platform/ directory is dissolved into runtime/, os/, and shortcuts/.

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.playbackSuppression setting offers off, duck, mute, or pause and 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

  • Remove Whispering-owned launch-at-login controls. Launch policy belongs to Epicenter.
  • No generic native executor, HTTP proxy, filesystem API, or runtime capability discovery for embedded SPAs.
  • No surface-catalog code generator: with one native SPA, generated tauri-specta bindings plus pinning tests are the whole contract; a generator is a framework without a second consumer.
  • Remove compatibility aliases, nullable browser stubs, stale specs, and obsolete architecture guides instead of preserving parallel paths.

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.

braden-w and others added 27 commits July 10, 2026 15:43
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.
@github-actions

github-actions Bot commented Jul 11, 2026

Copy link
Copy Markdown
Contributor

Preview Deployment

App Preview URL
whispering https://pr-2449-whispering.epicenter.workers.dev
landing https://pr-2449-landing.epicenter.workers.dev
opensidian ❌ build or upload failed (see logs)
honeycrisp https://pr-2449-honeycrisp.epicenter.workers.dev
vocab uploaded (no preview URL returned)

Previews update automatically with new commits to this PR. No cleanup needed: previews are version aliases on the existing workers.

Commit 426914b

braden-w added 2 commits July 10, 2026 20:11
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.
braden-w and others added 22 commits July 10, 2026 22:14
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.
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