Canopydocs v0.4.7

Building from source

Canopy is a Tauri 2 app: a Rust backend, a React and zustand frontend built by Vite, and three webview entry points.

Prerequisites#

ToolNotes
Rust (rustup)source "$HOME/.cargo/env" to put cargo on PATH.
Node 22+The repo pins nodejs v22.15.1 in .tool-versions. Vite 8 needs a modern Node.
Xcode Command Line Tools (macOS)For codesign.
Linux system packageslibwebkit2gtk-4.1-dev libgtk-3-dev libayatana-appindicator3-dev librsvg2-dev, plus build-essential curl wget file libxdo-dev libssl-dev.
Postgres and gitOnly to exercise the features that need them.

If your version manager's default Node is older than 22, put the pinned one first on PATH for build commands:

export PATH="$HOME/.asdf/installs/nodejs/v22.15.1/bin:$PATH" && source "$HOME/.cargo/env"

Commands#

npm install            # once

npm run dev            # Vite only, on http://localhost:1420 (browser, mock data)
npm run tauri dev      # the full app: both windows + the tray, hot-reloading
npm run build          # tsc type-check + Vite build of the frontend
npm run tauri build    # release bundle (see Shipping a release)
npm run csp:check      # load the built pages under the shipped CSP in headless Chromium

From src-tauri/:

cargo check            # fast Rust check
cargo test --lib       # unit tests (includes the CSP regression guard)
cargo clippy --all-targets --features devtools -- -D warnings

CI treats warnings as errors, so run clippy before you push.

Project layout#

index.html  popover.html  terminal.html      three webview entry points
src/
  main.tsx        app/App.tsx                the main window
  popover.tsx     popover/Popover.tsx        the menu-bar window
  terminal-window.tsx                        a detached terminal window
  onboarding/Onboarding.tsx                  first run + add repository
  app/canopy/*                               the redesigned shell: TopBar, SidebarNav,
                                             WorktreeView, ServiceRail, WorkSurface,
                                             LogsPane, StatusBar, Palette, modals
  app/nextAction.ts                          the workflow engine
  store.ts                                   zustand store + event subscriptions
  ipc.ts                                     typed IPC surface (invoke + events)
  types.ts  appearance.ts  mock.ts  icons.tsx
  styles/*.css                               tokens + shell + components + modals
src-tauri/src/
  lib.rs        app setup, plugins, window management
  commands.rs   every #[tauri::command]
  state.rs      the tree, port indices, worktree variables
  settings.rs   settings.json / state.json
  setup.rs      provisioning: config parsing, file templating, the setup runner
  services.rs   process table, spawn/stop, log ring buffers
  terminal.rs   PTY sessions for the agent lane
  git.rs        every git operation
  db.rs         Postgres helpers
  toolchain.rs  pinned-Node discovery
  tray.rs       tray icon + the macOS NSPanel popover
  stats.rs proc.rs error.rs csp.rs

Mock mode#

hasBackend() is false in a plain browser, so npm run dev at http://localhost:1420 runs the whole UI against src/mock.ts: three repositories, five worktrees, ticking stats, streaming logs, and start/stop that behaves.

npm run dev
open http://localhost:1420/index.html      # the main window
open http://localhost:1420/popover.html    # the menu-bar window

Mock mode is good for layout, states, dialogs, keyboard behaviour, theming and screenshots. It can't create or remove worktrees, run real setup, do database work, override ports, or open pop-out windows, and it says so with a toast wherever that matters.

The screenshots in this documentation are captured this way. See Building a release.

Architecture#

The Rust backend owns all state: registered repos, discovered worktrees, the process table, log ring buffers, port indices and overrides. Both windows hydrate by calling get_tree (and get_logs), then patch their local zustand store from backend events, and re-hydrate on window focus. That's implemented once, in store.ts's initSync(), and used by both windows; it is the sync mechanism. UI actions are optimistic, and the authoritative event overwrites them. See Commands and events.

Conventions#

Comments explain why. The codebase leans on short rationale comments above non-obvious decisions, including the ones that record a trap somebody already fell into. Match that density.

Backend honesty. A control with no backend renders as coming soon and disabled, never as something that looks functional. Wire one up and you delete its banner in the same change.

One answer per question. Anything offering "the next thing" routes through nextAction(), so the four surfaces can't drift. Add a state there, not in a component.

Fail closed on destructive paths. A failed dirty-probe disables removal instead of assuming clean.

Dialog keys: for a single-field prompt, ⌘⏎ when there's prose or several inputs.

Debugging#

WantDo
Backend logsRUST_LOG=debug npm run tauri dev. The file is in the platform log dir.
Keep the popover open while inspecting itWTM_NO_BLUR_HIDE=1 npm run tauri dev, since it otherwise hides on blur.
Frontend devtoolsRight-click → Inspect in a dev build.
Check a release build actually paintsnpm run build && npm run csp:check, which serves dist/ under the shipped CSP and fails on any violation or blank page.

Known pitfalls#

Tests#

cargo test --lib runs the Rust unit tests, including port-index stability and reclamation, override precedence, and the CSP regression guard that asserts the production policy still carries the directives the app needs.

npm run csp:check runs Playwright and Chromium over all three entry points under the shipped CSP.

There's no frontend unit-test suite. The CSP check is what catches "the release build renders a blank window".

Documentation for Canopy 0.4.7. Controls marked coming soon are present in the interface but have no implementation behind them yet.