feat(agents): redesign — drawers, design-system foundation, a11y, mobile #559

Merged
charles merged 25 commits from feat/agents-history-drawer into main 2026-04-29 23:31:47 +00:00
Collaborator

Summary

Agents page redesign + design-system foundation + accessibility pass. Agents page is now the reference pattern for the rest of the app (see #558 for the sweep).

Highlights

Agents page redesign

  • Single-list type-group cards, accordion-style. Whole header row clickable to expand.
  • Per-instance drawers: failover history (24h tier-time stacked bar + 7-day events), advanced edit (prompt appendix + notes + dispatch toggle).
  • Inline match-labels editor with chip suggestions.
  • Tier column shows status only — recovery actions (Reset, Resume) only appear when degraded or paused.
  • Mobile: type cards flex-wrap; instances table → stacked card layout below sm:.
  • Add-type CTA hoisted to the page header.
  • Autosave on drawer edits, Save button retired, inline role="status" save indicator.

Design system foundation

  • Extended <Button> primitive: iconOnly, loading, tone="error", leadingIcon / trailingIcon (typed LucideIcon).
  • New <Drawer> primitive: role="dialog" + aria-modal + aria-labelledby + focus trap + focus return + Esc/backdrop close + slide animations. Three call-sites migrated.
  • New useMediaQuery hook (SSR-safe via useSyncExternalStore).
  • lucide-react adopted; emoji glyphs replaced with icons (Pencil, Trash2, Clock, Settings, ChevronRight/Down, Pause/Play, RotateCcw, Plus, X, Fuel, AlertTriangle, ArrowUp/Down).

Accessibility (WCAG 2.1 AA)

  • Drawers are real modal dialogs (focus trap + focus return + labelledby).
  • Type-card header is a semantic <button> with aria-expanded (no more absolute-overlay swallowing content from screen readers).
  • Save status wrapped in role="status" aria-live="polite".
  • Tier glyph + cooldown have sr-only longform labels.
  • Form fields wrapped in <label>; aria-invalid + aria-describedby link errors to inputs.
  • Global prefers-reduced-motion reduces every animation/transition to ~0ms.
  • --ch-color-text-dim bumped (4.0:1 → ~4.9:1 dark, 2.5:1 → ~4.7:1 light).

Server-side cleanup

  • model_override removed end-to-end (UI input gone, server silently drops model on POST/PATCH, response always returns type.default_model). Per-instance overrides — Provider chain on the type is the only model surface.
  • cfg.repos now sources from the watched_repos SQLite table, not agents.json#/repos. Two-systems-fighting bug fixed — DB is the single source of truth (webhook ingress, board fetcher, settings UI all read the same rows).

Test plan

  • just qa — typecheck + lint clean
  • Server tests — 4 pre-existing failures (session JSONL + foreman CRUD), no regressions
  • Agents feature tests — 45 pass
  • Manual smoke: /agents page on desktop + mobile viewports, drawers (history / advanced / type editor) open + close + Esc + focus trap + focus return
  • Manual smoke: provider-chain reorder / remove buttons render with lucide arrows
  • Manual smoke: paused / degraded states render the correct controls
  • Manual smoke: board (/planner/board) populates triage with user-story issues from watched_repos

Follow-ups (separate tickets)

  • #556 — rip stats feature
  • #557 — unify Specs + Planner
  • #558 — responsive + a11y sweep across remaining routes (this PR sets the pattern)

🤖 Generated with Claude Code

## Summary Agents page redesign + design-system foundation + accessibility pass. Agents page is now the reference pattern for the rest of the app (see #558 for the sweep). ## Highlights ### Agents page redesign - Single-list type-group cards, accordion-style. Whole header row clickable to expand. - Per-instance drawers: failover history (24h tier-time stacked bar + 7-day events), advanced edit (prompt appendix + notes + dispatch toggle). - Inline match-labels editor with chip suggestions. - Tier column shows status only — recovery actions (Reset, Resume) only appear when degraded or paused. - Mobile: type cards flex-wrap; instances table → stacked card layout below `sm:`. - Add-type CTA hoisted to the page header. - Autosave on drawer edits, Save button retired, inline `role="status"` save indicator. ### Design system foundation - Extended `<Button>` primitive: `iconOnly`, `loading`, `tone="error"`, `leadingIcon` / `trailingIcon` (typed `LucideIcon`). - New `<Drawer>` primitive: `role="dialog"` + `aria-modal` + `aria-labelledby` + focus trap + focus return + Esc/backdrop close + slide animations. Three call-sites migrated. - New `useMediaQuery` hook (SSR-safe via `useSyncExternalStore`). - `lucide-react` adopted; emoji glyphs replaced with icons (`Pencil`, `Trash2`, `Clock`, `Settings`, `ChevronRight/Down`, `Pause/Play`, `RotateCcw`, `Plus`, `X`, `Fuel`, `AlertTriangle`, `ArrowUp/Down`). ### Accessibility (WCAG 2.1 AA) - Drawers are real modal dialogs (focus trap + focus return + labelledby). - Type-card header is a semantic `<button>` with `aria-expanded` (no more absolute-overlay swallowing content from screen readers). - Save status wrapped in `role="status" aria-live="polite"`. - Tier glyph + cooldown have sr-only longform labels. - Form fields wrapped in `<label>`; `aria-invalid` + `aria-describedby` link errors to inputs. - Global `prefers-reduced-motion` reduces every animation/transition to ~0ms. - `--ch-color-text-dim` bumped (4.0:1 → ~4.9:1 dark, 2.5:1 → ~4.7:1 light). ### Server-side cleanup - `model_override` removed end-to-end (UI input gone, server silently drops `model` on POST/PATCH, response always returns `type.default_model`). Per-instance overrides — Provider chain on the type is the only model surface. - `cfg.repos` now sources from the `watched_repos` SQLite table, not `agents.json#/repos`. Two-systems-fighting bug fixed — DB is the single source of truth (webhook ingress, board fetcher, settings UI all read the same rows). ## Test plan - [x] `just qa` — typecheck + lint clean - [x] Server tests — 4 pre-existing failures (session JSONL + foreman CRUD), no regressions - [x] Agents feature tests — 45 pass - [ ] Manual smoke: `/agents` page on desktop + mobile viewports, drawers (history / advanced / type editor) open + close + Esc + focus trap + focus return - [ ] Manual smoke: provider-chain reorder / remove buttons render with lucide arrows - [ ] Manual smoke: paused / degraded states render the correct controls - [ ] Manual smoke: board (`/planner/board`) populates triage with user-story issues from `watched_repos` ## Follow-ups (separate tickets) - #556 — rip stats feature - #557 — unify Specs + Planner - #558 — responsive + a11y sweep across remaining routes (this PR sets the pattern) 🤖 Generated with [Claude Code](https://claude.com/claude-code)
feat(dashboard): per-instance failover-history drawer
Some checks failed
qa / qa (pull_request) Has been cancelled
qa / dockerfile (pull_request) Has been cancelled
bdd62dbfaa
Closes the M26-3 follow-up flagged in PR #554 — surfaces the
`agent_provider_events` ledger that's been live since M26-1 but had
no UI consumer.

- New `History` button on each instance row alongside `Adv` / `Del`.
- Click → `InstanceHistoryDrawer` slides in from the right (matches
  the `TypeEditorDrawer` pattern: backdrop click + Esc + × button to
  close). Drawer width 640px (narrower than type editor's 900px since
  there's less to fit).
- Top: 24h tier-time stacked bar. `computeTierIntervals` walks the
  ledger chronologically + closes the last interval at `now`. Empty
  ledger → single full-width interval at `current_tier`. Legend
  underneath shows percent + minutes per tier.
- Bottom: chronological 7-day event list. Each row: relative time,
  tier transition (`tier 1 → 2` or `tier 2` for resets), failure
  kind, optional task id.
- Tier colours: tier 1 green, tier 2 amber, tier 3 red. Matches the
  TierBadge palette.
- React-query caches per agent for 10s; no SSE plumbing — the drawer
  is a debugging surface, occasional refetch is fine.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
feat(dashboard): slide-in animation on drawers
Some checks failed
qa / qa (pull_request) Has been cancelled
qa / dockerfile (pull_request) Has been cancelled
706b972a58
Two new keyframes in `apps/web/src/styles/index.css`:
- `ch-slide-in-right` — drawer slides 100% → 0 from the right edge.
- `ch-fade-in` — backdrop fades 0 → 1.

Both run for 200 ms on `cubic-bezier(0.16, 1, 0.3, 1)` (ease-out-expo —
fast start, gentle settle, matches the iOS / Tokyo Night feel).

Applied via utility classes (`ch-drawer-enter`, `ch-backdrop-enter`) to
both `TypeEditorDrawer` (Phase 2) and `InstanceHistoryDrawer` (M26-3
follow-up) for a consistent slide motion across the page.

No exit animation — drawers unmount immediately on close. Could add
later via Base UI's `Dialog` primitive (which handles enter/exit phases)
but the current shape is plain `<aside>` and the pop-out feels fine for
the operator workflow.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
feat(dashboard): drawer leave animation + dead-code cleanup
All checks were successful
qa / dockerfile (pull_request) Successful in 9s
qa / qa (pull_request) Successful in 15m38s
f284976b25
Adds slide-out + fade-out keyframes mirroring the enter animation:

- `ch-slide-out-right` — drawer translates 0 → 100 % off the right edge.
- `ch-fade-out` — backdrop opacity 1 → 0.
- 180 ms (slightly faster than enter's 200 ms — established UX
  convention; exits feel more responsive when shorter).
- Curve: `cubic-bezier(0.7, 0, 0.84, 0)` (ease-in-expo) — accelerates
  away. Mirror of the enter's ease-out-expo.

Implementation:
- Each drawer carries a local `leaving` boolean.
- `triggerClose` wraps `onClose`: sets `leaving = true`, then
  `setTimeout(onClose, 200)` unmounts via parent state.
- Idempotent — `leaving` guard prevents re-fire on rapid Esc /
  backdrop spam.
- All three close paths (Esc, backdrop click, × button) wired to
  `triggerClose` instead of `onClose` directly.
- Applied to both `TypeEditorDrawer` and `InstanceHistoryDrawer`.

Cleanup hitchhikers (lint surfaced once dead state was orphaned):
- Drop `ThresholdsSection` import (unused after Phase 1).
- Drop `healthLoading` / `healthError` destructure (FleetHealthStrip
  consumed them; component removed in Phase 1).
- Drop `Selection` type + `parseSelection` / `encodeSelection` /
  `selection` / `setSelection` — relics of the master-detail master
  view that Phase 2's TypeGroupCard list replaced.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
fix(dashboard): type editor opens with all sections collapsed
Some checks failed
qa / dockerfile (pull_request) Successful in 4s
qa / qa (pull_request) Has been cancelled
43bebbd2d0
Previously Provider was auto-open; everything else collapsed. Operator
prefers a clean slate — open whatever you came to edit. URL deep-link
via the `section` query-param still seeds the matching section open
so links from elsewhere land on the right block.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
fix(dashboard): type group cards collapse by default
All checks were successful
qa / dockerfile (pull_request) Successful in 10s
qa / qa (pull_request) Successful in 2m53s
a457974518
Boss / dev / reviewer / etc. now render with the instances list
collapsed at first paint. Operator clicks the chevron to expand the
group they care about. Matches the type-editor accordion's
"open-when-asked" behaviour from the previous commit.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
fix(dashboard): hide foreman from the agents page
All checks were successful
qa / dockerfile (pull_request) Successful in 13s
qa / qa (pull_request) Successful in 1m54s
678591bf5e
Foreman is the host-mode singleton — runs in-process, no container,
no instances to spawn or destroy. The card was rendering with a
read-only host explainer and an empty body, taking up vertical space
without offering any control. Filter it out of `typeNames`.

Config still lives in `agents.json::types.foreman`; operators who need
to edit prompt / plugins / template can drop into the (future) JSON
escape hatch or hand-edit the file. The host-mode allowlist gate
(M18-4) is unchanged.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
feat(dashboard): Adv into a drawer + icon-only row buttons
All checks were successful
qa / dockerfile (pull_request) Successful in 6s
qa / qa (pull_request) Successful in 1m55s
3e257610d2
- New `InstanceAdvancedDrawer` mirrors `InstanceHistoryDrawer` /
  `TypeEditorDrawer` (slide-in animation, leave guard, Esc/backdrop/×
  closes). Wraps the existing `InstanceAdvancedEdit` body so the
  prompt-appendix + notes textareas now live in a focused panel
  instead of an inline expansion row.
- Drop the inline-row machinery: `expanded` Set state, `toggleAdv`,
  the colSpan adv `<tr>`, and the `Fragment` wrapping that needed.
  Net: ~30 lines lighter.
- New `RowActionButton` — square 28×28 icon-only ghost button. Wraps
  `title` + `aria-label` + `data-testid` for consistency. Variants:
  default ghost or `tone="error"` (red on hover for destructive).
- History → 🕘 (clock).
- Adv edit → ✎ (pencil) — distinct from `⚙` used by type-level edit.
- Delete → 🗑.
- Operator routes the action via tooltip; row stays compact.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
fix(dashboard): stack advanced-edit textareas vertically + grow them
All checks were successful
qa / dockerfile (pull_request) Successful in 6s
qa / qa (pull_request) Successful in 1m43s
8ba229203a
Side-by-side `grid-cols-2` made the textareas tiny on the 640px drawer.
Switch to flex-col so each textarea spans the full width. Bump initial
rows: 10 for the prompt appendix (longer, multi-paragraph), 6 for
notes (shorter operator memo). Both keep `resize-y` so the operator
can drag-resize per their typing.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
feat(dashboard): autosave drawer edits, retire Save button
All checks were successful
qa / dockerfile (pull_request) Successful in 6s
qa / qa (pull_request) Successful in 1m57s
605abd29f2
The Save button was redundant — most page edits were already silent
auto-saves (inline model + match-labels + appendix + notes via
patchAgent on blur; tier reset/pause via dedicated POSTs). Only the
type-level drawer edits still required an explicit Save click.

Now they don't:

- New `useEffect` watches `typesDraft` + `globalsDraft`. 800 ms after
  the last mutation, fires `putAgentConfig` once. Saves cluster
  cleanly: editing a tier provider + cooldown_min back-to-back fires
  one PUT, not two.
- `lastSavedRef` carries the last successfully-saved JSON snapshot;
  the bootstrap effect updates it so the initial load doesn't trigger
  a redundant save.
- Page-header Save button → status indicator only:
  `Saving…` / `Saved` / `Save failed` (red on failure).
- Toast on success removed (would be noisy with autosave); error toast
  preserved so a 400 from the server is hard to miss.
- Server-side validation still rolls the file back on bad save, so
  the operator's edit-in-place draft never wedges the live config.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
fix(dashboard): trim new-agent dialog to 4 fields
All checks were successful
qa / dockerfile (pull_request) Successful in 6s
qa / qa (pull_request) Successful in 1m53s
8aaa4bae8d
Drop Type dropdown + Model override from the create-instance dialog.
Type is implicit from the `+ Instance` button on the type-group card
(passed via `presetType`); model defaults to the type's `default_model`
and the operator overrides inline on the row after creation.

Remaining fields: Name, Match labels, Prompt appendix, Notes — the
genuinely-per-instance overrides operators care about at creation time.

Title now reads "New <type> instance" (with the type as an accent-coloured
code span) so the operator can confirm which type they're spawning into
without a dropdown.

`AgentEditor` props slimmed: `types`, `models`, `initial` removed; only
`open`, `onClose`, `presetType` survive. Props that surfaced edit-mode
were dead anyway since #554 (edit lives inline + in the Adv drawer).
Also drops the now-unused `availableTypes` + `models` derived values
in `AgentsRoute`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
fix(dashboard): drop Model column, move model_override into Adv drawer
All checks were successful
qa / dockerfile (pull_request) Successful in 11s
qa / qa (pull_request) Successful in 1m54s
eaf9dce90f
Type-level Provider chain editor is the source of truth for model
selection — the per-row Model column duplicated it. Tier badge already
displays the active model id in the same row, so the column is
redundant.

- Drop the Model `<th>` + per-row `InlineModelEdit` cell.
- Drop the `InlineModelEdit` component (no consumer left).
- Add a `Model override` input to `InstanceAdvancedEdit` (Adv drawer
  body). Free-text, blur-commits, empty value clears the override
  and inherits `type.default_model`. Helper text explains it's rare.

Operator workflow:
- Same model for all instances of a type → set once on Provider chain
  tier 1 (drawer).
- One instance pinned to a different model → click ✎ on the row,
  type into Model override.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
refactor(agents): remove model_override completely
All checks were successful
qa / dockerfile (pull_request) Successful in 6s
qa / qa (pull_request) Successful in 1m59s
f411430bed
Per-instance Provider chain is now the only model surface. The model
field was a dead UI input — Provider chain on the type already covers
the use case via tier 1.

Server: response always reports `model: type.default_model` and
`model_override: null`. POST/PATCH silently drop the `model` key.
mergeAgent ignores the row column.

Web: drop ModelCombobox import, drop model from Create/Patch payloads.

Tests: rewrite the 4 model-override tests to assert the field is
ignored on PATCH/POST and that resolveAgent always returns
type.default_model.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
fix(dashboard): drop Queue + Status columns from instances table
Some checks failed
qa / dockerfile (pull_request) Successful in 5s
qa / qa (pull_request) Has been cancelled
4368e2afb7
Both surface live worker state — useful on the monitor page, noise on
the agents config page. Tier already conveys health via the failover
state machine.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
fix(dashboard): make whole type-card header clickable to expand
All checks were successful
qa / dockerfile (pull_request) Successful in 7s
qa / qa (pull_request) Successful in 1m57s
f77ce0d570
Absolute-positioned overlay button covers the full row; left content
gets pointer-events-none so clicks fall through to the toggle. Edit
type button stays clickable as a sibling above the overlay.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
feat(design-system): consolidate Button + adopt lucide-react icons
All checks were successful
qa / dockerfile (pull_request) Successful in 13s
qa / qa (pull_request) Successful in 2m40s
26e72f636d
Foundation primitive — every clickable in the app should now route
through `<Button>`. Agents page is the migration template; remaining
features sweep in follow-up PRs.

Button changes:
- Add `iconOnly` modifier (square aspect, sm/md/lg → 28/36/44 px)
- Add `loading` prop (Loader2 spinner, disables click, aria-busy)
- Add `tone="error"` (red focus ring + hover for destructive actions)
- Add `leadingIcon` / `trailingIcon` slots typed as `LucideIcon`
- `cursor-pointer` baseline; per-variant focus-visible rings

Icon library: lucide-react (~1-2 kB per icon, tree-shakable, MIT).
No other icon deps needed.

Agents page migration:
- RowActionButton primitive removed → `<Button variant="ghost" iconOnly>`
- 🕘 ✎ 🗑 → Clock / Pencil / Trash2 (row actions)
- ⚙ → Settings (Edit type)
- ▶ ▼ → ChevronRight / ChevronDown (collapse)
- ⏸ ▶ → Pause / Play (tier badge pause toggle)
- ↺ → RotateCcw (reset tier)
- × → X (drawer + dialog close, chip remove, env row remove, tier remove)
- ⚠ → AlertTriangle (provider chain validation)

Tier number glyphs (① ② ③) kept as semantic numbered markers.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
feat(agents): hoist add-type CTA to header + mobile responsive layout
All checks were successful
qa / dockerfile (pull_request) Successful in 11s
qa / qa (pull_request) Successful in 1m51s
7091db13a4
Page header:
- Move `+ Add agent type` button to title row (right-aligned)
- Hide save status on mobile (autosave is fast enough that the indicator
  is mostly cosmetic — drop the noise on small viewports)
- Compact label on mobile: "Add type" instead of "Add agent type"

Type card header:
- `flex-wrap` so summary chip + instance count fall to a second line
  cleanly on narrow viewports
- Instance count → numeric badge on mobile (×N), full text on desktop
- Edit-type button: icon-only on mobile, icon+label on desktop
- Tighter horizontal padding (px-3) on mobile

Instances surface:
- Desktop: keep table layout
- Mobile: stack each instance as a card (name + actions row, then tier
  badge, match labels, last-active line) — no more squashed columns
- Shared action-button render avoids duplication

Container padding: p-3 on mobile, p-4 on sm+.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
fix(agents): move dispatch-pause to drawer, clean up tier column
All checks were successful
qa / dockerfile (pull_request) Successful in 5s
qa / qa (pull_request) Successful in 1m48s
76eed90526
The pause button on the row read as "pause running task" — confusing
on idle agents (nothing to pause), and pausing flipped the row to a
red ✕ + recovery icons that looked like a crash state.

Tier column now shows status only:
- healthy: tier number + active model (no controls)
- degraded (tier > 1): adds the reset button
- paused: red "Paused" pill + Resume + Reset (recovery actions only)

Pause/disable moves into the Advanced drawer as a labelled toggle
("Dispatch · Enabled/Disabled"). It's a rare ops action — drawer has
the room to explain what it actually does (stop accepting new tasks).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three fixes from PR review.

1. Autosave race (data loss):
   - Mutation snapshots payload at fire-time, not at resolve-time, so
     edits made between PUT fire and resolve can't be rolled back into
     `lastSavedRef`.
   - Bootstrap effect now gates on `inFlightSaveRef` so a cache
     invalidation triggered by another query (or a refetch) mid-save
     can't stomp the user's in-progress edits.
   - Drop the redundant `invalidateQueries(["agent-config"])` on
     onSuccess — the stale-while-revalidate refetch already brings the
     server snapshot back; the explicit invalidate just doubled writes.

2. Duplicate testids in DOM:
   - `InstancesTable` previously rendered both desktop table and mobile
     stack with `hidden sm:*` classes. Both branches lived in the DOM,
     so every `agent-row-*`, `agent-inspect-*`, etc. testid duplicated
     and Playwright strict locators would throw.
   - Add `useMediaQuery` hook (SSR-safe via useSyncExternalStore) and
     mount only the active layout.

3. Drawer mutual exclusion:
   - Three independent state vars (editingTypeName, inspectingInstance,
     advEditingInstance) let drawers stack on top of each other.
   - Collapse to a single tagged-union state — opening any drawer
     closes the other two.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
UX polish from PR review.

- Tier column displays status only when not paused. Reset moves into
  the row's action cluster (next to History / Edit / Delete) so the eye
  doesn't confuse a Reset button for a status glyph — same trap the
  user hit with the previous red ✕.
- Replace  emoji with the lucide `Fuel` icon (last emoji left after
  the icon migration).
- Provider summary chip on the type-card header now truncates with
  ellipsis at `max-w-[55vw]` on mobile / `max-w-[280px]` on sm+, so
  long provider · model strings don't push the header to a third line
  on narrow viewports.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
chore: cleanup loose ends from PR review
All checks were successful
qa / dockerfile (pull_request) Successful in 5s
qa / qa (pull_request) Successful in 1m54s
6e04453d50
Three minor fixes:

1. DeleteAgentDialog now uses the new `tone="error"` Button modifier
   (was raw `border-error text-error` className override). Also wires
   `loading={mutation.isPending}` so the spinner replaces the text.

2. ProviderSection up/down buttons migrate from raw Unicode arrows to
   lucide `ArrowUp` / `ArrowDown` via the foundation Button. Closes the
   last spot where the lucide migration was inconsistent.

3. `model_override` field removed from the GET /agents response shape
   (was always null), from `AgentsListEntry` on the web side, and from
   the e2e fixture. PATCH response now forces `model: null` so legacy
   rows with stale non-null `model` columns don't leak through. The
   column itself is left in the DB — it's dead but harmless, and a
   migration to drop it is out of scope.

Tests updated to assert `model_override` is undefined on the response.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three drawers in agents.tsx duplicated the same scaffold (~50 LOC × 3):
leaving state, triggerClose with setTimeout, ESC listener, backdrop,
<aside> shell, header with close button. Extract to a reusable
component and migrate.

A11y wins from the audit, applied once in the primitive:

- role="dialog" + aria-modal="true" + aria-labelledby on the header
  heading (was a plain <aside> — screen readers got no dialog cue)
- Focus trap: Tab / Shift+Tab cycle inside the drawer
- Focus return: previously-focused element regains focus on close
  (captured at open via document.activeElement)
- ESC + backdrop click already covered, kept

Migrate TypeEditorDrawer (900px), InstanceHistoryDrawer (640px),
InstanceAdvancedDrawer (640px). Width is per-drawer via a `width`
prop; default 640.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Four findings from the a11y audit, all touching the agents page or
shared CSS.

1. Type-card header — drop the absolute-overlay button and convert
   the disclosure region to a real <button>. The previous layout had
   a position:absolute toggle covering the row with sibling
   pointer-events-none content; screen readers announced only the
   aria-label and skipped the visible chip content. The new layout
   has the chevron+name+chips+count cluster as the actual button,
   sibling Edit-type Button outside it. Tab focus + visible focus
   ring + correct SR reading order.

2. Save status live region — wrap the Saving…/Saved/Save failed
   indicator in role="status" aria-live="polite" so SR users hear
   autosave failures. The error variant is now also visible on
   mobile (was hidden:sm:flex previously); Saving and Saved stay
   sm-only because they're transient cosmetic feedback.

3. Tier glyph + cooldown — circled-digit Unicode (①②③) is read
   inconsistently across SR engines. Add an sr-only "tier N" span
   alongside the visible glyph, mark the glyph aria-hidden. Cooldown
   "(5m)" gets an sr-only "5 minutes until retry" longform.

4. prefers-reduced-motion — global @media block in index.css clamps
   every animation/transition to ~0ms so vestibular users don't get
   slide-in/spin/pulse motion. Required !important to win against
   per-element animation utilities; biome-ignore documents why.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
fix(a11y): form field semantics in AgentEditor + AddAgentTypeWizard
All checks were successful
qa / dockerfile (pull_request) Successful in 5s
qa / qa (pull_request) Successful in 1m56s
b72eebb45f
Form-related WCAG findings from the audit.

AgentEditor:
- Field wrapper changes from <div> to <label> so screen readers
  programmatically associate the visible label with the contained
  input. No htmlFor/id needed — the input is a descendant.
- Name input gets aria-invalid + aria-describedby pointing at the
  error <p> when an error is present, so SR users hear the validation
  message after submit failure.

AddAgentTypeWizard (Step2):
- Name + Forgejo-user inputs get aria-invalid + aria-describedby
  pointing at their respective inline error spans (the spans now
  carry stable IDs).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
fix(a11y): bump --ch-color-text-dim to clear WCAG 2.1 AA
All checks were successful
qa / dockerfile (pull_request) Successful in 6s
qa / qa (pull_request) Successful in 1m54s
9b61e4aa32
Token-only change. The previous values failed AA contrast for body
text:

- Dark: #565f89 on #1a1b26 — ~4.0:1 (fails 4.5:1)
- Light: #9aa5ce on #e9ecf2 — ~2.5:1 (fails 4.5:1)

Bumped both. New values:

- Dark:  #7d8aae (~4.9:1 on #1a1b26)
- Light: #5d6a92 (~4.7:1 on #e9ecf2)

`text-dim` is the most-faded text tier — used for "any" placeholders,
"No data."/"Loading…" hints, "skipped"/"pending" stage glyphs and
115 other call sites across the web app. Single-token change, no
component edits needed.

In the light theme, "dim" goes darker than "muted" because dimming a
foreground on a light surface means moving toward the bg, which
sacrifices contrast. The new value is less saturated than text-muted
so it still reads as visually faded.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
fix(config): cfg.repos sources from watched_repos DB, not agents.json
All checks were successful
qa / dockerfile (pull_request) Successful in 4s
qa / qa (pull_request) Successful in 15m23s
ec81e295f5
`cfg.repos` and `cfg.repoBindings` now derive from the SQLite
`watched_repos` table, not from `agents.json#/repos`. The DB has been
the dispatch / settings source of truth since F4 (#485) but the
loader still populated cfg.repos from the file. After the F4 boot
migration rewrote agents.json without the field, the board (and
every other reader of cfg.repos) silently saw an empty list and
returned zero results.

Two-systems-fighting bug, classic. Pick one source of truth, rip the
other.

Behaviour:
- First boot with `agents.json#/repos` set + DB empty → migration
  seeds the DB, then cfg.repos comes from the freshly-seeded DB
  (effectively the same data).
- All subsequent boots → cfg.repos comes from the DB. agents.json
  field is ignored (existing warning still fires if operators
  hand-edit it back in).
- Disabled rows (`enabled = 0`) no longer appear in cfg.repos —
  they were already excluded from dispatch but cfg.repos is now
  honest about it.

Touched only the loader; every reader (board, pipeline, slash
commands, janitor, breakdown gate, MCP wiring) continues to use
the same `cfg.repos` / `cfg.repoBindings` API.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
charles deleted branch feat/agents-history-drawer 2026-04-29 23:31:48 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
charles/claude-hooks!559
No description provided.