Skip to content

feat(tasks): filter the project task list by status, search it by PR/issue number, and bulk-edit a selection - #920

Open
sheeerth wants to merge 9 commits into
open-mercato:mainfrom
sheeerth:feat/task-list-filters-bulk-edit
Open

sheeerth wants to merge 9 commits into
open-mercato:mainfrom
sheeerth:feat/task-list-filters-bulk-edit

Conversation

@sheeerth

Copy link
Copy Markdown
Contributor

Tracking plan: .ai/runs/2026-08-23-task-list-filters-bulk-edit.md
Status: complete

🎯 Goal

  • The per-project Tasks list could only be narrowed by the Active/Archived tabs and a free-text box over title/branch/workflow, and every row had to be archived one at a time. This PR adds a Status facet filter with live per-option counts, makes the search box find a task by its PR or issue number, and adds row selection with a bulk action bar (archive, restore, mark read, mark unread) so a batch of finished tasks is one gesture instead of ten.

What Changed

  • packages/web/src/lib/tasks-table.ts — the pure filter model. referenceNeedle() recognizes #909, a bare 909, pr 909 and issue 42; filterRuns() answers them against taskReferences(), the same list the row's own chip is built from, so search can never surface a number the table would not show. Alongside it: TaskListFilters, filterTaskList() (facet AND search), statusFacetOptions() (options from the statuses present, counts computed the way the global page computes them), plus toggleStatusFilter / activeFilterCount / hasActiveTaskFilters for the Clear affordance. The facet's vocabulary is the status pill's word (deriveAttention().label), not RunRecord.status, so it can offer scheduled and monitoring and can never disagree with the column beside it.
  • packages/web/src/lib/task-selection.ts (new) — the pure selection model: toggleSelected, toggleAllVisible, the tri-state selectionSummary (which intersects the selected ids with the rows currently on screen, so a stale pick is inert), bulkActionTargets (per-action gating on the same rules the single-row actions use), and bulkResultMessage for an honest receipt.
  • packages/web/src/routes/tasks-overview.tsx — the Status pill and Clear in the header, a selection column on the table (tri-state header box) and on the <md cards, the bulk action bar, a filter-aware empty state, and the route-level fan-out: Promise.allSettled over the existing per-run endpoints, one invalidation, and a toast that can say "Archived 1 of 2 tasks — 1 failed: …".

No server, contract or persisted-state change: every action already had a per-run route (POST /runs/:id/archive, /read, /unread).

🧪 Tests

  • npm run typecheck ✅ · npm test6221 passed / 327 files · npm run test:unit ✅ 36 · npm run build ✅ (incl. check:pack) · npm run test:package ✅ 15.
  • New: 40 cases across lib/tasks-table.test.ts (reference needle parsing, reference search, status vocabulary, facet counts, the status-order drift guard), lib/task-selection.test.ts (toggling, stale picks, per-action gating, select-all under a filter, receipt wording) and routes/tasks-overview.test.tsx (filtering through the real facet popover, Clear, empty-state wording, selection mechanics, action gating, checkbox-does-not-navigate on both row and card, and a route-level bulk archive where one run is refused).
  • om-auto-review-pr --autofix returned approve — no blockers, no majors; one minor (the bulk receipt's wording) was fixed in a4ab6fec and the gate re-run green on that commit. The repository's own CI workflow (Unit, build, E2E, and package) passed on this head.
  • Not verified in a live browser: npm run test:e2e could not provision the agent-browser provider here and exited TEST_E2E_STATUS=skipped, which is not a pass. This is a UI change, so that gap is real — see the run-summary comment.
  • Note on the environment: 6 pre-existing server tests fail when TMPDIR points inside the repository (they mkdtemp a "not a git repo" fixture, which then resolves to this repo). The gate above was run with TMPDIR=/tmp; the failures are unrelated to this change and reproduce on a clean checkout.

💥 Breaking Changes

  • None. Cockpit-only; no API, contract or storage surface is touched. The table gains a leading selection column, which shifts nothing semantically (its cells carry data-column-id="select" and are excluded from the column assertions).

📋 Progress

See the Progress section in the tracking plan.


Opened from the fork branch sheeerth:feat/task-list-filters-bulk-edit. The same branch also carries the automation's own run artifacts — execution plan, code-review report and run summary — on sheeerth/cezar#3; nothing there is required to review this PR, the diff is identical.

…us facet

The Tasks table's search box only ever matched title, branch and workflow, so a
task known by its tracker number — the way it is named in a PR body or a standup
— could only be found by remembering its title. `referenceNeedle` recognizes
`open-mercato#909`, a bare `909`, `pr 909` and `issue 42`, and `filterRuns` answers them
against `taskReferences`: the same list the row's own chip is built from, so
search can never surface a number the table would not show.

Alongside it, the pure model the status filter needs: `TaskListFilters`,
`filterTaskList` (facet AND search), `statusFacetOptions` with counts computed
the way the global page computes them, and the toggle/count helpers the Clear
affordance reads. The facet's vocabulary is the status PILL's word
(`deriveAttention().label`), not `RunRecord.status`, so it can offer
`scheduled` and `monitoring` — sub-states the record spells as failed/running —
and can never disagree with the column beside it.
… supports

`lib/task-selection.ts` is the pure half of multi-edit: toggling a row, the
tri-state header checkbox, and which of archive / restore / mark-read /
mark-unread a given selection can actually carry out.

Two rules it exists to enforce. A selection is always read against the rows
CURRENTLY ON SCREEN, so an id that outlived its row — filtered away, archived,
patched out by an SSE tick — is inert rather than able to act. And an action
offers itself only for the rows it would really change, using the SAME gates the
single-row actions use: a `review` run is not archivable by checkbox any more
than it is by the "Archive finished" broom, and only a `canBeUnread` row can be
put back to unread.
…ion of rows

The Tasks table gets the two things it was missing next to the global page: a
Status facet — the same searchable multi-select pill, with per-option row counts
— and a selection column whose bulk bar can archive, restore, mark read and mark
unread any number of rows at once.

The facet reads the status PILL's word, so it can offer `scheduled` and
`monitoring`, and it ANDs with the search box; Clear counts and undoes both. An
empty result now blames the filter when a filter is what emptied it, instead of
reporting an empty archive.

Multi-edit is deliberately conservative. Select-all covers the FILTERED list and
never the rows a filter is hiding; each action is offered only for the rows it
would really change (a `review` run is no more archivable by checkbox than by
the broom); the fan-out is `allSettled` over the per-run endpoints, so one
refused write neither cancels the batch nor gets reported as a success — the
toast says "Archived 1 of 2 tasks — 1 failed: …" and the list is refetched from
the server rather than trusted from the cache.
Review finding (minor, user-facing copy): `Marked read 2 tasks.` is a log line,
not a sentence. English puts the object in the middle of "marked … read", so the
verb table now carries a trailing complement and the toast says "Marked 2 tasks
read." — and "Marked 0 of 1 task read — 1 failed" on the partial path.
@sheeerth

Copy link
Copy Markdown
Contributor Author

🧪 Verification, and two things a maintainer needs from me

Gate, re-run in full on the head commit a4ab6fec: npm run typecheck ✅ · npm test6221 passed / 327 files · npm run test:unit ✅ 36 · npm run build ✅ (incl. check:pack ok — 475 files) · npm run test:package ✅ 15. Branch is 9 commits ahead of main and 0 behind, so no rebase is pending.

One environment caveat worth knowing if you run the gate yourself: six server tests (git-worktree, server/git, server/git-changes, server/health-forge, server/projects-api, server/automations-api) fail whenever TMPDIR points inside the repository — they mkdtemp a "not a git repo" fixture which then resolves to the repo itself. They pass on the same commit with TMPDIR=/tmp, and they touch nothing this PR changes.

📸 The one real gap: no live-browser verification. This is a UI change, and npm run test:e2e could not provision the agent-browser provider in the environment this was built in (no network to the GitHub Releases / Chrome-for-Testing hosts) — the script exited TEST_E2E_STATUS=skipped, which is explicitly not a pass. The UI is covered at component and route level, including through the real Radix + cmdk popover in jsdom, but nobody has looked at it in a browser. A two-minute click-through of the Tasks page, or npm run test:e2e on a networked machine, is what closes it — the run-summary comment on the mirror PR has a step-by-step smoke test if that helps.

🏷️ Labels — I could not apply them (read-only access to this repository). Suggested set, with the reasoning so you can disagree with it cheaply:

  • 🔍 review — the work is finished and this is asking for a code review.
  • feature — new capability on an existing screen, not a fix.
  • 🟡 priority-medium — an ordinary usability improvement; nothing is broken without it.
  • 🟢 risk-low — additive and cockpit-only: no server route, no packages/contract schema, no .ai/cezar/ state shape, no new dependency. The only export-surface change is additive (FINISHED_STATUSES becoming exported so the bulk bar and the "Archive finished" broom share one definition of finished), filterRuns's signature is unchanged, and both new TasksOverview props are optional with defaults.
  • 🟡 needs-qa rather than skip-qa — precisely because of the e2e gap above. If you would rather trust the unit/route coverage, skip-qa is defensible; I would not claim it for a UI change nobody has seen render.

@CLAassistant

Copy link
Copy Markdown

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution.
You have signed the CLA already but the status is still pending? Let us recheck it.

@pat-lewczuk

Copy link
Copy Markdown
Collaborator

🤖 om-auto-review-pr started by @pat-lewczuk at 2026-08-28T21:35:11Z. Other auto-skills will skip this PR until the lock is released.

@pat-lewczuk pat-lewczuk left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🔍 Code Review: feat(tasks): filter the project task list by status, search it by PR/issue number, and bulk-edit a selection

🎯 Summary

This PR brings the per-project Tasks list (/p/:projectId/) up to the bar the global /tasks page already set, in three additive pieces. packages/web/src/lib/tasks-table.ts grows the pure filter model: referenceNeedle() recognizes #909 / 909 / pr 909 / issue 42, filterRuns() answers those against taskReferences() — the same list the row's own chip is built from, so search cannot surface a number the table would not show — and TaskListFilters / filterTaskList() / statusFacetOptions() add a status facet whose vocabulary is the status pill's word (deriveAttention().label) rather than RunRecord.status, which is what lets it offer scheduled and monitoring and is what stops it from disagreeing with the column beside it. The new packages/web/src/lib/task-selection.ts is the pure selection model — toggling, the tri-state selectionSummary, per-action gating in bulkActionTargets, and the receipt wording. packages/web/src/routes/tasks-overview.tsx wires both into the header (Status pill + Clear), a selection column on the table and the <md cards, a bulk action bar, a filter-aware empty state, and a route-level Promise.allSettled fan-out over the existing per-run endpoints.

I reviewed all seven changed files against the current head bea6fe58 in an isolated worktree, and ran the full validation gate locally. Three things stand out as genuinely well done rather than merely adequate. First, the selection is never trusted on its own: selectionSummary() intersects the id set with the rows currently on screen (task-selection.ts:82-90), so an id that outlived its row is inert rather than dangerous — and the component deliberately does not prune the set in an effect, which would have been a second source of truth racing the first. Second, bulkActionTargets() reuses the same gates the single-row actions use (FINISHED_STATUSES, isUnread, canBeUnread), and FINISHED_STATUSES was exported rather than restated, so the "Archive finished" broom and the bulk bar cannot drift apart about what finished means — a review run still cannot be swept away by a checkbox, which is exactly right. Third, the fan-out uses allSettled rather than all (tasks-overview.tsx:1246), so one refused write does not cancel the rest and the receipt can honestly say "3 of 5" instead of claiming work that never happened.

There is no server, contract, or persisted-state change: every action already had a per-run route, and the client helpers used (archiveRun, markRunSeen, markRunUnseen) are the queryScope()-based ones, which is the correct choice for a page that already stands inside /p/:projectId — the explicit-project twins (archiveProjectRun, setProjectRunRead) exist for the global page and are correctly not used here. Test coverage is unusually thorough for a UI change, including a route-level test that exercises a partial-failure batch through a mocked fetch. My findings are all minor or nit; none of them blocks.

Verdict

approve — there are no blockers and no majors. The full validation gate is green on the head commit, no protected surface from BACKWARD_COMPATIBILITY.md is touched, the change is additive and cockpit-only, and every behavior it adds carries tests. The four items below are minors and nits the author can pick up at their discretion; the one that is worth acting on is the partial-failure receipt losing the handle on the rows that failed.

🧪 Validation Gate

Run in an isolated worktree at bea6fe58 with TMPDIR=/tmp (see the note under the table).

Command Status Notes
npm run typecheck ✅ PASS All four workspaces clean — contract, api-client, server, web.
npm test ✅ PASS 6221 tests passed across 327 files, none skipped, in 100.8s. This includes design-guardian.test.ts, so the new violet/10 / violet/40 surfaces are token-compliant.
npm run test:unit ✅ PASS 36 node:test cases, 0 failures.
npm run build ✅ PASS Vite build succeeded and check:pack ok — 475 files, 85 under web/dist (shell + assets present).
npm run test:package ✅ PASS 15 packaged-CLI E2E cases, 0 failures.

I can confirm the environment caveat the author raised: the six server tests that mkdtemp a "not a git repo" fixture do fail when TMPDIR points inside the repository, and they pass on this commit with TMPDIR=/tmp. That is a pre-existing property of those tests and has nothing to do with this diff.

CI has not run on this pull request in this repository. There are zero GitHub Actions check-runs on head bea6fe58 here; the only reported check is license/cla, which is pending because the CLA is not signed (CLA assistant). Signing it is a hard prerequisite for merge and is the likely reason the workflows have not been dispatched for this fork branch. This approval therefore covers the code on the strength of the local gate above, not a green run in this repository, and I am not promising a CI follow-up comment — a CLA signature settles on a human's action, not on a timer, so there is nothing here for an agent to usefully wait on. The green CI the PR description cites is the run on the author's mirror PR (sheeerth/cezar#3), which is the same diff but not this repository's checks.

Findings

🔹 Minor

packages/web/src/routes/tasks-overview.tsx:177-182 — a partially failed batch leaves the reader with no handle on the rows that failed. runBulkAction calls onBulkAction(action, targets) and then setSelected(NO_SELECTION) synchronously, so the selection is cleared before the mutation resolves and the bar unmounts on the same render. When the fan-out comes back partial, the toast says Archived 3 of 5 tasks — 2 failed: run is locked (task-selection.ts:160-164) — a count and the first reason, but not which two. The reader is then left re-finding those rows by eye in a list where they are no longer distinguished by anything except still being there. The mitigation is real but weak: the failed rows do stay in the Active list, so the state is discoverable, just not pointed at. The cheapest fix that keeps the intent (the bar should not linger over rows that are leaving the view) is to have the route report the failed ids back and re-seed the selection with them on a partial failure — the selection becomes exactly "the ones that did not go through", which is also what you want to retry. Failing that, listing the failed task titles in the toast body would at least name them.

packages/web/src/lib/tasks-table.ts:213-221 — the doc comment on statusFacetOptions promises something the function does not do. It says options "come from every status PRESENT in the list, so a facet can never offer a value that could only ever empty the table". But present is computed from the unfiltered runs (line 232) while counts is computed from filterTaskList(runs, { ...filters, statuses: [] }) (line 228), which honors the search box. So with a query typed, the facet does offer options whose count is 0, and ticking one empties the table. The behavior is intentional and is pinned by a test (tasks-table.test.ts:290-306 asserts ['needs you', 0] and ['running', 0] under the query gamma) — the very next sentence in the same comment defends showing a 0 count on purpose. It is the first sentence that overstates, and in a module whose comments are this load-bearing, a promise the code does not keep is the kind of thing the next reader will trust. Reword it to say options come from every status present in the list before the search narrows it, so unticking always brings something back, and a 0 count is the facet telling you in advance that the search has already excluded that status.

💅 Nit

packages/web/src/lib/task-selection.ts:160-164 — the receipt is a sentence on success and not quite one on failure. The success branch ends with a full stop (Archived 3 tasks.), the partial branch does not (Archived 3 of 5 tasks — 2 failed: run is locked). The module's own comment on BULK_DONE_VERB argues, correctly, that this string is shown to a person and should read as a sentence; the failure path is the one where the reader is most likely to be reading carefully. Both branches are pinned by tests (task-selection.test.ts:118-131), so this is one character plus three test-string updates.

packages/web/src/routes/tasks-overview.test.tsx:1176-1184 — the bulkPending guard is asserted by injection rather than exercised. The test renders with bulkPending: true as a prop and checks the button is disabled. In the running app that state is reachable only by a narrow path: runBulkAction empties the selection, the bar unmounts, and pending can matter again only if the reader re-ticks rows while the first batch is still in flight. The assertion as written proves the prop is wired, not that the race it names is actually prevented. A test that ticks rows, fires the action, and re-ticks while the fetch is still unresolved would cover the real path; as it stands the test's name ("waits rather than letting a second batch race the first") claims more than it checks.

packages/web/src/routes/tasks-overview.tsx:526-527title duplicates aria-label on every tick box. SelectionCheckbox sets both to the same string, and some screen-reader/browser pairings announce the accessible name and then the tooltip, so a row reads as "Select Done one, Select Done one". Since the desktop row and the <md card are both mounted (hidden by CSS, not unmounted), that is now four announcements of the same name per run. Dropping title where it merely repeats aria-label is enough; keep it on the bulk-bar buttons, where it carries the distinct "why this is disabled" text that aria-label does not.

packages/web/src/routes/tasks-overview.tsx:160-169 — the filter pipeline runs three times per render. filterTaskList(inView, filters) for visible, again inside statusFacetOptions(inView, filters), and a third time as filterTaskList(all, filters) for the compare strips — each pass calling deriveAttention() per run, and, when the query parses as a reference needle, taskReferences() per run on top. Nothing is memoized, so every keystroke in the search box pays all three. The previous code already called filterRuns twice, so this is a widening rather than a new problem, and at the list sizes this page actually sees it is not worth a useMemo for its own sake. Worth knowing it is there if the project list ever grows past a few hundred rows.

💥 Breaking Changes

  • No exported/public symbol removed or renamed without a deprecation path. FINISHED_STATUSES moves from module-private to exported (tasks-table.ts:26), which is purely additive; nothing was removed.
  • No function signature changed in a breaking way. filterRuns(runs, query) keeps its signature; its behavior widens to also match tracker references, which strictly adds matches rather than removing any. TasksEmptyState's prop change from query to filters is module-private and not exported.
  • No required type field removed or narrowed. TaskListFilters and StatusFacetOption are new; nothing existing was narrowed.
  • No HTTP route URL removed or renamed; no method changed for an existing operation. The bulk fan-out reuses POST /runs/:id/archive, /read and /unread exactly as the single-row actions already did.
  • No field removed or retyped in an existing response shape. No server code is touched.
  • No event or message name renamed or removed; no payload field removed.
  • No CLI command or flag renamed or removed; no machine-parsed output format changed.
  • No database table or column renamed or removed; no column type narrowed. No .ai/cezar/ state shape is touched, so old runs.json files keep parsing unchanged.
  • No config key renamed and no default changed silently. The two new TasksOverview props (onBulkAction, bulkPending) are optional with defaults, so a direct render needs no stub.
  • Where a contract had to change: not applicable — no contract changed. Nothing in BACKWARD_COMPATIBILITY.md's nine protected surfaces is in this diff.

The table does gain a leading selection column, which shifts the column indices a DOM-order query would see. The diff handles that correctly by tagging the new cells data-column-id="select" and excluding them from the column assertions (tasks-overview.test.tsx:92-95, :345, :361) rather than by renumbering expectations, so the existing assertions still mean what they meant.

🧪 Test Coverage

The PR adds 40 cases across three files, and they cover the behavior rather than the implementation.

lib/tasks-table.test.ts covers needle parsing (#909, bare 909, pr 909, issue 42, and the kind-narrowing that stops pr 42 from surfacing an issue), the facet's status vocabulary, and the two-sided counting rule — that counts narrow with the search box but not with the facet's own ticks, which is the property that makes unticking a promise the facet can keep (:290-306). It also carries a drift guard (:308-332) pinning every label deriveAttention can currently emit to a place in STATUS_FILTER_ORDER; I checked this against lib/attention.ts and it is honest — the two ladder entries the test omits, needs permission and unseen, are wired to hard-coded false today by design, so they are unreachable rather than untested.

lib/task-selection.test.ts covers toggling without mutation, the tri-state header, per-action gating (including that a review row is selected but not archivable), select-all under a filter leaving hidden picks alone, and the receipt wording in all three shapes — full success, partial, and total failure.

routes/tasks-overview.test.tsx is the strongest part: it drives the real Radix + cmdk popover in jsdom rather than stubbing the facet, and it covers the ORing of statuses, the AND with the search box, the Clear count, the filter-aware empty state under data changing beneath a live filter, reference search end to end, the checkbox-does-not-navigate rule on both the row and the card, and a route-level bulk archive where one run is refused with a 409 and the batch still finishes.

The gaps are two, and neither is large. The bulkPending race is asserted by prop injection rather than exercised, as noted in the nits above. More materially, there is no real-browser verification of this change, which the author flags honestly in the PR description and in a comment: npm run test:e2e could not provision the agent-browser provider in their environment and exited TEST_E2E_STATUS=skipped, which is not a pass. This repository's own CODE_REVIEW.md names real-browser E2E as the QA layer for user-facing changes, and this is a user-facing change with new interactive controls (a popover, a tri-state checkbox whose indeterminate is set through a ref, a bar that appears and disappears). That gap is why I am applying needs-qa rather than skip-qa, and I have posted diff-derived manual-QA instructions in a separate comment. It is a QA-gate matter, not a code-review finding, so it does not affect this verdict.

@pat-lewczuk pat-lewczuk added merge-queue Approved, ready to merge feature New capability priority-medium Ordinary bug or feature risk-low Isolated, low blast radius needs-qa Requires manual QA before merge labels Aug 28, 2026
@pat-lewczuk

Copy link
Copy Markdown
Collaborator

🤖 om-auto-review-pr — 🏷️ label rationale

  • 🚀 merge-queue — the code review passed with no blockers and no majors, and the full validation gate (npm run typecheck, npm test, npm run test:unit, npm run build, npm run test:package) is green locally on head bea6fe58, so the change is queued to merge; note that the unsigned CLA (license/cla) and the QA-approval gate both still hold the actual merge.
  • 🧪 needs-qa — this is a user-facing cockpit change adding new interactive controls (a status facet popover, a tri-state header checkbox, a bulk action bar) and npm run test:e2e was skipped rather than passed in the author's environment, so nobody has yet seen it render in a real browser; when qaGate is on, this holds the merge until a QA reviewer adds qa-approved.
  • feature — a new capability on an existing screen (status filtering, reference search, multi-select bulk edit), not a bug fix or a refactor.
  • 🔹 priority-medium — an ordinary usability improvement to the project Tasks list; nothing is broken without it and no release is blocked on it.
  • 🟢 risk-low — additive and cockpit-only: no server route, no packages/contract schema, no .ai/cezar/ state shape, no new dependency, and none of the nine protected surfaces in BACKWARD_COMPATIBILITY.md is touched; the only export-surface change (FINISHED_STATUSES becoming exported) is additive, filterRuns's signature is unchanged, and both new TasksOverview props are optional with defaults.

@pat-lewczuk

Copy link
Copy Markdown
Collaborator

🧪 Manual QA instructions (needs-qa)

This PR is approved and requires manual QA (needs-qa, no skip-qa). It is queued in merge-queue but the QA-approval gate holds it until qa-approved is added. QA reviewer: when you pick it up, move it to qa by swapping the labels (remove merge-queue, add qa), then run the routes below.

Everything here lives on one screen — the per-project Tasks list, /p/:projectId/ — and no server route changed, so a local cez against a project with a handful of runs in mixed states is the whole setup. Useful seed: at least one running, one waiting, one done you have never opened, one done you have opened, one failed, one review, one already-archived run, and one run carrying a PR or issue chip.

P1 — Status facet filter

Where to click

  • /p/:projectId/ (the project's Tasks list), the Status pill in the header, on the Active tab and then again on the Archived tab.

What to verify

  • Opening the pill lists only the statuses actually present in the list you are looking at, in the list's own reading order (needs you → needs review → running → monitoring → scheduled → queued → done → failed → cancelled), not alphabetically, each with a row count beside it.
  • Ticking one status narrows the table to exactly that many rows; unticking it restores the list.
  • Ticking two statuses ORs them — picking failed and needs you shows both kinds, not their intersection.
  • Typing in the pill's own search box filters the option list; picking an option does not close the popover, so you can tick three in a row.
  • Switching to the Archived tab re-offers the statuses of the archived list only — no status that exists solely on an active run should appear there, and vice versa.
  • The facet's words match the Status pill in the row beside it exactly, including the sub-states scheduled (a usage-limit failure with a resume booked) and monitoring (a running task working on its own downstream work). A run showing scheduled must be reachable by ticking scheduled, not by ticking failed.

What can go wrong

  • The facet offers a status the list beside it spells differently — a scheduled row filed under failed, or a monitoring row under running. That is the exact drift this design exists to prevent, so it is the thing most worth probing.
  • The counts disagree with the number of rows you get after ticking.
  • Ticking a status changes the other options' counts (they are meant to ignore the facet's own ticks, so unticking always brings back the number it promised).
  • Options leaking across the Active/Archived split.

P1 — Search by PR or issue number

Where to click

  • The header search box on /p/:projectId/, now labelled "Search tasks, or a PR/issue number…".

What to verify

What can go wrong

P1 — Selecting rows and the bulk action bar

Where to click

  • /p/:projectId/ — the new leading checkbox column on the table, the tri-state checkbox in the header row, and the bulk action bar that appears above the table.
  • Then repeat the same passes at a mobile viewport (below md), where the list renders as cards each carrying its own tick box.

What to verify

  • Clicking a row's checkbox selects the row and does not navigate into the task. Clicking anywhere else on the row still opens it. Same on the mobile card — a tap on the box must not open the task.
  • The bar appears only once something is ticked, names the count (3 selected), and each button shows the number of rows it would really change, which is not always the size of the selection.
  • Select five rows of which two are already archived, and check Archive says three, not five.
  • Select a run whose status is review and confirm it is not counted by Archive — the disabled/reduced count is deliberate, a review run still wants a human, and the tooltip says so.
  • A button with nothing to do is disabled and its tooltip explains why (Nothing selected is archived., Nothing selected can be archived — only finished tasks can be.).
  • The header checkbox goes none → all → none, and shows the indeterminate (dash) state when only some rows are ticked; from indeterminate it clears rather than completing the selection.
  • Select-all under a filter selects only the filtered rows. Tick a status, press select-all, then clear the filter — the rows the filter had been hiding must not have been swept in.
  • Selecting a row and then filtering it away leaves it unable to act: the count drops and the action is sent only for rows still on screen.
  • Run each of the four actions and confirm the list and the toast agree: Archive moves rows to the Archived tab, Restore brings them back, Mark read clears the unread violet dot, Mark unread puts it back.
  • The toast reads as a sentence — Archived 3 tasks., Marked 2 tasks read. — and the selection empties afterwards.
  • Cold-load /p/:projectId/ and check the first useful state appears promptly and the table is interactive without a visible stall; the filter and selection work adds three filter passes per render, so a project with a few hundred runs is the interesting case for typing responsiveness in the search box.

What can go wrong

  • A checkbox click navigating away from the list (this is the regression the click-exemption change guards, and it is worth checking on both the row and the card).
  • The header box claiming "all" while rows a filter is hiding are also selected, or a bulk archive swallowing rows nobody looked at.
  • Desktop row and mobile card disagreeing about whether a task is ticked — they are one selection and must show the same state.
  • The review run being archivable through the checkbox when the "Archive finished" broom refuses it — the two must agree.
  • The bar staying on screen over rows that have just left it.

P1 — Partial failure and the empty state

Where to click

  • /p/:projectId/ with a selection that includes a run the server will refuse (for example, cancel or otherwise lock one run mid-flight, or archive a batch while another agent holds one of them).
  • The Status facet and the search box, pushed until they leave nothing on screen.

What to verify

  • When one write in a batch is refused, the rest still land, and the toast says so with the count and the reason — Archived 1 of 2 tasks — 1 failed: … — in the danger tone, rather than a flat success.
  • After a partial failure the list settles to the truth (the refused row is still in Active), even though the optimistic stream may briefly disagree.
  • Filtering everything away shows "No matching tasks" with "No tasks match the filters you picked." when only a status tick is responsible, and "No tasks match “{needle}”." when a search term is. Neither should be the "Nothing archived yet" / "No tasks yet" empty state — that would blame the list for the filter.
  • The Clear affordance appears only when something is narrowed, counts every narrowing (a status tick plus a typed word reads (2)), and one click restores the full list and empties the search box.

What can go wrong

  • A flat "Archived N tasks." over a batch where some writes were refused — that is a claim the list contradicts a second later, and it is the specific failure this design guards.
  • The empty state reading "Nothing archived yet" while a filter is on.
  • Clear's number disagreeing with what it actually clears.

Pass/fail

  • All routes pass → remove the qa label and add merge-queue plus qa-approved (this clears the QA-approval gate)
  • Any route fails → remove the qa label, add qa-failed, and leave a comment describing the failure.

@pat-lewczuk pat-lewczuk removed the in-progress Cezar agent is actively working this issue label Aug 28, 2026
@pat-lewczuk

Copy link
Copy Markdown
Collaborator

🤖 om-auto-review-pr completed: APPROVED. Lock released.

Full om-code-review pass on head bea6fe58 in an isolated worktree: no blockers, no majors — two minors (the partial-failure receipt loses the handle on the rows that failed; a doc comment on statusFacetOptions overstates what the function guarantees) and four nits, all listed in the review. The complete validation gate was run locally and is green: npm run typecheck ✅, npm test ✅ 6221 passed / 327 files, npm run test:unit ✅ 36, npm run build ✅ (incl. check:pack ok — 475 files), npm run test:package ✅ 15.

Labels: merge-queue + feature + priority-medium + risk-low + needs-qa, with the reasoning in the label-rationale comment and diff-derived QA routes in the manual-QA comment.

Two things still hold the merge, neither of them a code-review finding: the CLA is unsigned (license/cla is pending, and no GitHub Actions check-runs exist on this head in this repository), and the QA-approval gate waits on qa-approved. No CI follow-up is scheduled from this run — a CLA signature settles on a human's action, not on a timer, so there is nothing here for an agent to wait on.

autofix: skipped (not my PR — re-run with --autofix to fix it here). Nothing needed fixing in any case: the verdict is approve, and the remaining items are the author's call.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feature New capability merge-queue Approved, ready to merge needs-qa Requires manual QA before merge priority-medium Ordinary bug or feature risk-low Isolated, low blast radius

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants