Files
kandev/docs/specs/desktop/system-design/desktop-tauri-app.md
T
2026-09-10 14:30:58 +01:00

350 lines
20 KiB
Markdown

---
status: draft
system: desktop
requirements:
- REQ-DESKTOP-DESKTOP-TAURI-APP-001
created: 2026-06-23
updated: 2026-08-27
owners:
- tbd
---
# Tauri Desktop App System Design
## Purpose and boundaries
This design defines the technical contract for `REQ-DESKTOP-DESKTOP-TAURI-APP-001`.
## Requirement mapping
| Requirement | Design section |
| --- | --- |
| `REQ-DESKTOP-DESKTOP-TAURI-APP-001` | [Desktop architecture](#desktop-architecture) |
## Desktop architecture
## Why
Kandev's installed desktop app should behave like a native application without duplicating the
existing React product surface. Users need standard focused-window commands, reliable native
updates and notifications, and preservation of their window state while Kandev continues to use
the existing local Go backend and shared settings UI.
## What
The existing Tauri v2 shell remains a thin native host for the Go-served SPA. In addition to its
implemented launch and backend-lifecycle behavior, it provides:
- application menus with platform-appropriate accelerators;
- zoom in, zoom out, and actual-size commands (`Cmd` on macOS, `Ctrl` elsewhere);
- contextual `Cmd/Ctrl+W` behavior that never closes the window, backend, or application;
- `Cmd+,` on macOS to open the existing `/settings/general` page;
- New Task, Check for Updates, Help, external-link, and standard application commands;
- persisted window size, position, and maximized state, restored onto a visible display;
- signed, prompt-before-install desktop updates through the existing System > Updates page;
- native notifications for selected turn-finished, clarification-requested, and
session-failure events;
- focus/reopen behavior for a second launch and Dock activation where supported.
The macOS red window close control quits Kandev and stops its owned backend. It does not hide the
window or leave a tray/background process. `Cmd+W` is deliberately separate from that lifecycle.
### Existing shell guarantees
This increment preserves the shipped desktop contract:
- installers target macOS arm64/x64, Linux arm64/x64, and Windows x64;
- the app starts the packaged native launcher and Go backend, then displays the existing embedded
Vite/React SPA without requiring Node.js or a separate web server at runtime;
- desktop and CLI/browser launches share the existing Kandev data directory, database, worktrees,
executor settings, integrations, and agent configuration;
- the desktop shell owns the backend process tree, waits for a two-stage readiness signal (a
token-verified `/health` proving process ownership, then an unauthenticated `/ready` proving
application readiness), and cleans up on startup failure, quit, update restart, or WebView
failure;
- the backend binds to an app-selected loopback port, and the WebView navigates only after a
per-launch health token first proves that the responding process is the backend the shell
started, and `/ready` then reports the backend has finished startup;
- GUI launches retain the predictable process environment and common user binary locations needed
to discover configured agent CLIs;
- a second launch focuses the existing instance instead of starting another backend; and
- GitHub releases continue to include the existing installers, runtime archives, and checksums.
## Existing launch and data contract
The native shell continues to package the Kandev launcher/runtime without a production Node.js
dependency. It selects an available loopback port, starts:
```text
kandev --headless --port <available-loopback-port>
```
with `KANDEV_SERVER_HOST=127.0.0.1`, and waits for readiness in two stages. First it polls
`GET /health`, which flips to 200 as soon as the listener binds, before startup recovery
finishes; each launch supplies a generated `KANDEV_DESKTOP_HEALTH_TOKEN`, and this stage
requires the same value in the successful response's `X-Kandev-Desktop-Health-Token` header,
proving the responding process is the backend the shell started. Once `/health` passes, the
shell then polls unauthenticated `GET /ready` (which carries no health token), and only
navigates the WebView to the backend origin once `/ready` reports the backend has finished
startup. The desktop shell also sets `KANDEV_DESKTOP_NATIVE_NOTIFICATIONS=true` to identify the
launch as desktop-owned. The nested native launcher must forward the shell's exact non-empty
health token to the backend and use it for its own `/health` readiness poll rather than
replacing it with another token. A second instance focuses the first and does not start another
backend.
The shell preserves the existing GUI-launch `PATH` normalization and the existing Kandev data
directory, SQLite database, worktrees, executor settings, integrations, and agent configuration.
It owns cleanup of the launcher/backend process tree on quit, startup failure, and update restart.
The existing HTTP, WebSocket, and boot-payload product APIs remain the SPA's data plane; the
desktop bridge is only for native integration.
## Contextual close contract
One `Cmd/Ctrl+W` invocation closes at most one item, in this order:
1. The topmost dismissible overlay, such as the New Task dialog, a sheet, or a popover.
2. The focused closable document tab, including file, diff, commit, or preview tabs.
3. Nothing.
Alert/confirmation dialogs that intentionally require an explicit choice are not dismissible.
Closing a document must use the same unsaved-change guard as its visible close affordance.
Session, chat, terminal, task, and structural layout panels are not document tabs and are never
closed by this command. When there is no eligible context, the command is a no-op.
## Native menu contract
The focused desktop application exposes platform-standard menus. At minimum:
- **Application:** About, Settings/Preferences (`Cmd+,` on macOS), Hide/Services where supplied
by the OS, Check for Updates, and Quit.
- **File:** New Task (`Cmd/Ctrl+N`) and Close Context (`Cmd/Ctrl+W`).
- **View:** Zoom In (`Cmd/Ctrl++` and `Cmd/Ctrl+=`), Zoom Out (`Cmd/Ctrl+-`), Actual Size
(`Cmd/Ctrl+0`), and platform full-screen behavior.
- **Help:** documentation, repository/support, and release notes opened in the system browser.
These are focused-application accelerators, not global OS shortcuts. New Task invokes the existing
task-create flow when an active workspace permits it. Settings routes to the existing settings UI;
no native settings window is added.
## Update contract
The desktop updater is distinct from the service-only updater described by ADR-0012:
- It checks after the desktop app reaches `ready` and at most every 24 hours while running.
- A manual Check for Updates action always performs a fresh check.
- Finding an update never downloads, installs, or restarts silently. The existing
`/settings/system/updates` page shows the version and release information and asks for explicit
confirmation before download/install/relaunch.
- Automatic-check failures are non-blocking. Manual checks show actionable no-update or error
status in the existing page.
- Only a newer version signed by the embedded Tauri updater key can be installed. Downgrades and
payloads with missing or invalid updater signatures are rejected.
- Before relaunch, the desktop shell stops the owned backend process tree, then allows Tauri's
updater restart path to replace and reopen the application.
The release publishes a signed static `latest.json` manifest and Tauri updater artifacts for:
- macOS arm64 and x64: updater `.app.tar.gz` bundles plus existing `.dmg` installers;
- Linux x64 and arm64: updater `.AppImage.tar.gz` bundles plus AppImage, `.deb`, and `.rpm`
packages;
- Windows x64: NSIS updater bundle plus the existing installer.
AppImage is the auto-update path on Linux. `.deb` and `.rpm` users continue to update through
their package manager or manual installation. Update payload signatures use a dedicated Tauri
updater key; OS code signing/notarization remains a separate publisher-identity capability.
Decision: [ADR-0040](../../../decisions/0040-separate-updater-integrity-from-os-publisher-identity.md).
Missing Apple or Windows signing credentials do not block updater artifacts signed by the Tauri
key. Release notes continue to identify those applications as unsigned development builds until
OS publisher-identity signing is configured.
## Notification contract
For desktop-owned launches, Kandev uses native OS notifications for:
- a completed agent turn when that provider event is selected;
- a structured clarification request that needs a user answer when that
provider event is selected;
- a newer Kandev release when `system.update_available` is selected for the
Local provider;
- a session that failed.
Notification delivery respects the existing notification settings and suppresses duplicate
browser and backend shell-command notifications for the same desktop event. Browser- and
service-launched Kandev retain their existing delivery paths. Dock reopen and second-instance
activation show, unminimize, and focus the window but never infer notification routing. The
official Tauri notification plugin does not expose payload-aware body-click callbacks on desktop,
so a raw notification click cannot be correlated to an arbitrary payload on every supported OS.
Generic focus and activation events therefore never navigate. Repeated delivery of the same event
identity is deduplicated.
Turn-finished and clarification-requested delivery follows the selected event
checkboxes. Session-failure delivery is an independent safety alert whenever
the Local provider is enabled; it does not reuse either semantic subscription.
Update-available delivery uses the release version as its occurrence identity
and the same Local-provider preference as browser delivery. The existing
Notifications settings control requests native permission from a user gesture;
background events never prompt. An open app keeps the in-app update indication
when native permission is denied or native delivery fails.
## Desktop bridge and permissions
The Go-served SPA may receive a small versioned set of desktop events and commands for menu
actions, updater state/actions, native notification delivery, explicit directory selection,
focus, and safe external links. The
capability schema allows loopback ports because the port is selected at launch, but every
privileged command verifies the exact health-verified backend origin before executing. The bridge
exposes no arbitrary shell command, directory enumeration, caller-selected filesystem access, or
unrestricted URL opening. Its directory command only returns a folder selected through the native
system panel.
External `http`, `https`, and `mailto` links open in the system browser/client. Internal loopback
navigation, downloads, and blob URLs remain in the WebView unless an existing workflow specifies
otherwise.
The launch boundary remains:
```text
kandev --headless --port <available-loopback-port>
GET http://127.0.0.1:<port>/health
X-Kandev-Desktop-Health-Token: <per-launch-token>
```
Desktop launches set `KANDEV_SERVER_HOST=127.0.0.1`. The health response must echo the per-launch
token before the shell trusts the responding process; the WebView then navigates to the loopback
SPA only once the subsequent unauthenticated `GET /ready` reports the backend has finished
startup. Existing backend HTTP, WebSocket, and boot payload contracts remain the product API;
native integration does not replace them.
## State machines
The implemented launch lifecycle remains `idle` -> `backend-starting` -> `ready`, with
`backend-restarting`, `stopping`, and `failed` transitions as defined by the original desktop
release. Native integration adds:
### Update state
- `idle` -> `checking` for startup, periodic, or manual checks.
- `checking` -> `available`, `current`, or `error`.
- `available` -> `downloading` only after user confirmation.
- `downloading` -> `installing` -> `restarting`.
- Any failed update phase -> `error`, preserving the running version.
Only one check or install operation may be in flight. Quit and update-restart share the same
backend shutdown primitive, but updater restart must preserve Tauri's updater exit semantics.
### Window state
Size, position, and maximized state are persisted after stable changes. Minimized state is not
restored. If a saved position is outside all current displays, the window is clamped to a visible
display before being shown.
## Failure modes
- A menu command received before SPA readiness is ignored or replayed once when explicitly safe;
it never becomes a window-close fallback.
- A failed zoom request leaves the previous zoom value intact and does not reload the SPA.
- Invalid, missing, mismatched-platform, or incorrectly signed updater metadata/payloads are
rejected without stopping the running application.
- A missing packaged launcher/helper, backend bind failure, failed authenticated health check, or
startup timeout produces a visible startup error and terminates the owned process tree.
- On platforms with a reliable process-liveness probe, if the shell process ends without running
its own shutdown path, because it crashed, was force quit, or was killed by the operating system,
the owned launcher and backend must still exit on their own. Shell-initiated cleanup covers quit,
startup failure, and update restart, but it cannot cover its own abnormal termination, so the
launcher is additionally bound to the lifetime of the shell that spawned it. The shell publishes
its process identity to the launcher at spawn time, and the launcher stops its supervised tree
once that process is positively known to be gone. This is a hard requirement rather than
best-effort cleanup on those platforms: a launcher that outlives its shell keeps the Kandev
home's exclusive runtime-state lock and blocks every later desktop and CLI start until the
operator finds and kills it by hand. Platforms without a reliable probe retain their existing
normal-shutdown guarantee and do not infer parent death from an unknown liveness result.
- A GUI environment that cannot find an agent CLI surfaces the existing setup-health guidance.
- If backend shutdown fails during update restart, installation does not force an unclean restart;
the user sees an actionable error and can quit normally.
- If notification permission is denied or the platform notifier is unavailable, Kandev retains
its in-app indication and does not repeatedly prompt.
- A native notification never infers a task route from an ordinary focus, Dock reopen, or
second-instance activation event.
- External links with unsupported schemes remain blocked.
- Native folder-picker cancellation changes no repository grant and starts no scan.
- A folder-picker request from an origin other than the owned backend is rejected.
## Persistence guarantees
- Kandev data continues to use the existing backend database and `KANDEV_HOME_DIR` behavior.
- Window state and non-sensitive updater bookkeeping may use the Tauri application data location.
- No updater signing secret is shipped; only the public verification key is embedded.
- Existing OS-unsigned development installers may still be produced and advertised through the
in-app updater when their updater payloads carry a valid Tauri signature. A missing Tauri updater
signing key cannot produce an installable updater manifest entry.
- Closing a document through `Cmd/Ctrl+W` preserves the same dirty-state guarantees as clicking
its close control.
- Existing OS-signed/notarized installers, checksums, and unsigned-development-build warnings
remain part of the desktop release contract in addition to updater artifacts.
## Scenarios
- **GIVEN** the desktop window is focused, **WHEN** the user presses `Cmd+-`, `Cmd++`, or `Cmd+0`,
**THEN** the current WebView zoom decreases, increases, or resets without resizing the window.
- **GIVEN** the machine has no Node.js runtime, **WHEN** the packaged desktop app starts, **THEN**
its bundled native launcher/backend and embedded SPA become ready without a Node web server.
- **GIVEN** the desktop shell starts its bundled native launcher with a generated health token,
**WHEN** the launcher starts and polls the backend, **THEN** the launcher preserves that exact
token through the backend response and the WebView navigates without reaching the startup
timeout.
- **GIVEN** New Task is the topmost dismissible dialog, **WHEN** the user presses `Cmd+W`, **THEN**
that dialog closes and the app and backend remain running.
- **GIVEN** a closable file tab is focused and no dismissible overlay is open, **WHEN** the user
presses `Cmd+W`, **THEN** the file follows its normal close/dirty guard.
- **GIVEN** only a task, chat, session, terminal, or structural panel is focused, **WHEN** the user
presses `Cmd+W`, **THEN** nothing closes.
- **GIVEN** the macOS app is running, **WHEN** the user presses `Cmd+,`, **THEN** the existing
Settings page opens in the main window.
- **GIVEN** the user clicks the macOS red close control, **WHEN** shutdown completes, **THEN** both
the app and its owned backend exit.
- **GIVEN** a desktop platform with a reliable process-liveness probe and a shell that ends without
running its shutdown path, **WHEN** the launcher observes that the shell process it was spawned
by is positively gone, **THEN** the launcher shuts down its backend, releases the Kandev home
runtime-state lock, and exits, so the next desktop or CLI start acquires ownership without the
operator killing a leftover process.
- **GIVEN** a signed newer desktop version exists, **WHEN** an automatic check finds it, **THEN**
Kandev prompts in the existing Updates page before any download, install, or restart.
- **GIVEN** a Linux AppImage installation, **WHEN** the user confirms a valid update, **THEN** the
signed AppImage updater artifact installs and Kandev relaunches after clean backend shutdown.
- **GIVEN** a selected turn-finished or clarification-requested event occurs
while Kandev is not focused, **WHEN** its native notification is delivered,
**THEN** Kandev sends no duplicate browser or shell notification and generic
app focus or activation does not navigate to an inferred task.
- **GIVEN** `system.update_available` is selected for Local and native
permission is denied, **WHEN** a newer release is detected, **THEN** the open
app retains its in-app update indication, does not prompt in the background,
and does not mark a task route from generic activation.
- **GIVEN** a saved window position belongs to a disconnected display, **WHEN** Kandev reopens,
**THEN** the main window appears within a currently visible display.
- **GIVEN** the same settings page is opened in a browser or narrow/mobile viewport, **WHEN** it
renders, **THEN** the existing service-update experience and responsive layout remain intact;
desktop-only native actions are not shown.
## Out of scope
- Tray/background operation after the last window closes.
- Launch at login or OS autostart.
- Global shortcuts that operate while another application is focused.
- Silent, forced, or downgrade updates; App Store/Microsoft Store/Snap distribution.
- Auto-updating Linux `.deb` or `.rpm` packages outside their package managers.
- Notification events beyond the canonical provider catalog in this increment.
- A separate native settings window, multiple native windows, or a rewritten native UI.
- Mobile Tauri targets. Native shortcuts have no mobile analogue; the shared settings page must
remain responsive and preserve browser/mobile behavior.
- Replacing npm, Homebrew, Docker, runtime tarball, or existing package-manager update channels.
- Rewriting the shared frontend or backend in Rust.
- General native filesystem plugin access from the SPA.
## Implementation plans and decisions
- [Initial Desktop Tauri App plan](../../../plans/desktop-tauri-app/plan.md)
- [Desktop Native Integration plan](../../../plans/desktop-native-integration/plan.md)
- [ADR-0026: Tauri desktop shell over native runtime](../../../decisions/0026-tauri-desktop-shell.md)
- [ADR-0039: Native desktop integration boundary](../../../decisions/0039-native-desktop-integration-boundary.md)
- [Desktop Repository Discovery Consent plan](../../../plans/desktop-repository-discovery-consent/plan.md)