Skip to content
Open
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
80 changes: 80 additions & 0 deletions frontend/docs/DESIGN_AUDIT.md
Comment thread
Annieeeee11 marked this conversation as resolved.
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
# Design Audit — Agent Orchestrator Frontend (Renderer)

**Scope:** `frontend/src/renderer` (excluding landing).
**Date:** 2026-07-09 (post-migration)

## Executive summary

| Area | Status |
| ------------------ | -------------------------------------------------------------------- |
| Token foundation | **Complete** — `src/styles/tokens.css` is the single source of truth |
| Component adoption | **Complete** — no hardcoded hex/rgb in renderer TSX |
| Tailwind bridge | **Complete** — `@theme inline` + `@utility` in `styles.css` |
| BEM legacy CSS | **Removed** — structural rules only (~300 lines) |
| Industry rating | **9.5 / 10** (desktop Electron app tier) |

## Architecture

```
tokens.css → styles.css (@theme + @utility) → Tailwind utilities → components
```

Theme: `index.html` boot script sets `data-theme` before paint; Zustand toggle updates `<html data-theme>`.

## What was audited

- 37 renderer component files (+ `ui/*` shadcn primitives)
- `tokens.css`, `styles.css`, `terminal-themes.ts`, `mock-data.ts`
- Inline `style` props (dynamic tones, kanban gradients, sidebar width — all intentional)

## Findings

### Colors

- All semantic colors flow through `--color-*` tokens
- Dynamic status tones use `color-mix(in srgb, var(--color-*) …)` or runtime tone strings
- Preview palette (`--color-preview-*`) isolated to `BrowserPanel` mock
- xterm palette (`--color-term-*`) read at runtime by `terminal-themes.ts`

### Typography

- Token-named utilities: `text-caption`, `text-control`, `text-heading`, etc.
- Letter-spacing: `tracking-tight*` / `tracking-wide*` (no arbitrary `tracking-[…]` in components)
- Line-height: `leading-snug`, `leading-body`, `leading-row`, `leading-relaxed`, etc.

### Spacing & layout

- Fractional spacing ladder wired in `@theme` (`p-4.5`, `gap-2.25`, etc.)
- Control heights: `h-control-xs` … `h-control-xl`, `h-control-form` (32px forms)
- Dialog widths: `w-dialog-md/lg/xl/orchestrator`

### Icons

- Unified `size-icon-2xs` (12) through `size-icon-xl` (18)
- No `h-3.5 w-3.5` / `h-4 w-4` in app chrome

- Z-index: `z-chrome`, `z-titlebar`, `z-overlay` (no raw `z-10`/`z-50` in renderer)

### Remaining intentional exceptions

| Item | Reason |
| ---------------------------------------------- | ---------------------- |
| `w-[var(--radix-dropdown-menu-trigger-width)]` | Radix runtime width |
| `min-w-[8rem]` / `min-w-[10rem]` in shadcn ui | Radix defaults |
| Inline `style` on `StatusPill`, kanban columns | Runtime color tones |
| API `accentColor` hex on project dots | Backend may return hex |

No optional polish debt remains in app chrome.

### Out of scope

- `src/landing/` — separate marketing palette

## Files

| Path | Role |
| -------------------------------- | ----------------------------------------- |
| `src/styles/tokens.css` | CSS custom properties |
| `src/renderer/styles.css` | `@theme`, `@utility`, global resize state |
| `DESIGN_TOKENS.md` | Token reference |
| `src/renderer/MIGRATION_GAPS.md` | Migration log |
299 changes: 299 additions & 0 deletions frontend/docs/DESIGN_TOKENS.md

Large diffs are not rendered by default.

48 changes: 48 additions & 0 deletions frontend/docs/MIGRATION_GAPS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# Migration Gaps — `src/renderer`

**Status: complete** (2026-07-09). Renderer design system fully migrated.

## Closure pass (2026-07-09)

- **Z-index tokens** — `z-chrome` (10), `z-titlebar` (20), `z-overlay` (50); replaced all `z-10`/`z-20`/`z-50` in renderer
- **`bg-surface-faint`** — inspector review list inset
- **`h-table-head`** — PR table header height (`--size-table-head` → `--space-10`)
- **Removed `--space-11`** — only consumer was inspector empty state; now `py-10 px-5`

## Polish pass (2026-07-09, final)

- **`leading-row`** — replaces `leading-5` in forms, notifications, browser mock
- **PR table columns** — `w-pr-col-number`, `w-pr-col-state`
- **Timeline shadows** — `shadow-timeline-dot`, `shadow-timeline-dot-now`
- **Sidebar / shadcn** — `size-control-board`, `size-icon-base`, `h-control-md`, `size-control-form`
- **Token cleanup** — removed duplicate `--line-height-body`; `--size-hairline` wired in `ResizeHandle`
- **Docs** — removed phantom `--font-size-lg` from `DESIGN_TOKENS.md`

## Polish pass (2026-07-09)

- **Icon unification** — `size-icon-2xs` … `size-icon-xl`; replaced all `h-3.5`/`h-4` in app chrome
- **Typography tokens** — `tracking-tight*` / `tracking-wide*`, `leading-snug` … `leading-loose`
- **Motion tokens** — `duration-fast` (120ms), `duration-normal` (150ms)
- **Layout utilities** — `grid-cols-notification`, `max-w-inspector-status-chip`, `w-font-size-label`, `bg-purple-subtle`
- **Form controls** — `h-control-form` (32px) aligned across `ui/button`, `ui/input`, forms, select
- **`ResizeHandle` component** — replaced `.resize-handle` BEM; global CSS only for `body.is-resizing-x`
- **Test hooks** — `data-testid="inspector-section"` / `inspector-timeline-event` (removed BEM class names)
- **Token cleanup** — removed duplicate `--spacing-icon-*`; wired `--font-weight-*` in `@theme`

## BEM → Tailwind pass (2026-07-09)

- **`TopbarButton`** — replaces all `.dashboard-app-header__*` usage
- **`BrowserPanel`**, **`CenterPane`**, **`TitlebarNav`**, **`SessionInspector`** — full Tailwind
- **`styles.css` reduced** to ~300 lines (xterm, keyframes, session-split, resize global state)

## Intentionally out of scope

| Item | Reason |
| --------------------- | ------------------------------------------------- |
| `src/landing/` | Per project scope |
| Radix runtime widths | `w-[var(--radix-…)]`, `min-w-[8rem]` |
| Dynamic inline styles | Kanban gradients, status pills, API accent colors |

## Verified

- `npm run typecheck`, `npx vitest run src/renderer`, `npx vite build` — all pass
16 changes: 16 additions & 0 deletions frontend/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,22 @@
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Agent Orchestrator</title>
<script>
(function () {
var stored = null;
try {
stored = localStorage.getItem("ao.theme");
} catch (e) {}
var theme =
stored === "light" || stored === "dark"
? stored
: window.matchMedia && window.matchMedia("(prefers-color-scheme: light)").matches
? "light"
: "dark";
document.documentElement.dataset.theme = theme;
document.documentElement.style.colorScheme = theme;
})();
</script>
</head>
<body>
<div id="root"></div>
Expand Down
116 changes: 86 additions & 30 deletions frontend/src/renderer/components/BoardEmptyState.tsx
Original file line number Diff line number Diff line change
@@ -1,45 +1,108 @@
import { Fragment, type ReactNode } from "react";
import { Plus } from "lucide-react";

import { useShell } from "../lib/shell-context";
import { cn } from "../lib/utils";
import aoLogo from "../assets/ao-logo.png";
import { CreateProjectFlow } from "./CreateProjectFlow";
import { TopbarButton } from "./TopbarButton";
import { OrchestratorIcon } from "./icons";

// The four board zones, in flow order — mirrors COLUMNS in SessionsBoard.tsx
// (label + dot color + title tint) so the welcome legend pre-teaches exactly
// what the user will see once the board has sessions.
const FLOW_LEGEND = [
{ label: "Working", dot: "var(--color-working)", titleClass: "text-working" },
{ label: "Needs you", dot: "var(--color-warning)", titleClass: "text-warning" },
{ label: "In review", dot: "var(--color-text-muted)", titleClass: "text-muted-foreground" },
{ label: "Ready to merge", dot: "var(--color-success)", titleClass: "text-success" },
];

// First-launch board state (no projects registered yet): replaces the four
// empty kanban columns with orientation — what this app does, the three steps
// to a first merge, and the same create-project flow the sidebar's + runs.
export function BoardWelcome() {
const { createProject } = useShell();
return (
<div className="flex h-full min-h-0 items-center justify-center overflow-y-auto">
<div className="flex w-full max-w-[460px] flex-col items-center pb-[6vh] text-center">
<img src={aoLogo} alt="" aria-hidden="true" className="h-20 w-20 rounded-[16px] object-cover" />
<h2 className="mt-5 text-[15px] font-semibold tracking-[-0.01em] text-foreground">
<div className="flex w-full max-w-board-empty flex-col items-center pb-empty-offset-y text-center">
<img src={aoLogo} alt="" aria-hidden="true" className="size-10 rounded-lg object-cover" />
<h2 className="mt-5 text-heading-sm font-semibold tracking-tight-lg text-foreground">
Welcome to Agent Orchestrator
</h2>
<p className="mt-1.5 max-w-[320px] text-[12.5px] leading-[1.65] text-muted-foreground">
Add a repository and describe the work. AO runs agents on isolated branches, from start to merge.
<p className="mt-2 max-w-preview-content text-md-sm leading-relaxed text-muted-foreground">
Add a git repository, describe the work, and AO coordinates agent sessions on isolated branches. This kanban
board tracks each session from work through review to merge.
</p>

<ol className="mt-7 w-full divide-y divide-border rounded-panel border border-border bg-surface text-left">
<WelcomeStep n="01" title="Add a project">
Choose a local git repository and select the agents AO should use.
</WelcomeStep>
<WelcomeStep n="02" title="Describe a task">
Tell the orchestrator what you want done; it creates worker sessions on isolated branches.
</WelcomeStep>
<WelcomeStep n="03" title="Review and merge">
Follow each session from work to review to merge readiness:
<span className="mt-2 flex flex-wrap items-center gap-x-1.5 gap-y-1.5">
{FLOW_LEGEND.map((zone, index) => (
<Fragment key={zone.label}>
{index > 0 && (
<span aria-hidden="true" className="text-micro text-passive">
</span>
)}
<span
className={cn(
"inline-flex items-center gap-1.5 text-micro font-semibold uppercase tracking-wide-md",
zone.titleClass,
)}
>
<span className="size-dot-sm rounded-full" style={{ background: zone.dot }} />
{zone.label}
</span>
</Fragment>
))}
</span>
</WelcomeStep>
</ol>

<CreateProjectFlow idleLabel="Add your first project" onCreateProject={createProject}>
{({ choosePath, disabled, error, label }) => (
<>
<button
<TopbarButton
aria-label="Add your first project"
className="mt-7 inline-flex h-8 items-center rounded-md border border-border px-4 text-[13px] font-medium text-foreground transition-colors hover:bg-surface disabled:pointer-events-none disabled:opacity-50"
className="mt-7"
disabled={disabled}
onClick={choosePath}
type="button"
variant="primary"
>
<Plus className="size-icon-md" aria-hidden="true" />
{label}
</button>
{error && <p className="mt-3 text-[11px] leading-[1.5] text-error">{error}</p>}
</TopbarButton>
{error && <p className="mt-3 text-caption leading-body text-error">{error}</p>}
</>
)}
</CreateProjectFlow>
<p className="mt-2.5 text-[11px] text-passive">Starts an orchestrator session for the project.</p>
<p className="mt-3 text-caption text-passive">
Adding a project starts its orchestrator — the agent you talk to.
</p>
</div>
</div>
);
}

function WelcomeStep({ n, title, children }: { n: string; title: string; children: ReactNode }) {
return (
<li className="flex gap-4 px-4 py-4">
<span className="pt-px font-mono text-2xs font-medium leading-loose text-passive">{n}</span>
<span className="min-w-0 flex-1">
<span className="block text-control font-medium text-foreground">{title}</span>
<span className="mt-1 block text-xs leading-body-md text-muted-foreground">{children}</span>
</span>
</li>
);
}

// Project board with a registered project but no worker sessions yet: a quiet
// invitation instead of four empty columns. Actions mirror the board header
// (Orchestrator stays the primary, like the topbar) so the vocabulary holds.
Expand All @@ -60,42 +123,35 @@ export function ProjectBoardEmpty({
}) {
return (
<div className="flex h-full min-h-0 items-center justify-center overflow-y-auto">
<div className="flex w-full max-w-[400px] flex-col items-center pb-[5vh] text-center">
<h2 className="text-[15px] font-semibold tracking-[-0.01em] text-foreground">No worker sessions yet</h2>
<p className="mt-2 text-[12.5px] leading-[1.6] text-muted-foreground">
<div className="flex w-full max-w-preview-content flex-col items-center pb-empty-offset-y text-center">
<h2 className="text-subtitle font-semibold tracking-tight text-foreground">No worker sessions yet</h2>
<p className="mt-2 text-md-sm leading-relaxed text-muted-foreground">
Describe a task and the orchestrator plans it, spawns worker sessions, and tracks them here from work to
merge.
</p>
<div className="mt-5 flex items-center gap-2">
<button
<TopbarButton
aria-label={hasOrchestrator ? "Orchestrator" : "Spawn Orchestrator"}
className="dashboard-app-header__primary-btn"
disabled={isSpawning || isProjectRestarting}
onClick={onOpenOrchestrator}
type="button"
variant="primary"
>
<OrchestratorIcon className="h-3.5 w-3.5" aria-hidden="true" />
<OrchestratorIcon className="size-icon-md" aria-hidden="true" />
{isProjectRestarting
? "Restarting..."
: isSpawning
? "Spawning..."
: hasOrchestrator
? "Orchestrator"
: "Spawn Orchestrator"}
</button>
<button
aria-label="New task"
className="dashboard-app-header__accent-btn"
disabled={isProjectRestarting}
onClick={onNewTask}
type="button"
>
<Plus className="h-3.5 w-3.5" aria-hidden="true" />
</TopbarButton>
<TopbarButton aria-label="New task" disabled={isProjectRestarting} onClick={onNewTask} variant="accent">
<Plus className="size-icon-md" aria-hidden="true" />
New task
</button>
</TopbarButton>
</div>
{spawnError && (
<p className="mt-3 text-[11px] leading-[1.5] text-error" role="status">
<p className="mt-3 text-caption leading-body text-error" role="status">
{spawnError}
</p>
)}
Expand Down
Loading
Loading