perf(panel): an agent's transcript redraws the message that moved
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):
withAgentRunreturns the list unchanged when the agent already has that status and run start, andonStatuskeeps the list as it was.useAgentTranscriptkeeps itssubagentTitles,delegationsandsubagentsmaps 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.cardsandchatreach the rows through aLiveDoorscontext, 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). originis memoized on how the agent was briefed, the run, the task, the starter, the open-task handler and a stableopenGitHubhandler.agentPartsis cached per message object in aWeakMap.
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'sarrivedlist is memoized too, so neither is rebuilt per render.- The agent's
agentLinesand the worker tab'sworkerLines, delegation doors and origin are memoized.
Not carried:
- #3758's
MarkdownMessagememo andplan-body.tsx's stable task toggler, with itsplan-document-editortest.plan-body.tsxbelongs 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 throughAgentTab. To see which rows were drawn, it wraps the tab'scardsprop 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 thewithAgentRuncase incoding-agent-model.test.ts. vitest run packages/panel/src/tasks packages/panel/src/desktop: 169 files and 2038 tests pass.pnpm check:affectedis 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