| name | cockpit-tray-dev |
| description | Develop, test, and ship the cockpit-tray Tauri macOS menu-bar app (cockpit-tray/**) — the native shell that supervises the cockpit daemon and hosts the in-app cockpit window (sibling to cockpit-design, which owns the web UI). Encodes the two-layer mental model (cockpit-web changes auto-rebuild via the tray's watchers, mt#2297/mt#2299; the tray's own Rust binary does NOT and needs `bun run dev` or a rebuild+reinstall), the `tauri dev` iteration loop, why a merged change is not in the running menu bar, the release-install path, auto-update (tauri-plugin-updater, mt#2201), the testing tiers (mt#2226, including that `cargo check` does not verify GUI behavior), and Tauri gotchas (external-URL webviews lose IPC, dock suppression, standalone package, supervisor model ADR-014/mt#2241). Use when working in cockpit-tray/**, testing a tray change, asking 'why don't I see my change in the menu bar', or building the harness-host ladder (mt#2230). |
| user-invocable | true |
cockpit-tray-dev — develop, test, and ship the Minsky Cockpit tray app
You are working on cockpit-tray/**, the Tauri v2 macOS menu-bar app that supervises the cockpit daemon and hosts the in-app cockpit window. This is the native shell; the cockpit-design skill owns the cockpit web UI (src/cockpit/web/**). The two are different layers — confusing them is the most common mistake here (see "Two layers" below).
cockpit-tray/README.md is the detailed reference (daemon lifecycle, status labels, auto-rebuild, testing tiers). This skill is the agent-loadable mental model + the dev loop; read the README for depth.
When to invoke
- Changing the tray app's Rust (
cockpit-tray/src-tauri/src/main.rs): menu items, the cockpit window, daemon supervision.
- Testing a tray change, or answering "why don't I see my change in the menu bar?"
- Building any rung of the harness-host ladder (mt#2230) — the tray is its native vehicle.
- Distributing or updating the installed app.
Two layers — get this right first
cockpit-tray involves two distinct build artifacts. "Why don't I see my change?" almost always means you've confused them:
- The cockpit WEB app (
src/cockpit/web/**, src/cockpit/server.ts) — served by the daemon. A running tray app auto-rebuilds the web bundle (mt#2297) and auto-restarts the daemon on backend changes (mt#2299) when source changes (e.g. after git pull). So a cockpit-web change needs no manual rebuild — refresh the window, or let the watcher fire; the "Last build" / "Daemon uptime" menu lines confirm currency.
- The tray app's own Rust binary (
cockpit-tray/src-tauri/**) — the .app itself: menu items, the window, supervision logic. This is NOT auto-rebuilt. The installed /Applications/Minsky Cockpit.app is a static compiled bundle; merging a Rust change to main does nothing to it until you rebuild. This is the layer that bites: mt#2219 (the "Open Cockpit" menu item) was a tray-binary change, so it stayed invisible in the running menu bar until a rebuild.
Decision: cockpit-web change → no tray rebuild, just refresh. Tray-binary change → bun run dev (iterate) or build+reinstall (release).
The dev loop (tray-binary changes)
cd cockpit-tray
bun install
bun run dev
tauri dev watches the Rust source and rebuilds + relaunches on change. This is THE iteration loop — not build → copy → relaunch. Quit the installed /Applications app first so you don't end up with two tray icons.
Testing (mt#2226) — and what cargo check does NOT prove
cargo check / cargo test verify the Rust compiles and pure logic is correct. They do not verify GUI behavior (a menu click opening a window). There is no headless path — Tauri's WebDriver e2e is Windows/Linux-only (no macOS WKWebView driver). Three tiers:
cd cockpit-tray/src-tauri && cargo test — pure logic (status_label, decide_action, throttle_ok, …).
- CI (
.github/workflows/cockpit-tray-ci.yml) — cargo test + tauri build + bundle-exists, on every cockpit-tray/** change.
cockpit-tray/scripts/smoke-status.sh — local-only; reads the live menu status line via the macOS Accessibility API.
So GUI-behavior verification = bun run dev + a manual click. Do not report a tray UI change "verified" off cargo check alone — that proves compilation, nothing about the click.
Deep-link / minsky:// scheme verification (the one thing tauri dev can't do)
The minsky:// URL scheme is registered from the app's Info.plist by macOS Launch Services at INSTALL time, not at runtime — so tauri dev (an unregistered binary) cannot exercise deep links. This is a macOS platform constraint, not a Tauri one (Tauri deep-linking docs). It's the one tray change that needs a build+install instead of bun run dev.
Use the lean installer — it builds APP-ONLY (tauri build --bundles app), so it does NOT produce a .dmg and does NOT flash the macOS installer window on every rebuild (a full bun run build does both — mt#2553):
cockpit-tray/scripts/install-local.sh
cockpit-tray/scripts/install-local.sh --binary-only
cockpit-tray/scripts/verify-deeplink-hotstart.sh
cockpit-tray/scripts/verify-deeplink-coldstart.sh
Do NOT use the full bun run build (DMG-producing) path below for deep-link verification.
Environment-mutating verification: the end-state contract (mt#2545 Gap B)
install-local.sh and the release-install path below both mutate the principal's live
environment: they install/replace the running /Applications/Minsky Cockpit.app and cause
macOS Launch Services to (re-)register the minsky:// scheme against whichever bundle you just
installed. This is an observer-gap closure, not a courtesy step: the agent cannot observe the
principal's machine, so it cannot itself decide whether the newly-verified build should stay
installed or the prior one should come back — that decision belongs to the principal, not to a
default.
Originating incident (mt#2528, 2026-06-24): a verification session installed a throwaway
test build, confirmed the deep-link handler worked, then defaulted to restoring the principal's
prior app — which silently un-delivered the just-verified feature AND left Launch Services'
minsky:// registration pointing at a bundle that no longer existed. "Leave no trace" is the
wrong default for a feature-delivery verification: the trace IS the delivery.
Any verification that installs/replaces the running tray app or churns Launch Services
registration (install-local.sh, the release-install path, an ad hoc rebuild+reinstall to test a
fix) follows this sequence — do not skip a step or substitute a default:
- Backup. Before installing the build under test, note (or copy aside) what is currently
installed — at minimum, know that a prior
/Applications/Minsky Cockpit.app exists and could
be restored if asked to.
- Verify. Install the build under test and run the actual check (
bun run dev, the
verify-deeplink-*.sh scripts, a manual click). Confirm the specific behavior you set out to
verify.
- Explicit end-state decision — WITH the principal, not a default. Once verified, STOP and
ask: does the principal want to (a) keep the just-verified build installed (the normal case
— the feature is now delivered), or (b) restore the prior build (only when the test build
is genuinely disposable / not meant to ship yet)? Do not default to restore, and do not
default to keep, without saying so — state the decision and its basis explicitly. This is the
one step a hook cannot supply: detecting "an install happened" is mechanical, but the
keep-vs-restore call needs principal input.
- Clean-environment assertion. Whichever way the decision goes, confirm — don't assume — the
environment is left consistent: the installed
/Applications/Minsky Cockpit.app matches what
was decided, and the minsky:// Launch Services registration resolves to that SAME bundle (not
a deleted test build). State this confirmation explicitly rather than silently trusting the
install script exited 0.
Scope: this applies to install/replace of the running artifact plus its OS-registration
consequences — NOT every daemon restart or port change during ordinary bun run dev iteration,
which is transient and reversible by construction and carries none of this footgun.
Release install (rare — testing the packaged DMG experience, not iterating)
cd cockpit-tray && bun run build
cp -r "src-tauri/target/release/bundle/macos/Minsky Cockpit.app" /Applications/
xattr -dr com.apple.quarantine "/Applications/Minsky Cockpit.app"
open "/Applications/Minsky Cockpit.app"
Use this only to test the packaged experience or to update your own installed app — never as the iteration loop.
Keeping the installed app current = auto-update (current gap)
There is no auto-update yet, so a merged tray-binary change won't reach an installed app until someone rebuilds. The production answer is tauri-plugin-updater (a first-party Tauri plugin — checks a hosted latest.json / GitHub Releases and self-restarts), automatable with GitHub Actions on release. It requires code-signing, which is deferred to mt#2201 — fold the updater into that task rather than treating "rebuild + reinstall by hand" as the steady state.
Gotchas
- External-URL webviews lose Tauri IPC. The cockpit window loads an external URL (
http://localhost:3737), so that page cannot call Tauri commands. Fine for display-only (Rung 0). Per ADR-023 the SPA stays an untrusted external-URL client by design — so driving sessions (Rungs 2+ of mt#2230) does not route through a Tauri native bridge or a bundled frontendDist. The terminal is hosted server-side in the cockpit daemon (a PTY) and streamed to the SPA over WebSocket (the VS Code Server / code-server / ttyd model); native-to-SPA data flows through the daemon (HTTP + SSE) and native-to-SPA actions through a Rust-to-webview eval. mt#2237 owns the final PTY-transport decision against ADR-023.
- Dock-icon suppression. The app stays out of the Dock via
LSUIElement (mt#2202) plus ActivationPolicy::Accessory in main.rs. If you add a window and a Dock icon appears, that's the regression to fix.
- Standalone package.
cockpit-tray/ is NOT part of the root bun workspace — run bun install from inside it, not the repo root.
- Supervisor model (ADR-014 / mt#2241). The tray is the canonical owner of the daemon lifecycle: spawn / adopt / supervise (respawn-with-throttle) / teardown, plus startup bundle-rebuild (mt#2297) and backend auto-restart (mt#2299). This already implemented the "app-owns-the-server" decision; read ADR-014 before touching lifecycle logic. (It supersedes the lifecycle question framed in mt#2231.)
Cross-references
cockpit-tray/README.md — the detailed reference (lifecycle, status labels, auto-rebuild, testing).
docs/architecture/adr-014-cockpit-daemon-lifecycle-ownership.md — the daemon supervisor model.
cockpit-design skill + src/cockpit/CLAUDE.md — the cockpit web UI layer (the other half of the system).
- mt#2230 — harness-host ladder (the tray is its native vehicle); mt#2201 — signing / distribution (fold in auto-update); mt#2219 — the "Open Cockpit" window (the originating "why don't I see it" incident).
- mt#2545 / mt#2528 — the environment-mutating-verification end-state contract above (Backup → Verify → decide → assert);
.minsky/rules/claim-confidence.mdc — the delivery-state vocabulary (merged/deployed/usable) this contract's "restore un-delivers the feature" framing draws on.