## Context
We want a one-click path from the right-panel task detail into the
claude-agent-service runner, without the user copy-pasting the bead id
into a CLI or another tab. The runner expects a self-contained prompt
that restates the bead id, title, description, acceptance criteria, and
the guard rails the agent must operate under (no push, no file edits,
no terraform/kubectl/helm). The prompt template lives in `src/lib/` so
it can be tested and reused from the server-side dispatch route.
The right-panel button needs to:
- Only appear when the bead is actionable (`open` or `in_progress`).
- Disable itself while the claude-agent-service is already busy (the
service has a global `asyncio.Lock` — parallel dispatches 409).
- Disable itself when the bead lacks acceptance criteria. An agent that
doesn't know what "done" looks like burns budget and closes nothing.
- Surface the resulting `job_id` or any 409/error back to the user.
The project has no toast library (no `sonner`, no `react-hot-toast`), so
we render status inline under the button rather than pulling in a new
dependency for this single surface.
## This change
- `src/lib/dispatch-prompt.ts` exports `buildDispatchPrompt(bead)` which
produces the exact prompt the agent runner expects. Bead id, priority
(`P<n>`), issue type, description, and acceptance criteria are
interpolated; `<job_id>` stays a literal placeholder because the agent
only learns its own id at runtime (env var).
- `src/components/shared/dispatch-button.tsx` is a focused client
component with three responsibilities:
1. Poll `GET /api/agent-status` every 5 s while the panel is open
(plus an initial fetch on mount), mirror `busy` into local state.
2. On click, `POST /api/agent-dispatch` with `{taskId}`; branch on
200 / 409 / other.
3. Render an inline status line under the button (`text-xs`, tone
driven by `ok | info | error`) — no toast dep required.
The poll interval self-clears on unmount so closing the panel stops
network traffic.
- `src/components/shared/thread-drawer.tsx` renders `<DispatchButton>`
alongside the existing "Edit task" button in the summary section,
wrapped in a `flex-wrap` so the two controls reflow on narrow panes.
- Registers two new tests in `package.json`'s enumerated test script.
## What is NOT in this change
- The `/api/agent-dispatch` and `/api/agent-status` routes themselves —
those land in the next commit. The button calls them but the server
side is intentionally a separate step so each commit can be reviewed
in isolation.
- No real toast system is introduced; inline status is sufficient.
- No change to how task state transitions on dispatch. The agent itself
is expected to run `bd update --claim` / `bd close` via the prompt's
operating rules.
## Test Plan
### Automated
```
$ node --import tsx --test tests/lib/dispatch-prompt.test.ts \
tests/components/shared/dispatch-button.test.tsx
# tests 7 pass 7 fail 0
```
Covers:
- Bead id appears in opening paragraph and in both `bd note` / `bd close`
commands.
- Priority rendered as `P<n>`, issue type echoed.
- Description and acceptance criteria quoted verbatim when present.
- `(no description)` / `(no acceptance criteria)` fallbacks when null.
- Guard rails block present (no terraform/kubectl/helm, workspace bd
path, `bd update … --status blocked` fallback).
- DispatchButton module loads and exports both named and default.
`npm run typecheck` shows only the pre-existing `OrchestratorChatMessage`
type gap in `left-panel.tsx` that reproduces on untouched `main`.
### Manual Verification
1. `npm install`
2. `npm run dev`
3. Open `http://localhost:3000/?task=<some-open-bead-id>`
4. Expected: "Dispatch to Agent" button next to "Edit task" in the
right-panel summary section.
5. Button disabled on beads with `status in {closed, blocked, deferred}`
(they don't render the button at all).
6. Button disabled on beads missing acceptance criteria, with tooltip
"Task is missing acceptance criteria — cannot dispatch.".
7. Click: UI flips to "Dispatching…"; once the next commit is merged,
the agent-dispatch route will surface a `job_id` (today it returns
404 which renders as "Dispatch failed (HTTP 404)").
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
## Context
The left-panel task list surfaces every bead regardless of whether the task
has been specified thoroughly enough for an agent to pick it up. Tasks with
empty `acceptance_criteria` or very short descriptions are low-signal noise
that muddles the navigation spine — especially now that we plan to dispatch
tasks to Claude agents (which need concrete instructions to succeed).
Epics are deliberately exempt: their role is grouping, not execution, so
requiring acceptance criteria / long descriptions on epics would hide the
entire backbone of the navigation.
## This change
- Plumbs `acceptance_criteria` end-to-end: adds the optional field on
`BeadIssue`, and reads it both from `.beads/issues.jsonl` (parser) and
Dolt SQL (`read-issues-dolt.ts` row shape + normalizer).
- Extends `LeftPanelFilters` with `hideNoAcceptance` and
`hideShortDescription` (both default `true`) in the source-of-truth
`hooks/use-url-state.ts` and in the re-exported shadow type on
`components/shared/left-panel.tsx`.
- Updates `isTaskMatch` in both `left-panel.tsx` and `left-panel-new.tsx`
to skip non-epic tasks lacking acceptance criteria or with a description
shorter than `SHORT_DESCRIPTION_MIN_LENGTH` (200 chars). Epics bypass
both filters via the `issue_type === 'epic'` guard.
- Exposes `isTaskMatch` from `left-panel.tsx` so the filter tests can
assert behavior directly (previously only `shouldHideEpicEntry` was
exported).
- Adds two checkboxes under the existing "Hide Closed" button in both
left-panel variants (legacy `left-panel.tsx` and the one unified-shell
currently wires up — `left-panel-new.tsx`).
- Seeds both new filter flags as `true` in the `UnifiedShell` default
state so fresh sessions see the high-signal view without toggling.
## What is NOT in this change
- No mutation of `bd` / CLI behavior. Filters are purely UI-level.
- No localStorage persistence for the two new flags — existing
`hideClosed` is also React-only, so parity is preserved. If/when we
persist any of these, all three move together.
- No change to the `metadata.acceptance` path used by `kanban.ts`'s
`hasQualitySignal` — that's a separate signal with its own callers.
## Test Plan
### Automated
Tests run from `/home/wizard/code/beadboard`:
```
$ node --import tsx --test tests/components/shared/left-panel-filtering.test.ts
# tests 15
# pass 15
# fail 0
# duration_ms 604
```
All 15 filter cases pass: 6 pre-existing `shouldHideEpicEntry` cases plus
9 new `isTaskMatch` cases covering acceptance-criteria-empty hides,
acceptance-criteria-disabled shows, epic exemption, 199/200-char
description boundary, null description, and short-description flag
disabled.
Related suites still green:
```
$ node --import tsx --test tests/components/shared/left-panel.test.tsx \
tests/components/shared/unified-shell-hide-closed-contract.test.ts
# tests 7 pass 7 fail 0
```
Pre-existing failures in `tests/hooks/url-state-integration.test.ts`
(`view=activity` cases) and one pre-existing typecheck error in
`left-panel.tsx` thread prop are unrelated — both reproduce on `main`
before this change.
### Manual Verification
1. `npm install`
2. `npm run dev`
3. Open `http://localhost:3000` with bd project loaded.
4. Expected: two new checkboxes appear under "Hide Closed" in the left
sidebar — "Hide tasks without acceptance criteria" and
"Hide tasks with short description (<200 chars)". Both checked by
default.
5. Toggle each off. Expected: additional beads appear in epic expansions
(tasks that were previously hidden because they lack quality signal).
6. Confirm epics remain visible regardless of the checkbox state.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Various supporting changes made during the assign archetypes feature development:
- Added contextual-right-panel.tsx and swarm-command-feed.tsx
- Updated activity-panel.tsx with new features
- UI improvements to left-panel, mobile-nav
- Test updates for url-state-integration, mobile-nav, top-bar
- Package.json updates for dependencies
- Global CSS refinements
These changes support the main assign archetypes feature but are
not directly part of its core functionality.
STORY:
With the shell layout complete, we needed the actual content for each view.
Three agents worked in parallel on the card components that would populate
the Social and Swarm views, plus integrating the existing graph into the shell.
COLLABORATION:
Agent bb-98c (social-card-builder) created SocialCard:
- Task ID with teal styling
- UNLOCKS section (green) showing what this task unblocks
- BLOCKS section (amber) showing what's blocking this task
- Agent avatars with liveness glow
- View-jump icons for quick navigation
Agent bb-nuy (swarm-card-builder) created SwarmCard:
- Agent roster with liveness indicators
- Progress bar (ASCII block format: ████████░░░░)
- Attention items with warning styling
- View-jump icons
Agent bb-54x (graph-integrator) integrated WorkflowGraph:
- Created GraphView wrapper with Flow/Overview tabs
- Wired into UnifiedShell when view=graph
- Connected taskId to selectedId for URL sync
- Connected graphTab to URL state
DELIVERABLES:
- src/components/social/social-card.tsx: Task card for activity feed
- src/components/swarm/swarm-card.tsx: Swarm health card
- src/components/graph/graph-view.tsx: Graph wrapper with tabs
- src/components/shared/mobile-nav.tsx: Bottom tab bar
- Tests for all components
VERIFICATION:
- npm run typecheck: PASS
- npm run lint: PASS
- npm run test: PASS
CLOSES: bb-ui2.11, bb-ui2.16, bb-ui2.20
STORY:
Phase 1 of the Unified UX epic required a complete 3-panel shell layout
with responsive behavior across mobile, tablet, and desktop breakpoints.
The existing page structure was fragmented - we needed a cohesive shell.
COLLABORATION:
Three agents (bb-5am, bb-dwz, bb-3dv) worked in parallel on:
- TopBar: View tabs (Social/Graph/Swarm) with active states, filter input
- LeftPanel: Channel tree navigation with epic filtering, responsive collapse
- RightPanel: Detail strip with sidebar (desktop) / drawer (tablet/mobile) modes
We encountered a hydration mismatch error on mobile/tablet because
useResponsive was returning different values on server vs client.
Fixed by defaulting to desktop on server and only updating after mount.
Mobile navigation (bb-ui2.27) added:
- Hamburger menu for left panel access on mobile/tablet
- Bottom tab bar for thumb-friendly view switching
DELIVERABLES:
- src/components/shared/top-bar.tsx: TopBar with view tabs + hamburger
- src/components/shared/left-panel.tsx: Epic tree with expand/collapse
- src/components/shared/right-panel.tsx: Responsive sidebar/drawer
- src/components/shared/unified-shell.tsx: Main 3-panel grid layout
- src/components/shared/mobile-nav.tsx: Bottom tab bar for mobile
- src/hooks/use-responsive.ts: Breakpoint detection (mobile/tablet/desktop)
- Tests for all components
VERIFICATION:
- npm run typecheck: PASS
- npm run lint: PASS
- npm run test: PASS
CLOSES: bb-ui2.6, bb-ui2.7, bb-ui2.8, bb-ui2.9, bb-ui2.27
STORY:
The Sessions Hub needed clear visual distinction between healthy
agents and those in trouble. Users couldn't quickly identify stuck
or dead agents in the control center view.
COLLABORATION:
We added 'stuck' and 'dead' states to the AgentSessionState type,
created deriveSessionState() with Zero-Failure-Check priority,
and implemented restrained visual treatments:
- stuck: pulsing red border (ring-2 ring-red-500 animate-pulse)
- dead: strong ghosting (opacity-40 grayscale)
- evicted: milder ghosting (opacity-60 grayscale-[0.5])
Session cards now display STUCK/OFFLINE badges with aria-labels
for accessibility.
DELIVERABLES:
- AgentSessionState extended with stuck/dead states
- deriveSessionState() derives from ZFC state priority
- Visual treatments for stuck/dead/evicted
- Accessible badges with aria-label
TESTS:
- tests/lib/agent-sessions-state.test.ts: 6/6 PASS
- tests/components/shared/status-utils-visual.test.ts: 4/4 PASS
- tests/components/sessions/session-feed-card-state.test.tsx: 4/4 PASS
CLOSES: bb-buff.3.2
BLOCKS: bb-buff.3.3