Desktop Runtime
July 30, 2026 · View on GitHub
Maintainer-facing document for packaging, app data, and runtime policy. For a general architecture overview, read architecture.
This document covers desktop packaging and runtime invariants: Tauri app data, identifier safety, unsigned releases, bundled Python, and Mac / Windows notes.
Tauri Identifier
Tauri identifier binds Galley's app directories on all supported platforms.
The main SQLite DB follows tauri-plugin-sql and lives in the app config
directory:
- macOS:
~/Library/Application Support/{identifier}/ - Linux:
$XDG_CONFIG_HOME/{identifier}/or~/.config/{identifier}/ - Windows:
%APPDATA%/{identifier}/
Galley currently uses app.galley. SQLite data for sessions, projects,
tool events, prefs, and migrations lives under that identifier-controlled
directory.
Changing the identifier without migration makes user data appear to vanish, because the app starts reading an empty new directory.
Identifier Change Rule
Do not change the Tauri identifier unless the same change includes:
- Rust-side startup migration from old data directory to new data directory.
- Fallback copy / rename behavior if the new DB is missing but the old DB exists.
- Dogfood using a manually seeded old data directory.
- Release notes explaining the one-time migration.
Historical lesson: the 2026-05-13 rename from app.gaworkbench to app.galley
made sessions appear missing during dogfood. The data was not lost; the app was
looking at a new directory.
Signing Strategy
Pre-v1.0 Galley ships unsigned:
- macOS
.app/.dmgare not codesigned or notarized. - Windows
.exe/ NSIS installer is not signed with an EV certificate. - Release notes must explain first-launch friction.
Current decision, 2026-05-18: unsigned is acceptable for the current personal open-source stage. Re-evaluate before v1.0 or when user scale makes manual Gatekeeper / SmartScreen bypass too costly.
Expected user flow:
- macOS: right click the app, choose Open, then confirm the Gatekeeper dialog.
- Windows: SmartScreen -> More info -> Run anyway.
Any PR adding codesign, notarytool, or signtool.exe should also update
this policy and release workflow.
Auto Update Runtime
Galley checks for app updates on startup and exposes the current update state in Settings -> About / Runtime. Update delivery is enabled only for builds that provide both compile-time variables:
GALLEY_UPDATER_PUBKEY: Tauri updater public key embedded in the app.GALLEY_UPDATER_ENDPOINT: HTTPS URL for the updater manifest.
Current stable endpoint:
https://raw.githubusercontent.com/wangjc683/galley/galley-update-channel/updates/stable/latest.json
updates/beta/latest.json is kept as a legacy alias for builds compiled before
the stable channel cutover.
Without both values, the app reports that this build is not connected to an update channel and local development keeps working. This is intentional: Tauri updater package verification is mandatory and should not be bypassed just because Galley itself is still unsigned at the OS level.
Update installation is task-protected. If any session is actively running, Galley may remember that an update is available, but it will not download, install, or relaunch for that update until the session is idle. This avoids turning a background maintenance action into a lost-task event.
Background Mode
Galley runs in Background Mode by default on macOS and Windows. Closing the main window hides the window; it does not quit the app or shut down Galley Core. This keeps local socket access alive for the CLI, Supervisor automation, and external IM / agent frontends while the desktop window is out of the way.
The first time the user closes the window on a device (2026-07-18, replacing
the earlier one-time native info dialog), Galley does not hide. The Rust close
handler keeps the window visible and emits first-close-requested; the GUI
renders the in-app FirstCloseDialog asking what closing should mean — "keep
running in background" (primary, recommended) or "quit Galley". The verdict is
reported back via the resolve_first_close command, which records the choice,
writes it into the keep_in_background_on_close behavior (Settings → General
owns it from then on), and executes the hide or the true-quit path. Dismissing
the dialog (Esc / overlay) cancels the close entirely and asks again next
time. An explicit Settings → General toggle of the same preference also counts
as the choice, so the dialog never asks a user who already decided.
The choice state is persisted in the close_to_background_hint_seen pref (the
key keeps its legacy name, so users who dismissed the old hint are not
re-asked) and read back during Rust setup (right after migrations) to arm an
in-process guard before the window can be closed. Seeding in setup rather
than at GUI hydrate is deliberate — it keeps the question
at-most-once-per-device even if the user closes the window before the GUI
finishes hydrating. If the webview hasn't registered its event listener yet
(closing within the first moments of the very first launch), the emit is lost
and the window simply stays open; the next close attempt asks again.
Platform behavior:
- macOS: window close and
Cmd+Whide the main window. The Dock icon remains, and a right-side menu bar status item can reopen or hide Galley. - Windows: the window close button and
Alt+F4hide Galley to the system tray. The tray menu can reopen or hide Galley.
True application exit is explicit:
- The tray / status item exposes
Quit Galley. - macOS also exposes
Quit Galleyfrom the app menu withCmd+Q. - If any Agent task is running, true quit asks for confirmation before shutting down the app and interrupting active work.
The tray / status item provides the small set of actions useful while Galley is running in the background:
Open Galley/Hide Galley: mirror the current simple window state.New Chat: reopen Galley and start a new conversation.Settings: reopen Galley and open Settings.Check for Updates...: reopen Galley to Settings -> About and check the update channel.Quit Galley: explicitly exit the app.
The first version intentionally has no running / approval badge; task state remains inside the main UI.
Launch at Login
Settings -> General offers an opt-in "Launch at login" toggle (since
2026-07-17, tauri-plugin-autostart). Default is off — nothing is written
until the user enables it.
Mechanism per platform:
- macOS: a LaunchAgent plist under
~/Library/LaunchAgents. No permission prompt; the item is visible and revocable under System Settings -> Login Items, and macOS posts its own "login item added" notice. - Windows: an
HKCU\...\CurrentVersion\Runvalue. Current-user scope, no admin, survives NSIScurrentUserupdates (the exe path is stable).
The OS is the single source of truth. The GUI reads the plugin's
isEnabled() live on every Settings visit and after every toggle; the state
is deliberately not mirrored into Galley prefs. Removing the login item
from system settings therefore shows up in Galley as "off" without any
reconciliation logic.
Login launches are silent. The registered login item carries --autostart;
the setup hook reads that flag and leaves the main window hidden, so Galley
starts straight into Background Mode (status item / tray, toggle label
"Open Galley") with Galley Core, the local socket, and IM autostart all
live. To support this without a frame flash, the main window is created
hidden (visible: false in tauri.conf.json) and shown by the setup hook
on every non---autostart launch — any change to that show path must keep
both launch modes working, or normal launches open an invisible window.
Window size / position / maximized / fullscreen persist across launches via
tauri-plugin-window-state (since 2026-07-15), saved to
.window-state.json in the app config dir on true quit. Two state flags are
deliberately excluded:
VISIBLE: Background Mode hides the window instead of closing. Quitting from the tray while hidden must not restore an invisible window on the next launch.DECORATIONS: Windows runs with native decorations off as custom chrome (see the setup hook incore/src/app_setup.rs); the plugin must not restore a stale decorations value over that.
The curated default geometry (1480×920 centered, sidebar at 20%) is
reachable on demand via Reset to Default Layout (2026-07-30): the macOS
Window menu, the command palette entry (also the Windows entry point), and
a sidebar-separator double-click — all three run the full reset (the
separator is the one visible entry point, so its gesture carries the whole
command; upgraded from split-only same day). A hover tooltip on the
separator teaches the gesture. All converge on
gui/src/lib/layout-reset.ts + the reset_window_layout command; golden
constants live in core/src/app_setup.rs and must match tauri.conf.json.
First launches center via center: true, and fit_window_to_monitor
snaps the window to a monitor-clamped golden size only when it overflows
the current display (first run on small screens, or restored state after a
monitor swap). A brief same-day launch-amnesia experiment and its reversal
are chronicled in devlog
2026-07-30-window-geometry-amnesia.
Since 2026-07-29 the main window runs with dragDropEnabled: true: OS
drags arrive through Tauri's native onDragDropEvent stream (real
filesystem paths; the composer's only drop intake), and HTML5 drag-and-drop
never fires in the webview — including text / URL drags, which are consumed
with no payload and surfaced as a copy-paste toast. This is an upstream
wry limitation accepted deliberately; the reasoning and source-level
findings live in .scratch/composer-file-drop/issues/01 (devlog entry
lands when that feature's tracker closes).
Tauri updater signing is separate from macOS codesigning / Windows Authenticode. The private updater key must stay in release secrets; only the public key is safe to embed in app builds.
Release builds opt into signed updater artifacts by generating
core/tauri.updater.generated.conf.json inside CI. The default
tauri.conf.json intentionally does not enable createUpdaterArtifacts, so
local Dev and unsigned local builds do not require TAURI_SIGNING_PRIVATE_KEY.
The release workflow creates a candidate latest.json as a draft Release asset.
After smoke test and publishing the release, run the manual
promote-update-channel.yml workflow to update the beta manifest that installed
apps read.
Bundled Python
Since v0.1.1, Galley release builds embed CPython 3.11.15 plus GenericAgent core dependencies. Users do not need to configure Python or a venv for normal use.
Runtime policy:
- Managed / bundled GA must be open-and-run from Galley's own Python and packaged dependencies. A release that needs the user's Python to start the managed runtime is not release-ready.
- Attach / external GA also defaults to Galley's bundled Python in release builds. This gives user-owned GenericAgent checkouts the same baseline dependencies without letting Galley modify the checkout, venv, PATH, or state.
- External Python is an explicit escape hatch (
gaConfig.useExternalPython = true) for user-added dependencies, unsupported upstream frontends, or users who deliberately want their own interpreter. - Dev mode uses external Python because
$RESOURCEdoes not point at the final bundle resource directory duringpnpm tauri dev.
Implementation notes:
- Source:
python-build-standaloneinstall-only stripped builds. - Script:
scripts/bundle-python.sh. - Output:
core/python-bundle/python/generated per target arch. - Tauri resource:
python-bundle/python/is bundled as$RESOURCE/python/. - Tauri aliases:
python-bundledfor macOS / Linux (bin/python3)python-bundled-winfor Windows (python.exe)
- Release gate:
scripts/check-bundled-python-managed-ga.shverifies that the generated bundle can importmanaged-ga/codeand Galley-owned runtime deps.
Bundled GenericAgent core deps are audited during baseline upgrades. See GA baseline.
Managed GenericAgent Runtime
Managed / bundled GA keeps code and state separate:
- App resources contain
managed-ga/manifest.json,managed-ga/code/, andmanaged-ga/patches/. - Application Support contains
managed-ga-state/andmanaged-model-config/. - For unsigned release builds,
workbench.dbcontains encrypted managed model API key payloads plus the local encryption key. Generated runtime config stores onlyapiKeyRef, never plaintext API keys.
Galley may replace the managed code payload during an app update, but it must not overwrite managed state. Startup may create missing state directories only. Advanced diagnostics can show managed runtime paths, baseline commit, patch stack status, and generated config presence; diagnostics must never display API keys.
Backups of Application Support are expected to restore sessions, managed GA state, non-secret model metadata, and encrypted managed model credentials for unsigned release builds.
The managed-runtime product and upgrade rules live in managed GA runtime.
Release builds run node scripts/check-managed-ga-payload.mjs before packaging.
That gate verifies the Tauri resource mapping, managed GA manifest, prompt
files, patch stack, and absence of generated / local / secret-bearing artifacts
inside managed-ga/code.
Release Artifacts
Current release path:
- macOS Apple Silicon DMG via CI.
- macOS Intel DMG via CI cross-compile path where available, local fallback if needed.
- Windows x64 NSIS installer via CI.
The release-day checklist lives in release / update SOP. The detailed release process lives in release workflow. Windows manual smoke lives in windows build checklist.
Platform Terminology
macOSmeans the operating system. Use for platform names, system requirements, and release titles.Macmeans the hardware family or user device.- Release title example:
Galley v0.2.0.