Experience Spine
Behavioral contract for every surface in Slop. Information architecture, voice, component patterns, state handling, interaction primitives, and key user flows.
Foundation
Slop is a local, dark-only developer tool running at localhost:3100. The user is a technically sophisticated developer who wants autonomous issue-to-merge pipelines and intervenes only when automation cannot make the call.
Platform context
macOS desktop, full browser window, localhost. No mobile breakpoints. No light mode. No onboarding flow -- the Config page is the entry point for first-time setup and is always reachable from the nav.
Tone
Operator dashboard, not consumer app. The UI surfaces counts, verb labels, and machine state. It does not celebrate, encourage, or coach. A merged PR gets a success-green badge -- nothing more.
Surface palette
Carbon dark. Page background at bg-base, card/column background at bg-surface, elevated overlays at bg-elevated, popover chrome at bg-overlay. All interactive accent is violet. Status meanings are carried by success/info/warn/danger paired fg/bg fills. Brand green appears only in the SlopMark logo SVG.
Typography
--font-sans (Inter) for all UI copy; --font-mono (JetBrains Mono) for log output and code blocks. Base size 15px. Page titles use --text-page-title. Section labels use --text-section in uppercase.
Motion
--animate-enter (180ms fade + 4px slide) for rows that appear after SSE events. prefers-reduced-motion zeroes all animation durations -- no exceptions.
Information Architecture
| Surface | Route | Purpose |
|---|---|---|
| Board | / | Kanban pipeline. Central workspace. Issues flow left-to-right: Backlog, Ready, In Progress, Merged. |
| Plan | /issues | Internal issue tracker. File, manage, batch, and queue issues without leaving the app. |
| Workers | /workers | All workers -- active and done -- with live metrics: cost, tokens, CPU/RSS. |
| Worker Report | /workers/[id] | Detailed run report for a single completed worker: log, cost breakdown, generated report body. |
| Agents | /agents | Live agent/skill runs with log streaming and skill launcher. |
| Agent Detail | /agents/[id] | Single agent run: metadata, optional report, full log. |
| Config | /config | Global settings: GitHub token, watched repos, automation toggles, timeouts, cost config. |
| Docs | /docs | View and edit repo documentation in-place. |
| About | /about | Static reference: pipeline stages, label glossary, skills table. |
URL state
?repo=<id> is the primary scoping key. The selected repo is also persisted to a cookie so tab switches preserve context. ?batch=<id> filters the board to a single batch. ?worker=<id> auto-expands a row on the Workers page. ?issue=<id> deep-links to a detail panel on the Plan page.
Repo scope
All worker, issue, and config data is scoped to the selected repo. The RepoSwitcher in TabNav is the only mechanism for changing scope. When no repos are configured the Board renders a "no repos configured" prompt with a link to Config.
Voice and Tone
| Do | Don't |
|---|---|
| "3 issues in progress" | "You have 3 issues being worked on!" |
| "No workers match." | "Hmm, nothing here yet. Try a different filter." |
| "Merging..." | "Almost there!" |
| "Failed" (show the specific error) | "Something went wrong" |
| "Set Ready" (domain term) | "Send to Queue" |
| "Cancel" (destructive, danger variant) | "Stop everything" |
| "Implementing" (machine state label) | "Working on it" |
| Short factual empty state + action link | Multi-paragraph onboarding copy |
Inline role="alert" on the failed action | Toast notification for errors |
| "Loading..." for log panels; skeleton blocks for known shapes | Spinner covering the entire page |
Error copy
Show the raw error string from the { ok: false; error: string } action result, truncated with tooltip when long. Never swallow or rephrase machine errors -- the user is technical and needs the exact message.
Status labels
Match the STATE_META mapping exactly: "implementing", "waiting ci", "merging", "merged", "failed" -- lowercase, present participle for active states, past for terminal.
Component Patterns
| Component | Use | Behavioral rules |
|---|---|---|
| Button (primary) | Affirmative single action per section | Accent fill. One per form region. Shows Spinner + busyLabel when busy. |
| Button (secondary) | Adjacent non-destructive actions | bg-elevated fill, text-primary label. |
| Button (danger) | Destructive, irreversible actions | danger-fg text. Routes through ConfirmationDialog before executing. |
| Badge | Worker status, issue state, decision, source | Semantic tone prop only -- never raw color for status. rounded-full pill shape. |
| StateBadge | Worker status column and kanban cards | Maps WorkerStatus through STATE_META; includes role="status" and aria-label. Never show raw status string. |
| Card | Config sections, stat cards, report metadata | Consistent bg-surface background, border-default border, rounded-lg radius. |
| InlineError | Action failure feedback | role="alert", adjacent to the button that failed. Clears on next action attempt. |
| Skeleton | Loading placeholders | Sized to match the content they replace -- not generic full-width bars. |
| ConfirmationDialog | Destructive actions only | Requires explicit "Confirm" click. Escape and "Cancel" both dismiss. Never auto-confirm on timeout. |
| EmptyState | Zero-item lists | Short title, optional one-sentence description, one action link maximum. |
| FoldableSection | PR list, worktrees, worker groups | localStorage-persisted open/closed state when storageKey provided. |
| LogPanel | Worker and agent log streams | role="log", aria-live="polite". Auto-scrolls to bottom. Color-codes by level. |
| Tooltip | Icon-only buttons, truncated labels | Hover-only. Never a primary information surface. |
| Menu / MenuItem | Contextual overflow actions | separated prop draws a divider; use for destructive items at the bottom. |
State Patterns
| State | Surface | Treatment |
|---|---|---|
| Daemon booting | Full page | BootingScreen -- polls /api/health every second, router.refresh() on ready: true. No nav or content until ready. |
| No repos configured | Board, Workers, Plan | EmptyState with link to /config. No kanban chrome rendered. |
| SSE connected | Board | No indicator -- silence means healthy. |
| SSE disconnected | Board | Reconnection indicator in column header. Exponential backoff 1s to 30s. Re-fetches snapshot on reconnect. |
| Worker active | In Progress, Workers page | LiveDuration ticking. Status badge tone: info. animate-enter on first appearance. |
| Worker paused | In Progress, Workers page | Status badge tone: warn. "Resume" button replaces "Pause". |
| Worker failed | In Progress, Workers Done | Status badge tone: danger. Error message shown below the row. Restart and Retry buttons available. |
| Worker merged | Merged column, Workers Done | Status badge tone: success. Static -- no ticking time. "Report" link appears. |
| Optimistic drag/drop | Board columns | Card moves immediately. Drop target column gets accent ring on hover. Stale state pruned on next SSE confirmation. |
| Base CI red | Board filter bar, Merge button | BaseCiStatus badge shown. Manual Merge button disabled. BaseCiFixButton offered. |
| Poll paused | Board filter bar | PausePollingButton reflects paused state. No automatic issue pickup while paused. |
| Form submission | Any form with submit | busy prop on Button shows spinner. On ok:false, InlineError renders with role="alert". On ok:true, revalidatePath refreshes. |
Interaction Primitives
Drag and drop (Board)
HTML5 native drag. Only Backlog and Ready columns accept drops. Dragging a card from Backlog to Ready enqueues it; Ready to Backlog removes it from the queue. Intra-Ready drag reorders the queue. dragover highlights the column drop target with an accent ring. Drop errors surface as an InlineError below the kanban header.
Keyboard navigation
KeyboardNav registers global key listeners for tab switching. All interactive elements receive :focus-visible with outline: 2px solid accent -- no component-level override. Escape dismisses any open popover, overlay, or dialog.
Popover dismiss
Escape key or click outside. Popovers do not steal focus from outside their portal boundary. Submitting a popover form closes it on success.
HarnessPicker
Auto-closes on harness selection. No explicit confirm step needed.
IssuePickerDrawer
Bottom sheet (80--95vh). Checkbox issue list. "Apply" commits selection; "Cancel" or Escape dismisses with no change.
StoryPointsInput
Accepts only valid Fibonacci values (1/2/3/5/8/13/21). Adjacent wand button opens HarnessPicker, then fires suggestIssuePoints. Module-level pendingSuggestions Map deduplicates concurrent calls for the same issue.
ToggleModePill
Single click cycles override through: global default → explicit true → explicit false → global default. Label reflects the effective computed value, not the raw override enum.
RepoSwitcher / HarnessDropdown
Radio-style selection. Selecting an item updates the URL param or fires a server action immediately -- no confirm step.
Log autoscroll
LogPanel and LiveLogPanel auto-scroll to bottom when new events arrive. Manual scroll upward pauses autoscroll; scroll back to bottom re-enables it.
ConfirmationDialog
Focus is trapped inside the dialog while open. Confirm is the default-focused button for keyboard users.
Accessibility Floor
- All status indicators use
StateBadgewithrole="status"andaria-label-- never raw text or color alone. - Error feedback uses
InlineErrorwithrole="alert"so screen readers announce failures without focus movement. - Log regions use
role="log"witharia-live="polite"so screen readers can follow streaming output without interrupting the user. - Icon-only buttons must have an
aria-labelorTooltipproviding a text alternative. ConfirmationDialoguses a<dialog>element with focus trap. Backdrop click does not dismiss -- only explicit Cancel or Escape.:focus-visiblering is the sole global focus style -- components must not suppress it.prefers-reduced-motionzeroes all animation and transition durations globally inglobals.css. No component may override this with!important.- Color is never the sole differentiator for status. Each Badge tone carries a text label.
RelativeTimerenders a<time>element with a machine-readabledateTimeattribute.
Responsive and Platform
Slop is desktop-first and localhost-only. No mobile layout. No responsive breakpoints are defined.
| Constraint | Detail |
|---|---|
| Minimum viewport | ~1024px wide. Kanban columns have fixed minimum width and scroll horizontally if the window narrows below the board's natural width. |
| macOS specifics | node-pty requires native bindings; the app does not run on Windows. Scrollbars styled globally: 6px width, border-default track, rounded-full thumb. |
| Dark-only | The @theme block in globals.css defines a single Carbon dark palette. No prefers-color-scheme media query; no theme toggle. |
| Font loading | next/font injects --font-inter and --font-jetbrains-mono on <html>. These are referenced by --font-sans and --font-mono tokens. Always available before first paint. |
Inspiration and Anti-patterns
Draws from
- Linear's issue board: density-first kanban, direct action on cards, no modal-heavy flows
- GitHub Actions run page: live log streaming, status badges, elapsed time
- Vercel deployment dashboard: single-purpose live status, minimal chrome, compact metrics
Anti-patterns to avoid
| Anti-pattern | Why |
|---|---|
| Toast notifications for errors | Errors on actions belong inline, adjacent to the button that failed. Toasts require the user to look away from the context of failure. |
| Modal errors | Same reason. ConfirmationDialog is only for destructive pre-confirmation, never for error display. |
| Optimistic removal without SSE confirmation | Cards dragged to Ready stay in the UI -- they do not disappear until the daemon confirms the ReadyIssue row. |
| Global spinners covering the page | Use Skeleton blocks sized to the content they replace. Page-level blocking spinners make the UI feel brittle. |
| Raw status strings in the UI | Always route through StateBadge / STATE_META. The raw WorkerStatus enum values are internal. |
| Hardcoded colors or Tailwind palette utilities | All color choices use semantic token names. Raw hex or bg-purple-500 style classes must not appear in component files. |
| Bypassing ConfirmationDialog for destructive actions | "Clear finished workers", "Cancel worker", "Wipe database" all require explicit confirmation. |
| Gamification or progress celebrations | No confetti, no "great job" copy, no milestone banners. A merged badge is the terminal success state. |
| Auto-dismissing alerts | role="alert" errors stay visible until the user attempts the action again. Never set a timeout to clear error state. |
Key Flows
?worker=<id> deep-link to expand the highest-priority worker. She clicks Show logs to open the LogPanel. Log lines stream in, color-coded by level. She watches the first few lines confirm correct context, then collapses the panel.