Second lint pass caught the standalone setTopic(null) in the early-return
branch of the effect — still synchronous at the top of the useEffect. Fold
that guard into the async load() so every setState landing on this hook
runs inside the async wrapper.
Push right after ec1ddc6 tripped three eslint errors on the same-commit
lint pass. All three are cosmetic in behavior:
- ResearchList.tsx:48 + ResearchCanvas.tsx:64 — setState was called
synchronously at the top of a useEffect. Wrap the body in an inner
async load() and call setState inside it; effect still returns the
cleanup for the alive-flag guard.
- ResearchWizard.tsx:207 — bare apostrophe in JSX text ("workspace's").
Replace with ' to satisfy react/no-unescaped-entities.
Sixth commit of the Research + Loops arc. Replaces the placeholder
Research surface from commit 5 with the full working UI.
New:
- lib/api/research.ts — thin TypeScript client for every /api/research
endpoint (list, get, create, attach/detach agent, start, submit-review,
request-publish, list/approve/reject publish approvals, wizard refine).
- dashboard/ResearchWizard.tsx — 4-step modal (topic prompt → LLM refine
→ outcome kind → agents). The refine step calls
POST /api/research/wizard/refine which streams the workspace's default
LLM and returns {title, description}. Users can accept, edit, or refine
again. Manual fallback if the LLM call errors.
- dashboard/ResearchList.tsx — replaces the stub. Real topic cards with
status pill (standby / processing / reviewing / publishing / published),
outcome-kind chip, and the + button that opens the wizard. Re-fetches
when the parent bumps refreshKey.
- dashboard/ResearchCanvas.tsx — replaces the stub. Selected topic
detail: title, status header, outcome + published_at meta, assigned
agents grid (matched to workspace claws by id), the full description
in a monospace preformatted block, and a state-appropriate primary
action button (Start research → Submit for review → Request publish).
Wired into Dashboard.tsx via a `researchSel` / `researchRefresh` state
pair: selecting a card sets the id, mutations bump the counter so both
list + canvas re-fetch.
All API calls go through the existing /api/[...path] catch-all Next
proxy — no new server routes needed.
Fifth commit of the Research + Loops arc. Lights up the tier rail (both
the chip column and the top-bar crumbs) with two new peer surfaces:
[VIZ] [RESEARCH] [LOOPS] [AGENT] [INFRA]
The stubs (ResearchList / ResearchCanvas / LoopsList / LoopsCanvas) are
self-contained placeholder components — each renders a header with a
"0 topics" / "0 loops" chip and a "coming soon" body plus a canvas hero
with the tier motif. Real functionality (topic cards, wizard, canvas
timelines, publish gate UI) arrives in the next two commits, and slots
in by replacing the stub files without touching Dashboard.tsx again.
Dashboard.tsx changes:
- `Tier` type widened to include `"research" | "loops"`.
- railIcon record grew two SVG entries — book+lens for Research, orbit
arrows for Loops. Matches the placeholder canvas hero.
- TIER_TABS reordered as Viz → Research → Loops → Agent → Infra to
match the mental grouping (conceptual work first, machinery second).
- Top-bar crumbs mirror the rail order.
- New isResearch / isLoops selectors used everywhere the existing tiers
were tested; the Agents-header conditional now also skips for the new
tiers so ResearchList and LoopsList can supply their own headers.
- Sidebar swap short-circuits to ResearchList / LoopsList; canvas swap
short-circuits to ResearchCanvas / LoopsCanvas.