Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions .github/workflows/frontend-release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
name: Desktop release

# Builds and publishes the Electron desktop app via electron-forge.
# Generates a GitHub Release (draft) with installers + update manifests.
# Triggered by a `desktop-v*` tag or manually.
#
# ⚠️ Until macOS code signing + notarization secrets are configured (see
# frontend/docs/desktop-release.md), published builds are UNSIGNED and will
# NOT auto-update on macOS. The workflow still produces installable artifacts.

on:
push:
tags:
- "desktop-v*"
workflow_dispatch:

jobs:
release:
runs-on: macos-latest
permissions:
contents: write
defaults:
run:
working-directory: frontend
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: npm
cache-dependency-path: frontend/package-lock.json
- run: npm ci
- name: Publish
run: npm run publish
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# macOS signing + notarization — add as repository secrets and
# set osxSign/osxNotarize in forge.config.ts to enable.
CSC_LINK: ${{ secrets.CSC_LINK }}
CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }}
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -49,3 +49,7 @@ Thumbs.db

# Personal local overrides (not for the team)
.envrc.local

# electron-forge / vite build output
.vite/
dist-electron/
8 changes: 8 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,11 @@
# CLAUDE.md

Read and follow [`AGENTS.md`](AGENTS.md) for repository layout, commands, coding conventions, and hard rules.

## Design System

Always read [`DESIGN.md`](DESIGN.md) before making any visual or UI decision.
All fonts, colors, spacing, layout, the orchestrator-led flow, the topbar, and the
spawn modal are defined there. The UI matches **emdash** with one main orchestrator and
a refined-blue accent. Do not deviate without explicit user approval. In QA/review,
flag any renderer code that doesn't match DESIGN.md.
226 changes: 226 additions & 0 deletions DESIGN.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,226 @@
# Design System — ReverbCode

> Source of truth for the ReverbCode desktop UI (Electron + React 19 + Tailwind v4
>
> - Radix/shadcn + xterm, in `frontend/src/renderer`). Read this before any visual
> or UI change. Created by `/design-consultation` on 2026-06-09.

## Product Context

- **What this is:** ReverbCode is an Electron desktop app for supervising many parallel
AI coding-agent sessions, backed by a Go daemon (`backend/`). The `ao` CLI is the
thin client over the same daemon.
- **Who it's for:** professional software engineers running multiple coding agents at
once who need to delegate, watch, intervene, and ship PRs.
- **Space/peers:** agent orchestration / parallel-agent desktop tools. Closest peers:
**emdash** (the primary design reference), **PostHog Code**, Conductor.
- **Project type:** dark-mode-primary desktop app; terminal-dense; keyboard-driven;
runs all day.
- **The one memorable thing:** leverage and speed — "I'm more in control here than
babysitting N terminal tabs myself."

### Product flow (what the UI must serve)

ReverbCode is **orchestrator-led**, which is the one thing that differs from emdash
(a flat list of independent sessions). Grounded in the daemon
(`backend/internal/session_manager/manager.go`, `docs/architecture.md`):

- A **Project** is a registered git repo.
- Per project there is **one active Orchestrator** session plus **N Worker** sessions.
Both are the same underlying "session" (durable facts: `activity_state`,
`is_terminated`, PR facts); they differ only by `Kind` (`KindOrchestrator` vs the
default worker). A project may run the orchestrator on a different agent than its workers.
- The **Orchestrator is the human-facing coordinator**: you talk to it; it spawns
workers (`ao spawn`), messages them (`ao send`), tracks progress, and synthesizes
results. It avoids implementing unless necessary.
- A **Worker is a normal agent session** — nothing special-cased. It runs one focused
task in an isolated git worktree + branch, with the agent CLI in a terminal as the
conversation, producing a diff → commit/push → PR. It escalates to the orchestrator
only for true blockers or cross-session coordination.
- The daemon **observes** runtime + PR/CI/review facts and **derives** display status
at read time: `working`, `needs_input`, `ci_failed`, `changes_requested`,
`mergeable`, `approved`, `review_pending`, `pr_open`, `idle`, `terminated`, `merged`.
Never store display status; keep session facts small.

## Aesthetic Direction

- **Direction:** match **emdash** exactly — flat, near-black, hairline-bordered,
utilitarian. Industrial control surface, calm chrome, the terminal as the center of gravity.
- **Decoration level:** minimal. Type + 1px hairlines do all the work. No gradients,
glow, blobs, or emoji.
- **Mood:** low-glare, dense, keyboard-native; signal-over-noise.
- **Reference:** [emdash](https://github.com/generalaction/emdash) (primary, visual +
structural), [PostHog Code](https://github.com/PostHog/code) (secondary). Tokens
below were extracted from emdash's `src/renderer/index.css`.
- **Deliberate tradeoff:** to _be_ emdash, we use the **system font stack** (not a
custom typeface) and emdash's neutral palette. We diverge in exactly one place: the
accent is ReverbCode's **refined blue**, not emdash's jade green. The terminal keeps
green (it is the agent CLI).

## Typography

System fonts only, like emdash — no custom/Google fonts, zero font payload.

- **UI / body / display:** `-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
Oxygen, Ubuntu, Cantarell, "Fira Sans", "Helvetica Neue", sans-serif` (San Francisco
on macOS).
- **Mono / terminal / code / eyebrow labels:** `Menlo, Monaco, Consolas,
"Liberation Mono", "Courier New", monospace`.
- **Eyebrow labels** (section titles, dialog titles, the rail "PROJECTS" header):
mono, **uppercase**, `letter-spacing: .12–.14em`, `--foreground-passive`.
- **Scale:** 14px base UI / sidebar (`text-sm`, weight 400) · 12px secondary + labels
(`text-xs`) · 13px code/mono/terminal · 11px tiny · 10px micro + badges · 9px sidebar
badge label. Buttons are `font-normal` (400), not bold.

## Color

emdash's flat Radix-neutral near-black ramp carries the whole interface; color is rare
and meaningful. Values are sRGB approximations of emdash's `color(display-p3 …)` tokens.

### Dark (primary)

| Role | Hex |
| ------------------------------------ | --------------- |
| `--bg` canvas | `#111111` |
| `--bg-1` surface | `#191919` |
| `--bg-2` raised / hover / active row | `#222222` |
| `--bg-3` | `#2a2a2a` |
| `--fg` text | `#eeeeee` |
| `--fg-muted` | `#b4b4b4` |
| `--fg-passive` | `#6e6e6e` |
| `--border` hairline | `#3a3a3a` |
| `--border-1` | `#484848` |
| **`--accent` (blue)** | **`#5b9dff`** |
| `--needs-you` / in-progress (amber) | `#ffcc4a` |
| `--success` / mergeable (green) | `#6cb16c` |
| terminal green | `#7bd88f` |
| `--error` (red) | `#d4544f` |
| text selection | `#3f8ef7` @ 35% |
| terminal bg | `#161616` |

### Light (supported, not primary)

| Role | Hex |
| ------------------------- | --------------------------------- |
| canvas / surface / raised | `#fcfcfc` / `#ffffff` / `#ededee` |
| text / muted / passive | `#1a1a1a` / `#666666` / `#9a9a9a` |
| border | `#e3e3e5` |
| accent (blue) | `#2563eb` |
| amber / green / red | `#9a6b00` / `#1a7f37` / `#c0392b` |

### Accent rules

- **Blue** = the live edge only: primary buttons, the active/selected session, focus
rings. Never decorative.
- **Amber** = an agent needs you (blocked / `needs_input` / `review_pending`).
- **Green** = `mergeable`/success and terminal/agent CLI text.
- **Red** = `ci_failed` / destructive.
- These map 1:1 to the daemon's derived statuses.

### Status indicator (no text badges)

Session status is a single ~14px glyph in one fixed slot, never a text pill/badge:

- **Working / active** → an animated spinner (accent).
- **Has an open PR** → a PR icon, tinted by PR state: mergeable/approved green,
`ci_failed` red, review/`changes_requested` amber, plain `pr_open` muted.
- **Otherwise** → a filled dot: `needs_input` amber (pulsing), idle/done muted gray.

Precedence: **working spinner > PR icon > dot**. Implemented as `StatusGlyph` in
`components/SideRail.tsx`; used in the orchestrator's Workers list. (Worker rows in the
left rail stay name-only — no glyph.)

## Spacing

- **Base unit:** 4px (Tailwind scale: 1=4, 1.5=6, 2=8, 3=12, 4=16, 5=20, 6=24).
- **Density:** compact / desktop-tight.
- **Control + row height:** `h-8` = 32px default; `h-7` = 28px small; `h-6` = 24px xs.
- Inputs `px-2.5 py-1`; buttons `px-2.5`, gap 1–1.5.

## Layout

- **Approach:** fixed three-pane app shell, opens into the workbench (no marketing/dashboard home).
- **Panes:** `[ rail 240px ] [ center 1fr ] [ side rail 316px ]`.
- **Rail (240px), top → bottom:**
1. **Orchestrator anchor** — pinned, single, visually distinct (blue 2px left bar,
`--bg-2` fill, hub/`waypoints` icon, name "Orchestrator", a `5 agents · 2 need you`
mono summary). This is ReverbCode's one addition over emdash. Default landing view.
2. `PROJECTS` eyebrow label + a `+`.
3. Project rows (folder icon + name) with nested **worker rows beneath**. Each project
row has a hover-revealed **`+`** that opens the New-worker modal pre-scoped to that
project (distinct from the `PROJECTS` header `+`, which registers a repo).
4. **Footer:** `Search ⌘K`, `Settings ⌘,`. (No Library.)
5. **Account** row pinned at the very bottom.
- **Worker rows are name-only.** Just the session name, truncated. Status, branch, diff,
and PR live in the panes and topbar, never in the row. Selection = `--bg-2` fill + a
2px blue left bar. (emdash itself shows a faint trailing timestamp; we omit it by choice.)
- **Center = the conversation.** Orchestrator → its coordination terminal (delegate here;
composer reads "tell the orchestrator what to build"). Worker → the agent CLI terminal
(tabbed per agent, e.g. `claude-code (1)`), with a composer (model selector, worktree
path, `Accept edits`). The terminal **is** the conversation; no separate chat surface.
- **Side rail (316px):** orchestrator → a quiet **Workers** list (name + project + derived
status). Worker → the **Git review rail**: `Changed N` → All files / Discard all / Stage
all → file rows (`+adds −dels`, stage toggle) → `Commit message` + `Description` →
**Commit & Push** (primary blue) → branch + `Create PR`.
- **Border radius:** `sm` 4px (scrollbar) · `md` 6px (buttons, inputs, toggles) ·
`lg` 8px (rows, cards, panels) · `xl` 12px (modals) · `full` (badges/pills/dots).
- **Icons:** **lucide** only. No emoji.

### Topbar

- **Left (both):** `project / session` breadcrumb + pin; for the orchestrator, a hub icon
- `Orchestrator`.
- **Right — worker session:** a **PR/CI status pill** that is the action
(`PR #156 · mergeable` green / `CI failed` red / `review requested` amber /
`Open PR` when none) → **Changes / Files / Terminal** view toggles → **⋯ session menu**
(rename, restart, kill, claim PR — the `ao session …` commands).
- **Right — orchestrator:** **+ New worker** → Terminal toggle → **⋯ menu**. No diff toggles.

### Spawn-worker modal (mirrors emdash's Create Task)

You mostly let the orchestrator spawn workers from its conversation; the manual paths
(the topbar `+ New worker`, a project row's hover `+`, or `ao spawn`) open a modal that
mirrors emdash exactly. Launching from a project row pre-fills the Project field:

- Centered dialog, **12px radius**, `max-w` ~512px, `bg` canvas, `ring-1` at 10% fg,
fade + zoom-95 enter.
- **Header:** eyebrow mono-uppercase title `New worker` + `×` close.
- **Body** (`gap` 15–16px): a **borderless large name field** (18px, auto-focus, slug
rule "letters, numbers, hyphens") → **Project** selector → **Agent** selector
(claude-code / codex / opencode / …) → a **"Based on"** bordered card with a segmented
control `Branch · Issue · Pull Request` revealing a combobox → a **Prompt / Workspace**
tab where Prompt is the worker's initial task (textarea).
- **Footer:** right-aligned single primary **`Spawn worker`** (blue) with a `⌘↵` keycap,
disabled until valid.

## Motion

- **Approach:** minimal-functional. The one expressive exception: a status dot/spinner
pulse on active/working sessions (opacity breathe) so "alive" is glanceable. Never
animate text or layout.
- **Easing:** enter `ease-out`, exit `ease-in`, move `ease-in-out`.
- **Duration:** micro 80ms · short 160ms · medium 240ms · status pulse 1.8s loop ·
modal enter ~150ms fade+zoom-95.

## Implementation notes

- The renderer (`frontend/src/renderer/styles.css`) currently uses **Inter** and a
grayscale-blue theme. Migrate to this system: drop the Inter `font-family`, adopt the
system stack, and replace the token values with the emdash neutral ramp + blue accent above.
- Keep tokens as CSS custom properties under `:root` (dark) and `:root[data-theme="light"]`.
- A faithful HTML reference of all of the above (both views + topbar + spawn modal,
light/dark) is saved under
`~/.gstack/projects/aoagents-agent-orchestrator/designs/design-system-20260609/`.

## Decisions Log

| Date | Decision | Rationale |
| ---------- | ---------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------- |
| 2026-06-09 | Match emdash's visual language exactly | User direction; emdash is the demonstrated reference for this app's UI. |
| 2026-06-09 | System font, not a custom typeface (e.g. Geist) | emdash uses the system stack; fidelity + native feel + zero font payload chosen over brand type. |
| 2026-06-09 | Refined **blue** accent, not emdash's jade green | User's explicit pick; blue for primary/active/focus, terminal stays green. |
| 2026-06-09 | Single global **Orchestrator** anchor, orchestrator-first default view | The one real difference from emdash; orchestrator is the human-facing coordinator you delegate to. |
| 2026-06-09 | **Name-only** worker rows | User direction; status/branch/diff live in panes + topbar, not the row. |
| 2026-06-09 | Removed **Library** from the rail footer | User direction; footer is Search + Settings only. |
| 2026-06-09 | Topbar right = PR/CI pill + view toggles + ⋯ menu (worker) | Surfaces the actionable PR/CI state from the daemon; emdash/PostHog Code precedent. |
| 2026-06-09 | Spawn modal mirrors emdash's Create Task | Consistency with the reference; mapped to `ao spawn` params. |
37 changes: 37 additions & 0 deletions backend/internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"os"
"path/filepath"
"strconv"
"strings"
"time"
)

Expand All @@ -33,6 +34,18 @@ const (
DefaultAgent = "claude-code"
)

// DefaultAllowedOrigins are the browser origins the daemon's CORS boundary
// trusts, beyond loopback-served content (which the middleware always trusts —
// local pages can reach the no-auth daemon directly anyway). The daemon has no
// auth, so every entry must be an origin web content cannot present:
// app://renderer is the packaged Electron renderer, served from a custom
// scheme only the desktop app registers — no website can bear it. The opaque
// "null" origin (file:// pages, sandboxed iframes on any website) must never
// be added.
var DefaultAllowedOrigins = []string{
"app://renderer",
}

// Config is the fully-resolved daemon configuration. It is immutable once
// built by Load.
type Config struct {
Expand All @@ -54,6 +67,9 @@ type Config struct {
// Manager (see DefaultAgent). Selected by AO_AGENT; startSession fails fast
// if no adapter with this id is registered.
Agent string
// AllowedOrigins are the browser origins granted CORS read access (see
// DefaultAllowedOrigins). Overridden by AO_ALLOWED_ORIGINS.
AllowedOrigins []string
}

// Addr returns the host:port the HTTP server binds. It uses net.JoinHostPort so
Expand All @@ -74,6 +90,7 @@ func (c Config) Addr() string {
// AO_RUN_FILE running.json path (default <state-dir>/running.json)
// AO_DATA_DIR durable state dir (default <state-dir>/data)
// AO_AGENT agent adapter id (default claude-code)
// AO_ALLOWED_ORIGINS CORS origins, comma-separated (default DefaultAllowedOrigins)
//
// The bind host is not configurable: the daemon is loopback-only by design.
func Load() (Config, error) {
Expand All @@ -83,6 +100,7 @@ func Load() (Config, error) {
RequestTimeout: DefaultRequestTimeout,
ShutdownTimeout: DefaultShutdownTimeout,
Agent: DefaultAgent,
AllowedOrigins: DefaultAllowedOrigins,
}

if raw := os.Getenv("AO_PORT"); raw != "" {
Expand Down Expand Up @@ -116,6 +134,25 @@ func Load() (Config, error) {
cfg.Agent = raw
}

if raw, ok := os.LookupEnv("AO_ALLOWED_ORIGINS"); ok && raw != "" {
// Explicit override replaces the defaults entirely so a deployment can
// also narrow the list. The "null" origin is rejected, never silently
// dropped: an operator allowing it would open the no-auth daemon to
// every sandboxed iframe on the web.
origins := make([]string, 0, 4)
for _, origin := range strings.Split(raw, ",") {
origin = strings.TrimSpace(origin)
if origin == "" {
continue
}
if origin == "null" || origin == "*" {
return Config{}, fmt.Errorf("invalid AO_ALLOWED_ORIGINS entry %q: wildcard and null origins are not allowed", origin)
}
origins = append(origins, origin)
}
cfg.AllowedOrigins = origins
}

runFile, err := resolveRunFilePath()
if err != nil {
return Config{}, err
Expand Down
Loading