feat(dashboard): file-mode Epic display (epic_ref + file:// links)#199
Conversation
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)
📝 WalkthroughWalkthroughThis PR implements file-mode epic display in the dashboard by introducing an ChangesFile-Mode Epic Display Feature
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
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related issues
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. Comment |
| /** What to render when there's no Epic at all (both ids null). */ | ||
| fallback?: string; | ||
| }) { | ||
| if (epicNumber !== null) return <>#{epicNumber}</>; |
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
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(); |
There was a problem hiding this comment.
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, |
There was a problem hiding this comment.
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.
Reviewer's brief — PR #199 (closes #187)What this is. The dashboard's Supersedes #189. The earlier PR was stacked on How to run itgit 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 passFor the live read path specifically: bun test packages/dashboard/test/api.test.ts -t "file-mode"What to verify (and what "correct" looks like)
How to review
Fragile / extra eyes
|
There was a problem hiding this comment.
🧹 Nitpick comments (1)
packages/dashboard/src/app/components/EpicRef.tsx (1)
27-36: ⚡ Quick win
EpicRefexport 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
EpicRefexport has no directly-attached doc comment.epicFileHrefhas its own; mirror that onEpicRef(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
📒 Files selected for processing (15)
packages/dashboard/src/app/components/EpicRef.tsxpackages/dashboard/src/app/components/Inspector.tsxpackages/dashboard/src/app/components/RunnerRow.tsxpackages/dashboard/src/bridge.tspackages/dashboard/src/db-deps.tspackages/dashboard/src/wire.tspackages/dashboard/test/api.test.tspackages/dashboard/test/app.test.tsxpackages/dashboard/test/epic-ref.test.tsxpackages/dashboard/test/helpers.tspackages/dashboard/test/sse.test.tspackages/dispatcher/src/workflow-record.tspackages/dispatcher/test/workflow-record.test.tsplanning/issues/187/decisions.mdplanning/issues/187/plan.md
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 afile://planning/epics/<slug>.mdlink instead of a blank cell. github-mode rows render exactly as before (plain#N).Acceptance criteria
db-deps.tsselectsepic_refand the/api/*response shape gainsepicRef: string | null— proven end-to-end by a realBun.serveintegration 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 surfacesepicRef:api.test.tsfile://planning/epics/<slug>.mdlink whenepicRefis set andepicNumberis null, with no GitHub link in file mode —EpicRefcomponent, used in the IN-FLIGHT runner row + Inspector header; verified byepic-ref.test.tsxepicRefalongsideepic—bridge.tsrepo-channel nudge; verified by a live-SSE test insse.test.ts#N, byte-identical) — verified byepic-ref.test.tsx(toBe("#42")) andapi.test.tsWhat changed
packages/dispatcher/src/workflow-record.ts—getWorkflowreads the migration-008epic_refcolumn into a newWorkflowRecord.epicRef(read path only; the dispatch write path is untouched — file-mode dispatch owns it).packages/dashboard/src/db-deps.ts,wire.ts— selectepic_ref;RunnerSummary/RunnerPanelgainepicRef: string | null.packages/dashboard/src/app/components/EpicRef.tsx(new) — github → plain#N; file →file://slug link; blank/null → fallback. Used inRunnerRow.tsx+Inspector.tsx.packages/dashboard/src/bridge.ts— the repo-channel workflow nudge emitsepicRef.Why these changes
The issue cites
db-deps.ts:83— the/api/*read plane that reads theworkflows.epic_refcolumn. AC4 is the load-bearing constraint: github-mode rows render plain#Ntext today (no anchor exists), soEpicRefpreserves that byte-for-byte and adds thefile://link only for file mode — adding a GitHub anchor would itself be a behavior change. The slug isencodeURIComponent-collapsed into one safe path segment so a malformed value can't traverse out ofplanning/epics/or inject markup. Full reasoning inplanning/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 test— 1174 pass, 0 fail (full suite). Affected:packages/dashboard/**+packages/dispatcher/test/workflow-record.test.ts→ 136 pass.Per-criterion evidence:
packages/dashboard/test/api.test.tsboots a realBun.serveover a migrated temp db; a seeded file-mode row (epic_numberNULL,epic_refslug) surfacesepicRefoverGET /api/repos/:repo(inFlight[].epicRef) andGET /api/sessions/:session(RunnerPanel.epicRef); a github-mode row carriesepic: 7, epicRef: null.packages/dashboard/test/epic-ref.test.tsx— all three render modes (github#Nno-anchor, filefile://slug link, both-null fallback), blank/whitespace slug → fallback, slug encoding (a/../b→a%2F..%2Fb,<script>neutralized), plus RunnerRow/Inspector rendering.packages/dashboard/test/sse.test.ts— a live SSE connection asserts theworkflownudge carriesepicRef(github row →null; file row → the slug).packages/dispatcher/test/workflow-record.test.ts—getWorkflowround-tripsepic_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)epicRefwould have rendered an empty-labelledfile://…/.mdlink — now guarded (trimmed-truthy slug → fallback) with tests. Adjacent surfaces it flagged are out of scope and tracked below.Stumbling points
feat/file-backed-epic-store(PR feat(epic-store): foundation — spec + plan + parser/renderer/migrations #188). When feat(epic-store): foundation — spec + plan + parser/renderer/migrations #188 merged tomainand its branch was deleted, feat(dashboard): file-mode Epic display (epic_ref + file:// links) #189 could no longer auto-retarget and was closed. On re-dispatch the branch still carried feat(epic-store): foundation — spec + plan + parser/renderer/migrations #188's (now-merged, rebased-with-different-SHAs) foundation commits, so I rebased--onto origin/mainfrom the foundation boundary — dropping the duplicated foundation, keeping the 6 feat(dashboard): file-mode Epic display (epic_ref + file:// links) #187 commits — and re-ran every gate against currentmain. This PR is the result.#N. AC4 ("no behavior change") is the tie-breaker — preserved plain text for github mode.createWorkflowRecorddoesn't writeepic_ref. Migration 008's comment claims it does; it doesn't yet. That's the file-mode dispatch write path, out of scope here — github rendering keys onepic_number, so a nullepic_refon new github rows is harmless.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 thanmain. 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 togit rebase --onto origin/main <foundation-boundary>and open a fresh PR againstmain.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:
/control/events(main.tsbroadcastWorkflow→ControlWorkflowFrame) still shows—for file-mode rows — a separate (control-plane) data path the issue doesn't cite.db-deps.ts, recommender state-issue ranking) collapses a file-mode slug to#0viaNumber(slug) || 0, andRepos.tsxkeys the list onn.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 throughNextUpItemand key onrank.createWorkflowRecordepic_ref write path — the file-mode dispatch write path.Out of scope
mm dispatch --epic <slug>is the path for now).Summary by CodeRabbit
New Features
file://planning/epics/slug.md) and as plain text in GitHub mode (e.g.,#N).Tests