diff --git a/.plans/README.md b/.plans/README.md index 9283e963..07fb7f10 100644 --- a/.plans/README.md +++ b/.plans/README.md @@ -32,6 +32,7 @@ - `web-i18n-rollout.md` - Multilingual i18n rollout *(in progress)* - `templates-community-system.md` - Community templates sharing, discovery, and customization - `workspace-file-tree.md` - Workspace file tree sidebar +- `sidebar-branch-enhancements.md` - Sidebar thread enrichment & branch picker enhancements *(planned)* ## Archived (completed or superseded) diff --git a/.plans/sidebar-branch-enhancements.md b/.plans/sidebar-branch-enhancements.md new file mode 100644 index 00000000..60564147 --- /dev/null +++ b/.plans/sidebar-branch-enhancements.md @@ -0,0 +1,723 @@ +# Sidebar & Branch Picker Enhancements + +## Summary + +Enrich the sidebar thread items and branch picker with information-dense metadata +inspired by Superset and VS Code branch management UIs. All 8 features use data +that already exists on the client — no new server APIs required. + +**Reference:** `DESIGN.md` (project root) for design rules and constraints. + +--- + +## Features + +| # | Feature | Component | Risk | New API? | +|---|---------|-----------|------|----------| +| 1 | Two-line thread items (branch subtitle) | Sidebar.tsx | Low | No | +| 2 | Diff stats per thread (+N -N) | Sidebar.tsx, Sidebar.logic.ts | Low | No | +| 3 | PR number badge inline | Sidebar.tsx | Low | No | +| 4 | Thread count in project header | Sidebar.tsx | Trivial | No | +| 5 | Recent branches at top of picker | BranchToolbarBranchSelector.tsx | Low | No | +| 6 | Fetch button in branch picker | BranchToolbarBranchSelector.tsx | Low | Yes (git.fetch) | +| 7 | Show remote branches grouped | BranchToolbarBranchSelector.tsx, BranchToolbar.logic.ts | Low-Med | No | +| 8 | "New Branch from X" base branch | BranchToolbarBranchSelector.tsx | Low | No | + +--- + +## Feature 1: Two-Line Thread Items (Branch Subtitle) + +### Goal + +Show the thread's git branch name as a second line below the title, making it +possible to identify which branch a thread operates on without clicking into it. + +### Data Source + +- `thread.branch` (type: `string | null`) — already on every Thread object +- Already passed to `MemoizedThreadRow` via the `thread` prop + +### Changes + +**File: `apps/web/src/components/Sidebar.tsx`** + +Modify `MemoizedThreadRow` (lines 375-446). Replace the current single-line layout: + +```tsx +// BEFORE (line 421-444): + +
+ +
+ +``` + +With a two-line layout: + +```tsx +// AFTER: + +
+ {/* Line 1: title + diff stats */} +
+ + {/* Feature 2: DiffStats go here */} +
+ {/* Line 2: branch + PR badge (only when branch is set) */} + {thread.branch ? ( +
+ + {thread.branch} + + {/* Feature 3: PR badge goes here */} +
+ ) : null} +
+``` + +Remove the `CloudUploadIcon` — it serves no function and will be replaced by +diff stats and PR badges. + +### Memo Comparator Update + +Add `thread.branch` to the memo equality check (line 449-465): + +```tsx +if (prev.thread.branch !== next.thread.branch) return false; +``` + +### Visual Result + +**Thread with branch:** +``` +[CircleDotIcon] My thread title +42 -7 + feature/add-login 🔗 #123 +``` + +**Thread without branch (draft, no git):** +``` +[CircleDotIcon] My thread title +``` + +--- + +## Feature 2: Diff Stats Per Thread (+N -N) + +### Goal + +Show aggregate lines added/deleted by the thread, right-aligned on the first line. + +### Data Source + +- `thread.turnDiffSummaries: TurnDiffSummary[]` — already on every Thread object +- Each summary has `files: TurnDiffFileChange[]` with `additions?: number` and + `deletions?: number` +- Aggregate: sum all `additions` and `deletions` across all turns and files + +### Changes + +**File: `apps/web/src/components/Sidebar.logic.ts`** + +Add a new pure function: + +```tsx +export function aggregateThreadDiffStats( + turnDiffSummaries: ReadonlyArray, +): { additions: number; deletions: number } | null { + let additions = 0; + let deletions = 0; + for (const summary of turnDiffSummaries) { + for (const file of summary.files) { + additions += file.additions ?? 0; + deletions += file.deletions ?? 0; + } + } + return additions === 0 && deletions === 0 ? null : { additions, deletions }; +} +``` + +**File: `apps/web/src/components/Sidebar.tsx`** + +Inside `MemoizedThreadRow`, compute and render: + +```tsx +const diffStats = aggregateThreadDiffStats(thread.turnDiffSummaries); + +// In the JSX, after : +{diffStats ? ( + + +{diffStats.additions} + -{diffStats.deletions} + +) : null} +``` + +### Memo Comparator Update + +Add `thread.turnDiffSummaries` to the memo equality check: + +```tsx +if (prev.thread.turnDiffSummaries !== next.thread.turnDiffSummaries) return false; +``` + +### Edge Cases + +- Threads with no turns yet: `turnDiffSummaries` is empty → `null` → nothing rendered +- Threads with only additions: show `+42 -0` +- Very large numbers: use compact formatting if > 9999 (e.g., `+12.3k`) + +--- + +## Feature 3: PR Number Badge Inline + +### Goal + +Show the PR number as a small clickable badge on line 2 of the thread row, +replacing the current icon-only indicator that hides details behind a tooltip. + +### Data Source + +- `prByThreadId: Map` — already computed and passed as prop +- `ThreadPr` includes `number`, `url`, `state`, `title` + +### Changes + +**File: `apps/web/src/components/Sidebar.tsx`** + +Inside `MemoizedThreadRow`, after the branch name on line 2: + +```tsx +{prStatus ? ( + event.stopPropagation()} + className={cn( + "inline-flex shrink-0 items-center gap-0.5 rounded-full px-1.5 py-0.5", + "text-[10px] leading-none transition-colors hover:underline", + prStatus.label === "PR open" && "text-emerald-600", + prStatus.label === "PR merged" && "text-violet-600", + prStatus.label === "PR closed" && "text-muted-foreground/50", + )} + title={prStatus.tooltip} + > + + #{prByThreadId.get(thread.id)?.number} + +) : null} +``` + +### ThreadIcon Simplification + +With the PR badge now visible inline, the `ThreadIcon` at the start of the row +should revert to always using the **thread status icon** (Working, Error, +Completed, etc.) regardless of PR state. The PR icon was overloading the status +icon because there was nowhere else to show PR info — that constraint is now gone. + +Update the icon resolution (lines 354-367): + +```tsx +// BEFORE: PR icon overrides thread status icon +const ThreadIcon = prStatus ? prStatus.icon : ... + +// AFTER: Always use thread status icon +const ThreadIcon = threadStatus?.label === "Completed" + ? CheckCircleIcon + : threadStatus?.label === "Error" + ? XCircleIcon + : ... // (rest unchanged, remove prStatus override) + +const threadIconColor = threadStatus ? threadStatus.colorClass : "text-muted-foreground/50"; +``` + +--- + +## Feature 4: Thread Count in Project Header + +### Goal + +Show the number of threads per project next to the project name, like `superset (10)`. + +### Data Source + +- `projectThreads` (line 1319) — already computed as + `sortedThreadsByProjectId.get(project.id)` + +### Changes + +**File: `apps/web/src/components/Sidebar.tsx`** + +In `renderProjectItem`, after the project name span (line 1389-1404): + +```tsx +// BEFORE: + + {project.name} + + +// AFTER: + + {project.name} + {projectThreads.length > 0 ? ( + + ({projectThreads.length}) + + ) : null} + +``` + +### Notes + +- Count includes draft threads (already merged into `sidebarThreads`) +- When collapsed, count gives a quick sense of project activity +- No conditional logic — always shown when threads exist + +--- + +## Feature 5: Recent Branches at Top of Picker + +### Goal + +Show the 5 most recently used branches at the top of the branch picker dropdown, +separated from the full list by a subtle divider. + +### Data Source + +- New: `localStorage` key `okcode:recent-branches:v1` +- Format: `Record` keyed by project cwd, values are branch names + (most recent first, max 5) + +### Changes + +**File: `apps/web/src/components/BranchToolbar.logic.ts`** + +Add recent branches storage helpers: + +```tsx +const RECENT_BRANCHES_KEY = "okcode:recent-branches:v1"; +const MAX_RECENT_BRANCHES = 5; + +export function getRecentBranches(cwd: string): ReadonlyArray { + try { + const stored = localStorage.getItem(RECENT_BRANCHES_KEY); + if (!stored) return []; + const parsed = JSON.parse(stored) as Record; + return parsed[cwd]?.slice(0, MAX_RECENT_BRANCHES) ?? []; + } catch { + return []; + } +} + +export function trackRecentBranch(cwd: string, branchName: string): void { + try { + const stored = localStorage.getItem(RECENT_BRANCHES_KEY); + const parsed: Record = stored ? JSON.parse(stored) : {}; + const existing = parsed[cwd] ?? []; + const updated = [branchName, ...existing.filter((name) => name !== branchName)] + .slice(0, MAX_RECENT_BRANCHES); + parsed[cwd] = updated; + localStorage.setItem(RECENT_BRANCHES_KEY, JSON.stringify(parsed)); + } catch { + // Silent fail — non-critical feature + } +} +``` + +**File: `apps/web/src/components/BranchToolbarBranchSelector.tsx`** + +1. Call `trackRecentBranch(branchCwd, selectedBranchName)` inside `selectBranch` + and `createBranch` after successful operations. + +2. Partition `filteredBranchPickerItems` into recent and rest: + +```tsx +const recentBranchNames = useMemo( + () => (branchQueryCwd ? getRecentBranches(branchQueryCwd) : []), + [branchQueryCwd, isBranchMenuOpen], // re-read when picker opens +); + +const { recentItems, remainingItems } = useMemo(() => { + if (normalizedDeferredBranchQuery.length > 0 || recentBranchNames.length === 0) { + return { recentItems: [], remainingItems: filteredBranchPickerItems }; + } + const recentSet = new Set(recentBranchNames); + const recent = filteredBranchPickerItems.filter( + (item) => recentSet.has(item) && !item.startsWith("__"), + ); + const rest = filteredBranchPickerItems.filter( + (item) => !recentSet.has(item) || item.startsWith("__"), + ); + return { recentItems: recent, remainingItems: rest }; +}, [filteredBranchPickerItems, normalizedDeferredBranchQuery, recentBranchNames]); +``` + +3. Render with divider in the `ComboboxList`: + +```tsx +{/* Special items (PR checkout) */} +{/* Recent branches section */} +{recentItems.length > 0 && ( + <> +
+ Recent +
+ {recentItems.map((item, index) => renderPickerItem(item, index))} +
+ +)} +{/* All branches */} +{remainingItems.map((item, index) => + renderPickerItem(item, recentItems.length + index) +)} +``` + +### Notes + +- When searching, recent grouping is suppressed — filter applies to flat list +- Recent branches that no longer exist in the branch list are silently skipped +- Virtual scrolling index math accounts for the section header + divider + +--- + +## Feature 6: Fetch Button in Branch Picker + +### Goal + +Add a fetch button in the branch picker header that refreshes remote refs, +so users can discover new remote branches without leaving the picker. + +### Server API + +This is the **only feature requiring a new server API**. + +**File: `packages/contracts/src/git.ts`** + +Add input/result schemas: + +```tsx +export const GitFetchInput = Schema.Struct({ + cwd: TrimmedNonEmptyStringSchema, +}); +export type GitFetchInput = typeof GitFetchInput.Type; + +export const GitFetchResult = Schema.Struct({ + status: Schema.Literal("fetched", "failed"), +}); +export type GitFetchResult = typeof GitFetchResult.Type; +``` + +**File: `apps/server/src/...` (git service)** + +Implement `git.fetch`: + +```tsx +async fetch(input: GitFetchInput): Promise { + // Run: git fetch --prune + // Return { status: "fetched" } or { status: "failed" } +} +``` + +**File: `apps/web/src/lib/gitReactQuery.ts`** + +Add mutation: + +```tsx +export function gitFetchMutationOptions(input: { + cwd: string | null; + queryClient: QueryClient; +}) { + return { + mutationKey: ["git", "mutation", "fetch", input.cwd], + mutationFn: async () => { + const api = readNativeApi(); + if (!api || !input.cwd) throw new Error("No API or CWD"); + return api.git.fetch({ cwd: input.cwd }); + }, + onSettled: () => invalidateGitQueries(input.queryClient), + }; +} +``` + +### UI Changes + +**File: `apps/web/src/components/BranchToolbarBranchSelector.tsx`** + +Add a fetch button next to the search input in the picker header: + +```tsx +
+ setBranchQuery(event.target.value)} + /> + + fetchMutation.mutate()} + disabled={fetchMutation.isPending} + > + {fetchMutation.isPending ? ( + + ) : ( + + )} + + } + /> + Fetch remote branches + +
+``` + +--- + +## Feature 7: Show Remote Branches Grouped + +### Goal + +Show remote-only branches (those without a local counterpart) in a separate +section of the branch picker, making remote branches discoverable. + +### Data Source + +- `dedupeRemoteBranchesWithLocalMatches()` — **already exists** in + `BranchToolbar.logic.ts` (line 115-141) but is not wired into the picker +- `filterSelectableBranches()` currently filters out all remote branches (line 74-78) + +### Changes + +**File: `apps/web/src/components/BranchToolbarBranchSelector.tsx`** + +Replace the current filtering logic: + +```tsx +// BEFORE (line 98-101): +const branches = useMemo( + () => filterSelectableBranches(branchesQuery.data?.branches ?? []), + [branchesQuery.data?.branches], +); + +// AFTER: +const allBranches = branchesQuery.data?.branches ?? []; +const localBranches = useMemo( + () => filterSelectableBranches(allBranches), + [allBranches], +); +const remoteOnlyBranches = useMemo( + () => dedupeRemoteBranchesWithLocalMatches(allBranches).filter((b) => b.isRemote), + [allBranches], +); +``` + +Build two separate name lists and combine them with a sentinel separator: + +```tsx +const REMOTE_DIVIDER = "__remote_divider__"; + +const branchPickerItems = useMemo(() => { + const items: string[] = []; + // Special items (PR checkout) + if (checkoutPullRequestItemValue) items.push(checkoutPullRequestItemValue); + // Local branches + items.push(...localBranchNames); + // Create branch action + if (createBranchItemValue && !hasExactBranchMatch) items.push(createBranchItemValue); + // Remote divider + remote branches + if (remoteOnlyBranches.length > 0) { + items.push(REMOTE_DIVIDER); + items.push(...remoteOnlyBranches.map((b) => b.name)); + } + return items; +}, [localBranchNames, remoteOnlyBranches, ...]); +``` + +Render the divider in `renderPickerItem`: + +```tsx +if (itemValue === REMOTE_DIVIDER) { + return ( +
+ Remote +
+ ); +} +``` + +Update the branch lookup map to include remote branches: + +```tsx +const branchByName = useMemo( + () => new Map([...localBranches, ...remoteOnlyBranches].map((b) => [b.name, b] as const)), + [localBranches, remoteOnlyBranches], +); +``` + +### Filtering Behavior + +When the user types a search query, filter applies across both local and remote +branches. The "Remote" divider is hidden during search (same as "Recent" in +Feature 5). + +--- + +## Feature 8: "New Branch from X" Base Branch + +### Goal + +When creating a new branch, use the currently highlighted branch in the picker +as the starting point instead of always branching from HEAD. + +### Data Source + +- Highlighted branch tracked by Combobox via `onItemHighlighted` callback + (line 419-422) +- `git.createBranch` API — check if it supports a `startPoint` parameter + +### Changes + +**File: `packages/contracts/src/git.ts`** + +Check/extend `GitCreateBranchInput`: + +```tsx +export const GitCreateBranchInput = Schema.Struct({ + cwd: TrimmedNonEmptyStringSchema, + branch: TrimmedNonEmptyStringSchema, + startPoint: Schema.optional(TrimmedNonEmptyStringSchema), // NEW — base branch or commit +}); +``` + +**File: Server git implementation** + +Update `createBranch` to pass `startPoint`: + +```bash +# BEFORE: git branch +# AFTER: git branch [startPoint] +``` + +**File: `apps/web/src/components/BranchToolbarBranchSelector.tsx`** + +1. Track the highlighted branch: + +```tsx +const [highlightedBranchName, setHighlightedBranchName] = useState(null); + +// In Combobox: +onItemHighlighted={(value, eventDetails) => { + setHighlightedBranchName(value && !value.startsWith("__") ? value : null); + // ... existing scroll logic +}} +``` + +2. Pass the highlighted branch as startPoint in `createBranch`: + +```tsx +const createBranch = (rawName: string) => { + // ...existing validation... + const startPoint = highlightedBranchName ?? undefined; + + runBranchAction(async () => { + setOptimisticBranch(name); + try { + await api.git.createBranch({ cwd: branchCwd, branch: name, startPoint }); + // ...rest unchanged... + } + }); +}; +``` + +3. Update the "Create new branch" item label to show the base: + +```tsx + + + Create "{trimmedBranchQuery}" + {highlightedBranchName ? ( + from {highlightedBranchName} + ) : null} + + +``` + +--- + +## Implementation Order + +Recommended sequence to minimize conflicts and enable incremental review: + +### Phase 1: Sidebar Thread Enrichment (Features 1-4) + +These four changes are all in `Sidebar.tsx` + `Sidebar.logic.ts` and can be done +in a single PR. They are purely additive — no behavior changes, no new APIs. + +1. **Feature 4** first (thread count) — trivial, 1-line change, validates the PR workflow +2. **Feature 1** (two-line layout) — structural change to ThreadRow, needed before 2 and 3 +3. **Feature 2** (diff stats) — adds `aggregateThreadDiffStats` + renders on line 1 +4. **Feature 3** (PR badge) — adds badge on line 2, simplifies ThreadIcon + +### Phase 2: Branch Picker Enhancements (Features 5-8) + +These four changes are in `BranchToolbarBranchSelector.tsx` + `BranchToolbar.logic.ts` +and can be done in a second PR. + +5. **Feature 7** (remote branches grouped) — biggest structural change to the picker, + do first so 5 and 6 build on top +6. **Feature 5** (recent branches) — adds localStorage tracking + section grouping +7. **Feature 8** (new branch from X) — extends create-branch with startPoint +8. **Feature 6** (fetch button) — only feature needing a new server API, do last + +### Phase 3: Tests & Polish + +- Add unit tests for `aggregateThreadDiffStats` and `getRecentBranches`/`trackRecentBranch` +- Add test for `dedupeRemoteBranchesWithLocalMatches` integration into picker items +- Verify memo comparator correctness with branch/diff changes +- Verify virtual scrolling still works with section dividers +- Cross-theme visual QA (all 6 themes, light + dark) + +--- + +## Files Modified (Summary) + +| File | Features | Type of Change | +|------|----------|---------------| +| `apps/web/src/components/Sidebar.tsx` | 1, 2, 3, 4 | Layout, rendering | +| `apps/web/src/components/Sidebar.logic.ts` | 2 | New pure function | +| `apps/web/src/components/BranchToolbarBranchSelector.tsx` | 5, 6, 7, 8 | Layout, data, state | +| `apps/web/src/components/BranchToolbar.logic.ts` | 5 | New localStorage helpers | +| `packages/contracts/src/git.ts` | 6, 8 | Schema additions | +| `apps/web/src/lib/gitReactQuery.ts` | 6 | New mutation | +| Server git service | 6, 8 | New fetch endpoint, extend createBranch | + +--- + +## Testing Checklist + +- [ ] Sidebar thread with branch shows two-line layout +- [ ] Sidebar thread without branch shows single-line layout (no empty second line) +- [ ] Diff stats show correct aggregate across all turns +- [ ] Diff stats handle zero additions or zero deletions gracefully +- [ ] PR badge is clickable and opens PR URL +- [ ] PR badge shows correct state color (open/merged/closed) +- [ ] Thread count updates when threads are added/removed +- [ ] Thread count includes draft threads +- [ ] Recent branches section appears with correct ordering +- [ ] Recent branches are suppressed during search +- [ ] Fetch button spins during fetch and refreshes branch list +- [ ] Remote branches appear below "Remote" divider +- [ ] Remote branches with local counterparts are hidden +- [ ] "Create branch from X" shows highlighted branch name +- [ ] New branch created from highlighted base is correct (`git log` check) +- [ ] Virtual scrolling works with section dividers +- [ ] All features render correctly in all 6 themes (light + dark) +- [ ] Memo comparator prevents unnecessary re-renders +- [ ] Keyboard navigation through recent → all → remote sections works diff --git a/DESIGN.md b/DESIGN.md new file mode 100644 index 00000000..4cbdf224 --- /dev/null +++ b/DESIGN.md @@ -0,0 +1,294 @@ +# OK Code Design System + +This document is the authoritative reference for OK Code's visual design philosophy, +component patterns, and UI rules. Every contributor and AI agent should consult this +before making interface changes. + +--- + +## 1. Design Philosophy + +OK Code is a **desktop-first orchestration platform for interactive coding agents**. +The interface exists to get out of the way and let the developer focus on the +conversation with the agent. Every pixel must earn its place. + +### Core Principles + +1. **Clarity over decoration.** Remove anything that doesn't help the user make a + decision or understand state. No gratuitous gradients, no ornamental dividers, + no filler icons. + +2. **Information density over whitespace.** Developers tolerate — and prefer — dense + interfaces. Pack useful information in, but keep it scannable. Two lines of + meaningful metadata per thread row is better than one line with padding. + +3. **Keyboard-first, pointer-friendly.** Every primary action must be reachable via + keyboard. Pointer interactions are a convenience layer, never the only path. + Command palette (`Cmd+K`) is the universal escape hatch. + +4. **State visibility at a glance.** The user should never have to click into something + to learn its status. Branch name, diff stats, PR state, sync status — surface + them where the user already is (sidebar, toolbar), not behind a hover or modal. + +5. **Progressive disclosure.** Show the 80% case by default; let the 20% reveal on + interaction. Tooltips, expandable sections, and context menus are the right homes + for secondary actions. + +6. **Performance is a feature.** Virtual scrolling for long lists. Deferred values for + search. Memoized components with explicit equality checks. Never block the main + thread for a pretty animation. + +7. **Theme parity.** Every theme (light and dark) must receive equal visual care. + Never design for dark-only and bolt on a light variant. All 6 premium themes are + first-class citizens. + +8. **Composability.** Small, focused components with clear props boundaries. + Composite patterns (`Select > SelectTrigger > SelectValue > SelectPopup`) over + monolithic components with flag props. + +--- + +## 2. Visual Identity + +### Typography + +| Role | Size | Weight | Tracking | Font Stack | +|------|------|--------|----------|------------| +| Thread title (sidebar) | `text-xs` (0.75rem) | `font-normal` | default | Inter, system-ui, sans-serif | +| Thread subtitle / metadata | `text-[10px]` | `font-normal` | default | Inter, system-ui, sans-serif | +| Badge text | `text-[10px]` | `font-medium` | default | Inter, system-ui, sans-serif | +| Button text | `text-sm` (0.875rem) | `font-medium` | default | Inter, system-ui, sans-serif | +| Heading / dialog title | `text-lg` (1.125rem) | `font-semibold` | `-0.01em` | Inter, system-ui, sans-serif | +| Code / terminal | `text-sm` | `font-normal` | default | SF Mono, Consolas, monospace | +| Project name | `text-xs` | `font-semibold` | default | Inter, system-ui, sans-serif | + +### Color Semantics + +Colors are referenced through CSS custom properties, never hardcoded hex values. + +| Token | Usage | +|-------|-------| +| `text-foreground` | Primary text | +| `text-muted-foreground` | Secondary/deemphasized text | +| `text-muted-foreground/50` | Tertiary/metadata text (branch names, timestamps) | +| `bg-background` | Page background | +| `bg-accent` | Hover state, active row highlight | +| `bg-accent/60` | Active sidebar item | +| `bg-accent/40` | Selected sidebar item | +| `text-emerald-600` | Additions / success (green) | +| `text-rose-500` | Deletions / error (red) | +| `text-warning` | Warning states, behind-upstream | +| `text-destructive` | Destructive actions (delete) | +| `border-border/60` | Subtle badge borders | + +### Spacing Rules + +- **Sidebar item height:** `min-h-7` (28px) minimum, `h-auto` for multi-line +- **Sidebar item padding:** `px-2 py-1` (8px horizontal, 4px vertical) +- **Icon size in sidebar:** `size-3.5` (14px) +- **Badge size in sidebar:** `text-[10px]`, `px-1.5 py-0.5` +- **Gap between icon and content:** `gap-2` (8px) +- **Gap between metadata items:** `gap-1` (4px) or `gap-1.5` (6px) +- **Border radius:** Use `rounded-md` (6px) for sidebar items, `rounded-full` for badges + +### Themes + +Six premium themes, each with light and dark variants: + +| Theme | Vibe | +|-------|------| +| **Iridescent Void** | Futuristic, expensive, slightly alien | +| **Solar Witch** | Magical, cozy, ritualistic | +| **Carbon** | Stark, modern, performance-focused | +| **Vapor** | Refined, fluid, purposeful | +| **Cotton Candy** | Sweet, dreamy, pink and blue | +| **Cathedral Circuit** | Sacred machine, techno-gothic | + +All themes define the same set of CSS custom properties. Components must use semantic +tokens (`bg-accent`, `text-muted-foreground`) — never theme-specific values. + +--- + +## 3. Component Patterns + +### API Conventions + +Every UI primitive follows these rules: + +```tsx +// 1. data-slot for DOM identification +
+ +// 2. CVA for variant management +const buttonVariants = cva("base classes", { + variants: { variant: { ... }, size: { ... } }, + defaultVariants: { variant: "default", size: "default" }, +}); + +// 3. cn() for conditional class merging +
+ +// 4. Composite pattern for complex components + +``` + +### Focus States + +All interactive elements use the same focus ring: +``` +focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] +``` + +### Disabled States + +``` +disabled:pointer-events-none disabled:opacity-50 +``` + +### Animation Rules + +- **Transitions:** `transition-colors` or `transition-shadow` — never `transition-all` + unless specifically needed for layout shifts. +- **Durations:** 150ms for hover, 200ms for modals/drawers. +- **Reduced motion:** Always respect `prefers-reduced-motion`. Use Tailwind's + `motion-reduce:` prefix or the existing `.no-transitions` guard. +- **No decorative animation.** Animations must communicate state change (opening, + closing, loading). Pulse is reserved for "Working" status indicators. + +--- + +## 4. Sidebar Rules + +The sidebar is the primary navigation surface. These rules are non-negotiable: + +### Thread Row Layout + +``` +[StatusIcon] [Title] [DiffStats] + [branch-name] [PR badge] +``` + +- **Line 1:** Status icon (3.5px) + editable title + right-aligned diff stats +- **Line 2:** Branch name in muted text + right-aligned PR badge (if applicable) +- Single-line fallback when no branch is set (title-only row) +- Active row: `bg-accent/60 text-foreground` +- Selected row: `bg-accent/40 text-foreground` +- Default row: `text-muted-foreground hover:bg-accent/40` + +### Project Header Layout + +``` +[ProjectName] (threadCount) [+ New Thread] +``` + +- Thread count shown as `(N)` in muted text next to the project name +- Color-coded background per project via `getProjectColor()` +- Collapsible with "Show more" / "Show less" for 10+ threads + +### Data Freshness + +- Thread metadata (branch, PR, diff stats) rendered from existing store data +- No additional network requests from the sidebar — use data already fetched +- PR status polled per-cwd at 60s intervals (existing `threadGitStatusQueries`) + +--- + +## 5. Branch Picker Rules + +The branch picker is a `Combobox` dropdown. These rules apply: + +### Structure + +``` +[Search input] [Fetch button] +───────────────────────────────────────────────── +Recent Branches + main [current] [default] + feature/foo [worktree] +───────────────────────────────────────────────── +All Branches + bugfix/bar + feature/baz [remote] +───────────────────────────────────────────────── +Create new branch "typed-query" (when search has no exact match) +Checkout Pull Request #123 (when search matches PR ref pattern) +``` + +- **Recent branches** at top (3-5 most recently switched-to, tracked in localStorage) +- **All branches** below, with local branches first, then remote-only branches + separated by a subtle divider +- **Remote branches** shown only when they have no local counterpart + (via existing `dedupeRemoteBranchesWithLocalMatches`) +- **Badges** per branch: `current`, `default`, `worktree`, `remote`, `stash N` +- **Create branch** allows specifying a base: when a branch is highlighted, + creating a new branch uses the highlighted branch as the starting point +- **Fetch button** in the picker header refreshes remote refs + +### Data Rules + +- Branch list virtualized at 40+ items (existing behavior) +- No per-branch ahead/behind counts (too expensive to compute for all branches) +- Ahead/behind shown only for the current branch in the toolbar (existing behavior) + +--- + +## 6. Git Actions Rules + +Git operations follow the "stacked action" pattern. The user always works with +a single flow: + +``` +[Quick action button] [Dropdown menu with all actions] +``` + +Quick action resolves automatically based on git state: +- Has changes + no PR → "Commit, push & PR" +- Has changes + existing PR → "Commit & push" +- No changes + ahead → "Push & create PR" +- Behind upstream → "Pull" or "Sync branch" +- Conflicts → "Resolve conflicts" + +**Never fragment git actions** into multiple surfaces. The branch picker handles +navigation (switching, creating, fetching). The git actions control handles +mutations (commit, push, PR). + +--- + +## 7. Right Panel Rules + +The right panel (`useRightPanelStore`) hosts context-dependent content: + +- **Code viewer** — file browsing and editing +- **Diff panel** — unified diff for file changes +- **Workspace panel** — file tree + +New panels should use the same toggle mechanism and respect the existing +split/stacked responsive layout (split at 600px+, stacked below). + +--- + +## 8. Don'ts + +1. **Don't add new modals** for information that could be inline or in a tooltip. +2. **Don't hide status behind clicks.** If the user needs to know it, show it. +3. **Don't add loading spinners** for operations under 200ms. Use optimistic updates. +4. **Don't add new Zustand stores** without justification. Prefer extending existing + stores or using React Query for server state. +5. **Don't add new polling intervals.** Reuse existing git status queries (5s stale, + 15s refetch) or branch queries (15s stale, 60s refetch). +6. **Don't hardcode colors.** Use semantic tokens from the theme system. +7. **Don't break the memo contract.** When adding props to `MemoizedThreadRow`, add + corresponding equality checks to the memo comparator. +8. **Don't add destructive actions to compact pickers.** Delete belongs in context + menus with confirmation, not in branch dropdowns. +9. **Don't duplicate git actions.** Push belongs in GitActionsControl, not in the + branch picker. Commit belongs in GitActionsControl, not in a right panel. +10. **Don't add features that require new server APIs** when the data already exists + on the client. Compute derived values from existing queries.