Skip to content

feat(dashboard): file-mode Epic display (epic_ref + file:// links)#199

Merged
thejustinwalsh merged 7 commits into
mainfrom
middle-issue-187
Jun 3, 2026
Merged

feat(dashboard): file-mode Epic display (epic_ref + file:// links)#199
thejustinwalsh merged 7 commits into
mainfrom
middle-issue-187

Conversation

@thejustinwalsh

@thejustinwalsh thejustinwalsh commented Jun 3, 2026

Copy link
Copy Markdown
Owner

Summary

Closes #187

Plumbs the file-mode Epic identifier (epic_ref, a slug) through the dashboard's /api/* read plane so file-mode workflows (epic_number IS NULL, epic_ref = '<slug>') render a file://planning/epics/<slug>.md link instead of a blank cell. github-mode rows render exactly as before (plain #N).

The epic_ref column foundation (PR #188, feat/file-backed-epic-store) has merged to main, so this PR targets main directly. The diff is the 5 dashboard/dispatcher display commits only. (This supersedes the now-closed #189, which was stacked on #188's branch and went stale when that base merged.)

Acceptance criteria

  • db-deps.ts selects epic_ref and the /api/* response shape gains epicRef: string | null — proven end-to-end by a real Bun.serve integration test that boots the server against a migrated db and issues a GET to /api/repos/:repo + /api/sessions/:session, asserting a file-mode row surfaces epicRef: api.test.ts
  • The UI renders the slug as a file://planning/epics/<slug>.md link when epicRef is set and epicNumber is null, with no GitHub link in file mode — EpicRef component, used in the IN-FLIGHT runner row + Inspector header; verified by epic-ref.test.tsx
  • Bridge events emit epicRef alongside epicbridge.ts repo-channel nudge; verified by a live-SSE test in sse.test.ts
  • No behavior change for github-mode rows (plain #N, byte-identical) — verified by epic-ref.test.tsx (toBe("#42")) and api.test.ts

What changed

  • packages/dispatcher/src/workflow-record.tsgetWorkflow reads the migration-008 epic_ref column into a new WorkflowRecord.epicRef (read path only; the dispatch write path is untouched — file-mode dispatch owns it).
  • packages/dashboard/src/db-deps.ts, wire.ts — select epic_ref; RunnerSummary / RunnerPanel gain epicRef: string | null.
  • packages/dashboard/src/app/components/EpicRef.tsx (new) — github → plain #N; file → file:// slug link; blank/null → fallback. Used in RunnerRow.tsx + Inspector.tsx.
  • packages/dashboard/src/bridge.ts — the repo-channel workflow nudge emits epicRef.

Why these changes

The issue cites db-deps.ts:83 — the /api/* read plane that reads the workflows.epic_ref column. AC4 is the load-bearing constraint: github-mode rows render plain #N text today (no anchor exists), so EpicRef preserves that byte-for-byte and adds the file:// link only for file mode — adding a GitHub anchor would itself be a behavior change. The slug is encodeURIComponent-collapsed into one safe path segment so a malformed value can't traverse out of planning/epics/ or inject markup. Full reasoning in planning/issues/187/decisions.md, distilled into inline review comments on this PR.

Verification

All gates green locally against current main (re-run after rebasing onto the merged foundation):

  • bun run typecheck — clean.
  • bun run lint — clean.
  • bun test1174 pass, 0 fail (full suite). Affected: packages/dashboard/** + packages/dispatcher/test/workflow-record.test.ts → 136 pass.

Per-criterion evidence:

  • Integration (live API): packages/dashboard/test/api.test.ts boots a real Bun.serve over a migrated temp db; a seeded file-mode row (epic_number NULL, epic_ref slug) surfaces epicRef over GET /api/repos/:repo (inFlight[].epicRef) and GET /api/sessions/:session (RunnerPanel.epicRef); a github-mode row carries epic: 7, epicRef: null.
  • Component: packages/dashboard/test/epic-ref.test.tsx — all three render modes (github #N no-anchor, file file:// slug link, both-null fallback), blank/whitespace slug → fallback, slug encoding (a/../ba%2F..%2Fb, <script> neutralized), plus RunnerRow/Inspector rendering.
  • Bridge: packages/dashboard/test/sse.test.ts — a live SSE connection asserts the workflow nudge carries epicRef (github row → null; file row → the slug).
  • Read path: packages/dispatcher/test/workflow-record.test.tsgetWorkflow round-trips epic_ref (slug / number-string / null).

Self-review

Ran an adversarial clean-eyes review pass over the diff before marking ready. It confirmed the diff clean against the ACs and surfaced one in-blast-radius hardening — a blank (""/whitespace) epicRef would have rendered an empty-labelled file://…/.md link — now guarded (trimmed-truthy slug → fallback) with tests. Adjacent surfaces it flagged are out of scope and tracked below.

Stumbling points

Suggested CLAUDE.md updates

A note (root or packages/dispatcher/CLAUDE.md) that issues which are phases of a multi-PR rollout are dispatched on a branch stacked on the foundation PR's branch: retarget the PR base to that branch rather than main. And — the lesson this re-dispatch added — if the foundation merges and deletes its branch before the stacked PR is ready, GitHub closes the stacked PR (it can't reopen, base is gone); the recovery is to git rebase --onto origin/main <foundation-boundary> and open a fresh PR against main.

Follow-ups (tracked by the file-backed Epic store rollout plan)

The remaining file-mode surfaces are owned by the file-backed Epic store rollout, so filing standalone issues would duplicate planned scope:

  • Queue tab / /control/events (main.ts broadcastWorkflowControlWorkflowFrame) still shows for file-mode rows — a separate (control-plane) data path the issue doesn't cite.
  • NEXT UP (db-deps.ts, recommender state-issue ranking) collapses a file-mode slug to #0 via Number(slug) || 0, and Repos.tsx keys the list on n.epic — a latent React duplicate-key collision once two file-mode rows both map to #0. Whoever wires NEXT UP for file mode should carry the raw ref through NextUpItem and key on rank.
  • createWorkflowRecord epic_ref write path — the file-mode dispatch write path.

Out of scope

  • File-mode dispatch endpoint via the dashboard UI (CLI mm dispatch --epic <slug> is the path for now).
  • Sub-issue rendering from the Epic file.

Summary by CodeRabbit

  • New Features

    • Epic references now render as clickable file links in file mode (e.g., file://planning/epics/slug.md) and as plain text in GitHub mode (e.g., #N).
    • Inspector and runner row displays updated to show epic references appropriately based on operational mode.
  • Tests

    • Added test coverage for epic reference rendering, link construction, URL encoding, and fallback behavior.
    • Verified epic reference handling across file and GitHub modes.

getWorkflow now reads the migration-008 epic_ref column into a new
WorkflowRecord.epicRef field (slug in file mode, String(epic_number) or
null in github mode). Read accessor only — the dispatch write path is
unchanged. Unblocks the dashboard bridge emitting epicRef (#187).
…ridge

db-deps selects epic_ref and projects it onto RunnerSummary and
RunnerPanel (epicRef: string | null); the repo-channel workflow nudge in
bridge.ts emits epicRef alongside epic. github-mode rows are unaffected
(epic_number drives their rendering). (#187 AC1, AC3)
New EpicRef component: github mode → plain #N (unchanged, no anchor —
AC4); file mode → the slug as a file://planning/epics/<slug>.md link (no
GitHub link); both-null → caller fallback. Wired into the IN-FLIGHT
runner row and the Inspector header, the db-deps.ts:83-fed surfaces the
issue cites. Slug is URL-encoded into one safe path segment. (#187 AC2)
EpicRef component (all three render modes + slug encoding), RunnerRow /
Inspector rendering, the bridge file-mode emit, and the integration path
(real Bun.serve + migrated db): a file-mode workflow row surfaces epicRef
over /api/repos/:repo and /api/sessions/:session. seedWorkflow gains an
epicRef option. (#187)
…/ link

A present-but-blank slug (empty or whitespace) took the file-mode branch
and rendered <a href="file://planning/epics/.md"></a>. Guard on a
trimmed-truthy slug so it falls through to the fallback, and trim
surrounding whitespace in the label + href. Self-review hardening within
the EpicRef blast radius. (#187)
@coderabbitai

coderabbitai Bot commented Jun 3, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This PR implements file-mode epic display in the dashboard by introducing an EpicRef React component that conditionally renders epic references—as plain #N text for GitHub mode or as file://planning/epics/<slug>.md links for file mode—and plumbs the epicRef field through the dispatcher, database, bridge, and API layers with comprehensive tests.

Changes

File-Mode Epic Display Feature

Layer / File(s) Summary
Type Contracts for Epic Reference
packages/dashboard/src/wire.ts
RunnerSummary and RunnerPanel types gain epicRef: string | null field to represent the canonical epic reference for rendering.
EpicRef Component and Rendering Logic
packages/dashboard/src/app/components/EpicRef.tsx, packages/dashboard/test/epic-ref.test.tsx
epicFileHref(slug) constructs file:// URLs with percent-encoded slugs. EpicRef component renders #N for GitHub mode (when epicNumber is set), file://planning/epics/<slug>.md links for file mode, or a fallback value when both epic values are absent or blank. Tests cover rendering in RunnerRow and Inspector contexts, path encoding safety, whitespace trimming, and fallback behavior.
Dispatcher Workflow Epic Reference Backend
packages/dispatcher/src/workflow-record.ts, packages/dispatcher/test/workflow-record.test.ts
WorkflowRecord exposes epicRef: string | null field read from the database epic_ref column. getWorkflow() maps row.epic_ref into the returned record. Tests verify null epicRef on creation, explicit DB updates, and file-mode workflows with null epicNumber but non-null epicRef.
Dashboard Database Epic Reference Projection
packages/dashboard/src/db-deps.ts
Dashboard db-deps.ts selects the epic_ref column, adds it to WorkflowRow type, and projects it as epicRef in toRunnerSummary() and getRunnerPanel() response objects.
Bridge SSE Payload with Epic Reference
packages/dashboard/src/bridge.ts
bridgeWorkflowsToBus() includes epicRef in the workflow nudge SSE payload alongside existing id, repo, epic, and state fields.
UI Component Integration in RunnerRow and Inspector
packages/dashboard/src/app/components/RunnerRow.tsx, packages/dashboard/src/app/components/Inspector.tsx
RunnerRow and Inspector import and render the EpicRef component with epicNumber, epicRef, and a fallback value instead of inline epic text rendering.
API and SSE Integration Tests with Test Fixtures
packages/dashboard/test/api.test.ts, packages/dashboard/test/sse.test.ts, packages/dashboard/test/app.test.tsx, packages/dashboard/test/helpers.ts
End-to-end API tests verify epicRef is correctly mapped in in-flight data and session panels for both GitHub and file modes. SSE tests validate epicRef is included in workflow transition payloads and file-mode epic references are preserved. Test fixtures updated with epicRef: null field. Test helpers extended to seed epicRef directly for workflow rows.
Planning and Decision Documentation
planning/issues/187/decisions.md, planning/issues/187/plan.md
Structured decisions document rendering rules (GitHub mode #N text vs file mode file:// link), file://planning/epics/${encodeURIComponent(slug)}.md href construction, read-only scope (adding to getWorkflow only, not createWorkflowRecord), and explicit out-of-scope surfaces (Queue tab, NEXT UP). Implementation plan outlines affected modules and test coverage expectations.

Sequence Diagram(s)

sequenceDiagram
    participant Dispatcher as Dispatcher<br/>getWorkflow()
    participant DBDeps as Dashboard<br/>db-deps.ts
    participant Bridge as Dashboard<br/>Bridge SSE
    participant RunnerRow as UI<br/>RunnerRow
    participant EpicRef as Component<br/>EpicRef

    Dispatcher->>Dispatcher: Read epic_ref from DB<br/>into WorkflowRecord.epicRef
    Dispatcher->>DBDeps: WorkflowRecord.epicRef
    DBDeps->>DBDeps: Project WORKFLOW_COLUMNS<br/>select epic_ref
    DBDeps->>DBDeps: Map row.epic_ref → epicRef<br/>in RunnerSummary/RunnerPanel
    DBDeps->>Bridge: Emit runner with epicRef
    Bridge->>Bridge: Include epicRef in SSE<br/>WORKFLOW_EVENT payload
    Bridge->>RunnerRow: Send epicRef to subscriber
    RunnerRow->>EpicRef: Pass epicNumber,<br/>epicRef, fallback
    EpicRef->>EpicRef: If epicNumber: render `#N`
    EpicRef->>EpicRef: Else if epicRef:<br/>render file:// link
    EpicRef->>EpicRef: Else: render fallback
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related issues

  • #187 — The main issue directly addresses file-mode epic support in the dashboard, implementing the exact code-level changes for epicRef plumbing through dispatcher, db-deps, bridge, wire types, UI components, and comprehensive tests.
  • #190 — Complements this PR by providing file-backed epic store ownership; this PR surfaces and displays the epic_ref slugs produced by that backing system.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 70.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: adding file-mode Epic display support with epic_ref column and file:// link rendering in the dashboard.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Comment @coderabbitai help to get the list of available commands and usage tips.

/** What to render when there's no Epic at all (both ids null). */
fallback?: string;
}) {
if (epicNumber !== null) return <>#{epicNumber}</>;

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

github mode renders plain #N, not an anchor — on purpose. The issue Context describes the current rendering as "a numeric link to GitHub", but the surfaces in fact render plain #N text today (no <a> exists). AC4 is the hard constraint — no behavior change for github-mode rows — so adding a GitHub anchor would itself be a behavior change. The literal reading wins: github mode keeps its exact pre-#187 plain-text output; file mode is the only new rendering. (toBe("#42") in the test asserts byte-identical markup.)

* path segment, URL-encoded so a malformed value can't break out of the
* `planning/epics/` directory or inject markup into the `href`.
*/
export function epicFileHref(slug: string): string {

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

file:// href + slug encoding. AC2 names exactly file:// + planning/epics/<slug>.md; the repo's absolute root isn't known client-side, so the path stays repo-relative as specified (yes, file://planning/... parses planning as the URL authority — it's a display affordance, not a guaranteed click-to-open). encodeURIComponent collapses the slug to one safe path segment, so a malformed value (../, quotes, angle brackets) can't traverse out of planning/epics/ or inject markup into the href; a normal kebab-case slug encodes to itself.

// null; an empty/whitespace value (no real writer produces one today, but a
// future one could) would otherwise render an empty-labelled link to
// `planning/epics/.md`, so treat it as "no Epic" and fall through.
const slug = epicRef?.trim();

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Blank-slug guard (self-review hardening). A present-but-blank epicRef ("" / whitespace) would otherwise take the file-mode branch and render <a href="file://planning/epics/.md"></a>. No writer produces a blank value today, but guarding on a trimmed-truthy slug encodes the real intent ("a present, non-blank slug is the file-mode signal") and survives a future writer. Trimming also keeps stray whitespace out of the label and href.

kind: row.kind as WorkflowRecord["kind"],
repo: row.repo,
epicNumber: row.epic_number,
epicRef: row.epic_ref,

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Read path only. getWorkflow reads the migration-008 epic_ref column verbatim so the dashboard bridge (AC3) can emit it. The dispatch write path (createWorkflowRecord) is deliberately untouched — populating epic_ref on insert is the file-backed Epic store rollout's job (the file-mode dispatch write path), out of scope here. github rendering keys on epic_number, so a null epic_ref on new github rows is harmless.

The migration was renumbered 008→009 when the file-backed Epic store
foundation rebased onto main (main already had an 008). Self-review
caught the stale '008' reference in the WorkflowRecord.epicRef doc
comment and the two #187 planning docs.
@thejustinwalsh thejustinwalsh marked this pull request as ready for review June 3, 2026 06:48
@thejustinwalsh thejustinwalsh added the ready-for-review All phases done and verified — PR ready for final human review and merge label Jun 3, 2026
@thejustinwalsh

Copy link
Copy Markdown
Owner Author

Reviewer's brief — PR #199 (closes #187)

What this is. The dashboard's /api/* read plane now carries the file-mode Epic identifier (epic_ref, a slug) and renders it as a file://planning/epics/<slug>.md link where it used to show a blank cell. github-mode rows are unchanged (plain #N).

Supersedes #189. The earlier PR was stacked on feat/file-backed-epic-store (the epic_ref column foundation, PR #188). That foundation has since merged to main and its branch was deleted, which auto-closed #189 (a closed PR can't be reopened once its base branch is gone). On re-dispatch I rebased the 6 #187 commits --onto origin/main (dropping the now-merged, duplicated foundation), re-ran every gate against current main, and opened this fresh PR targeting main directly. The diff is the dashboard/dispatcher display work only.

How to run it

git fetch origin && git checkout middle-issue-187
bun install
bun run typecheck          # clean
bun run lint               # clean
bun test packages/dashboard packages/dispatcher/test/workflow-record.test.ts   # 136 pass

For the live read path specifically:

bun test packages/dashboard/test/api.test.ts -t "file-mode"

What to verify (and what "correct" looks like)

  1. packages/dashboard/src/app/components/EpicRef.tsx — the rendering rule. github mode (epicNumber !== null) → plain #N, no anchor (AC4: byte-identical to before). file mode (epicNumber null, epicRef a non-blank slug) → the slug as a file://planning/epics/<slug>.md link, no GitHub link. Both null → caller fallback (#— for the runner surfaces). Confirm epic_number is what drives github rendering, so a null epic_ref on github rows is harmless.
  2. Slug safetyepicFileHref runs the slug through encodeURIComponent, so ../, quotes, angle brackets collapse to one inert path segment (tests assert a/../ba%2F..%2Fb). The file://planning/... shape is a deliberate literal reading of AC2 (repo root unknown client-side) — it's a display affordance, not guaranteed click-to-open. Worth a sanity check that you're OK with that shape.
  3. Read-path plumbinggetWorkflow (workflow-record.ts) reads epic_ref verbatim; db-deps.ts selects it into RunnerSummary/RunnerPanel; bridge.ts emits it on the repo-channel nudge. The dispatch write path is untouched (the file-mode dispatch write path owns it) — confirm you agree createWorkflowRecord populating epic_ref is out of scope here.

How to review

  • Decisions are distilled into 4 inline review comments on the diff (EpicRef render rule, href/encoding, blank-slug guard, read-path-only) — start there for the why.
  • Acceptance evidence is the checklist in the PR body; each criterion links its proving test. The integration criterion is api.test.ts (real Bun.serve + migrated db).

Fragile / extra eyes

  • AC4 is the load-bearing constraint. github epics render plain #N text today (no anchor ever existed); the issue's "numeric link to GitHub" is aspirational. EpicRef preserves the plain text exactly. If you'd actually prefer a real GitHub anchor for github mode, that's a deliberate behavior change to request — it's not what shipped.
  • Adjacent surfaces still show file-mode as blank/#0 (Queue tab via /control/events; NEXT UP via the recommender's state issue, which also has a latent key={n.epic} duplicate-key collision once two slugs map to #0). These are separate data paths owned by the file-backed Epic store rollout plan, called out in the PR's Follow-ups section — not regressions introduced here.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
packages/dashboard/src/app/components/EpicRef.tsx (1)

27-36: ⚡ Quick win

EpicRef export lacks an attached TSDoc comment.

The detailed block at lines 1–16 reads as module-level documentation (separated by the blank line 17), so the EpicRef export has no directly-attached doc comment. epicFileHref has its own; mirror that on EpicRef (e.g. move/duplicate the behavior summary onto the component declaration) so the contract travels with the symbol.

As per coding guidelines: "Every public export in a module must carry a TSDoc/JSDoc comment."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/dashboard/src/app/components/EpicRef.tsx` around lines 27 - 36, Add
a TSDoc/JSDoc comment directly above the EpicRef export (the function named
EpicRef) that mirrors the behavior summary used on epicFileHref: describe what
the component renders (link to epic file when epicRef present, numeric label
when only epicNumber present, and fallback when both are null), document the
props epicNumber, epicRef and fallback (types/semantics and default), and note
any important rendering details (e.g. fallback default "—"); ensure the comment
is attached immediately to the EpicRef declaration so the symbol carries the
contract.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@packages/dashboard/src/app/components/EpicRef.tsx`:
- Around line 27-36: Add a TSDoc/JSDoc comment directly above the EpicRef export
(the function named EpicRef) that mirrors the behavior summary used on
epicFileHref: describe what the component renders (link to epic file when
epicRef present, numeric label when only epicNumber present, and fallback when
both are null), document the props epicNumber, epicRef and fallback
(types/semantics and default), and note any important rendering details (e.g.
fallback default "—"); ensure the comment is attached immediately to the EpicRef
declaration so the symbol carries the contract.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: cb1e87c1-5a36-4b4e-a145-94e24462c993

📥 Commits

Reviewing files that changed from the base of the PR and between 78d09ac and 2b2ed28.

📒 Files selected for processing (15)
  • packages/dashboard/src/app/components/EpicRef.tsx
  • packages/dashboard/src/app/components/Inspector.tsx
  • packages/dashboard/src/app/components/RunnerRow.tsx
  • packages/dashboard/src/bridge.ts
  • packages/dashboard/src/db-deps.ts
  • packages/dashboard/src/wire.ts
  • packages/dashboard/test/api.test.ts
  • packages/dashboard/test/app.test.tsx
  • packages/dashboard/test/epic-ref.test.tsx
  • packages/dashboard/test/helpers.ts
  • packages/dashboard/test/sse.test.ts
  • packages/dispatcher/src/workflow-record.ts
  • packages/dispatcher/test/workflow-record.test.ts
  • planning/issues/187/decisions.md
  • planning/issues/187/plan.md

@thejustinwalsh thejustinwalsh merged commit 421425d into main Jun 3, 2026
1 check passed
@thejustinwalsh thejustinwalsh deleted the middle-issue-187 branch June 3, 2026 07:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ready-for-review All phases done and verified — PR ready for final human review and merge

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(dashboard): file-mode Epic display (epic_ref + file:// links)

1 participant