From fe3a9bd8336fb2d6b3f8abbc24a27013926b515b Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Fri, 24 Apr 2026 01:34:48 +0700 Subject: [PATCH] chore(lint): hoist inline callbacks in app (jsx-no-new-function-as-prop) Finish eliminating jsx-no-new-function-as-prop warnings in agent-status-bar.tsx and complete refactor of git-diff-pane.tsx by extracting per-item components and using stable useCallback handlers. --- .../app/src/components/agent-status-bar.tsx | 260 +++++++++------- packages/app/src/components/git-diff-pane.tsx | 294 +++++++++++------- 2 files changed, 323 insertions(+), 231 deletions(-) diff --git a/packages/app/src/components/agent-status-bar.tsx b/packages/app/src/components/agent-status-bar.tsx index 62179eb19..f6ab12284 100644 --- a/packages/app/src/components/agent-status-bar.tsx +++ b/packages/app/src/components/agent-status-bar.tsx @@ -170,22 +170,6 @@ function alwaysTrue() { return true; } -function modeBadgeStyle({ pressed, hovered }: PressableStateCallbackType) { - return [styles.modeBadge, hovered && styles.modeBadgeHovered, pressed && styles.modeBadgePressed]; -} - -function modeIconBadgeStyle({ pressed, hovered }: PressableStateCallbackType) { - return [ - styles.modeIconBadge, - hovered && styles.modeBadgeHovered, - pressed && styles.modeBadgePressed, - ]; -} - -function sheetSelectStyle({ pressed }: PressableStateCallbackType) { - return [styles.sheetSelect, pressed && styles.sheetSelectPressed]; -} - function getModeIconColor( colorTier: AgentModeColorTier | undefined, palette: { @@ -1170,6 +1154,124 @@ export const AgentStatusBar = memo(function AgentStatusBar({ })); }, [modelSelection.thinkingOptions]); + const agentProvider = agent?.provider; + const activeModelId = modelSelection.activeModelId; + + const handleSelectMode = useCallback( + (modeId: string) => { + if (!client) { + return; + } + void client.setAgentMode(agentId, modeId).catch((error) => { + console.warn("[AgentStatusBar] setAgentMode failed", error); + toast.error(toErrorMessage(error)); + }); + }, + [agentId, client, toast], + ); + + const handleSelectModel = useCallback( + (modelId: string) => { + if (!client || !agentProvider) { + return; + } + void updatePreferences((current) => + mergeProviderPreferences({ + preferences: current, + provider: agentProvider, + updates: { + model: modelId, + }, + }), + ).catch((error) => { + console.warn("[AgentStatusBar] persist model preference failed", error); + }); + void client.setAgentModel(agentId, modelId).catch((error) => { + console.warn("[AgentStatusBar] setAgentModel failed", error); + toast.error(toErrorMessage(error)); + }); + }, + [agentId, agentProvider, client, toast, updatePreferences], + ); + + const handleToggleFavoriteModel = useCallback( + (provider: string, modelId: string) => { + void updatePreferences((current) => + toggleFavoriteModel({ preferences: current, provider, modelId }), + ).catch((error) => { + console.warn("[AgentStatusBar] toggle favorite model failed", error); + }); + }, + [updatePreferences], + ); + + const handleSelectThinkingOption = useCallback( + (thinkingOptionId: string) => { + if (!client || !agentProvider) { + return; + } + if (activeModelId) { + void updatePreferences((current) => + mergeProviderPreferences({ + preferences: current, + provider: agentProvider, + updates: { + model: activeModelId, + thinkingByModel: { + [activeModelId]: thinkingOptionId, + }, + }, + }), + ).catch((error) => { + console.warn("[AgentStatusBar] persist thinking preference failed", error); + }); + } + void client.setAgentThinkingOption(agentId, thinkingOptionId).catch((error) => { + console.warn("[AgentStatusBar] setAgentThinkingOption failed", error); + toast.error(toErrorMessage(error)); + }); + }, + [activeModelId, agentId, agentProvider, client, toast, updatePreferences], + ); + + const handleSetFeature = useCallback( + (featureId: string, value: unknown) => { + if (!client || !agentProvider) { + return; + } + void updatePreferences((current) => + mergeProviderPreferences({ + preferences: current, + provider: agentProvider, + updates: { + featureValues: { + [featureId]: value, + }, + }, + }), + ).catch((error) => { + console.warn("[AgentStatusBar] persist feature preference failed", error); + }); + void client.setAgentFeature(agentId, featureId, value).catch((error) => { + console.warn("[AgentStatusBar] setAgentFeature failed", error); + toast.error(toErrorMessage(error)); + }); + }, + [agentId, agentProvider, client, toast, updatePreferences], + ); + + const handleModelSelectorOpen = useCallback(() => { + refetchSnapshotIfStale(agentProvider); + }, [agentProvider, refetchSnapshotIfStale]); + + const fallbackModeOptions = useMemo( + () => + modeOptions.length > 0 + ? modeOptions + : [{ id: agent?.currentModeId ?? "", label: displayMode }], + [agent?.currentModeId, displayMode, modeOptions], + ); + if (!agent) { return null; } @@ -1177,106 +1279,23 @@ export const AgentStatusBar = memo(function AgentStatusBar({ return ( 0 - ? modeOptions - : [{ id: agent.currentModeId ?? "", label: displayMode }] - } + modeOptions={fallbackModeOptions} selectedModeId={agent.currentModeId ?? undefined} providerDefinitions={agentProviderDefinitions} allProviderModels={agentProviderModels} - onSelectMode={(modeId) => { - if (!client) { - return; - } - void client.setAgentMode(agentId, modeId).catch((error) => { - console.warn("[AgentStatusBar] setAgentMode failed", error); - toast.error(toErrorMessage(error)); - }); - }} + onSelectMode={handleSelectMode} modelOptions={modelOptions} selectedModelId={modelSelection.activeModelId ?? undefined} - onSelectModel={(modelId) => { - if (!client) { - return; - } - void updatePreferences((current) => - mergeProviderPreferences({ - preferences: current, - provider: agent.provider, - updates: { - model: modelId, - }, - }), - ).catch((error) => { - console.warn("[AgentStatusBar] persist model preference failed", error); - }); - void client.setAgentModel(agentId, modelId).catch((error) => { - console.warn("[AgentStatusBar] setAgentModel failed", error); - toast.error(toErrorMessage(error)); - }); - }} + onSelectModel={handleSelectModel} favoriteKeys={favoriteKeys} - onToggleFavoriteModel={(provider, modelId) => { - void updatePreferences((current) => - toggleFavoriteModel({ preferences: current, provider, modelId }), - ).catch((error) => { - console.warn("[AgentStatusBar] toggle favorite model failed", error); - }); - }} + onToggleFavoriteModel={handleToggleFavoriteModel} thinkingOptions={thinkingOptions.length > 1 ? thinkingOptions : undefined} selectedThinkingOptionId={modelSelection.selectedThinkingId ?? undefined} - onSelectThinkingOption={(thinkingOptionId) => { - if (!client) { - return; - } - const activeModelId = modelSelection.activeModelId; - if (activeModelId) { - void updatePreferences((current) => - mergeProviderPreferences({ - preferences: current, - provider: agent.provider, - updates: { - model: activeModelId, - thinkingByModel: { - [activeModelId]: thinkingOptionId, - }, - }, - }), - ).catch((error) => { - console.warn("[AgentStatusBar] persist thinking preference failed", error); - }); - } - void client.setAgentThinkingOption(agentId, thinkingOptionId).catch((error) => { - console.warn("[AgentStatusBar] setAgentThinkingOption failed", error); - toast.error(toErrorMessage(error)); - }); - }} + onSelectThinkingOption={handleSelectThinkingOption} features={agent.features} - onSetFeature={(featureId, value) => { - if (!client) { - return; - } - void updatePreferences((current) => - mergeProviderPreferences({ - preferences: current, - provider: agent.provider, - updates: { - featureValues: { - [featureId]: value, - }, - }, - }), - ).catch((error) => { - console.warn("[AgentStatusBar] persist feature preference failed", error); - }); - void client.setAgentFeature(agentId, featureId, value).catch((error) => { - console.warn("[AgentStatusBar] setAgentFeature failed", error); - toast.error(toErrorMessage(error)); - }); - }} + onSetFeature={handleSetFeature} isModelLoading={snapshotIsLoading || selectedProviderIsLoading} - onModelSelectorOpen={() => refetchSnapshotIfStale(agent?.provider)} + onModelSelectorOpen={handleModelSelectorOpen} onDropdownClose={onDropdownClose} disabled={!client} /> @@ -1334,6 +1353,17 @@ export function DraftAgentStatusBar({ selectedThinkingOptionId || mappedThinkingOptions[0]?.id || undefined; const hasSelectedProvider = selectedProvider !== null; + const handleToggleFavorite = useCallback( + (provider: string, modelId: string) => { + void updatePreferences((current) => + toggleFavoriteModel({ preferences: current, provider, modelId }), + ).catch((error) => { + console.warn("[DraftAgentStatusBar] toggle favorite model failed", error); + }); + }, + [updatePreferences], + ); + if (platformIsWeb) { return ( @@ -1344,13 +1374,7 @@ export function DraftAgentStatusBar({ selectedModel={selectedModel} onSelect={onSelectProviderAndModel} favoriteKeys={favoriteKeys} - onToggleFavorite={(provider, modelId) => { - void updatePreferences((current) => - toggleFavoriteModel({ preferences: current, provider, modelId }), - ).catch((error) => { - console.warn("[DraftAgentStatusBar] toggle favorite model failed", error); - }); - }} + onToggleFavorite={handleToggleFavorite} isLoading={isAllModelsLoading} disabled={disabled} onOpen={onModelSelectorOpen} @@ -1391,17 +1415,11 @@ export function DraftAgentStatusBar({ onSelectMode={onSelectMode} modelOptions={modelOptions} selectedModelId={selectedModel} - onSelectModel={(modelId) => onSelectModel(modelId)} + onSelectModel={onSelectModel} onSelectProviderAndModel={onSelectProviderAndModel} isModelLoading={isAllModelsLoading} favoriteKeys={favoriteKeys} - onToggleFavoriteModel={(provider, modelId) => { - void updatePreferences((current) => - toggleFavoriteModel({ preferences: current, provider, modelId }), - ).catch((error) => { - console.warn("[DraftAgentStatusBar] toggle favorite model failed", error); - }); - }} + onToggleFavoriteModel={handleToggleFavorite} thinkingOptions={mappedThinkingOptions.length > 0 ? mappedThinkingOptions : undefined} selectedThinkingOptionId={effectiveSelectedThinkingOption} onSelectThinkingOption={onSelectThinkingOption} diff --git a/packages/app/src/components/git-diff-pane.tsx b/packages/app/src/components/git-diff-pane.tsx index 6815ddf1e..c10d1a8b6 100644 --- a/packages/app/src/components/git-diff-pane.tsx +++ b/packages/app/src/components/git-diff-pane.tsx @@ -132,6 +132,19 @@ function getWrappedTextStyle(wrapLines: boolean): WrappedWebTextStyle | undefine : { whiteSpace: "pre", overflowWrap: "normal" }; } +function HighlightedToken({ + text, + color, + lineHeight, +}: { + text: string; + color: string; + lineHeight: number; +}) { + const tokenStyle = useMemo(() => ({ color, lineHeight }), [color, lineHeight]); + return {text}; +} + function HighlightedText({ tokens, wrapLines = false }: HighlightedTextProps) { const { theme } = useUnistyles(); const isDark = theme.colorScheme === "dark"; @@ -144,12 +157,20 @@ function HighlightedText({ tokens, wrapLines = false }: HighlightedTextProps) { return colors[style as HighlightStyleKey] ?? baseColor; }; + const containerStyle = useMemo( + () => [styles.diffLineText, { lineHeight, ...getWrappedTextStyle(wrapLines) }], + [lineHeight, wrapLines], + ); + return ( - + {tokens.map((token, index) => ( - - {token.text} - + ))} ); @@ -180,17 +201,21 @@ function DiffGutterCell({ type: DiffLine["type"] | undefined | null; gutterWidth: number; }) { + const containerStyle = useMemo( + () => [styles.gutterCell, lineTypeBackground(type), { width: gutterWidth }], + [type, gutterWidth], + ); + const textStyle = useMemo( + () => [ + styles.lineNumberText, + type === "add" && styles.addLineNumberText, + type === "remove" && styles.removeLineNumberText, + ], + [type], + ); return ( - - - {formatDiffGutterText(lineNumber)} - + + {formatDiffGutterText(lineNumber)} ); } @@ -198,23 +223,28 @@ function DiffGutterCell({ function DiffTextLine({ line, wrapLines }: { line: DiffLine; wrapLines: boolean }) { const visibleTokens = hasVisibleDiffTokens(line.tokens) ? line.tokens : null; + const containerStyle = useMemo( + () => [styles.textLineContainer, lineTypeBackground(line.type)], + [line.type], + ); + const textStyle = useMemo( + () => [ + styles.diffLineText, + getWrappedTextStyle(wrapLines), + line.type === "add" && styles.addLineText, + line.type === "remove" && styles.removeLineText, + line.type === "header" && styles.headerLineText, + line.type === "context" && styles.contextLineText, + ], + [line.type, wrapLines], + ); + return ( - + {line.type !== "header" && visibleTokens ? ( ) : ( - - {formatDiffContentText(line.content)} - + {formatDiffContentText(line.content)} )} ); @@ -229,23 +259,28 @@ function SplitTextLine({ }) { const visibleTokens = line && hasVisibleDiffTokens(line.tokens) ? line.tokens : null; + const containerStyle = useMemo( + () => [styles.textLineContainer, lineTypeBackground(line?.type)], + [line?.type], + ); + const textStyle = useMemo( + () => [ + styles.diffLineText, + getWrappedTextStyle(wrapLines), + line?.type === "add" && styles.addLineText, + line?.type === "remove" && styles.removeLineText, + line?.type === "context" && styles.contextLineText, + !line && styles.emptySplitCellText, + ], + [line, wrapLines], + ); + return ( - + {visibleTokens ? ( ) : ( - - {formatDiffContentText(line?.content)} - + {formatDiffContentText(line?.content)} )} ); @@ -264,34 +299,43 @@ function DiffLineView({ }) { const visibleTokens = hasVisibleDiffTokens(line.tokens) ? line.tokens : null; + const containerStyle = useMemo( + () => [styles.diffLineContainer, lineTypeBackground(line.type)], + [line.type], + ); + const gutterStyle = useMemo( + () => [styles.lineNumberGutter, { width: gutterWidth }], + [gutterWidth], + ); + const gutterTextStyle = useMemo( + () => [ + styles.lineNumberText, + line.type === "add" && styles.addLineNumberText, + line.type === "remove" && styles.removeLineNumberText, + ], + [line.type], + ); + const textStyle = useMemo( + () => [ + styles.diffLineText, + getWrappedTextStyle(wrapLines), + line.type === "add" && styles.addLineText, + line.type === "remove" && styles.removeLineText, + line.type === "header" && styles.headerLineText, + line.type === "context" && styles.contextLineText, + ], + [line.type, wrapLines], + ); + return ( - - - - {formatDiffGutterText(lineNumber)} - + + + {formatDiffGutterText(lineNumber)} {line.type !== "header" && visibleTokens ? ( ) : ( - - {formatDiffContentText(line.content)} - + {formatDiffContentText(line.content)} )} ); @@ -308,34 +352,43 @@ function SplitDiffLine({ }) { const visibleTokens = line && hasVisibleDiffTokens(line.tokens) ? line.tokens : null; + const containerStyle = useMemo( + () => [styles.diffLineContainer, lineTypeBackground(line?.type)], + [line?.type], + ); + const gutterStyle = useMemo( + () => [styles.lineNumberGutter, { width: gutterWidth }], + [gutterWidth], + ); + const gutterTextStyle = useMemo( + () => [ + styles.lineNumberText, + line?.type === "add" && styles.addLineNumberText, + line?.type === "remove" && styles.removeLineNumberText, + ], + [line?.type], + ); + const textStyle = useMemo( + () => [ + styles.diffLineText, + getWrappedTextStyle(wrapLines), + line?.type === "add" && styles.addLineText, + line?.type === "remove" && styles.removeLineText, + line?.type === "context" && styles.contextLineText, + !line && styles.emptySplitCellText, + ], + [line, wrapLines], + ); + return ( - - - - {formatDiffGutterText(line?.lineNumber ?? null)} - + + + {formatDiffGutterText(line?.lineNumber ?? null)} {visibleTokens ? ( ) : ( - - {formatDiffContentText(line?.content)} - + {formatDiffContentText(line?.content)} )} ); @@ -356,15 +409,28 @@ function SplitDiffColumn({ }) { const [scrollWidth, setScrollWidth] = useState(0); + const wrapCellStyle = useMemo( + () => [styles.splitCell, showDivider && styles.splitCellWithDivider], + [showDivider], + ); + const rowCellStyle = useMemo( + () => [styles.splitCell, showDivider && styles.splitCellWithDivider, styles.splitCellRow], + [showDivider], + ); + const linesContainerRowStyle = useMemo( + () => [styles.linesContainer, scrollWidth > 0 && { minWidth: scrollWidth }], + [scrollWidth], + ); + if (wrapLines) { return ( - + {rows.map((row, i) => { if (row.kind === "header") { return ( - {row.content} + {row.content} ); } @@ -383,9 +449,7 @@ function SplitDiffColumn({ } return ( - + {rows.map((row, i) => { if (row.kind === "header") { @@ -415,12 +479,12 @@ function SplitDiffColumn({ style={styles.splitColumnScroll} contentContainerStyle={styles.diffContentInner} > - 0 && { minWidth: scrollWidth }]}> + {rows.map((row, i) => { if (row.kind === "header") { return ( - {row.content} + {row.content} ); } @@ -486,12 +550,13 @@ const DiffFileHeader = memo(function DiffFileHeader({ [toggleExpanded], ); + const containerStyle = useMemo( + () => [styles.fileSectionHeaderContainer, isExpanded && styles.fileSectionHeaderExpanded], + [isExpanded], + ); + return ( - + 0 ? bodyWidth : scrollViewWidth; + const linesContainerRowStyle = useMemo( + () => [styles.linesContainer, availableWidth > 0 && { minWidth: availableWidth }], + [availableWidth], + ); + return ( - + {(() => { if (file.status === "too_large" || file.status === "binary") { return ( @@ -579,7 +646,7 @@ function DiffFileBody({ if (layout === "split") { const rows = buildSplitDiffRows(file); return ( - + 0 ? bodyWidth : scrollViewWidth; return ( - + {computedLines.map(({ line, lineNumber, key }) => ( - 0 && { minWidth: availableWidth }]} - > + {computedLines.map(({ line, key }) => ( ))} @@ -1156,6 +1220,11 @@ export function GitDiffPane({ serverId, workspaceId, cwd, hideHeaderRow }: GitDi [], ); + const flatExtraData = useMemo( + () => ({ expandedPathsArray, effectiveLayout, wrapLines }), + [expandedPathsArray, effectiveLayout, wrapLines], + ); + const hasChanges = files.length > 0; const diffErrorMessage = diffPayloadError?.message ?? null; const prErrorMessage = githubFeaturesEnabled ? (prPayloadError?.message ?? null) : null; @@ -1253,7 +1322,7 @@ export function GitDiffPane({ serverId, workspaceId, cwd, hideHeaderRow }: GitDi renderItem={renderFlatItem} keyExtractor={flatKeyExtractor} stickyHeaderIndices={stickyHeaderIndices} - extraData={{ expandedPathsArray, effectiveLayout, wrapLines }} + extraData={flatExtraData} style={styles.scrollView} contentContainerStyle={styles.contentContainer} testID="git-diff-scroll" @@ -1987,3 +2056,8 @@ const styles = StyleSheet.create((theme) => ({ color: theme.colors.foreground, }, })); + +const HEADER_LINE_TEXT_STYLE = [styles.diffLineText, styles.headerLineText]; +const FILE_SECTION_BODY_STYLE = [styles.fileSectionBodyContainer, styles.fileSectionBorder]; +const DIFF_CONTENT_SPLIT_ROW_STYLE = [styles.diffContent, styles.splitRow]; +const DIFF_CONTENT_ROW_STYLE = [styles.diffContent, styles.diffContentRow];