mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-21 08:22:20 +02:00
## Thinking Path > - Paperclip lets operators manage agent work through tasks. > - Task chat keeps responses and run history together. > - A live update rendered every historical bubble and hidden tool row again. > - New image callbacks also forced unchanged markdown to parse again. > - Long conversations saturated the browser main thread. > - This change reuses unchanged history and mounts folded tools on first inspection. > - Operators can read and reply while work continues. ## Linked Issues or Issue Description **What happened?** Chat-style tasks with substantial scrollback became almost unusable. A deterministic browser reproduction with 200 long responses and 4,000 tools consumed 97.8% of the main thread during live updates. It delivered only 8 updates during the sample. **Expected behavior** The task should remain responsive during streaming. Historical markdown and unopened run details should not repeat expensive render work. **Steps to reproduce** 1. Install dependencies with `pnpm install`. 2. Run `pnpm exec playwright test --config tests/perf/task-chat/playwright.config.ts`. 3. Compare the attached performance JSON. The new test fails against the original rendering code. **Paperclip version or commit** Reproduced at `a05b828bc`. The branch is rebased on current master. **Deployment mode** Local Chromium and Vite with deterministic fixtures. No database or agent credentials are required. Related work: #10463 reduces the issue-page bundle. This change addresses repeated rendering after the page loads. No duplicate scrollback fix was found. ## What Changed - Keep the bubble image callback stable so unchanged markdown can skip parsing. - Memoize the settled history separately from the header and streaming tail. Keep the brief renderer and default attachment array stable. - Mount folded tool history on first expansion. Keep it mounted afterward to preserve child state and closing motion. Runtime request receipts remain visible. - Add deterministic rendering tests to the normal Vitest suite and an opt-in Chromium regression fixture. - Document the reproduction, commands, scope, and local measurements. ## Verification - Browser reproduction: 97.8% main-thread utilization before; 11.6% after for tail-only updates; 31.5% after when projection recreates history objects. Both fixed cases delivered 32 updates. - Browser checks pass for scroll-position retention, typing, return to latest, tool inspection, and retained expansion state. - Focused component suite: 155 tests passed. Post-rebase thread/performance rerun: 101 tests passed. - Recursive typecheck, build, Storybook build, and token gates passed. UI typecheck passed after the final edits. - Local UI/CLI lane: 5,910 tests passed; ten files hit worker-start timeouts, then all ten passed with two workers (20 tests). Shared/adapter lane: 3,128 tests passed. - Full local `pnpm test:run` was attempted and is **not green**: its server lane recorded 10,517 passes and 11 failures plus fixture/setup errors. Queue (31 tests), Cursor/Git-load (9 tests), and missing-binary failures cleared on isolated reruns / building the runner test binaries. Two native suites still cannot initialize embedded PostgreSQL on this host. - The remaining native-session recovery assertion was reproduced in a clean worktree at base `a20ecce40` (1 failed, 67 passed across the native/queue suites). It expects a settled-session error but receives a semantic-tool-input digest error. No server or runner files changed in this PR. - All substantive CI jobs have passed, including build, typecheck, all server/workspace test shards, all three e2e shards, and the canary dry run. The unchanged Slack ordering test exhausted its one-second wait on the first run; its shard passed on rerun. Final aggregate verification passed: **31 passing checks**, no failures or pending checks; two optional Storybook deployment/visual checks were skipped. Greptile is **5/5**, with no unresolved review threads. - The supplemental local serialized route run was stopped after CI passed all five serialized shards; it had reported no failures. - The browser fixture uses the real chat rendering components with a plain textarea. It does not test the full composer or server transport. Timing results are local samples; deterministic render-count tests provide normal CI coverage. ## Risks - Closed tool content becomes available to DOM search only after first expansion. Visible run summaries and runtime request receipts remain available immediately. - Opened run history remains mounted to preserve child state. The first full markdown render still scales with conversation size. - Memo dependencies must stay current when adding render inputs. Tests check content edits and replacement gallery callbacks. - No API, database, or permission changes. ## Model Used OpenAI GPT-6 (Codex). Exact serving variant and context-window size are not exposed in this session. Used reasoning, repository tools, code execution, and browser testing. No sub-agents were used. ## 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 - [ ] I have run tests locally and they pass — targeted/UI/workspace checks pass; the full local server suite has the baseline/host failures documented above - [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 - [x] All Paperclip CI gates are green - [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups - [x] I will address all Greptile and reviewer comments before requesting merge Co-authored-by: Paperclip <noreply@paperclip.ing>
78 lines
3.1 KiB
TypeScript
78 lines
3.1 KiB
TypeScript
// @vitest-environment jsdom
|
|
|
|
import { memo } from "react";
|
|
import { flushSync } from "react-dom";
|
|
import { createRoot, type Root } from "react-dom/client";
|
|
import { afterEach, beforeEach, expect, it, vi } from "vitest";
|
|
import { TaskChatThreadView } from "./TaskChatThreadView";
|
|
import { IssueGalleryContext } from "@/context/IssueGalleryContext";
|
|
import type { TaskChatItem, TaskChatMessageItem } from "./task-chat-model";
|
|
|
|
const renders = vi.hoisted(() => vi.fn());
|
|
vi.mock("@/components/MarkdownBody", () => ({
|
|
// Preserve the real markdown component's memo contract while counting work.
|
|
MarkdownBody: memo((props: { children: string; onImageClick?: (src: string) => void }) => {
|
|
renders(props.children);
|
|
return <button onClick={() => props.onImageClick?.("image.png")}>{props.children}</button>;
|
|
}),
|
|
}));
|
|
|
|
let host: HTMLDivElement;
|
|
let root: Root;
|
|
beforeEach(() => {
|
|
renders.mockClear();
|
|
host = document.body.appendChild(document.createElement("div"));
|
|
root = createRoot(host);
|
|
});
|
|
afterEach(() => {
|
|
flushSync(() => root.unmount());
|
|
host.remove();
|
|
});
|
|
|
|
const history: TaskChatMessageItem[] = Array.from({ length: 200 }, (_, index) => ({
|
|
id: `message-${index}`, kind: "message", author: "agent", text: `Response ${index}`,
|
|
}));
|
|
|
|
it("does no historical render work for a live-tail-only update", () => {
|
|
const actions = vi.fn(() => null);
|
|
const render = (tick: number) => flushSync(() => root.render(
|
|
<TaskChatThreadView items={history} scroll={false} renderMessageActions={actions} tail={<p>Live {tick}</p>} />,
|
|
));
|
|
render(0);
|
|
renders.mockClear();
|
|
actions.mockClear();
|
|
for (let tick = 1; tick <= 10; tick++) render(tick);
|
|
expect(renders).not.toHaveBeenCalled();
|
|
expect(actions).not.toHaveBeenCalled();
|
|
expect(host.textContent).toContain("Live 10");
|
|
});
|
|
|
|
it("does not reparse unchanged markdown when transcript projection recreates rows", () => {
|
|
const render = (items: TaskChatItem[]) => flushSync(() => root.render(
|
|
<TaskChatThreadView items={items} scroll={false} />,
|
|
));
|
|
render(history);
|
|
renders.mockClear();
|
|
for (let tick = 0; tick < 10; tick++) render(history.map((item) => ({ ...item })));
|
|
expect(renders).not.toHaveBeenCalled();
|
|
render(history.map((item, index) => index === 199 ? { ...item, text: "Edited response" } : item));
|
|
expect(renders).toHaveBeenCalledExactlyOnceWith("Edited response");
|
|
expect(host.textContent).toContain("Edited response");
|
|
});
|
|
|
|
it("uses the current gallery callback after the provider changes", () => {
|
|
const firstGallery = vi.fn(() => true);
|
|
const nextGallery = vi.fn(() => true);
|
|
const render = (gallery: (src: string) => boolean) => flushSync(() => root.render(
|
|
<IssueGalleryContext.Provider value={gallery}>
|
|
<TaskChatThreadView items={history.slice(0, 1)} scroll={false} />
|
|
</IssueGalleryContext.Provider>,
|
|
));
|
|
render(firstGallery);
|
|
flushSync(() => host.querySelector<HTMLButtonElement>("button")!.click());
|
|
render(nextGallery);
|
|
flushSync(() => host.querySelector<HTMLButtonElement>("button")!.click());
|
|
expect(firstGallery).toHaveBeenCalledExactlyOnceWith("image.png");
|
|
expect(nextGallery).toHaveBeenCalledExactlyOnceWith("image.png");
|
|
});
|