mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-11-14 08:53:19 +01:00
## Thinking Path
> - Paperclip is the open source app people use to manage AI agents for
work
> - The board UI sidebar has a Recents list of the tasks a person opened
last
> - The Recents list renders from a local snapshot, but its links had no
warm-up. A click waited for the task-detail code and about nine
first-frame reads
> - Task lists (Inbox, Tasks) already warm a task on hover, press and
click, and #20355 added a batched `chat-bootstrap` read that had no UI
consumer
> - The hosted database is CPU-bound, so a fix must add at most one read
per page load and must not compete with the page's own reads
> - This pull request gives Recents the same open-intent prefetch as
task lists, warms the task-detail code, and preloads the top ten recent
tasks with one `chat-bootstrap` request after the page is idle
> - The benefit is that a click on a recent task shows its chat from
cache, without a wait for the API
## Linked Issues or Issue Description
Refs #20355 (server read this PR consumes). Builds on #20389, #20398 and
#20419.
**Is your feature request related to a problem?**
Opening a task from the sidebar's Recents was slower than opening it
from a task list. The Recents links had no prefetch. The task page
waited for its first-frame reads after the click.
**Describe the solution you'd like**
Recents links use the task lists' open-intent prefetch. When the page is
idle, one batched request loads the first frame of the most recent tasks
into the task page's caches.
**Describe alternatives you've considered**
Per-task prefetch of every recent task. That makes up to ten times nine
requests per page load, which the hosted database cannot afford.
**Additional context**
I searched open PRs and remote branches for "chat-bootstrap" and
"instant task opens". The only related work is the server-side branches
that merged as #20355. There is no other in-flight UI consumer of
`chat-bootstrap`.
## What Changed
- `ui/src/hooks/useIssueNavigationPrefetch.ts`: the open-intent handlers
from `IssueLinkQuicklook` move into one hook (light warm on pass-over;
`history: true` on a resting hover or focus, a mouse or pen press, and a
click). `IssueLinkQuicklook` now uses the hook. Its behavior does not
change.
- `ui/src/components/RecentTaskNavItem.tsx`: one Recents link wrapper
that applies the hook. The classic list (`SidebarRecentTasks`), the
Coordinator task rows and `DesignSidebar` use it. `SidebarNavItem`
accepts the handlers through a new optional `linkHandlers` prop.
- Classic Recents links now open `/issues/<identifier>` (Coordinator
already did). The old UUID link remounted the task page after a redirect
to the identifier route.
- `ui/src/hooks/useRecentTaskOpenWarmup.ts`: when Recents shows a task,
it fires the task-detail code warm-up (`useWarmTaskDetailCode`). After
two frames, a visible tab, no fetching queries and an idle callback, it
sends one `issues/chat-bootstrap?ids=` request for the ten most recent
tasks. It skips tasks whose first frame is cached and fresh. It sends no
request for an empty list or a hidden tab. It sends at most one request
per account per page load. Unmount or an account change aborts the
request.
- `ui/src/lib/issueChatBootstrapCache.ts`: `seedIssueChatBootstrap`
writes each field under the keys the task page reads, for the requested
ref, the id and the identifier: core issue, recent comments, comment
input facts (one batch, through the new `seedIssueIdBatch`), recent
activity, the recent run window (through the new shared
`buildRecentIssueRunWindow`), interactions, attachments, live runs and
active run. An explicit `null` active run is stored. A missing field is
not stored. It does not replace data that is as new or newer, data with
a read in flight, or data with a pending invalidation. Keys whose page
merges observed history (comments, activity, runs, input facts) are
filled only when empty. Items for another company are ignored.
- The seed fills the `?view=core` key, not the full detail key.
`seedIssueDetailCache` is not used: the core payload cannot stand in for
the detail read's related data, and seeding it there would suppress that
read.
- `issuesApi.chatBootstrap` client method.
- `tests/perf/collections/recent-task-open.spec.ts` and its README
section.
## Verification
Local disposable instance (`local_trusted`, production UI build,
embedded PostgreSQL, collection fixture plus 12 comments on each of 8
recent tasks, streamlined UI with the Coordinator sidebar). Journey:
open Projects with 8 Recents, wait for network idle plus 3 s, click a
recent task, measure until its latest comment is on screen. Each run
uses a fresh browser context (empty HTTP cache and storage apart from
Recents). Median of 5 runs, same server and fixture for both arms:
| Arm | Click to first chat frame | Idle page API requests |
| --- | --- | --- |
| `master` (b6f4008f9b), loopback | 227 ms (first run: 249 ms) | 24
(range 21–25) |
| This PR, loopback | 134 ms | 22 (range 21–27) |
| `master`, +150 ms on each API response | 455 ms | 19 |
| This PR, +150 ms on each API response | 135 ms | 20 (+1
`chat-bootstrap`) |
With added latency, the first frame no longer depends on API latency.
- `PAPERCLIP_COLLECTION_PERF_BASE_URL=http://127.0.0.1:<port> pnpm exec
playwright test --config tests/perf/collections/playwright.config.ts
recent-task-open.spec.ts recent-task-summaries.spec.ts`: 5 passed. On
`master` the two assertion cases fail (0 `chat-bootstrap` requests; 10
first-frame reads after the click).
- Reads that still follow a click on a preloaded task: full detail (`GET
/issues/:id`), `work-products`, `documents`, `feedback-votes`,
`tree-control/state`, `tree-holds`, `runner-goal`, `browsers`, and the
`read` mark. The `chat-bootstrap` payload does not cover them. None
gates the chat's first frame (the frame shows at 135 ms while every API
response waits 150 ms).
- Unit tests: `RecentTaskNavItem.test.tsx` (intent levels, href),
`useRecentTaskOpenWarmup.test.tsx` (one request for the top ten, code
warm-up, wait for page reads, hidden tab, fresh entries skipped, once
per account, user isolation, abort on unmount),
`issueChatBootstrapCache.test.ts` (each key in the endpoint shape,
`null` versus missing fields, company isolation, no downgrade of newer,
in-flight or invalidated data, merge keys filled only when empty).
- `ui` Vitest: `src/components src/hooks src/lib src/api` 694 files
passed; `src/pages src/App src/context` 188 files passed. `pnpm
check:token-gates`: clean. `pnpm verify` exits 2 because a `tests/` file
is a global CI input; required CI supplies the broad coverage.
## Risks
- Low to medium. One extra read per page load when Recents has tasks
without a fresh cached first frame. The read starts only after the
page's own queries settle, and the server reads at most two tasks at a
time.
- A live event that arrives while the preload is in flight, for a task
with no cached keys, can leave seeded data up to 30 s old until the next
live event or the next open after the stale time. Inbox hover prefetch
has the same window.
- Classic Recents links now use the identifier route instead of the UUID
route. The task page already redirected UUID links to this route.
- `tests/perf/collections` is a manual suite. CI does not run it.
## Model Used
- Claude Opus 5.5 (`claude-opus-5-5`), Anthropic, in Claude Code with
tool use (shell, file edits, browser automation through Playwright).
Standard context window.
## 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
- [ ] 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>
156 lines
9.5 KiB
Markdown
156 lines
9.5 KiB
Markdown
# Tasks and Inbox cold-open checks
|
|
|
|
Run against a disposable local instance with production UI assets. The suite
|
|
creates 220 unassigned backlog tasks and 110 board approvals. It does not invoke
|
|
a model. The origin must be a loopback address.
|
|
|
|
```sh
|
|
pnpm --filter @paperclipai/ui build
|
|
PAPERCLIP_COLLECTION_PERF_BASE_URL=http://127.0.0.1:3219 \
|
|
pnpm exec playwright test --config tests/perf/collections/playwright.config.ts
|
|
```
|
|
|
|
Start the isolated instance with `PAPERCLIP_UI_DEV_MIDDLEWARE=false` to serve the
|
|
production build. The instance and its database must be disposable. These tests
|
|
retain and reuse their fixture in `test-results/collections/fixture.json`; they
|
|
only replace it when the previous company's API returns 404. Other errors fail.
|
|
|
|
The journeys check desktop and phone row budgets, incremental scrolling,
|
|
retained row identity and height, shared count-only navigation, the Coordinator
|
|
Inbox with Streamlined UI disabled, and the page-owned Needs You feed and Needs
|
|
attention task index. Screenshots and failure traces stay in `test-results`.
|
|
Request counts and row budgets are acceptance checks. They do not establish an
|
|
end-to-end latency improvement. Measure cold, warm, and Back timings separately
|
|
with the same fixture, assets, cache, viewport, and CPU/network settings.
|
|
|
|
`parent-context.spec.ts` holds the complete Mine response, including parent
|
|
metadata, then checks row geometry through deferred visible diagnostics. Mine
|
|
uses one assembled source read; it does not fetch parent or failed-task context
|
|
from the browser before reveal. Recent retains its separate index/context path.
|
|
`startup-regressions.spec.ts` covers session bootstrap and legacy fallback,
|
|
partial source errors, resolved failure context, scoped 429 recovery, and
|
|
archive/Undo persistence. Its authenticated session and failed-run responses are
|
|
simulated transport fixtures over a real local board. They do not prove hosted
|
|
login or model execution.
|
|
The embedded bootstrap case holds separate company/access responses until Mine
|
|
rows appear. Initial WebSocket reconciliation may request those endpoints in the
|
|
background; their responses must not block the first rows. Missing or mismatched
|
|
bootstrap data still requires the ordinary fallback reads.
|
|
The two 429 cases keep the existing automatic backoff and `Retry-After` policy.
|
|
They allow 45 seconds for its bounded retry window before exercising manual
|
|
Retry. Other cases keep the normal short assertion timeout. Timing samples do
|
|
not accelerate the browser clock.
|
|
|
|
`mine-workspaces.spec.ts` guards Mine against waiting on a company-wide
|
|
execution-workspace list. Its fixture (`workspace-fixture.ts`) adds a company
|
|
with 6,000 execution workspaces, 4,000 of them created for finished tasks, and
|
|
12 Mine tasks that each run in their own isolated workspace. The API cannot
|
|
create execution workspaces, so this fixture writes those rows to the instance
|
|
database. Set `PAPERCLIP_CONFIG` to the disposable instance's `config.json`
|
|
(embedded PostgreSQL), or `PAPERCLIP_COLLECTION_PERF_DATABASE_URL` to its
|
|
loopback database. The fixture is retained in
|
|
`test-results/collections/workspace-fixture.json` with the same 404 rule as the
|
|
main fixture. The spec routes isolated workspaces on for its own pages. It
|
|
checks that Mine requests only `ids`-filtered workspace summaries for the
|
|
workspaces its rows reference, that rows appear while those reads are held when
|
|
neither the Workspace column nor workspace grouping is active, and that the
|
|
Workspace column still shows each row's workspace name. Its timing case turns on
|
|
the real isolated-workspaces setting, restores it, and records cold time from
|
|
navigation start to the first Mine row (two frames after mount) over
|
|
`PAPERCLIP_COLLECTION_PERF_TIMING_RUNS` runs (default and minimum 5). It records
|
|
a native scenario without request interception and a separate diagnostic scenario that delays
|
|
workspace reads by 3 seconds. The timings are attached evidence, not budgets.
|
|
|
|
`recent-task-open.spec.ts` covers opening a task from the sidebar's Recents.
|
|
It gives eight fixture tasks twelve saved comments each and seeds them as the
|
|
account's Recents. An idle Projects page must make exactly one
|
|
`issues/chat-bootstrap` request (at most ten tasks) and no per-task reads.
|
|
Clicking a preloaded task must show its latest comment without any of the
|
|
chat's first-frame reads (core issue, recent comments, comment input facts,
|
|
recent activity, recent runs, interactions, attachments, live and active run).
|
|
The full detail read and the task page's other side reads still follow the
|
|
first frame; the spec records them as an annotation. Its timing cases measure
|
|
click to the latest comment on screen over at least 5 fresh browser contexts
|
|
(empty HTTP cache and storage apart from Recents), natively and with 150 ms
|
|
added to every API response. They also record the idle page's API request
|
|
count. The timings are attached evidence, not budgets.
|
|
|
|
Run these focused browser checks before the full collection suite:
|
|
|
|
```sh
|
|
PAPERCLIP_COLLECTION_PERF_BASE_URL=http://127.0.0.1:<candidate-ui-port> \
|
|
pnpm exec playwright test --config tests/perf/collections/playwright.config.ts \
|
|
parent-context.spec.ts startup-regressions.spec.ts
|
|
```
|
|
|
|
To collect timings, build the pinned baseline production assets once and retain
|
|
that output. Build candidate UI assets only when their inputs change. Serve both
|
|
outputs from stable loopback origins. If server behavior changes, each origin
|
|
must proxy its matching pinned API revision against the same disposable database.
|
|
Using the candidate API for both arms proves only a UI comparison. Keep agents
|
|
and external providers inactive. Neither arm may migrate or reseed the shared
|
|
fixture during a comparison.
|
|
|
|
The harness checks the configured API against each UI proxy, records HTML and
|
|
entry-asset SHA-256 receipts, and records both serving API revisions. Supply full
|
|
source SHAs to label the client artifacts, then run:
|
|
|
|
```sh
|
|
INBOX_BASELINE_URL=http://127.0.0.1:<baseline-port> \
|
|
INBOX_CANDIDATE_URL=http://127.0.0.1:<candidate-port> \
|
|
INBOX_BASELINE_API_URL=http://127.0.0.1:<baseline-api-port> \
|
|
INBOX_CANDIDATE_API_URL=http://127.0.0.1:<candidate-api-port> \
|
|
INBOX_BASELINE_SOURCE_REVISION=<full-baseline-sha> \
|
|
INBOX_CANDIDATE_SOURCE_REVISION=<full-candidate-sha> \
|
|
node --import ./server/node_modules/tsx/dist/loader.mjs \
|
|
tests/perf/collections/measure-mine-context.ts
|
|
```
|
|
|
|
The script retains a four-parent, 30-child fixture, archives three completed
|
|
parents, and enables the personal Coordinator preference. Both APIs must expose
|
|
that same company. It runs five alternating pairs by default. `INBOX_PAIRS`,
|
|
`INBOX_FIXTURE_FILE`, and `INBOX_OUTPUT` override the count and evidence paths.
|
|
No request interception is used in timings.
|
|
`INBOX_NETWORK_LATENCY_MS` accepts an integer from 0 to 2000 and defaults to 0
|
|
(native loopback). A nonzero value uses Chromium CDP network emulation on every
|
|
page, with unlimited throughput and unchanged CPU. Record native and 100 ms
|
|
latency runs in separate `INBOX_OUTPUT` files; each sample labels its scenario.
|
|
CDP specifies minimum request-to-response-header latency. This controlled extra
|
|
round-trip scenario tests serial request dependencies; it does not prove hosted
|
|
latency or reproduce the full staging network.
|
|
|
|
By default, `INBOX_HTTP_CACHE=warm` warms each browser context once, then reloads
|
|
the document for each initial sample. This retains HTTP cache while clearing app
|
|
query state. `INBOX_HTTP_CACHE=cold` uses fresh browser contexts;
|
|
`INBOX_HTTP_CACHE=both` records these scenarios separately. Warm Recent-to-Mine
|
|
navigation and task-to-Mine Back retain query state. Use `INBOX_HEADFUL=1` and
|
|
`PAPERCLIP_PLAYWRIGHT_CHANNEL=chrome` when installed Chrome is the intended browser.
|
|
|
|
Initial readiness requires visible Mine rows plus two animation frames. The
|
|
report retains every sample and failure, exact row membership/order, row geometry,
|
|
asset identities, full API resource timings, request statuses and primary-request
|
|
start. Health, session, board access, companies and preferences remain timing
|
|
controls. A faster initial total alone does not attribute a gain to its final
|
|
context response. The fixed post-reveal window defaults to two seconds;
|
|
`INBOX_TAIL_MS` changes it, up to 30 seconds. Counts retain 429s and unfinished
|
|
requests rather than treating them as successful work. Use the same immutable
|
|
asset cache policy on both origins and stop builds and suites during timing runs.
|
|
|
|
Startup cases also check that shared collection controls do not require the
|
|
task-detail bundle before reveal, even when a different organization is remembered.
|
|
Code warms after the collection paints, or on task-link intent. A successful
|
|
immutable response allows module preloading to compile the cached chunk without
|
|
executing it. A failed fetch leaves the collection usable and permits navigation
|
|
without a document reload. Direct task URLs preload their static dependencies
|
|
while identity is pending. These cases must run with
|
|
production assets: development modules do not exercise the bundle boundaries.
|
|
|
|
The functional cases route feature settings and delayed responses. Routing
|
|
disables browser HTTP caching. For warm-cache timings, set the real local feature
|
|
settings and use fresh browser contexts without request interception. Keep normal
|
|
cache writes enabled so code warmed after paint can be used on the next task open.
|
|
Retain intercepted runs as separate diagnostic evidence.
|
|
Match the server's hashed asset cache policy (`public, max-age=31536000, immutable`);
|
|
Vite preview's default `no-cache` policy adds revalidation that the shipped server
|
|
does not require. Keep HTML uncached and return 404 for missing assets.
|