Slop Docs Home
Slop · UX

Experience Spine

Behavioral contract for every surface in Slop. Information architecture, voice, component patterns, state handling, interaction primitives, and key user flows.

Operator dashboard Dark-only Desktop-first localhost

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

SurfaceRoutePurpose
Board/Kanban pipeline. Central workspace. Issues flow left-to-right: Backlog, Ready, In Progress, Merged.
Plan/issuesInternal issue tracker. File, manage, batch, and queue issues without leaving the app.
Workers/workersAll 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/agentsLive agent/skill runs with log streaming and skill launcher.
Agent Detail/agents/[id]Single agent run: metadata, optional report, full log.
Config/configGlobal settings: GitHub token, watched repos, automation toggles, timeouts, cost config.
Docs/docsView and edit repo documentation in-place.
About/aboutStatic 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

DoDon'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 linkMulti-paragraph onboarding copy
Inline role="alert" on the failed actionToast notification for errors
"Loading..." for log panels; skeleton blocks for known shapesSpinner 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

ComponentUseBehavioral rules
Button (primary)Affirmative single action per sectionAccent fill. One per form region. Shows Spinner + busyLabel when busy.
Button (secondary)Adjacent non-destructive actionsbg-elevated fill, text-primary label.
Button (danger)Destructive, irreversible actionsdanger-fg text. Routes through ConfirmationDialog before executing.
BadgeWorker status, issue state, decision, sourceSemantic tone prop only -- never raw color for status. rounded-full pill shape.
StateBadgeWorker status column and kanban cardsMaps WorkerStatus through STATE_META; includes role="status" and aria-label. Never show raw status string.
CardConfig sections, stat cards, report metadataConsistent bg-surface background, border-default border, rounded-lg radius.
InlineErrorAction failure feedbackrole="alert", adjacent to the button that failed. Clears on next action attempt.
SkeletonLoading placeholdersSized to match the content they replace -- not generic full-width bars.
ConfirmationDialogDestructive actions onlyRequires explicit "Confirm" click. Escape and "Cancel" both dismiss. Never auto-confirm on timeout.
EmptyStateZero-item listsShort title, optional one-sentence description, one action link maximum.
FoldableSectionPR list, worktrees, worker groupslocalStorage-persisted open/closed state when storageKey provided.
LogPanelWorker and agent log streamsrole="log", aria-live="polite". Auto-scrolls to bottom. Color-codes by level.
TooltipIcon-only buttons, truncated labelsHover-only. Never a primary information surface.
Menu / MenuItemContextual overflow actionsseparated prop draws a divider; use for destructive items at the bottom.

State Patterns

StateSurfaceTreatment
Daemon bootingFull pageBootingScreen -- polls /api/health every second, router.refresh() on ready: true. No nav or content until ready.
No repos configuredBoard, Workers, PlanEmptyState with link to /config. No kanban chrome rendered.
SSE connectedBoardNo indicator -- silence means healthy.
SSE disconnectedBoardReconnection indicator in column header. Exponential backoff 1s to 30s. Re-fetches snapshot on reconnect.
Worker activeIn Progress, Workers pageLiveDuration ticking. Status badge tone: info. animate-enter on first appearance.
Worker pausedIn Progress, Workers pageStatus badge tone: warn. "Resume" button replaces "Pause".
Worker failedIn Progress, Workers DoneStatus badge tone: danger. Error message shown below the row. Restart and Retry buttons available.
Worker mergedMerged column, Workers DoneStatus badge tone: success. Static -- no ticking time. "Report" link appears.
Optimistic drag/dropBoard columnsCard moves immediately. Drop target column gets accent ring on hover. Stale state pruned on next SSE confirmation.
Base CI redBoard filter bar, Merge buttonBaseCiStatus badge shown. Manual Merge button disabled. BaseCiFixButton offered.
Poll pausedBoard filter barPausePollingButton reflects paused state. No automatic issue pickup while paused.
Form submissionAny form with submitbusy 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 StateBadge with role="status" and aria-label -- never raw text or color alone.
  • Error feedback uses InlineError with role="alert" so screen readers announce failures without focus movement.
  • Log regions use role="log" with aria-live="polite" so screen readers can follow streaming output without interrupting the user.
  • Icon-only buttons must have an aria-label or Tooltip providing a text alternative.
  • ConfirmationDialog uses a <dialog> element with focus trap. Backdrop click does not dismiss -- only explicit Cancel or Escape.
  • :focus-visible ring is the sole global focus style -- components must not suppress it.
  • prefers-reduced-motion zeroes all animation and transition durations globally in globals.css. No component may override this with !important.
  • Color is never the sole differentiator for status. Each Badge tone carries a text label.
  • RelativeTime renders a <time> element with a machine-readable dateTime attribute.

Responsive and Platform

Slop is desktop-first and localhost-only. No mobile layout. No responsive breakpoints are defined.

ConstraintDetail
Minimum viewport~1024px wide. Kanban columns have fixed minimum width and scroll horizontally if the window narrows below the board's natural width.
macOS specificsnode-pty requires native bindings; the app does not run on Windows. Scrollbars styled globally: 6px width, border-default track, rounded-full thumb.
Dark-onlyThe @theme block in globals.css defines a single Carbon dark palette. No prefers-color-scheme media query; no theme toggle.
Font loadingnext/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-patternWhy
Toast notifications for errorsErrors on actions belong inline, adjacent to the button that failed. Toasts require the user to look away from the context of failure.
Modal errorsSame reason. ConfirmationDialog is only for destructive pre-confirmation, never for error display.
Optimistic removal without SSE confirmationCards dragged to Ready stay in the UI -- they do not disappear until the daemon confirms the ReadyIssue row.
Global spinners covering the pageUse Skeleton blocks sized to the content they replace. Page-level blocking spinners make the UI feel brittle.
Raw status strings in the UIAlways route through StateBadge / STATE_META. The raw WorkerStatus enum values are internal.
Hardcoded colors or Tailwind palette utilitiesAll 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 celebrationsNo confetti, no "great job" copy, no milestone banners. A merged badge is the terminal success state.
Auto-dismissing alertsrole="alert" errors stay visible until the user attempts the action again. Never set a timeout to clear error state.

Key Flows

Flow 1 -- Alex queues a weekend batch and goes hands-off
Solo developer. Goal: queue issues Friday night, wake up Saturday to merged PRs.
1
Alex opens the Board at localhost:3100. The RepoSwitcher in TabNav shows the active repo. No issues are in the Ready column.
2
Alex clicks the Plan tab. IssuesView loads. He sees five internal issues he filed earlier with no story points.
3
Alex clicks Auto plan (SuggestAllButton). The wand fires suggestIssuePoints for all five issues. Each card shows a Skeleton while inference runs, then snaps to the returned point value with animate-enter.
4
Alex drags all five cards from Backlog to Ready in priority order. Each drag is confirmed by the column's accent drop-target ring. The queue reorders with the first drag establishing position and subsequent drops appending below.
5
Alex opens the AutomationChip popover. He enables Autopilot, Auto-merge, and Auto-review. The chip color shifts to accent to indicate active automation.
6
Alex closes the browser tab. The daemon is still running. He checks the next morning.
Saturday morning: The Board's Merged column shows four cards with success-green badges. The fifth is In Progress with a ticking LiveDuration. One has a warn-yellow "waiting address" badge. Alex opens the Worker Report, reads the review comment, and clicks Address. The worker transitions through in_address back to waiting_merge and ten minutes later to merged.
Failure mode: One worker shows danger-red "failed". Alex expands the log row, sees a CI timeout, and clicks Restart. If the failure recurs, he opens the Worker Report for the full event log and fires a fix manually.
Flow 2 -- Jordan manages a feature sprint for a small team
Engineering lead, 5-person startup. Goal: import a PRD as a batch, refine issues to spec quality, then let Slop implement them in parallel.
1
Jordan opens the Plan tab. In the right sidebar she finds Import Stories. She pastes the PRD text and clicks Import. Slop generates a batch of internal issues. The BatchesAccordion shows the new batch with an issue count badge.
2
Jordan clicks into the batch, opens the first issue, and clicks Refine. The button label changes to "refining" (optimistic). Within seconds the body updates with clarified acceptance criteria and a technical approach note.
3
Jordan clicks the gear icon on the issue. IssueConfigPopover opens. She sets the model to Claude Opus, effort to high, auto-review to true, and accepted review level to "approved". The story points field on the card updates to reflect the higher-effort tier.
4
Repeat for remaining issues. Jordan drags the entire batch to Ready in sprint-priority order.
5
Jordan switches to the Workers tab. Three workers spin up in "implementing" state within the first daemon cycle (bounded by the repo parallelism cap). Each row shows LiveDuration and an info-blue status badge.
6
Jordan uses the ?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.
Forty minutes later: Four PRs have been opened. Two passed CI and were auto-merged with success-green badges and "Report" links. One is in "in_review" with an info badge. One is in "fixing_ci" with a warn badge after a flaky test. Jordan clicks Merge manually on the "waiting merge" worker. The card transitions to "merging" then "merged".
Failure mode: The BaseCiStatus badge in the filter bar turns danger-red. The Board shows "base CI red" and the Merge button is disabled across all workers in "waiting_merge". Jordan clicks BaseCiFixButton. A new base_ci_fix worker spins up. If it succeeds, waiting workers auto-advance. If it fails, Jordan checks the Worker Report and resolves the base branch issue manually.
Parallel execution: Both flows benefit from Slop's parallel worker model -- multiple issues are implemented simultaneously in isolated git worktrees, each with its own agent process. The user does not need to queue sequentially or wait for one issue to finish before the next starts.