Skip to content

Latest commit

 

History

History
369 lines (315 loc) · 20.5 KB

NATIVE_DESKTOP.md

File metadata and controls

369 lines (315 loc) · 20.5 KB

Native desktop

Reference. This covers the desktop build and packaging. For the everyday workflow see the user guide, subscriptions and local models.

The main branch builds one Rust desktop executable with the React interface embedded. Tauri hosts it in the system WebKit webview. The interface calls the Rust engine through IPC; desktop operation needs no HTTP listener, Python interpreter, Node runtime, or browser launcher.

The archived release gates record the acceptance checks of earlier packages; current release steps are in RELEASING.md.

Attaching to a running engine

Open the desktop with the same profile as a running shadowcode serve or TUI. The window attaches over the private Unix socket instead of opening another SQLite manager. Its project and conversation selection remain independent of other clients. Global settings, approvals, task history and project queues are shared. A banner explains that closing this attached window leaves engine-owned tasks and background processes running. Stop them explicitly, or stop the owning server/TUI. A desktop that starts the engine itself still stops managed work when it closes.

Each attached view holds one private connection; ordinary requests use separate connections so a slow command cannot block status reads or cancellation. At most four views may attach. Closing the view, losing its connection, or stopping the owner releases its navigation state. Views do not persist project selection to the owner's remembered-workspace file. A temporary foreground CLI command is not a persistent host and rejects desktop attachment. Version mismatches are rejected before attachment.

Attached windows receive event hints through their private lease connection; there is no window timer repeatedly waking an idle engine. The event reader still fetches committed rows and polls as a fallback. Each hint reaches the window as shadowcode:events {session_id, type}; pending approvals and the job list are read (GET /api/feed) only for hints that can change them, with a 15 s backstop read. Approval, failure, plan-limit and completion hints use the same native notification path as an engine-owning desktop, respecting the profile's notification settings and suppressing notifications while focused on that conversation. Hints never contain model-stream bodies or complete tool output. Their frame limit is 16 KiB, the local broadcast buffer holds 64 hints, and completion summaries contain at most 180 Unicode characters. A lagging receiver requests durable replay; a blocked socket releases its view after five seconds.

If the owner exits or restarts, the attached window waits for a matching engine on the same socket and reopens its view lease. Reattach does not start jobs, retry POST/mutating commands, or replay completed tools. Existing subscribers receive view.reattached on the same wakeup channel and then fetch committed rows from the last cursor. Closing a disconnected window still succeeds if the new engine never returns.

Build and run

On Ubuntu 24.04, install the native build dependencies:

sudo apt-get install build-essential pkg-config libgtk-3-dev \
  libwebkit2gtk-4.1-dev librsvg2-dev libayatana-appindicator3-dev patchelf

Use Rust 1.95 and Node 22.12+ or 24+ for development. From the repository root:

npm --prefix ui ci
npm --prefix ui run build
cargo build -p shadowcode-desktop --locked
./target/debug/shadowcode --version
./target/debug/shadowcode --profile /tmp/shadowcode-dev --workspace /path/to/project

--profile keeps config, secrets, SQLite history, and webview storage separate from the installed application. Without it, the application uses the existing shadow-agent XDG directories and backs up SQLite before migrating. Do not run the legacy Python application against the same profile during migration.

Native startup restricts its config, data and state directories to the current account (mode 700), retaining existing files and parent-directory permissions. Those three application directories must be real directories owned by your account. To relocate storage through a symlink, link the XDG base or explicit profile parent instead. An unsafe native.lock (symlink, extra hard link, foreign owner or special file) causes a startup error without replacing it.

Pick a subscription, a local GGUF model or an OpenRouter model (after adding a key, see OpenRouter) in the composer picker. Local models need the managed llama.cpp runtime (scripts/build-llama.cpp.sh for a development checkout; packages bundle it). See local models.

The native CLI uses this same executable. Commands such as run, sessions, and health run without a display and share an open desktop's engine without changing its selected project. serve explicitly hosts the engine headlessly for detached tasks and background servers.

Project inspection and diagnostics are available as /understand, /understand --save, /doctor, /doctor --test-model and /why [path] in the conversation. Health shows the same native checks with separate warning, failure and not-checked labels and readable repair suggestions. Model connectivity is tested only when explicitly requested. Automatic native diagnostic repairs remain a migration requirement.

Project and task notes persist through native commands and the CLI/MCP server, with bounded continuation context, independent branch copies and note exports. Approval decisions carry the displayed prompt's conversation ID so a navigation change cannot send the decision to another task.

Native SQLite inspection gives models read-only table/query tools, also available through the CLI and MCP server, with parameter binding, live WAL support, cancellation and result limits.

Queued follow-ups let you send the next instruction while the current task streams. The project queue shows waiting messages across conversations, retains their model/mode choices and supports cancellation. Reloads return to the running task before waiting follow-ups.

Background tools let models start and inspect project servers under shell permissions and stop them with explicit approval. The same processes appear in the Background panel; they continue across coding tasks until stopped or the owning application closes. Plan and Review tasks can inspect them without process control.

Project lifecycle commands can be reviewed and enabled in Settings › Advanced › Hooks or through the same native CLI. Their command gates and completion checks run inside the native task lifecycle without Python callbacks.

Native MCP stdio and HTTP servers can be registered and enabled in Settings › Advanced › MCP. The agent discovers their tools lazily and requests approval for each exact call. HTTP bearer secrets resolve only on connection, and remote hosts require network access. Connections close before the task finishes. shadowcode mcp serve also exposes the selected project to another client over stdio, with read-only defaults and explicitly delegated write/approval access. The native executable also serves authenticated loopback HTTP with a fixed project and a gateway owner that survives individual client reconnects. The server guide lists the implemented tools and remaining migration requirements.

To work on the live interface, run ./ui/node_modules/.bin/tauri dev from the repository root. The Tauri configuration starts Vite and builds the native app. Build hooks explicitly use the ui directory, including when the CLI is invoked from the repository root.

Development packages

Packaging also requires a working Docker engine (or Podman with SHADOW_CONTAINER_ENGINE=podman) for the pinned AppImage runtime build. From the repository root, build both package formats with:

node scripts/build-native.mjs
node scripts/check-native-package.mjs \
  target/release/bundle/appimage/ShadowCode_VERSION_amd64.AppImage \
  target/release/bundle/deb/ShadowCode_VERSION_amd64.deb
node scripts/test-native-runtime.mjs
node --test scripts/test-native-source-fetch.mjs
node scripts/test-native-runtime-write-errors.mjs
node scripts/test-native-runtime-sources.mjs

The build script collects dependency notices, restores the original executable before packaging each format because Tauri modifies its bundle-type marker, and repacks the AppImage with notices for its actual bundled system libraries. The source-built runtime uses private extraction directories and forwards ordinary shutdown signals. Its recipe, notices, and source archive are verified with the package. Build tools are cached in target/.tauri/ and target/native-runtime/. Unknown dependencies or a changed AppImage runtime stop packaging until their notices are supplied. The checker verifies FUSE-free startup, the legacy shadow ui launch form, native ELF code, package versions, dependency resolution on the build host, and absence of Python interpreters, libraries, and sidecars. It also verifies the SHA-256 digest of every listed notice in each extracted package. It produces checksums in artifacts/native-package/.

The published packages are built and inspected on Ubuntu 24.04. The AppImage includes the native GTK/WebKit runtime; the Debian package uses the system libraries. Both packages are checked for embedded Python/Node runtimes and include dependency notices and matching SHA-256 checksums. Concurrent extraction-mode launches use independent private temporary directories. One CLI exit or secondary activation cannot remove a running window's files. The runtime regression checks concurrent owners, deferred resources, signals, cleanup, nohup, and long paths. The packaged window test also checks repeated default-profile activation using isolated XDG storage and a private DBus session.

scripts/build-native.mjs constructs a deterministic packaging PATH before spawning cargo, Tauri, or linuxdeploy. It keeps /usr/bin, /bin, /usr/sbin, /sbin, the Node directory that launched the script, the selected CARGO_HOME/bin (default ~/.cargo/bin) for Rustup, rust-dev extract bins when present, and target/{release,debug,.tauri}. It drops /usr/local/bin, /snap/bin, and any directory whose node/npm resolves into Hermes or /root. The calling shell does not need to sanitize PATH; a dirty host PATH that includes /usr/local/bin/node → /root/.hermes/... is ignored. The same helper is used by scripts/native-runtime.mjs and scripts/build-linux.sh. Prove it with node --test scripts/test-native-packaging-env.mjs.

Desktop integration

  • Native folder selection and conversation export use OS dialogs. External HTTP/HTTPS links open through the OS; remote pages cannot replace the app view.
  • Default-profile launches focus the existing instance. Isolated profiles have separate webview storage and still enforce their own profile locks.
  • For the default profile the window reopens maximized or full screen if it was, and on X11 at its last place (moved up or left until it fits that screen's work area); a Wayland compositor chooses the place itself. Its content size (inside the title bar, in logical pixels) is remembered in window-content-size.json (window_size.rs); whatever the window reports beyond the size it asked for at launch (GTK's header bar and shadow on Wayland) is taken off before saving, so reopening never grows the window, and sizes saved before 2.1.0 are ignored. It opens at that size or 1380x920, capped at 90% of a smaller screen with room for the title bar (the X11 window manager's frame, or GTK's header bar inside the size on Wayland), on the screen it reopens on (X11) or fitting every screen (Wayland). It opens maximized only when no such screen holds the 520x640 minimum. Desktop notifications cover approvals waiting (and a warning 2 minutes before a vendor approval is denied), failed tasks, plan limits and finished tasks, each switchable in Settings › Appearance with an optional sound. They appear while the window is not focused or the task's conversation is not the one on screen; on Linux, clicking one focuses the window and opens that conversation.
  • Close requests and termination signals cancel agent and manual terminal work, wait for cleanup, and retain a visible error if shutdown needs attention.
  • Live events are wakeups for ordered SQLite replay. A periodic read recovers missed notifications; streamed messages are replaced by their final text.
  • Goals run persisted milestones through the same task engine, with visible results, command verification, pause/resume, and safe recovery.
  • Model routing selects a registered model for Plan, Build, Review, and Test. Provider-scoped IDs keep identical model names on different servers distinct; conversations retain the actual selection and fallback notices.
  • Commands and project skills run selected workflows through the native task engine and retain command results and source provenance.
  • Background processes run project servers and watchers alongside coding tasks, with live bounded logs, retained history, stop controls, and process-group cleanup during application shutdown.
  • A private Unix socket connects native CLI clients to the desktop's engine. Each client keeps independent navigation state; managed shutdown closes the connection and cancels its in-flight manual commands before engine cleanup.

Checks

cargo fmt --all --check
cargo clippy --workspace --all-targets --locked -- -D warnings
cargo test --workspace --locked
npm --prefix ui test
node scripts/test-native-cli.mjs

The actual window test uses Tauri's WebDriver bridge and a scripted compatible model, with a disposable profile and project. Install webkit2gtk-driver, xvfb, and tauri-driver 2.0.6, then run:

cargo install tauri-driver --version 2.0.6 --locked
node scripts/run-native-x11.mjs node scripts/test-native-desktop.mjs
node scripts/run-native-x11.mjs node scripts/test-native-markdown.mjs

The wrapper declares X11 before creating the private D-Bus session and uses a fresh runtime directory with mode 0700. HOME and the original XDG config/data/state/cache roots remain available for the existing read-only provider probes. Runtime service discovery therefore differs from the physical desktop. No GTK portal backend is mandated; this qualifies the controlled X11 app and Markdown/CSP behavior, not physical Wayland or all portal features.

To run the same window test on a real Wayland connection without touching your desktop, use the private Wayland wrapper. It starts its own Weston compositor (Pixman software rendering) with a fresh runtime directory and private D-Bus session, removes DISPLAY and the host bus from the app's environment, and stops the compositor and its helpers afterwards:

node scripts/run-native-wayland.mjs node scripts/test-native-desktop.mjs

It needs Weston 13 or later (weston on PATH, or SHADOW_WESTON; an unpacked, uninstalled Weston also needs SHADOW_WESTON_LIBRARY_PATH and SHADOW_WESTON_MODULE_MAP). By default Weston runs on its X11 backend inside a private Xvfb, which gives the compositor a keyboard and pointer seat; SHADOW_WAYLAND_BACKEND=headless uses Weston's headless backend instead (no input seat). Results go to artifacts/native-wayland/run-* (override SHADOW_WAYLAND_ARTIFACTS). This qualifies the app over the Wayland protocol (GTK's Wayland backend, window sizing, WebView input through WebDriver, approvals, reload and quit), not a physical desktop: no GPU output, real input devices, IME, compositor shortcuts, cross-app clipboard or mixed DPI.

For an explicit test on the current physical Wayland desktop, run the desktop probe directly under a private bus instead of the X11 wrapper:

SHADOW_NATIVE_DISPLAY=wayland dbus-run-session -- node scripts/test-native-desktop.mjs

This opens a test window on your current display, uses disposable application state, and removes X11 fallback from the launched process. It records the compositor socket, display dimensions and executable identity. Resize failures remain failures even when the probe continues other interactions; an overall nonzero result must not be presented as complete Wayland qualification. Some tiling compositors control window dimensions independently of application requests. The AppImage launcher keeps the bundled X11 default when no backend is chosen and preserves an explicit GDK_BACKEND=wayland selection.

Both probes capture their direct private session identity and clean only newly activated services bound to that exact bus, with PID/start/executable/user revalidation. Cleanup is bounded and fails closed. The outer wrapper then requires command/output closure, disappearance of recorded session processes, and no surviving late activation before removing its runtime directory. It never performs a general process kill. Incomplete output, unknown ownership, a residual process or missing cleanup report fails the gate. A live process of the same user whose environment cannot be read (a non-dumpable helper, as hosted CI runners start) counts as unknown ownership unless its branch of the process tree started before the private daemon; such a process is listed under ignored in the cleanup report instead. Identity checks are not atomic pidfd protection against a hostile PID-reuse race.

Session diagnostics are retained under artifacts/native-x11/run-* (override SHADOW_X11_ARTIFACTS). The generated private-bus fixture exercises lifecycle checks without opening a display:

node --test scripts/test-native-test-session.mjs

Optional environment variables SHADOW_DESKTOP_BINARY, SHADOW_TAURI_DRIVER, and SHADOW_WEBKIT_DRIVER select explicit executable paths. Logs, screenshots, and a machine-readable result are written to artifacts/native/. This test checks the real embedded window, Rust IPC, approval, file and terminal tools, reload, cancellation, compact layout, model routing/fallback notices, goals and their live transcript, pause, manual and model-managed background processes, CLI coexistence and project isolation, project inspection and diagnostic status presentation, light/dark/compact/goals/routing/background/skills/hooks/MCP/inspection/diagnostics accessibility, background start/stop approval and shared panel state, and managed shutdown. It also verifies that the executable embeds the current compiled interface and does not load libpython.

To run this same workflow against the actual AppImage, set SHADOW_DESKTOP_BINARY to its absolute path, SHADOW_DESKTOP_ARGS='["--appimage-extract-and-run","ui"]', and SHADOW_NATIVE_ARTIFACTS to a separate output directory. Its test profile and workspace remain disposable, so it does not migrate the installed app's data.

See verification evidence for engine stress tests and separate real-model coding probes. The scripted window test establishes UI and engine integration; the real-model probes establish provider/tool behavior.

To exercise an installed Ollama model through the actual native window, run:

xvfb-run -a -s '-screen 0 1440x1100x24' dbus-run-session -- \
  node scripts/probe-native-model.mjs gpt-oss:20b
# Repeat with another installed model:
xvfb-run -a -s '-screen 0 1440x1100x24' dbus-run-session -- \
  node scripts/probe-native-model.mjs qwen3:14b

This manual probe defaults to target/release/shadowcode and accepts the same binary, argument, driver, and artifact environment variables as the scripted window test. It uses a temporary Rust project and isolated profile, approves only cargo test --offline --lib in that project, checks the minimal edit and unchanged tests independently, reloads the conversation, submits a read-only follow-up, clicks Stop during actual model streaming, restores the checkpoint, and verifies native-process shutdown. Results and screenshots are saved under artifacts/native-model/<model>/. Ollama must already have the selected model; the probe does not download models or change the installed app's profile.

For the packaged single-instance regression, set SHADOW_NATIVE_DEFAULT_PROFILE=1 when running the window test under dbus-run-session. The harness redirects XDG settings/history to its disposable profile, then activates the existing window three times and reloads it before continuing the full workflow. Run this mode only on a private test DBus session.