apps/macos contains the native app shell for germ. It is a SwiftPM macOS
app that wraps the localhost daemon instead of replacing it — the same
architecture as oída's shell: the daemon stays the single source of truth and
every surface renders the same state.
apps/macos/
Package.swift
Sources/GermMacOS/
App/ SwiftUI entry point and AppKit launch delegate
Services/ Shell store (health polling, daemon lifecycle),
daemon supervisor, launch-at-login
Support/ Repository locator
Views/ Main window (embedded dashboard), menu bar extra, settings
Resources/ AppIcon.svg + AppIcon.icns (generated by script/make_icon.py)
script/ build_and_run.sh, make_icon.py
Build and launch:
apps/macos/script/build_and_run.sh--build-only stages apps/germ.app without launching it, so the runnable
bundle is visible beside the repository's app sources. The icon and brand PNG
family are generated deterministically by script/make_icon.py (stdlib-only
Python + iconutil). The vector sources are Resources/AppIcon.svg and
docs/assets/germ-logo-{light,dark}.svg; the same centered-culture geometry
also produces the 2048 px light/dark PNGs and web icon sizes. Its single arc
is oída's outer listening arc mirrored exactly, creating the corresponding
lower-right opening around three centered cells.
The main window embeds the daemon's own dashboard
(http://127.0.0.1:5178/dashboard) in a WKWebView, so the mac app and the
browser render the exact same page and cannot diverge: same Chamber, same
modules and connections, same library, same providers.
Cross-surface session continuity comes from the daemon, not the WebView:
the dashboard autosaves the live graph to PUT /sessions/current (debounced)
and restores it on boot when its own graph is empty, and named sessions live
at /sessions under output/sessions/. Opening the mac app after patching in
a browser (or the other way around) restores the same modules and
connections. Both surfaces write last-writer-wins; the client_id field
records which surface saved last.
The WKWebView grants microphone capture natively (macOS permission still
applies) so the Chamber's Record module works in-app, saves the dashboard's
WAV/JSON/GWT downloads to the user's Downloads folder, and opens
target="_blank" links in the default browser. Audio Snapshot uses WebKit's
native display-sharing picker; choose a display or app and include its audio,
subject to the normal macOS Screen Recording permission.
If no daemon is reachable at launch, the shell locates the repository root by
walking up from the bundle/current directory or by reading
GERM_REPOSITORY_ROOT, then starts one:
uv run uvicorn server.main:app --host 127.0.0.1 --port 5178Common development paths are appended to PATH so a GUI-launched app can
find uv, and Homebrew dylib paths are exported for the audio toolchain.
The shell stops only processes it started itself — on explicit Stop Managed
Daemon and on app quit. A small standard-library process supervisor watches
the owning app PID and tears down the uvicorn process group if the app exits
unexpectedly, so managed daemons cannot be orphaned. A daemon
already running outside the shell is observed over HTTP and never owned.
The menu bar extra shows daemon status (/health: provider, device, loaded
models) plus open-window / open-in-browser / settings / start / stop / quit.
Settings exposes the persisted light/dark appearance and accent palette, the
daemon base URL (for an alternate-port daemon), launch-at-login through
SMAppService.mainApp, and the managed daemon's recent log lines. Appearance
changes synchronize with the embedded dashboard and its browser-local saved
preferences.
- The shell is unsigned by default; see oída's
docs/macos-signing-notarization.mdfor the Developer ID flow (the same script layout applies). - The shell adds no audio processing of its own — everything sonic stays in the dashboard's engine and the Python DSP routes.