# CAI Desktop Sidecar — Contributor & Distribution Notes

The sidecar is a Tauri v2 app (Rust shell + system webview) in the `gui/`
workspace, built and shipped **separately** from the `condense-ai` npm package.
This keeps the CLI dependency-light: a developer who only wants the CLI never
pulls Rust, Tauri, or a frontend bundle.

It is a tabbed **hub** over the CAI engines, not a single drift screen. The
tabs are **Drift · Suggestions · Watchdog · Health · Timeline**; Drift is the
default on launch. The project/scaffold root is resolved once (via `check`) and
shared by every tab, and each tab loads its own CLI surface lazily — a failure
in one tab renders that tab's error/empty state without breaking the others.

## Layout

```
gui/
  package.json            # frontend deps + tauri scripts (never published to npm)
  src/                    # web frontend (static; no bundler)
    index.html
    styles.css            # design system, extracted from docs/cai-sidecar.html
    contract.js           # frontend port of src/gui/contract.ts (parity-tested)
    app.js                # render layer + sync wiring (approve-before-apply)
    mascots.js icons.js
  src-tauri/              # Rust shell
    src/lib.rs            # Tauri commands (one per engine, see boundary table)
    src/backend.rs        # CLI-as-backend boundary + error states
    tauri.conf.json
```

The TS contract (`src/gui/contract.ts`) is the unit-tested source of truth for
mood derivation, file grouping, and version compatibility. `gui/src/contract.js`
is a faithful runtime port; `test/gui-contract-parity.test.ts` fails if the two
diverge on the fixtures in `test/fixtures/drift-reports.ts`.

## CLI ↔ GUI boundary

The GUI never re-implements engine logic. The Rust shell shells out to the `cai`
CLI as child processes in the resolved project directory; the frontend adapts
each raw output through a versioned, parity-tested contract.

| Tauri command | Tab(s) | CLI invocation | Contract adapter |
|---|---|---|---|
| `check` | Drift (+ resolves shared root) | `cai check --json` | `toDriftViewContract` |
| `suggest` | Drift, Suggestions | `cai suggest-structure --json` | `toSuggestionsViewContract` |
| `sync` | Drift (Apply) | `cai sync --export --file <path>` | — (returns stdout) |
| `watchdog` | Watchdog | `cai watchdog --json [--session <id>]` | `toWatchdogViewContract` |
| `health` | Health | `cai health --json` (fallback `cai heartbeat --json`) | `toHealthViewContract` |
| `timeline` | Timeline | `cai timeline --json` | `toTimelineViewContract` |
| `log` | Timeline (add event) | `cai log <kind> <message>` | — (write path, returns stdout) |
| `suggest_apply` | Suggestions (Apply) | `cai suggest-structure --brief <n>` + `sync --export` | — (returns the brief) |

Notes on the new surfaces:

- **Watchdog**: the GUI passes the active session id when known; with no bridge
  file or transcript the CLI emits `source: "none"`, which the tab renders as a
  first-class "no live session signal" state, not an error. The condense brief
  (keep / drop / decisions / next / blockers / reload) is shown only at warn or
  critical level.
- **Health**: `cai health --json` carries both the freshness pulse
  (`files[].status`) and the drift-score trend (`driftTrend`). The heartbeat
  fallback (`{ ok, staleFiles }`) is used only when health is unavailable;
  score/trend are then absent.
- **Suggestion apply** is approval-gated and never edits files silently: the
  shipped `suggest-structure` produces a delegate-able brief rather than a
  destructive apply, so the GUI captures that brief and queues it to
  `.cai/sync-queue/` via the same `sync --export` path drift uses. The user
  applies the queued brief with their AI tool.

Surfacing suggestions alongside drift (and in the "all clean" state) keeps a
zero-drift scaffold a launchpad for the next action rather than a dead end.

Every failure is an explicit, frontend-renderable error state — CLI absent,
wrong version, non-zero exit, unparseable output, or no scaffold (`ROUTER.md`) —
isolated per tab so one engine failing never breaks the others.

## Build

Requires the Rust toolchain and Tauri CLI (`cargo install tauri-cli` or the
workspace `@tauri-apps/cli`).

```bash
cd gui
npm install
npm run build      # cargo tauri build → gui/src-tauri/target/release/cai-gui
```

`cai gui` resolves the binary from the build output, from `CAI_GUI_DIR`, or
from an explicit `CAI_GUI_BIN`.

## Distribution channel (resolved)

**GitHub Releases.** The GUI binaries (macOS `.app`/`.dmg`, Windows `.exe`/
installer) are attached to tagged GitHub releases, decoupled from the npm
publish cadence so the CLI and GUI version independently (the contract is
versioned for exactly this).

- *Rejected — npm optional package:* would risk Tauri/Rust leaking into the
  install graph and complicates the dependency-light guarantee.
- *Rejected — Homebrew cask (for now):* viable later, but adds a tap to
  maintain; GitHub Releases is the lowest-friction first channel.

## Code signing / notarization (first milestone)

The first milestone ships **unsigned dev builds** for internal validation:

- **macOS:** unsigned `.app`; users right-click → Open the first time
  (Gatekeeper). Full signing + notarization (Developer ID cert,
  `xcrun notarytool`) is tracked as later work and wired via Tauri's
  `bundle.macOS.signingIdentity` + `notarize` config when certs are available.
- **Windows:** unsigned `.exe`; SmartScreen warns on first run. Authenticode
  signing (`signtool` / Tauri `bundle.windows.certificateThumbprint`) is later
  work pending a code-signing certificate.

Signing is intentionally deferred — it gates on certificates and CI secrets, not
on application code.

## Not built here

Booting and visually confirming the window on macOS and Windows, and the
cross-OS release pipeline, require a desktop session (and a Windows host). The
Rust shell compiles and its backend is unit-tested; the macOS release binary
builds cleanly via `cargo tauri build`.

## Release artifact contract

When publishing a new sidecar release to `t3mr0i/cai-sidecar-dist`, the asset
naming must match `cai-sidecar-${label}.${ext}` (always with a suffix —
`download.js` requires `startsWith("cai-sidecar-${label}.")`).

| Platform | `label`         | Ext    | Example                            |
|----------|-----------------|--------|------------------------------------|
| macOS    | `macos-arm64`   | `.bin` | `cai-sidecar-macos-arm64.bin`      |
| macOS    | `macos-x64`     | `.bin` | `cai-sidecar-macos-x64.bin`        |
| Windows  | `windows-x64`   | `.exe` | `cai-sidecar-windows-x64.exe`      |

Each release must also include a `checksums.txt` in `sha256sum`-format:

```
<sha256-hex> *<asset-filename>
```

Build flow:

1. `cd gui && npm install && cargo tauri build --target aarch64-apple-darwin` (or just `cargo build --release` for the bare executable).
2. Copy/rename the produced binary from `gui/src-tauri/target/release/<bare>` to the asset name above.
3. Generate `checksums.txt` (`shasum -a 256 * > checksums.txt`).
4. `gh release create v<sidecar-version>` on `t3mr0i/cai-sidecar-dist` with both files.

The CLI's `cai-sidecar-${label}.bin` lookup uses `startsWith(<prefix> + ".")`, so a bare binary without a suffix will not match — this is the bug behind the v4.1.0 404.

## First-run user journey

Reproducible walk-through after a clean install:

1. **Fresh project, no scaffold.** User runs `cai gui` from a directory without `.cai/`. The sidecar opens. Every tab shows the onboarding state: a critical-mood card with title "No scaffold yet", body explaining what CAI does, and a primary button "Initialize scaffold (minimal)" plus a secondary "What does this do?" that opens an explainer modal.
2. **Click Initialize.** The card transitions to a concerned-mood spinner ("Initializing scaffold… Running cai setup --minimal…"). On success the user is routed to the Drift tab and the dashboard populates with the first drift score.
3. **First drift card.** The user clicks Apply on a card. The in-window confirm modal opens (Escape=cancel, Enter=confirm). On confirm, the in-card spinner shows "Applying…"; success flips to "Applied. Refreshing…"; failure to a coral-coloured error with the buttons re-enabled.
4. **Tab switch.** Click Suggestions, Watchdog, Health, Timeline in turn. Each loads from the CLI in isolation — a failure on one tab leaves the others intact.
5. **Logout / re-launch.** Snoozed cards and tab caches are session-scoped; a re-launch starts fresh.

If any of these steps leaves the user without an actionable button (a dead-end text-only error), the GUI has regressed — that's the contract this walk-through enforces.
