From 3108bca6749ffaf17e71a1b42b793efff2c68715 Mon Sep 17 00:00:00 2001 From: Vikash Loomba Date: Mon, 19 Jan 2026 23:36:26 -0800 Subject: [PATCH 1/2] perf(messages): reduce rerenders during streaming --- package-lock.json | 14 +- src/features/messages/components/Messages.tsx | 716 ++++++++++-------- 2 files changed, 419 insertions(+), 311 deletions(-) diff --git a/package-lock.json b/package-lock.json index b8d2c5026..7eb3e77ea 100644 --- a/package-lock.json +++ b/package-lock.json @@ -128,7 +128,6 @@ "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", @@ -2127,7 +2126,6 @@ "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.8.tgz", "integrity": "sha512-3MbSL37jEchWZz2p2mjntRZtPt837ij10ApxKfgmXCTuHWagYg7iA5bqPw6C8BMPfwidlvfPI/fxOc42HLhcyg==", "license": "MIT", - "peer": true, "dependencies": { "csstype": "^3.2.2" } @@ -2188,7 +2186,6 @@ "integrity": "sha512-4Z+L8I2OqhZV8qA132M4wNL30ypZGYOQVBfMgxDH/K5UX0PNqTu1c6za9ST5r9+tavvHiTWmBnKzpCJ/GlVFtg==", "dev": true, "license": "BSD-2-Clause", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "7.18.0", "@typescript-eslint/types": "7.18.0", @@ -2510,8 +2507,7 @@ "version": "5.5.0", "resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-5.5.0.tgz", "integrity": "sha512-hqJHYaQb5OptNunnyAnkHyM8aCjZ1MEIDTQu1iIbbTD/xops91NB5yq1ZK/dC2JDbVWtF23zUtl9JE2NqwT87A==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/acorn": { "version": "8.15.0", @@ -2519,7 +2515,6 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -2872,7 +2867,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -3690,7 +3684,6 @@ "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", @@ -6758,7 +6751,6 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -6920,7 +6912,6 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.3.tgz", "integrity": "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==", "license": "MIT", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -6930,7 +6921,6 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.3.tgz", "integrity": "sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==", "license": "MIT", - "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -8057,7 +8047,6 @@ "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -8247,7 +8236,6 @@ "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", diff --git a/src/features/messages/components/Messages.tsx b/src/features/messages/components/Messages.tsx index 6aa59bc3d..8e3150022 100644 --- a/src/features/messages/components/Messages.tsx +++ b/src/features/messages/components/Messages.tsx @@ -1,4 +1,4 @@ -import { memo, useEffect, useRef, useState } from "react"; +import { memo, useCallback, useEffect, useRef, useState } from "react"; import { Check, Copy } from "lucide-react"; import type { ConversationItem } from "../../../types"; import { Markdown } from "./Markdown"; @@ -24,6 +24,47 @@ type ToolSummary = { type StatusTone = "completed" | "processing" | "failed" | "unknown"; +type WorkingIndicatorProps = { + isThinking: boolean; + processingStartedAt?: number | null; + lastDurationMs?: number | null; + hasItems: boolean; +}; + +type MessageRowProps = { + item: Extract; + isCopied: boolean; + onCopy: (item: Extract) => void; + onOpenFileLink?: (path: string) => void; + onOpenFileLinkMenu?: (event: React.MouseEvent, path: string) => void; +}; + +type ReasoningRowProps = { + item: Extract; + isExpanded: boolean; + onToggle: (id: string) => void; + onOpenFileLink?: (path: string) => void; + onOpenFileLinkMenu?: (event: React.MouseEvent, path: string) => void; +}; + +type ReviewRowProps = { + item: Extract; + onOpenFileLink?: (path: string) => void; + onOpenFileLinkMenu?: (event: React.MouseEvent, path: string) => void; +}; + +type DiffRowProps = { + item: Extract; +}; + +type ToolRowProps = { + item: Extract; + isExpanded: boolean; + onToggle: (id: string) => void; + onOpenFileLink?: (path: string) => void; + onOpenFileLinkMenu?: (event: React.MouseEvent, path: string) => void; +}; + function basename(path: string) { if (!path) { return ""; @@ -150,6 +191,13 @@ function cleanCommandText(commandText: string) { return stripped.trim(); } +function formatDurationMs(durationMs: number) { + const durationSeconds = Math.max(0, Math.floor(durationMs / 1000)); + const durationMinutes = Math.floor(durationSeconds / 60); + const durationRemainder = durationSeconds % 60; + return `${durationMinutes}:${String(durationRemainder).padStart(2, "0")}`; +} + function statusToneFromText(status?: string): StatusTone { if (!status) { return "unknown"; @@ -204,6 +252,321 @@ function scrollKeyForItems(items: ConversationItem[]) { } } +const WorkingIndicator = memo(function WorkingIndicator({ + isThinking, + processingStartedAt = null, + lastDurationMs = null, + hasItems, +}: WorkingIndicatorProps) { + const [elapsedMs, setElapsedMs] = useState(0); + + useEffect(() => { + if (!isThinking || !processingStartedAt) { + setElapsedMs(0); + return undefined; + } + setElapsedMs(Date.now() - processingStartedAt); + const interval = window.setInterval(() => { + setElapsedMs(Date.now() - processingStartedAt); + }, 1000); + return () => window.clearInterval(interval); + }, [isThinking, processingStartedAt]); + + return ( + <> + {isThinking && ( +
+ +
+ {formatDurationMs(elapsedMs)} +
+ Working… +
+ )} + {!isThinking && lastDurationMs !== null && hasItems && ( +
+ + + Done in {formatDurationMs(lastDurationMs)} + + +
+ )} + + ); +}); + +const MessageRow = memo(function MessageRow({ + item, + isCopied, + onCopy, + onOpenFileLink, + onOpenFileLinkMenu, +}: MessageRowProps) { + return ( +
+
+ + +
+
+ ); +}); + +const ReasoningRow = memo(function ReasoningRow({ + item, + isExpanded, + onToggle, + onOpenFileLink, + onOpenFileLinkMenu, +}: ReasoningRowProps) { + const summaryText = item.summary || item.content; + const summaryLines = summaryText.split("\n"); + const trimmedLines = summaryLines.map((line) => line.trim()); + const titleLineIndex = trimmedLines.findIndex(Boolean); + const rawTitle = + titleLineIndex >= 0 ? trimmedLines[titleLineIndex] : "Reasoning"; + const cleanTitle = rawTitle + .replace(/[`*_~]/g, "") + .replace(/\[(.*?)\]\(.*?\)/g, "$1") + .trim(); + const summaryTitle = + cleanTitle.length > 80 + ? `${cleanTitle.slice(0, 80)}…` + : cleanTitle || "Reasoning"; + const reasoningTone: StatusTone = summaryText ? "completed" : "processing"; + const bodyText = + titleLineIndex >= 0 + ? summaryLines + .filter((_, index) => index !== titleLineIndex) + .join("\n") + .trim() + : ""; + const showReasoningBody = Boolean(bodyText); + return ( +
+ + {showReasoningBody && ( + + )} +
+ + ); +}); + +const ReviewRow = memo(function ReviewRow({ + item, + onOpenFileLink, + onOpenFileLinkMenu, +}: ReviewRowProps) { + const title = item.state === "started" ? "Review started" : "Review completed"; + return ( +
+
+ {title} + + Review + +
+ {item.text && ( + + )} +
+ ); +}); + +const DiffRow = memo(function DiffRow({ item }: DiffRowProps) { + return ( +
+
+ {item.title} + {item.status && {item.status}} +
+
+ +
+
+ ); +}); + +const ToolRow = memo(function ToolRow({ + item, + isExpanded, + onToggle, + onOpenFileLink, + onOpenFileLinkMenu, +}: ToolRowProps) { + const isFileChange = item.toolType === "fileChange"; + const isCommand = item.toolType === "commandExecution"; + const commandText = isCommand + ? item.title.replace(/^Command:\s*/i, "").trim() + : ""; + const summary = buildToolSummary(item, commandText); + const changeNames = (item.changes ?? []) + .map((change) => basename(change.path)) + .filter(Boolean); + const hasChanges = changeNames.length > 0; + const tone = toolStatusTone(item, hasChanges); + const summaryLabel = isFileChange + ? changeNames.length > 1 + ? "files edited" + : "file edited" + : isCommand + ? "" + : summary.label; + const summaryValue = isFileChange + ? changeNames.length > 1 + ? `${changeNames[0]} +${changeNames.length - 1}` + : changeNames[0] || "changes" + : summary.value; + const shouldFadeCommand = + isCommand && !isExpanded && (summaryValue?.length ?? 0) > 80; + const showToolOutput = isExpanded && (!isFileChange || !hasChanges); + return ( +
+ + {isExpanded && summary.detail && !isFileChange && ( +
{summary.detail}
+ )} + {isExpanded && isCommand && item.detail && ( +
+ cwd: {item.detail} +
+ )} + {isExpanded && isFileChange && hasChanges && ( +
+ {item.changes?.map((change, index) => ( +
+
+ {change.kind && ( + + {change.kind.toUpperCase()} + + )} + + {basename(change.path)} + +
+ {change.diff && ( +
+ +
+ )} +
+ ))} +
+ )} + {isExpanded && isFileChange && !hasChanges && item.detail && ( + + )} + {showToolOutput && summary.output && ( + + )} +
+ + ); +}); + export const Messages = memo(function Messages({ items, threadId, @@ -219,7 +582,6 @@ export const Messages = memo(function Messages({ const [expandedItems, setExpandedItems] = useState>(new Set()); const [copiedMessageId, setCopiedMessageId] = useState(null); const copyTimeoutRef = useRef(null); - const [elapsedMs, setElapsedMs] = useState(0); const scrollKey = scrollKeyForItems(items); const { openFileLink, showFileLinkMenu } = useFileLinkOpener(workspacePath); @@ -236,7 +598,7 @@ export const Messages = memo(function Messages({ useEffect(() => { autoScrollRef.current = true; }, [threadId]); - const toggleExpanded = (id: string) => { + const toggleExpanded = useCallback((id: string) => { setExpandedItems((prev) => { const next = new Set(prev); if (next.has(id)) { @@ -246,7 +608,7 @@ export const Messages = memo(function Messages({ } return next; }); - }; + }, []); const visibleItems = items; @@ -258,20 +620,23 @@ export const Messages = memo(function Messages({ }; }, []); - const handleCopyMessage = async (item: Extract) => { - try { - await navigator.clipboard.writeText(item.text); - setCopiedMessageId(item.id); - if (copyTimeoutRef.current) { - window.clearTimeout(copyTimeoutRef.current); + const handleCopyMessage = useCallback( + async (item: Extract) => { + try { + await navigator.clipboard.writeText(item.text); + setCopiedMessageId(item.id); + if (copyTimeoutRef.current) { + window.clearTimeout(copyTimeoutRef.current); + } + copyTimeoutRef.current = window.setTimeout(() => { + setCopiedMessageId(null); + }, 1200); + } catch { + // No-op: clipboard errors can occur in restricted contexts. } - copyTimeoutRef.current = window.setTimeout(() => { - setCopiedMessageId(null); - }, 1200); - } catch { - // No-op: clipboard errors can occur in restricted contexts. - } - }; + }, + [], + ); useEffect(() => { if (!bottomRef.current) { @@ -302,31 +667,6 @@ export const Messages = memo(function Messages({ }; }, [scrollKey, isThinking]); - useEffect(() => { - if (!isThinking || !processingStartedAt) { - setElapsedMs(0); - return undefined; - } - setElapsedMs(Date.now() - processingStartedAt); - const interval = window.setInterval(() => { - setElapsedMs(Date.now() - processingStartedAt); - }, 1000); - return () => window.clearInterval(interval); - }, [isThinking, processingStartedAt]); - - const elapsedSeconds = Math.max(0, Math.floor(elapsedMs / 1000)); - const elapsedMinutes = Math.floor(elapsedSeconds / 60); - const elapsedRemainder = elapsedSeconds % 60; - const formattedElapsed = `${elapsedMinutes}:${String(elapsedRemainder).padStart(2, "0")}`; - const lastDurationSeconds = lastDurationMs - ? Math.max(0, Math.floor(lastDurationMs / 1000)) - : 0; - const lastDurationMinutes = Math.floor(lastDurationSeconds / 60); - const lastDurationRemainder = lastDurationSeconds % 60; - const formattedLastDuration = `${lastDurationMinutes}:${String( - lastDurationRemainder, - ).padStart(2, "0")}`; - return (
-
- - -
-
+ ); } if (item.kind === "reasoning") { - const summaryText = item.summary || item.content; - const summaryLines = summaryText.split("\n"); - const trimmedLines = summaryLines.map((line) => line.trim()); - const titleLineIndex = trimmedLines.findIndex(Boolean); - const rawTitle = - titleLineIndex >= 0 ? trimmedLines[titleLineIndex] : "Reasoning"; - const cleanTitle = rawTitle - .replace(/[`*_~]/g, "") - .replace(/\[(.*?)\]\(.*?\)/g, "$1") - .trim(); - const summaryTitle = - cleanTitle.length > 80 - ? `${cleanTitle.slice(0, 80)}…` - : cleanTitle || "Reasoning"; - const reasoningTone: StatusTone = summaryText ? "completed" : "processing"; const isExpanded = expandedItems.has(item.id); - const bodyText = - titleLineIndex >= 0 - ? summaryLines - .filter((_, index) => index !== titleLineIndex) - .join("\n") - .trim() - : ""; - const showReasoningBody = Boolean(bodyText); return ( -
- - {showReasoningBody && ( - - )} -
- + ); } if (item.kind === "review") { - const title = - item.state === "started" ? "Review started" : "Review completed"; return ( -
-
- {title} - - Review - -
- {item.text && ( - - )} -
+ ); } if (item.kind === "diff") { - return ( -
-
- {item.title} - {item.status && {item.status}} -
-
- -
-
- ); + return ; } if (item.kind === "tool") { - const isFileChange = item.toolType === "fileChange"; - const isCommand = item.toolType === "commandExecution"; - const commandText = isCommand - ? item.title.replace(/^Command:\s*/i, "").trim() - : ""; - const summary = buildToolSummary(item, commandText); - const changeNames = (item.changes ?? []) - .map((change) => basename(change.path)) - .filter(Boolean); - const hasChanges = changeNames.length > 0; - const tone = toolStatusTone(item, hasChanges); const isExpanded = expandedItems.has(item.id); - const summaryLabel = isFileChange - ? changeNames.length > 1 - ? "files edited" - : "file edited" - : isCommand - ? "" - : summary.label; - const summaryValue = isFileChange - ? changeNames.length > 1 - ? `${changeNames[0]} +${changeNames.length - 1}` - : changeNames[0] || "changes" - : summary.value; - const shouldFadeCommand = - isCommand && !isExpanded && (summaryValue?.length ?? 0) > 80; - const showToolOutput = isExpanded && (!isFileChange || !hasChanges); return ( -
- - {isExpanded && summary.detail && !isFileChange && ( -
- {summary.detail} -
- )} - {isExpanded && isCommand && item.detail && ( -
- cwd: {item.detail} -
- )} - {isExpanded && isFileChange && hasChanges && ( -
- {item.changes?.map((change, index) => ( -
-
- {change.kind && ( - - {change.kind.toUpperCase()} - - )} - - {basename(change.path)} - -
- {change.diff && ( -
- -
- )} -
- ))} -
- )} - {isExpanded && isFileChange && !hasChanges && item.detail && ( - - )} - {showToolOutput && summary.output && ( - - )} -
- + item={item} + isExpanded={isExpanded} + onToggle={toggleExpanded} + onOpenFileLink={openFileLink} + onOpenFileLinkMenu={showFileLinkMenu} + /> ); } return null; })} - {isThinking && ( -
- -
- {formattedElapsed} -
- Working… -
- )} - {!isThinking && lastDurationMs !== null && items.length > 0 && ( -
- - - Done in {formattedLastDuration} - - -
- )} + 0} + /> {!items.length && (
Start a thread and send a prompt to the agent. From 82fc41ac992fa292900d23d4bf188231761845a4 Mon Sep 17 00:00:00 2001 From: Vikash Loomba Date: Mon, 19 Jan 2026 23:48:18 -0800 Subject: [PATCH 2/2] chore: drop lockfile changes --- package-lock.json | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/package-lock.json b/package-lock.json index 7eb3e77ea..b8d2c5026 100644 --- a/package-lock.json +++ b/package-lock.json @@ -128,6 +128,7 @@ "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", @@ -2126,6 +2127,7 @@ "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.8.tgz", "integrity": "sha512-3MbSL37jEchWZz2p2mjntRZtPt837ij10ApxKfgmXCTuHWagYg7iA5bqPw6C8BMPfwidlvfPI/fxOc42HLhcyg==", "license": "MIT", + "peer": true, "dependencies": { "csstype": "^3.2.2" } @@ -2186,6 +2188,7 @@ "integrity": "sha512-4Z+L8I2OqhZV8qA132M4wNL30ypZGYOQVBfMgxDH/K5UX0PNqTu1c6za9ST5r9+tavvHiTWmBnKzpCJ/GlVFtg==", "dev": true, "license": "BSD-2-Clause", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "7.18.0", "@typescript-eslint/types": "7.18.0", @@ -2507,7 +2510,8 @@ "version": "5.5.0", "resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-5.5.0.tgz", "integrity": "sha512-hqJHYaQb5OptNunnyAnkHyM8aCjZ1MEIDTQu1iIbbTD/xops91NB5yq1ZK/dC2JDbVWtF23zUtl9JE2NqwT87A==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/acorn": { "version": "8.15.0", @@ -2515,6 +2519,7 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -2867,6 +2872,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -3684,6 +3690,7 @@ "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", @@ -6751,6 +6758,7 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -6912,6 +6920,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.3.tgz", "integrity": "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==", "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -6921,6 +6930,7 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.3.tgz", "integrity": "sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==", "license": "MIT", + "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -8047,6 +8057,7 @@ "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -8236,6 +8247,7 @@ "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0",