Skip to content

Tasks: load open work first, patch single tasks on signals, window list rows - #4442

Open
MGrin wants to merge 1 commit into
get-bb:mainfrom
MGrin:tasks-open-first-incremental
Open

MGrin wants to merge 1 commit into
get-bb:mainfrom
MGrin:tasks-open-first-incremental

Conversation

@MGrin

@MGrin MGrin commented Sep 28, 2026 •

Copy link
Copy Markdown
Contributor

What was wrong

On a large board the Tasks page froze for seconds on open and again on every change. Three causes, all in the page's data layer. listAllTasks paged every task, done and cancelled included, before anything painted. useInvalidation refetched the whole query on every tasks:changed and threads:changed signal, even though each signal names the task it is about, and agents emit these in bursts. The list then mounted a DOM row for every task. The board view had the same refetch, plus one listAttachments call per card on every refetch.

What changed

  • shell/data.ts. useTasksQuery takes an optional applySignals patcher. Signals are batched for 50 ms. When every signal in a batch names a task and no load is in flight, the patcher updates the current data in place. Otherwise, or when the patcher fails, it falls back to the existing full refetch. New helpers: listTasksOpenFirst, which fetches open and closed statuses concurrently and publishes open work as soon as it arrives, and patchTasks, which re-reads only the named tasks via getTask, drops deleted tasks and re-checks filter membership.
  • views/list/data.ts, views/list/index.tsx. The list loads open work first and patches rows on tasks:changed. Active-thread metadata is fetched only for tasks with live threads (activeOnly, the same predicate as isActiveThread) and patched per task on threads:changed.
  • views/list/row-window.ts (new), views/list/row.tsx. Only rows near the viewport render, with 800 px of overscan, and groups under 120 rows render in full. The window recomputes every 200 px of scroll. TaskRow is memoized and onOpen now takes the task key, so a step mounts only the new rows.
  • views/board/index.tsx. On a signal, the board re-reads only the named card: getTask, plus that card's attachments and live threads. It recomputes subtask progress locally and keeps the server's (position, id) order. projects:changed still reloads the board.

No contract, server or wire changes. No behaviour change for a signal without a taskId, which still triggers a full refetch.

How you verified

  • pnpm exec turbo run test typecheck lint --filter=bb-plugin-tasks: 402/402 tests (393 before, plus 9 new), typecheck clean, lint 0 errors.
    • shell/incremental.test.tsx (new, 5 tests): a burst of 5 signals for one task makes 1 getTask and 0 listTasks calls; a deleted task drops without a refetch; a signal with no taskId still refetches; the board re-reads only the changed card; thread signals for tasks outside the board cost nothing. With the patch path disabled, 4 of the 5 fail. The fifth is the fallback test, which is correct.
    • views/list/row-window.test.ts (new, 4 tests).
    • Two existing shell.test.tsx mocks held only the latest deferred listTasks call. The page now makes two concurrent calls (open and closed), so the mocks release every pending call. Their assertions are unchanged.
  • Measured against main on the same machine and the same copy of a real board (≈4.7k top-level tasks, ≈5k with subtasks): a pnpm start:worktree instance with its own data dir, driven from a headless browser. The same harness ran on both builds.
main this PR
All tasks: first rows painted 5.5–6.0 s (5 runs) 0.18–0.41 s
All tasks: longest frame while loading 4.9–5.3 s 99–112 ms
One project (≈1.3k tasks): first rows 1.2–1.4 s 63–90 ms
One task edited: row updated 2.7–3.3 s, 11 listTasks calls 78–129 ms, 0 list refetches ¹
Scroll the full list, idle 60 fps 60 fps
Scroll the full list while a task changes every 3 s 6–9 fps, worst frame 1.6–3.6 s 59 fps, worst frame 35–53 ms
DOM rows mounted (All) 4,685 ≈90

¹ The one remaining listTasks call per edit is the sidebar's activeOnly count, not the page.

Not measured: the board view's load time, and a production React build (the dev instance runs React in development mode, so absolute times are pessimistic for both columns).

🤖 Generated with Claude Code

AGENT GENERATED

…d memoize list rows

The Tasks page paged every task on each visit and refetched the whole set
on every tasks:changed and threads:changed signal, then mounted a DOM row
for every task. On a large board that froze the page for seconds on open
and on every change.

- Open-first: the list fetches open and closed statuses concurrently and
  paints open work as soon as it arrives.
- Per-task patching: signals are batched for 50 ms; a signal that names a
  task re-reads only that task (getTask), and the board re-reads only that
  card's attachments and threads. A signal without a taskId, or
  projects:changed, still triggers a full refetch.
- The list renders only rows near the viewport, stepping the window every
  200 px, and rows are memoized so a step mounts only the new ones.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

This branch has not been deployed

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant