From fb2a35c0b581f064f10ee1151ee7b1eb57d4cf71 Mon Sep 17 00:00:00 2001 From: Brooklyn Nicholson Date: Sun, 19 Jul 2026 22:58:22 -0500 Subject: [PATCH] perf(desktop): stop tool rows re-rendering on session/cwd change + memo leaves MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two tool-render wins during streaming / on session switch: 1. Every ToolEntry did useStore($activeSessionId)+useStore($currentCwd), so any session or cwd change re-rendered *every* mounted tool row — but they're only read inside the preview-artifact effect. Read .get() at fire time instead (the effect only runs when a previewable target appears); no subscription. 2. memo() AnsiText + CompactMarkdown. Their text props are string values (value-equal across renders), so memo skips the re-render — and the per-tick ANSI parse / Streamdown re-run — when a parent ToolEntry re-renders on an unrelated stream delta. No behavior change. typecheck + eslint clean; tool fallback tests green (30). --- .../src/components/assistant-ui/ansi-text.tsx | 7 +++---- .../src/components/assistant-ui/tool/fallback.tsx | 15 ++++++++++----- .../src/components/chat/compact-markdown.tsx | 11 +++++++++-- 3 files changed, 22 insertions(+), 11 deletions(-) diff --git a/apps/desktop/src/components/assistant-ui/ansi-text.tsx b/apps/desktop/src/components/assistant-ui/ansi-text.tsx index 99ced1f6c44e..100e0f739c8f 100644 --- a/apps/desktop/src/components/assistant-ui/ansi-text.tsx +++ b/apps/desktop/src/components/assistant-ui/ansi-text.tsx @@ -1,5 +1,4 @@ -import type { FC } from 'react' -import { useMemo } from 'react' +import { memo, useMemo } from 'react' import { ansiColorClass, hasAnsiCodes, parseAnsi } from '@/lib/ansi' import { cn } from '@/lib/utils' @@ -12,7 +11,7 @@ interface AnsiTextProps { /** Renders text with embedded ANSI SGR codes as colored / bold spans. Falls * back to a plain string node when no codes are present so the parser cost * is paid only when there's something to colorize. */ -export const AnsiText: FC = ({ className, text }) => { +export const AnsiText = memo(({ className, text }: AnsiTextProps) => { const segments = useMemo(() => (hasAnsiCodes(text) ? parseAnsi(text) : null), [text]) if (!segments) { @@ -31,4 +30,4 @@ export const AnsiText: FC = ({ className, text }) => { ))} ) -} +}) diff --git a/apps/desktop/src/components/assistant-ui/tool/fallback.tsx b/apps/desktop/src/components/assistant-ui/tool/fallback.tsx index 5b14de5fead5..724ca6991121 100644 --- a/apps/desktop/src/components/assistant-ui/tool/fallback.tsx +++ b/apps/desktop/src/components/assistant-ui/tool/fallback.tsx @@ -311,17 +311,22 @@ function ToolEntry({ part }: ToolEntryProps) { // in the composer status stack rather than a bulky inline card. Uses the same // detected target the old inline card did, keyed to the active session the // stack reads from. Idempotent + dedup'd, so re-renders don't churn. - const activeSessionId = useStore($activeSessionId) - const currentCwd = useStore($currentCwd) const previewTarget = view.previewTarget useEffect(() => { - if (isPending || !activeSessionId || !previewTarget || !isPreviewableTarget(previewTarget)) { + if (isPending || !previewTarget || !isPreviewableTarget(previewTarget)) { return } - recordPreviewArtifact(activeSessionId, previewTarget, currentCwd || '') - }, [activeSessionId, currentCwd, isPending, previewTarget]) + // Read (don't subscribe) session/cwd: this only fires when a previewable + // target appears, and subscribing re-rendered every tool row on any session + // or cwd change. + const activeSessionId = $activeSessionId.get() + + if (activeSessionId) { + recordPreviewArtifact(activeSessionId, previewTarget, $currentCwd.get() || '') + } + }, [isPending, previewTarget]) const detailSections = useMemo(() => { if (!view.detail) { diff --git a/apps/desktop/src/components/chat/compact-markdown.tsx b/apps/desktop/src/components/chat/compact-markdown.tsx index 79e96e8fa657..ed3389464a49 100644 --- a/apps/desktop/src/components/chat/compact-markdown.tsx +++ b/apps/desktop/src/components/chat/compact-markdown.tsx @@ -1,4 +1,5 @@ import type { ComponentProps, ElementType, FC } from 'react' +import { memo } from 'react' import { Streamdown } from 'streamdown' import { ExternalLink, ExternalLinkIcon } from '@/lib/external-link' @@ -102,7 +103,13 @@ const COMPONENTS = { ul: tagged('ul') } -export function CompactMarkdown({ className, text }: { className?: string; text: string }) { +export const CompactMarkdown = memo(function CompactMarkdown({ + className, + text +}: { + className?: string + text: string +}) { return (
@@ -110,4 +117,4 @@ export function CompactMarkdown({ className, text }: { className?: string; text:
) -} +})