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:
) -} +})