perf(panel): an agent's transcript redraws the message that moved

Merged
DA
6d ago
+1001-127

https://entire.io/gh/ReviewStage/luke/trails/50

What was asked

Re-land the work of the closed PRs #3726 and #3758 on today's main, as one PR. When a page touches one message of a running agent's transcript, only that message should redraw, not every message and the plan beside it. On main, AgentMessage wasn't memoized, its arrived props were rebuilt inline, and todosPlace, placedMessages and openReply ran over every message on every render. The long-poll's setHeld also rebuilt three Maps on every page. So each transcript page, and each app-state patch, redrew every message.

What changed

Ported from #3726 (apps/desktop/src/renderer/tasks → packages/panel/src/tasks):

  • withAgentRun returns the list unchanged when the agent already has that status and run start, and onStatus keeps the list as it was.
  • useAgentTranscript keeps its subagentTitles, delegations and subagents maps when a page lists the workers exactly as they were (keptIfSame).

Ported from #3758, the transcript part, with Devin's fixes from 26ae75c57:

  • SteadyAgentMessage = memo(AgentMessage, sameMessageDraw): a message redraws only when the message itself, its place, or its doors change.
  • cards and chat reach the rows through a LiveDoors context, so a message that isn't redrawn still shows today's cards. The rows that use it are delegation rows, target rows, calls about an agent (AgentCallView) and chat notices.
  • Handlers keep the same identity across renders (useSteady). The context values (message place by id list, Mac owner, mentioned servers, steering and undelivered lines) stay the same objects while their contents are unchanged. Whether the tab offers Fork has its own boolean context (ForkOfferedContext).
  • origin is memoized on how the agent was briefed, the run, the task, the starter, the open-task handler and a stable openGitHub handler.
  • agentParts is cached per message object in a WeakMap.

Re-derived against today's agent-tab.tsx:

  • transcriptPlacing (to-do placement, the first message, the replay opening, placedMessages, openReply) is memoized on the messages. Each message's arrived list is memoized too, so neither is rebuilt per render.
  • The agent's agentLines and the worker tab's workerLines, delegation doors and origin are memoized.

Not carried:

  • #3758's MarkdownMessage memo and plan-body.tsx's stable task toggler, with its plan-document-editor test. plan-body.tsx belongs to another worker in this rollout.
  • Virtualizing the list: a follow-up.
  • packages/host: not touched.

No bundle-budget change: main has since raised the app bundle's baseline to 981789, which already covers this change's ~1.1 KB.

Tests

  • New agent-tab-kept.test.ts, driven through AgentTab. To see which rows were drawn, it wraps the tab's cards prop in a recorder that notes each row asking for a card as it draws:
    • When a page rewrites the reply still being written, only that reply's rows are drawn again. This test fails without this PR.
    • When the window redraws the tab with new handlers, no message is drawn again. This test fails without this PR.
    • Ported from #3758: a message no page touched still shows the card the window hands now, after the other agent is renamed, including on a refused call. Its worker's row shows the worker's status from the newest page. This test fails if the rows read cards from the memoized props instead of LiveDoors.
  • Ported from #3726: use-agent-transcript.test.ts (a page repeating the workers keeps the maps, and a changed worker is still picked up) and the withAgentRun case in coding-agent-model.test.ts.
  • vitest run packages/panel/src/tasks packages/panel/src/desktop: 169 files and 2038 tests pass. pnpm check:affected is green.

The brief asked for React Testing Library. The repository doesn't use it, so these tests use its own createRoot + act harness, as every panel test does, rather than adding a dependency.

To make pnpm check:affected pass in this 8 GB sandbox, I had to install Electron's binary and give NODE_OPTIONS=--max-old-space-size=6144 to the web typecheck and build, which otherwise run out of memory.

Made with Luke


Devin Review