350 lines
20 KiB
Markdown
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)
|