From 4d400b0a277f777eb114252ce645fd8e36b9ba38 Mon Sep 17 00:00:00 2001 From: Josh Mabry Date: Sun, 21 Jun 2026 02:07:44 -0700 Subject: [PATCH] feat(board-view): section the console list by state in collapsible groups (v0.22.0) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The console Board view's list projection rendered every feature as one flat table. Group it by board state into collapsible sections — COLS order (backlog → ready → in_progress → in_review → done) + a blocked group + a cancelled group (the #47 terminal edge) — mirroring the Kanban's column grouping so a dense board stays scannable. Each section header shows the state name + a count; clicking it collapses the group (header stays, rows omitted). Empty states are omitted. Collapse state lives in a module-scoped Set so the 10s auto-reload re-render doesn't re-expand what the user closed. Token-driven styling (--pl-*), consistent with the existing board CSS. Kanban view untouched — this is the list projection only. Verified: node syntax-check of the module script, and a simulation of the grouping algorithm (COLS order, empty-section omission, counts, collapse, unknown-state-last, empty-board fallback). Contract-tested against the page string (no JS runtime in CI). Closes #26 Co-Authored-By: Claude Opus 4.8 (1M context) --- board_view.py | 45 ++++++++++++++++++++++++++++++++++++---- protoagent.plugin.yaml | 2 +- pyproject.toml | 2 +- tests/test_board_view.py | 33 +++++++++++++++++++++++++++++ 4 files changed, 76 insertions(+), 6 deletions(-) create mode 100644 tests/test_board_view.py diff --git a/board_view.py b/board_view.py index 682a4dd..20bf75a 100644 --- a/board_view.py +++ b/board_view.py @@ -49,6 +49,15 @@ a.pr{color:var(--pl-color-status-info);text-decoration:none} a.pr:hover{text-decoration:underline} .hide{display:none} + /* List view — collapsible per-state group headers (#26). Token-driven, mirrors the + Kanban column grouping as sections in the dense list. */ + #list tr.grp{cursor:pointer;user-select:none} + #list tr.grp>td{background:var(--pl-color-bg-raised);text-transform:uppercase; + letter-spacing:.06em;font-size:11px;font-weight:600;color:var(--pl-color-fg-muted); + padding:var(--pl-space-2) 4px} + #list tr.grp:hover>td{color:var(--pl-color-fg)} + #list tr.grp .tw{display:inline-block;width:1.1em;text-align:center} + #list tr.grp .gl{margin-right:var(--pl-space-2)} /* Narrow/mobile: the JS auto-switches to the list; if Kanban is forced, stack it. */ @media (max-width:760px){ .board{grid-template-columns:1fr} .wrap{padding:var(--pl-space-3)} } @@ -118,6 +127,14 @@ // State → DS dot variant (for the list view chip). const DOT_VARIANT = {ready:"pl-dot--success", in_review:"pl-dot--info", blocked:"pl-dot--error"}; +// List view sections: the Kanban columns + the `blocked` flag-state + `cancelled` +// (the second terminal edge, #47), rendered as collapsible groups in COLS order (#26). +const LIST_SECTIONS = [...COLS, "blocked", "cancelled"]; +// States the user has collapsed — module-scoped so the 10s auto-reload re-render +// doesn't re-expand what they closed. +const COLLAPSED = new Set(); +function toggleGroup(state){ COLLAPSED.has(state) ? COLLAPSED.delete(state) : COLLAPSED.add(state); render(); } + function render(){ if (VIEW === "kanban"){ $("kanban").innerHTML = COLS.map(state => { @@ -134,12 +151,30 @@ + ''+items.length+''+cards+''; }).join(""); } else { - $("rows").innerHTML = FEATURES.map(f => + // List: group rows under a collapsible per-state header (COLS order + blocked + + // cancelled), mirroring the Kanban's grouping so a dense board stays scannable (#26). + const row = (f) => ''+esc(f.id)+''+esc(f.title)+'' + '' + ''+esc(f.state)+'' - + 'P'+f.priority+''+flags(f)+''+pr(f)+'' - ).join("") || '
No features yet — create some via the board tools or API.
'; + + 'P'+f.priority+''+flags(f)+''+pr(f)+''; + const byState = {}; + FEATURES.forEach(f => (byState[f.state] = byState[f.state] || []).push(f)); + // COLS order + blocked + cancelled; any unexpected state lands in its own group last. + const order = LIST_SECTIONS.slice(); + Object.keys(byState).forEach(s => { if (!order.includes(s)) order.push(s); }); + let html = ""; + order.forEach(state => { + const items = byState[state] || []; + if (!items.length) return; // omit empty sections + const collapsed = COLLAPSED.has(state); + html += '' + + ''+(collapsed?"▸":"▾")+'' + + ''+esc(state.replace("_"," "))+'' + + ''+items.length+''; + if (!collapsed) html += items.map(row).join(""); // collapsed → header only (rows omitted) + }); + $("rows").innerHTML = html || '
No features yet — create some via the board tools or API.
'; } } @@ -157,8 +192,10 @@ } } -// Module scripts are scoped — expose the view-toggle's inline onclick handlers. +// Module scripts are scoped — expose the inline onclick handlers (view toggle + +// the list's per-state collapse). window.setView = setView; +window.toggleGroup = toggleGroup; setView(VIEW); // sync the toggle + visibility to the initial view (list on mobile) // Boot ONCE, on whichever fires first: the handshake (the bearer arrives with // protoagent:init, so the gated /features pull authenticates) or a short timer diff --git a/protoagent.plugin.yaml b/protoagent.plugin.yaml index 824bb06..a122e37 100644 --- a/protoagent.plugin.yaml +++ b/protoagent.plugin.yaml @@ -1,6 +1,6 @@ id: project_board name: Project Board (coding orchestration) -version: 0.21.0 +version: 0.22.0 description: >- A board-driven coding-orchestration plugin: a lean 6-state board (backlog → ready → in_progress → in_review → done, + a blocked flag) backed by **beads** (`br`), an diff --git a/pyproject.toml b/pyproject.toml index 5f97ee4..c136d58 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "project-board" -version = "0.21.0" +version = "0.22.0" description = "Board-driven coding-orchestration plugin for protoAgent (beads board + ACP spawn loop + planning layer + console view)." requires-python = ">=3.11" diff --git a/tests/test_board_view.py b/tests/test_board_view.py new file mode 100644 index 0000000..9288b0c --- /dev/null +++ b/tests/test_board_view.py @@ -0,0 +1,33 @@ +"""Board-view contract tests (#26). + +The board page is a no-build, vanilla-JS HTML string (``BOARD_PAGE``) — there's no JS +runtime in the suite, so (like ``test_api`` guarding the served path) these assert the +structural contract of the page: the list view groups features into collapsible +per-state sections, and the Kanban grouping is left untouched. +""" + +from __future__ import annotations + +from project_board.board_view import BOARD_PAGE + + +def test_list_sections_cover_cols_plus_blocked_and_cancelled(): + """The list groups by COLS order + the blocked flag-state + cancelled (the second + terminal edge), so every board state a feature can be in has a home in the list.""" + assert 'const LIST_SECTIONS = [...COLS, "blocked", "cancelled"];' in BOARD_PAGE + + +def test_list_groups_are_collapsible_and_persist_across_reloads(): + """A per-state header toggles its group; collapse state lives in a module-scoped Set + so the 10s auto-reload re-render doesn't re-expand what the user closed.""" + assert "function toggleGroup(state)" in BOARD_PAGE + assert "const COLLAPSED = new Set();" in BOARD_PAGE + assert "window.toggleGroup = toggleGroup;" in BOARD_PAGE # exposed for the inline onclick + # the group header row carries the state name + a count badge, and omits empty sections + assert 'class="grp"' in BOARD_PAGE + assert "if (!items.length) return;" in BOARD_PAGE + + +def test_kanban_columns_are_unchanged(): + """#26 is the list projection only — the Kanban's 5 state columns stay as they were.""" + assert 'const COLS = ["backlog", "ready", "in_progress", "in_review", "done"];' in BOARD_PAGE