feat(web): reposition UI as viewer + light CRUD#8
Conversation
The 'Start Work' button posted to /epics/:id/work which only updated task status in SQLite — it never spawned Claude Code workers. That was a fake promise. Replaced with a 'Copy /flow-code:work <id>' action plus explainer text making clear that agent execution belongs in the Claude Code terminal. fn-18-reposition-web-ui-as-viewer-light-crud.1
Documents the divide of labor between Claude Code terminal (agent execution), web UI (browse + light CRUD), and flowctl CLI (scripting). Makes it explicit that the web UI is a read-only dashboard, not an orchestrator — you copy the work command from the UI and run it in your Claude Code session. fn-18-reposition-web-ui-as-viewer-light-crud.2
When epic count grows past ~10, scanning the grid becomes painful. Added a search input (matches id or title, case-insensitive) and a status dropdown derived from live epic statuses. Shows filtered/total count when any filter is active, with a clear-filters button. Domain filter deliberately skipped — domain is per-task, not per-epic, so filtering epics by domain would require pulling task lists into the /epics response. Not worth the cost now. fn-18-reposition-web-ui-as-viewer-light-crud.3
Adds GET /api/v1/tasks/{id} endpoint returning task + evidence +
runtime state (including duration_seconds). Frontend exposes a
clickable title on done tasks that opens an EvidenceModal showing
status, duration, diff stats, commits, tests, review iterations,
and workspace_changes if recorded.
Backend: new get_task_handler in handlers/task.rs merges TaskRepo,
EvidenceRepo, and RuntimeRepo into a single response payload.
Frontend: EvidenceModal component with Escape/backdrop close,
loading/error states, and conditional rendering per evidence field.
fn-18-reposition-web-ui-as-viewer-light-crud.4
There was a problem hiding this comment.
Pull request overview
This PR repositions the web UI as a state/evidence viewer with light CRUD (not an agent orchestrator), removes a misleading “Start Work” action, adds dashboard filtering, and introduces a task evidence/detail viewer backed by a new daemon endpoint.
Changes:
- Replace “Start Work” with a “Copy
/flow-code:work …” affordance + inline execution explainer on Epic detail. - Add dashboard epic search + status filtering via a new
EpicFilterscomponent. - Add a task detail/evidence modal in the frontend and a new daemon endpoint
GET /api/v1/tasks/{id}to serve task+evidence+runtime details.
Reviewed changes
Copilot reviewed 8 out of 8 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| README.md | Documents the Web UI vs terminal/CLI responsibilities and launch instructions. |
| frontend/src/pages/EpicDetail.tsx | Removes the fake “Start Work” call; adds copy-to-clipboard and evidence modal entry point. |
| frontend/src/pages/Dashboard.tsx | Adds in-memory epic filtering (search + status) and empty/clear states. |
| frontend/src/components/EvidenceModal.tsx | New modal for task details/evidence fetched from the daemon. |
| frontend/src/components/EpicFilters.tsx | New reusable filter UI (search, status dropdown, counts, clear). |
| flowctl/crates/flowctl-daemon/src/server.rs | Registers the new REST endpoint GET /api/v1/tasks/{id}. |
| flowctl/crates/flowctl-daemon/src/handlers/task.rs | Implements get_task_handler combining task + evidence + runtime (+ duration). |
| flowctl/crates/flowctl-daemon/src/handlers/mod.rs | Exports the new task handler. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| | Surface | Responsibility | | ||
| |---|---| | ||
| | **Claude Code terminal** | Run agents: `/flow-code:plan`, `/flow-code:work`, `/flow-code:brainstorm`, reviews, worker spawning, permission prompts | | ||
| | **Web UI** | Browse epics/tasks/specs/DAG/memory/evidence; light CRUD (create epic, edit deps, add gaps, archive epics) | | ||
| | **`flowctl` CLI** | Scripting, CI, automation, anything headless | | ||
|
|
There was a problem hiding this comment.
The markdown table in this section uses double leading pipes (||) on each row, which renders as an extra empty column (or breaks table rendering depending on the renderer). Use single leading/ending pipes (|) for standard GitHub-flavored markdown tables.
| (evidence.tests?.length ?? 0) > 0 || | ||
| (evidence.files_changed ?? 0) > 0 || |
There was a problem hiding this comment.
hasEvidence doesn't consider evidence fields that the modal can display (e.g. insertions, deletions, review_iterations). If those are present while files_changed is null/0 and there are no commits/tests, the UI will incorrectly show “No evidence recorded…”. Include all displayed fields (and optionally prs) in the hasEvidence check.
| (evidence.tests?.length ?? 0) > 0 || | |
| (evidence.files_changed ?? 0) > 0 || | |
| (evidence.tests?.length ?? 0) > 0 || | |
| (evidence.prs?.length ?? 0) > 0 || | |
| (evidence.files_changed ?? 0) > 0 || | |
| (evidence.insertions ?? 0) > 0 || | |
| (evidence.deletions ?? 0) > 0 || | |
| (evidence.review_iterations ?? 0) > 0 || |
| return ( | ||
| <div | ||
| className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 p-4" | ||
| onClick={onClose} | ||
| > | ||
| <div | ||
| className="relative max-h-[85vh] w-full max-w-2xl overflow-y-auto rounded-lg border border-border bg-bg-secondary shadow-2xl" | ||
| onClick={(e) => e.stopPropagation()} | ||
| > |
There was a problem hiding this comment.
This modal is built from raw <div>s without dialog semantics or focus management, which hurts keyboard/screen-reader accessibility (no role="dialog"/aria-modal, no initial focus, no focus trap). Consider using the existing <dialog>-based modal component pattern (or add the missing ARIA + focus handling here).
| <button | ||
| onClick={() => setEvidenceTaskId(t.id)} | ||
| className="text-left font-medium hover:text-accent transition-colors" | ||
| title="View evidence" | ||
| > | ||
| {t.title} | ||
| </button> |
There was a problem hiding this comment.
The clickable task title is rendered as a <button> without an explicit type. If this component ever ends up nested inside a <form>, the default type="submit" can cause accidental form submissions. Set type="button" on this button.
| // ── New RESTful endpoints ────────────────────────────── | ||
| .route("/api/v1/epics/{id}/plan", post(handlers::set_epic_plan_handler)) | ||
| .route("/api/v1/epics/{id}/work", post(handlers::start_epic_work_handler)) | ||
| .route("/api/v1/tasks/{id}", get(handlers::get_task_handler)) | ||
| .route("/api/v1/tasks/{id}/start", post(handlers::start_task_rest_handler)) | ||
| .route("/api/v1/tasks/{id}/done", post(handlers::done_task_rest_handler)) | ||
| .route("/api/v1/tasks/{id}/block", post(handlers::block_task_rest_handler)) |
There was a problem hiding this comment.
A new GET route for fetching task details was added, but there’s no corresponding handler/router test coverage alongside the existing server/router tests in this crate. Add a test that creates a task (and optionally evidence/runtime rows) and asserts GET /api/v1/tasks/{id} returns the expected shape (including nulls when rows don’t exist).
Adds a lightweight per-task narrative dump at `.flow/outputs/<task-id>.md` that workers write in new Phase 5c and the next worker auto-reads during Phase 1 re-anchor. Gated on a new `outputs.enabled` config key (default true), fully independent from `memory.enabled` — outputs is a narrative handoff layer, not part of the verified memory system. - flowctl-core: new `OutputEntry` protocol type (convention #8) - flowctl-service: new `OutputsStore` (file-system native, no libSQL) - flowctl-cli: new `flowctl outputs write|list|show` commands - Phase 5c `outputs_dump` added to worker sequence dynamically based on `outputs.enabled` config; worker.md Phase 1 extended to list+show prior outputs (limit 3) and Phase 5c added between Phase 5 and Phase 5b - `done_task()` in lifecycle.rs intentionally unchanged — worker owns the dump explicitly in Phase 5c, keeping runtime agnostic of narrative shape - Missing `## Surprises`/`## Decisions` sections allowed (pitfall #2)
P0 fixes (state loss — root cause of 5 issues): - get_flow_dir() now walks up directory tree (FLOW_STATE_DIR env → walk-up → CWD) Fixes: #1 state loss, #3 state not persistent, #5 worker parallel fail, #9 .flow symlink issues. Same pattern as git finding .git. - flowctl recover --epic <id> [--dry-run]: rebuilds task completion status from git log. Fixes #11 no recovery after state loss. P1 fixes (guard + review): - Guard graceful fallback: missing tools → "skipped" (not "failed"). Only actual failures block pipeline. Fixes #8. - Review-backend availability check: if rp-cli/codex not in PATH, auto-fallback to "none" with warning. Fixes #7. P2 fixes (UX): - Slug max length 40→20 chars. "Django+React platform with account management" → "fn-3-django-react-plat" not 40-char monster. Fixes #2 #12. - Brainstorm auto-skip: trivial tasks (≤10 words, contains "fix"/"typo"/etc) skip brainstorm entirely. Fixes #6. - --interactive flag: pause at key decisions. Fixes #10. 370 tests pass. Zero new dependencies. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Summary
Aligns the web UI with architectural truth: Claude Code terminal runs agents, web UI browses state. Removes a fake-promise button, documents the divide of labor, and adds viewer features that justify the web UI's existence.
The Fake Promise Fix
EpicDetail.tsxhad a prominent "Start Work" button that calledPOST /epics/:id/work. The daemon handler only updated task status in SQLite — it never spawned Claude Code workers. Agent execution requires a live Claude Code session (via theAgent()tool). Replaced the button with "Copy /flow-code:work " that copies to clipboard, plus an explainer: "Agent execution happens in your Claude Code terminal."Changes
Task 1 — Replace Start Work → Copy command
handleStartWork+apiPost('/epics/:id/work')callTask 2 — README Web UI section
## Web UIsection documenting responsibility splitflowctl serve --port 3737Task 3 — Dashboard search + status filter
EpicFilterscomponent (search by id/title + status dropdown)Task 4 — Evidence viewer modal
GET /api/v1/tasks/:idendpoint (merges Task + Evidence + RuntimeState)EvidenceModalshows: status, duration, diff stats, commits, tests, review_iterations, workspace_changesVerification
cargo build --release -p flowctl-daemonbun run typecheckbun run buildbun testArchitecture note
The daemon's
POST /epics/:id/workendpoint is left intact — MCP clients and scripts may depend on it. The web UI simply stops calling it. Future epic (deferred) could add real execution via Rustclaude-agent-sdkor a conductor skill, but that was deemed out of scope until real user demand exists.Epic: fn-18-reposition-web-ui-as-viewer-light-crud