<!-- Written per the the-loop:writing skill. -->
Tasks: de-cluttering the control plane UI (Option C — Minimal)
The last spec artifact (requirements → design → testing plan → tasks). Derived mechanically from the C columns of
design.md's component table and element inventory, and fromtesting-plan.md's matrix — both locked by the owner atdesign-approvalon 2026-10-20 (decision-148). No gate follows this artifact (issue-281): the graph advances on its shape.
Twelve tasks in ui/src/, ui/scripts/ and two documents. Every task is a removal, a relocation behind an existing idiom, or a condensation drawn with the existing primitives (R4.2); none touches ui/src/api/, ui/src/state/ or ui/src/demo/ (R5.1), adds a dependency, or adds a control, route, request or setting (R5.5). The leaf helpers come first (1–5), the header's new home for the panel's facts and verbs next (6), the two assemblies that change what the screen shows (7, 8), then the proof that nothing was lost (9), the evidence tooling (10), the documents (11) and the checkpoint that runs the whole plan (12).
Task list
Each task is a checkbox, references the requirement(s) it satisfies, declares its dependencies so the-loop can build the execution DAG, and names the test(s) that will prove it — a row of testing-plan.md's matrix, so the DAG and the plan cannot describe different work. TDD invariant: no production code without a failing test that motivates it — write/adjust the test first, watch it go red, then make it green. The abuse cases of design.md § Security design are tests like any other (task 9).
- [x] 1. The pure helpers:
railNotewithout the node, two sidebar groupsui/src/views/WorkItemDetail.tsx→railNote: a parked item returnsparked — awaiting a human(noat <node>suffix); otherwise the attention detail, as today (inventory rows 33, 34; § Where the current node is drawn).ui/src/views/grouping.ts: addSIDEBAR_GROUPS_MINIMAL(needs-you→ Needs you,everything→ Everything else) and a pure mapping fromsidebarGroup's four-way answer to the two keys, with Everything else sorted bylastActivitydescending (the views arrive sorted by it already — the mapping preserves that order andneeds-youkeeps the group's order).sidebarGroup,itemStatus,repoOfandfilterViewsare unchanged (design § Components, last paragraph: no new helper beyond this mapping — the facts render through the existingKV).SIDEBAR_GROUPSstays exported until task 8 retires its last caller.- Depends on: none
- Requirements: R2.1, R2.5, R3.3
- Test: T1 —
ui/src/views/WorkItemDetail.test.tsx(railNoteof a parked view equalsparked — awaiting a humanand never contains the node id; the attention detail otherwise) andui/src/views/grouping.test.ts(the two-group mapping puts needs-you first, everything else bylastActivity;sidebarGroupstill answers four ways) (red→green)
- [x] 2.
GraphStripbecomes the band behind Full graphui/src/components/GraphStrip.tsx: the component takesexpanded: booleanfrom its parent and renders only while expanded — the loop name as the row's label and the full node row (every node, the current one highlighted, skipped struck through,aria-current="step", the<ol role="list" aria-label="loop position">kept). The counts line (n of m nodes passed,n skipped), theat <node>statement, the note and the strip's own Full graph button leave the component (rows 30–36); the control and the note move to the header meta in task 7. The empty-rail sentence stays for an expanded strip with no nodes.compactRail,railLabelandhumanizestay exported and unchanged (railLabelstill feeds the PR tab'sinner loopfact).- Depends on: none
- Requirements: R2.4, R2.5, R3.3, R6.1
- Test: T2 / T9 —
ui/src/components/GraphStrip.test.tsx: nothing renders while collapsed; expanded renders every node witharia-current="step"on the current one and noof … passed/skipped/at <node>text;compactRailandrailLabelcases unchanged (red→green)
- [x] 3.
SessionTabsrenders only for more than one session, without the tallyui/src/components/SessionTabs.tsx: returnnullwhenview.pullRequestsis empty (row 37, R2.6, D4); delete then sessions · 1 work item loop · …span (row 38). Tabs keep their links,aria-label= short ref,aria-current="page"and the pause glyph.- Depends on: none
- Requirements: R2.4, R2.6, R3.3, R6.2
- Test: T2 —
ui/src/views/WorkItemDetail.test.tsx:Scenario: the session tab bar renders only when the item has more than one session(nonavigationnamed Sessions for #205; present for #214 with #216 and loop-docs#47) andScenario: the tab bar carries no session tally(red→green)
- [x] 4. The composer's hint and the trace head
ui/src/components/Transcript.tsx→ChatBar: the hint slot renders only the blocked reason (chatBlocked, unchanged);delivers to tmux · <target>,bracketed paste + enterand⌘⏎ to sendleave (rows 47, 48);⌘⏎stays in the send button'stitle. ThetmuxTargetprop goes with the hint if nothing else reads it.ui/src/views/WorkItemDetail.tsxline ~190: the trace head readsHarness traceand the Verbose switch; the harness name leaves (row 40 — it lives in the Session details facts from task 6).- Depends on: none
- Requirements: R2.1, R2.4, R3.2, R5.3
- Test: T2 —
ui/src/components/Transcript.test.tsx(Scenario: the composer carries no delivery hint; the blocked composer still shows its reason; the send title carries ⌘⏎) andui/src/views/WorkItemDetail.test.tsx(the trace head text has no harness name;role="log", its name andtabindexunchanged) (red→green)
- [x] 5.
GateBannersays Human gate, never the nodeui/src/views/WorkItemDetail.tsx→GateBanner: headingHuman gate · waiting <age>(rows 42, 43, D5); reason, Approve — advance graph (→graphComplete, its disabled reason when no checkout) and Request changes on the ticket unchanged (row 44).QuestionBannerunchanged (row 45).- Depends on: none
- Requirements: R2.1, R2.5, R3.4
- Test: T2 —
ui/src/App.test.tsx:Scenario: the current node is written once — the phase chip — and the gate banner says Human gate without the node(for #205 the node id occurs exactly once in the main column, inside the phase chip); the existing gate-approval scenario still passes (red→green)
- [x] 6.
SessionFactsand the header's verbs: the panel's content, relocatedui/src/components/SessionAside.tsx: extract the facts into an exportedSessionFacts({ view, viewed })— Harness (harness, status, started, session id in full, transcript path in full) · tmux (cwd in full, the attach command button — the one copy control, unchanged) · Ticket / Pull request (inner loopfor a PR viarailLabel, Open on GitHub). Leave out: the note banner, instance, current node, the tmuxtargetrow, repo and number (rows 50, 51, 55, 58, 61). Render every value through the existingKVfrom the typedSessionRecord/PullRequestViewfields (abuse case 2); add no copy control (D6, critic C5). Extract the verbs into an exportedSessionControls({ view, onChanged }): the contextual pause / resume / stop / startControlButtons callingcontrolSessionwith the work item's ref, busy labels and therole="alert"error line as today.SessionAsideitself keeps composing them for the file's existing shape but is no longer rendered on Work (task 7).ui/src/components/HeaderBar.tsx: render the Open session panel button only whenchrome.onOpenAsideis set (Work stops setting it in task 7; Standing and Settings never did); the header's root gainsflex-wrapso the title keeps its width at 390 px (design § Accessibility & responsiveness).ui/src/views/WorkItemDetail.tsxheader actions:<SessionControls>preceded by a visiblework itemKickerwhenviewed !== view.ref(a PR session is selected — row 64, D9, critic C3) · copy ref · GitHub · theme (fromHeaderBar) · Session details: a native<details>whose<summary>is theIconButtonidiom (accessible name Session details) and whose body is<SessionFacts>in a popover anchored below the actions,width: min(20rem, calc(100vw - 2rem)),max-height: calc(100vh - 9rem),overflow-y: auto(critic C4) — Tailwind utilities over the existing tokens, no new token or shadow (R4.1). Closed by default at every width (R6.3).- Depends on: none
- Requirements: R2.3, R3.3, R3.4, R4.2, R5.2, R5.5, R6.1, R6.3
- Test: T2 / T8 / T9 — new
ui/src/components/SessionFacts.test.tsx:Scenario: Session details is a closed details element whose body shows harness, status, started, session id, transcript path, cwd, the attach command and Open on GitHub in full(the summary is named;openis absent by default; every value'stextContentequals the record's field; nocurrent node,target,repoornumberrow; exactly one copy button);Scenario: pause, resume, stop and start render in the header and call controlSession with the work item's ref;Scenario: with a PR session selected the verbs carry a visible work item label and still target the work item; abuse case 2 — a record whosecwd,harnessSessionIdand transcript path carry<b>x</b>renders them as text, nobelement (red→green)
- [x] 7. The main column assembled: header meta, one statement of the position, no panel
ui/src/views/WorkItemDetail.tsx: the header meta is ref chip ·PhaseChip· therailNotequalifier (when non-empty) ·updated <age>· Full graph / Collapse graph (a button witharia-expanded, the state local to the view, off by default); the repository line and the instance chip leave the header (rows 24, 26, D11 — the ref chip is the one copy of the repository).<GraphStrip expanded={…}>renders beneath the header;<SessionTabs>beneath it (task 3). The copy-ref and GitHub icon buttons stay (row 28).ui/src/views/Work.tsx:SessionAsideis not rendered on Work (row 65); theChromehanded toWorkItemDetailcarries noasideOpen/onOpenAside, so the header draws no open-panel button (row 29).Panels.asideOpenand its setter stay inApp's state untouched (ui/src/state/is not edited, R5.1) and are simply unread on Work; the<1280 pxrule now applies to the popover, which always starts closed (R6.3).- The demo banner, connection banner and the footer's health word are not touched (row 66, 21; fail-closed).
- Depends on: 1, 2, 3, 4, 5, 6
- Requirements: R2.1, R2.5, R3.3, R3.4, R5.2, R5.3, R6.1, R6.3
- Test: T2 / T9 —
ui/src/App.test.tsxandui/src/views/WorkItemDetail.test.tsx:Scenario: the header meta is the ref chip, the phase chip, the note and the age — the repository line and the instance chip are gone;Scenario: Full graph in the header meta reveals the node row and Collapse graph hides it(aria-expandedflips; no[data-node]while collapsed); the one-session scenario of task 3 on the assembled column; no element named Open session panel or Close panel on Work;Scenario: copy ref and copy attach command write the clipboard(re-pointed at the popover); the existing question + reply, gate approval and theme-toggle scenarios still pass (red→green)
- [x] 8. The sidebar: two groups, one-line rows, PRs under the selected item, the finder behind an icon
ui/src/components/Sidebar.tsx: groups fromSIDEBAR_GROUPS_MINIMALvia task 1's mapping; the count renders on Needs you only (row 15). A row is one line — dot ·#n· title (truncate) ·InstanceChip(fleet only) · the flag at the right only whenrowFlag(view).urgent(needs input,human gate,blocked; rows 5–12) — witharia-label=#n — title — flagandaria-current="page"when selected (R6.2). PR rows nest only under the selected item (the row whoserefor whosepullRequestscontain the active ref), without their age (rows 13, 14); thePull requests for …list label stays. The Standing group and Manage standing sessions → leave (rows 17, 18). The search box and the fleet-only instance<select>move behind oneIconButtonin the brand row (Search work items,aria-expanded, closed by default; rows 3, 4; R2.6 keeps the filter off a worker); the empty-result sentences are unchanged. The footer (Settings, service line,HealthDot) is untouched (rows 19–21).ui/src/views/grouping.ts: retireSIDEBAR_GROUPSif the sidebar was its only caller; keep it otherwise.- Depends on: 1
- Requirements: R2.2, R2.4, R2.6, R3.3, R5.2, R6.1, R6.2
- Test: T2 / T9 —
ui/src/components/Sidebar.test.tsxandui/src/App.test.tsx:Scenario: a sidebar row is one line carrying the number, the title and the urgent flag, and its accessible name carries all three;Scenario: a paused, merged or closed item shows no flag on its row;Scenario: pull request rows nest only under the selected item;Scenario: only the Needs you group shows a count;Scenario: the Standing group does not render in the sidebar and the Standing surface still lists every standing session(ui/src/views/Standing.test.tsxunchanged and green);Scenario: the search box and the instance filter appear behind the search toggle, which carries aria-expanded;Scenario: an instance filter renders on a fleet and not on a worker(the manager transport ofui/src/test/manager.ts); the sidebar's collapse and reopen scenario still passes (red→green)
- [x] 9. Nothing lost, nothing trusted: the abuse cases, the fail-closed signals, the hashes, the breakpoints
- Add, in the suites of tasks 6–8, the negative tests
design.md§ Security design names: abuse case 1 — a fixture-shaped view whose title is<b>x</b>, whose current node is**n**and whose transcript line carries backticks renders them as text through the one-line row, the row'saria-label, the header title and phase chip, the Session details popover and the trace (textContentequals the input;querySelector("b")is null;renderInlinestill yieldsstrong/codewith string children). Fail-closed — with the demo transport the demo banner renders; with a failing transport the connection banner (role="alert") renders above the columns; a stopped daemon renders thedegradedhealth word; #187's blocked composer shows its reason — all in the C layout.Scenario: loading, empty board, missing deep link, no-transcript fallback, truncated tail and blocked composer render their sentences in the C layout.Scenario: every hash the 20.5.1 app parses lands on the same surface(ui/src/state/route.test.tsunchanged and green;App.test.tsxopens#/,#/item/<ref>,#/standing,#/settings,#/instances,#/events).Scenario: below 768 px the sidebar starts closed and reopens from the header(thematchMediastub the suite already uses). Theprefers-reduced-motionguard is asserted by T9's grep, not a test. - Depends on: 7, 8
- Requirements: R5.2, R5.3, R5.4, R6.3, R6.4, Security AC1, AC2, fail-closed
- Test: T8 / T9 / T2 — the scenarios above in
App.test.tsx,Sidebar.test.tsx,SessionFacts.test.tsx,primitives.test.tsx;grep -rn "dangerouslySetInnerHTML\|innerHTML\|eval(" ui/src ui/index.htmlempty (red→green)
- Add, in the suites of tasks 6–8, the negative tests
- [x] 10. The screenshot run covers the C layout
ui/scripts/screenshots.mjs: re-point the selectors the change moved — the Verbose switch (the script still says Tool calls),Full graphnow in the header, the sidebar's Collapse sidebar, no Close panel — and add the states T5 lists: Session details open (13-details-open), the search toggle open (14-search-open), the sidebar collapsed (11becomes sidebar only), and at 390 px the sidebar closed and Session details open (12c-mobile-details); keep the PR #216 session (thework itemkicker), the #205 gate, the #214 question, #187's fallback, Standing and Settings, both themes, 1440×900 and 390×844. Names map todesign/screenshots/c-*.pnginevidence/ui-visual.md's table (task 12).- No dependency is added: Playwright is installed without saving, as the header comment documents (NFR).
- Depends on: 7, 8
- Requirements: R7.1, R7.2
- Test: T5 —
node ui/scripts/screenshots.mjs <out-dir> http://localhost:4173/the-loop/ui/againstbun run preview --port 4173overdist/;capture.loglists every state and nopageerror/console.errorline (the run is the test; red = a selector no longer resolves)
- [x] 11. The documents describe the C layout
ui/README.md§ Where the screens get their data: the one-screen paragraph describes two columns — the sidebar (Needs you / Everything else, one-line rows#n · title · flag, PRs nested under the selected item, the finder behind the search icon, Settings and the health word in the footer) and the main column (title; meta = ref chip · phase chip · qualifier · age · Full graph revealing the node row; tabs only for more than one session; the trace with Verbose; the gate and question banners; the composer with only a blocked reason); the session facts and verbs in the header's Session details popover and action row. § Layout: thecomponents/line namesSessionAsideas the facts/verbs module (SessionFacts,SessionControls). § Screenshot evidence: unchanged.docs/capabilities/control-plane.md§ Current behaviour: the "one screen of three columns" bullet becomes the C layout (two columns, the bands above, the<1280 pxrule now the popover's, the sidebar's<768 pxrule unchanged), with issue-511 cited; § History gains an issue-511 row at the top linking the spec, decision-148 and the issue. The design-system sentence is unchanged (R4).- Depends on: 7, 8
- Requirements: R8
- Test: T13 / T14 —
npx --yes markdownlint-cli2@0.18.1 "ui/README.md" "docs/capabilities/control-plane.md" "docs/specs/issue-511/*.md"clean; the two sections read against the inventory's C column and quoted inevidence/documentation.md(task 12)
- [x] 12. Checkpoint: the whole plan, the evidence records, the bundle
- Run
testing-plan.md§ Verification activities end to end and tick each only when it has run:cd ui && bun run lint && bun run test && bun run build;ls -l ui/dist/assetsagainst JS 341 685 B / CSS 24 469 B (not larger);git diff --stat origin/main -- ui/src/api ui/src/state ui/src/demoempty; the T8 and T9 greps; the T5 run of task 10 with the PNGs copied underevidence/ui/; the T13 lint over**/*.md; the T14 reading. - Write the records the evidence plan names:
evidence/unit.md,evidence/integration.md,evidence/security.md,evidence/lint-and-typecheck.md,evidence/build.md,evidence/ui-visual.md(each still beside itsdesign/screenshots/c-*.pngcounterpart, plus the before/after count of inventory elements drawn by default — 56 → 27 expected, recounted from the built app for R7.2),evidence/documentation.md; filltesting-plan.md§ Verification results. Evidence is redacted (no local paths, no hostnames). - Depends on: 9, 10, 11
- Requirements: R7, NFR (build, bundle, no dependency), R5.1
- Test: T12 / T13 / T5 / T14 — the commands above; every activity ticked in
testing-plan.md
- Run
Dependency graph (DAG)
Tasks 1–6 are independent leaves and run in any order; 7 and 8 are the two assemblies; 9, 10 and 11 fan out from them; 12 closes.
Checkpoints
Each task's commit carries its test command and the red→green transition in its message (issue-365: no progress log). cd ui && bun run test runs after every task; bun run lint and bun run build after tasks 7, 8 and 12. Task 12 is the checkpoint that executes testing-plan.md in full — the verification node ticks each activity and records its command, outcome and committed evidence — and only then do the review phases run the two self-reviews, the one codex critic round and the security review gate (recorded in evidence/self-review.md, evidence/critic-review.md, evidence/security-review.md) before the code explainer, the reviewer briefing and the owner's approval.
The implementation crosses the phase boundary with a cleared context: it executes from the locked requirements.md, design.md (C columns, inventory, § Security design), testing-plan.md and this file on disk, with design/option-c-minimal.html and design/screenshots/c-*.png as the visual contract.
Review comments
Appended by the-loop's
record-feedbackhook when a human gate approves with comments (issue-109). Append-only and attributed: an approval never silently discards a reviewer's suggestions, and the feedback travels with the document it concerns rather than living in a side-channel tracker.