From 7ea02716fc104c348198fae67c1cf0faa5397486 Mon Sep 17 00:00:00 2001 From: Chris Arderne Date: Thu, 20 Aug 2026 09:59:39 +0100 Subject: [PATCH] fix(webapp): avoid mutating render inputs (#4716) ## Summary Keeps render inputs and shared regular expressions immutable. Grouped selects now compute each section's shortcut offset directly from preceding sections, which also makes numeric shortcuts follow the displayed item order reliably. --- apps/webapp/app/components/code/CodeBlock.tsx | 20 +++++++++---------- .../dashboard-agent/report-sparkline.tsx | 3 +-- .../app/components/primitives/Select.tsx | 9 +++++---- 3 files changed, 16 insertions(+), 16 deletions(-) diff --git a/apps/webapp/app/components/code/CodeBlock.tsx b/apps/webapp/app/components/code/CodeBlock.tsx index 0dc2cb703..904328854 100644 --- a/apps/webapp/app/components/code/CodeBlock.tsx +++ b/apps/webapp/app/components/code/CodeBlock.tsx @@ -221,35 +221,35 @@ export const CodeBlock = forwardRef( const [modalCopied, setModalCopied] = useState(false); const [isModalOpen, setIsModalOpen] = useState(false); const [isWrapped, setIsWrapped] = useState(wrap); + const normalizedCode = code?.trim() ?? ""; const onCopied = useCallback( (event: React.MouseEvent) => { event.preventDefault(); event.stopPropagation(); - navigator.clipboard.writeText(code); + navigator.clipboard.writeText(normalizedCode); setCopied(true); setTimeout(() => { setCopied(false); }, 1500); }, - [code] + [normalizedCode] ); const onModalCopied = useCallback( (event: React.MouseEvent) => { event.preventDefault(); event.stopPropagation(); - navigator.clipboard.writeText(code); + navigator.clipboard.writeText(normalizedCode); setModalCopied(true); setTimeout(() => { setModalCopied(false); }, 1500); }, - [code] + [normalizedCode] ); - code = code?.trim() ?? ""; - const lineCount = code.split("\n").length; + const lineCount = normalizedCode.split("\n").length; const maxLineWidth = lineCount.toString().length; let maxHeight: string | undefined = undefined; if (maxLines && lineCount > maxLines) { @@ -345,7 +345,7 @@ export const CodeBlock = forwardRef( {shouldHighlight ? ( ( )} dir="ltr" > - {highlightSearchText(code, searchTerm)} + {highlightSearchText(normalizedCode, searchTerm)} )} @@ -400,7 +400,7 @@ export const CodeBlock = forwardRef( {shouldHighlight ? ( ( className="overflow-auto px-3 py-3 scrollbar-thin scrollbar-track-transparent scrollbar-thumb-surface-control" >
-                  {highlightSearchText(code, searchTerm)}
+                  {highlightSearchText(normalizedCode, searchTerm)}
                 
)} diff --git a/apps/webapp/app/components/dashboard-agent/report-sparkline.tsx b/apps/webapp/app/components/dashboard-agent/report-sparkline.tsx index 09e263b03..cdbf77c89 100644 --- a/apps/webapp/app/components/dashboard-agent/report-sparkline.tsx +++ b/apps/webapp/app/components/dashboard-agent/report-sparkline.tsx @@ -187,7 +187,7 @@ export function ReportFindingLine({ * entities mono, verdict phrases bright and medium, everything else dimmed. * Colour stays reserved for severity, so emphasis here is weight only. */ -const QUANTITY_RE = /~?\d[\d,.]*\s?(?:%|×|\/min|ms\b|s\b|min\b|h\b)?/g; +const QUANTITY_RE = /~?\d[\d,.]*\s?(?:%|×|\/min|ms\b|s\b|min\b|h\b)?/; const VERDICT_PHRASES = [ "not your code", @@ -249,7 +249,6 @@ export function ReportProse({ text, entities }: { text: string; entities?: strin segments = splitBy( segments, (t) => { - QUANTITY_RE.lastIndex = 0; const m = QUANTITY_RE.exec(t); return m && m[0].trim().length > 0 ? { start: m.index, end: m.index + m[0].length } : null; }, diff --git a/apps/webapp/app/components/primitives/Select.tsx b/apps/webapp/app/components/primitives/Select.tsx index 70cc919a0..04b7467de 100644 --- a/apps/webapp/app/components/primitives/Select.tsx +++ b/apps/webapp/app/components/primitives/Select.tsx @@ -413,19 +413,20 @@ function SelectGroupedRenderer({ ) => React.ReactNode; enableItemShortcuts: boolean; }) { - let count = 0; return ( <> {items.map((section, index) => { - const previousItem = items.at(index - 1); - count += previousItem ? previousItem.items.length : 0; + const startIndex = items + .slice(0, index) + .reduce((count, previousSection) => count + previousSection.items.length, 0); + return ( {children(section.items as ItemFromSection[], { shortcutsEnabled: enableItemShortcuts, section: { title: section.title, - startIndex: count - 1, + startIndex, count: section.items.length, }, })}