mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-11-14 00:26:28 +01:00
## Thinking Path > - Paperclip is the open source app people use to manage AI agents for work > - People open tasks from task lists, the Inbox and the sidebar many times each hour > - A task's chat shows its first frame only after about seven separate reads finish > - #20355 added one batched `chat-bootstrap` read that returns all of these reads. #20450 used it only for the sidebar's Recents > - The hosted database is CPU-bound, so the UI must open tasks faster and must also send fewer requests > - This pull request uses the bootstrap for task lists, for open intent on any task link, for cold opens and for a hard load of a task URL. It shares one coordination module between all of these paths > - The benefit is that a task opens from cache more often, a cold open sends one chat request instead of about seven, and the first content paint comes sooner ## Linked Issues or Issue Description Refs #20355 (server read that this pull request consumes). Refs #20450 (Recents preload, now on the shared path). **Is your feature request related to a problem?** A click on a task in a task list waited for about seven separate chat reads. The default task list (Coordinator) had no preload at all. A press or a resting hover on a task link sent the same seven reads, and the page then sent a second set. A hard load of a task URL started its chat reads only after the JavaScript bundle and the company list loaded. Sidebar project task links had no open-intent prefetch. **Describe the solution you'd like** Use the single `chat-bootstrap` read everywhere a task is about to open. Let the page's gating reads share a bootstrap that is in flight instead of sending their own requests. Keep the core issue read separate so the header does not wait for the larger read. **Describe alternatives you've considered** Preload 30 rows per list in three requests (the first prototype). Each task costs the server about fifty queries, and a resting hover warms any other row before the click, so this change preloads 10 rows in one request. ## What Changed - `ui/src/lib/issue-chat-bootstrap.ts`: one module decides when to send a bootstrap and lets page reads share it (`readFromIssueChatBootstrap`). Seeding uses the existing `issueChatBootstrapCache` (replace-style reads are refreshed when newer, merge-style reads are filled only when empty, other companies are ignored). It skips tasks that are loading, loaded in the last 30 seconds, or already fresh in cache, under the id or the identifier. It never aborts a request in flight, so a click that unmounts the list still uses the answer. Each request is bound to the account that sent it; a request sent before the first session read is bound to the first account the session reports, and a sign-out revokes it. A page read shares a bootstrap only while its key holds no data (a refresh after a write always reads the server), and waits at most 5 seconds for it. Every ref of a task (id and identifier) is tracked at request start. Entries expire after 30 seconds. - Task lists preload the chat of their top 10 rows in one request, after the list's own reads settle and the browser is idle (`usePreloadTaskChats`, `when-page-settles.ts`): the Coordinator task list (`TaskList` with `preloadCompanyId`), the streamlined and legacy issues lists and the streamlined Inbox. Recents now uses the same path, so a task in both places loads once. A list that stays open preloads each task at most once in five minutes: live events (#20563's lanes) keep a preloaded task's cache current or mark it stale, and a stale key is read from the server, never from an older bootstrap. All seeded keys are per-task keys on the fast live lane, not the ambient lane. - Open intent (press, click, resting hover or focus) on a task with no chat history in cache warms it with one bootstrap request instead of six separate reads (`prefetchIssueDetailForNavigation`). A task seen before still refreshes its cached reads one by one. - A cold open starts one bootstrap. The gating reads (recent comments, activity, runs, input facts, interactions, attachments, live runs, active run) wait for it instead of sending their own requests. The core issue read stays independent. - A hard load of a task URL starts `GET /api/issues/:ref/chat-bootstrap` from the inline preload script, in parallel with the JavaScript bundle. The page adopts that answer. - Sidebar project task links (Coordinator sidebar) get the same hover, press and focus warming as Recents and `IssueLinkQuicklook`. - Tests: unit tests for the coordination module, Recents, `TaskList` preload, sidebar warming and the cold-intent path. A new layout journey checks that a task the Inbox preloaded reveals once at its latest message without reading its chat again. Journeys that hold one separate read now also hold or fail the bootstrap. The Inbox reveal journey held a read that the chat no longer sends on open (it was flaky on master too), so it now holds the recent comment window. - The perf harness gains `PAPERCLIP_ISSUE_PERF_LIST_DWELL_MS` and the README documents it. ## Verification Measurement setup: production UI builds of master (`e01fe7f84a` and the earlier base `8d2bf78323`) and of this branch, served by one local server with `PAPERCLIP_UI_DEV_MIDDLEWARE=false` and embedded PostgreSQL. Only `index.html` changed between variants (both asset sets were present), so the server, database and seeded task were the same. Four rounds of the issue-detail harness, with master and branch alternated in each round: 20 pairs per S1 cell, 40 per S2 cell. The host was shared with other agents (load average 16 to 43), so the spread is large. Content paint (the task is readable), median paired delta, branch minus master: | Scenario | Profile | Pairs | Content paint | Branch faster in | API requests before content (master / branch) | |---|---|---:|---:|---:|---:| | S1 preloaded click (dwell 1500 ms) | unthrottled | 20 | -45 ms | 15/20 | 25 / 18 | | S1 preloaded click (dwell 1500 ms) | Fast 4G, 4x CPU | 20 | -23 ms | 13/20 | 26 / 18 | | S1 click, no dwell | unthrottled | 20 | -51 ms | 15/20 | 25 / 18 | | S1 click, no dwell | Fast 4G, 4x CPU | 20 | -21 ms | 12/20 | 26 / 18 | | S2 cold deep link | unthrottled | 40 | -61 ms | 28/40 | 54 / 40 | | S2 cold deep link | Fast 4G, 4x CPU | 40 | -129 ms | 29/40 | 47 / 39 | Medians (p25 to p75), content paint in ms: S1 dwell unthrottled 295 (283-315) → 248 (217-276). S1 dwell Fast 4G 1133 (1114-1226) → 1093 (853-1176). S1 no dwell unthrottled 295 (277-328) → 260 (233-332). S1 no dwell Fast 4G 984 (842-1196) → 923 (816-1110). S2 unthrottled 1002 (677-1235) → 896 (619-1280). S2 Fast 4G 4643 (4155-5061) → 4516 (4298-4894). Header paint is neutral to slightly later in two cells: S1 dwell unthrottled +39 ms (3/20 faster) and S2 Fast 4G +56 ms (17/40). In those cells the header now paints in the same frame as the content. In the first two rounds, the median gap from header to content fell from 105-157 ms to 29-74 ms unthrottled and from 557 ms to 298 ms for S2 on Fast 4G. Other header cells are -27 to +1 ms. The local server answers each read in a few milliseconds, so the network wait that the preload removes is small here. The request count is the stable result: a cold open sends one chat request instead of about seven. List page: the preload request starts after the first task row is in the DOM (64 ms after it unthrottled, 200-280 ms after it on Fast 4G, page clock). Time to the first visible row, 30-task list: unthrottled master 1863 ms vs branch 1563 ms (6 pairs, branch faster in 5). Fast 4G master 3626 ms vs branch 3727 ms (14 pairs, branch faster in 6, mixed signs). The preload starts after the row paints, so it cannot delay it; the Fast 4G difference is noise. One 10-task bootstrap took 78-162 ms on the server. Checks run: - `pnpm exec vitest run` for the bootstrap, cache, prefetch, Recents, sidebar, quicklook, `TaskList`, issues list, Inbox, issue detail and Coordinator tests: 18 files, 646 tests pass. - `pnpm exec tsc -p ui/tsconfig.json --noEmit`: clean, except missing generated Storybook catalog files that this change does not touch. - `pnpm check:token-gates`: clean. - `tests/e2e/task-loading-frame.spec.ts`, `disconnect-resilience.spec.ts` and `connection-reviews.spec.ts` (production build): 21 passed, 1 skipped. - `tests/perf/issue-detail/layout-stability.spec.ts` and `plan-startup.spec.ts` against the production build: all pass. The Inbox reveal journey and the new preloaded journey passed 4 of 4 repeats each. - `pnpm verify` asks for broader CI coverage because an e2e spec changed. ## Risks - More server work for list views: one bootstrap of up to 10 tasks per list visit (about fifty queries per task), skipped for tasks already loaded in the last 30 seconds. It runs only after the page's own reads settle and the browser is idle, and only while the tab is visible. - A resting hover on a never-opened task now sends one bootstrap instead of about six reads. The server work per task is about the same; the request count is lower. - If a bootstrap fails, each page read falls back to its own request. The fallback is slower than before for that one open (the reads start when the page mounts, not at the press). - The header can paint a few tens of milliseconds later in the same frame as the content when everything is cached. - An answer that arrives after a sign-out or account change is dropped. An answer for another company is ignored. ## Model Used - Claude Opus 5.5 (`claude-opus-5-5`), Anthropic, used through Claude Code with tool use (shell, file edits, Playwright runs). ## Checklist - [x] I have included a thinking path that traces from project context to this change - [x] I have specified the model used (with version and capability details) - [x] I have checked ROADMAP.md and confirmed this PR does not duplicate planned core work - [x] I have searched GitHub for duplicate or related PRs and linked them above - [x] I have either (a) linked existing issues with `Fixes: #` / `Closes #` / `Refs #` OR (b) described the issue in-PR following the relevant issue template - [x] I have not referenced internal/instance-local Paperclip issues or links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip` URLs) - [x] My branch name describes the change (e.g. `docs/...`, `fix/...`) and contains no internal Paperclip ticket id or instance-derived details - [x] I have run tests locally and they pass - [x] I have added or updated tests where applicable - [x] I have updated relevant documentation to reflect my changes - [x] I have considered and documented any risks above - [ ] All Paperclip CI gates are green - [ ] Greptile is 5/5 with no open P2s, recommendations, or follow-ups - [x] I will address all Greptile and reviewer comments before requesting merge 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>