chore(lint): memoize inline styles and objects in stream/status panes

Clears 10 react-perf/jsx-no-new warnings across welcome-screen,
file-explorer-pane, terminal-pane, agent-stream-view, and
agent-status-bar by extracting per-item subcomponents, hoisting constant
style tuples, and memoizing derived arrays/objects.
This commit is contained in:
Mohamed Boudra
2026-04-24 02:45:53 +07:00
parent 1a53673535
commit d50bf740ea
5 changed files with 190 additions and 136 deletions

View File

@@ -456,10 +456,14 @@ function ControlledStatusBar({
[canSelectMode, disabled],
);
const sheetSelectStyle = useMemo(
() => [styles.sheetSelect, modelDisabled && styles.disabledSheetSelect],
[modelDisabled],
);
const renderSheetModelTrigger = useCallback(
({ selectedModelLabel }: { selectedModelLabel: string }) => (
<View
style={[styles.sheetSelect, modelDisabled && styles.disabledSheetSelect]}
style={sheetSelectStyle}
pointerEvents="none"
testID="agent-preferences-model"
>
@@ -470,7 +474,7 @@ function ControlledStatusBar({
<ChevronDown size={theme.iconSize.md} color={theme.colors.foregroundMuted} />
</View>
),
[ProviderIcon, modelDisabled, theme.colors.foregroundMuted, theme.iconSize.md],
[ProviderIcon, sheetSelectStyle, theme.colors.foregroundMuted, theme.iconSize.md],
);
if (!hasAnyControl) {
@@ -1353,6 +1357,15 @@ export function DraftAgentStatusBar({
selectedThinkingOptionId || mappedThinkingOptions[0]?.id || undefined;
const hasSelectedProvider = selectedProvider !== null;
const modelOptions = useMemo<StatusOption[]>(
() =>
models.map((model) => ({
id: model.id,
label: model.label,
})),
[models],
);
const handleToggleFavorite = useCallback(
(provider: string, modelId: string) => {
void updatePreferences((current) =>
@@ -1400,11 +1413,6 @@ export function DraftAgentStatusBar({
);
}
const modelOptions: StatusOption[] = models.map((model) => ({
id: model.id,
label: model.label,
}));
return (
<ControlledStatusBar
provider={selectedProvider ?? ""}

View File

@@ -8,6 +8,7 @@ import {
useRef,
useState,
type ComponentProps,
type ReactNode,
} from "react";
import {
View,
@@ -535,7 +536,7 @@ const AgentStreamViewComponent = forwardRef<AgentStreamViewHandle, AgentStreamVi
(nextItem === undefined && agent.status !== "running"));
return (
<View style={[stylesheet.streamItemWrapper, { marginBottom: gapBelow }]}>
<StreamItemWrapper gapBelow={gapBelow}>
{content}
{isEndOfAssistantTurn ? (
<TurnCopyButtonSlot
@@ -544,7 +545,7 @@ const AgentStreamViewComponent = forwardRef<AgentStreamViewHandle, AgentStreamVi
startIndex={index}
/>
) : null}
</View>
</StreamItemWrapper>
);
},
[getGapBetween, renderStreamItemContent, agent.status, streamRenderStrategy],
@@ -916,6 +917,15 @@ function PermissionRequestCard({
const isPlanRequest = request.kind === "plan";
const title = isPlanRequest ? "Plan" : (request.title ?? request.name ?? "Permission Required");
const description = request.description ?? "";
const resolvedToolCallDetail = useMemo(
() =>
request.detail ?? {
type: "unknown" as const,
input: request.input ?? null,
output: null,
},
[request.detail, request.input],
);
const resolvedActions = useMemo((): AgentPermissionAction[] => {
if (request.kind === "question") {
return [];
@@ -1121,13 +1131,7 @@ function PermissionRequestCard({
{!isPlanRequest ? (
<ToolCallDetailsContent
detail={
request.detail ?? {
type: "unknown",
input: request.input ?? null,
output: null,
}
}
detail={resolvedToolCallDetail}
maxHeight={200}
/>
) : null}
@@ -1317,3 +1321,16 @@ const permissionStyles = StyleSheet.create((theme) => ({
fontWeight: theme.fontWeight.normal,
},
}));
interface StreamItemWrapperProps {
gapBelow: number;
children: ReactNode;
}
function StreamItemWrapper({ gapBelow, children }: StreamItemWrapperProps) {
const wrapperStyle = useMemo(
() => [stylesheet.streamItemWrapper, { marginBottom: gapBelow }],
[gapBelow],
);
return <View style={wrapperStyle}>{children}</View>;
}

View File

@@ -1,4 +1,4 @@
import { useCallback, useEffect, useMemo, useRef } from "react";
import { useCallback, useEffect, useMemo, useRef, type ReactNode } from "react";
import { useQuery } from "@tanstack/react-query";
import {
ActivityIndicator,
@@ -146,29 +146,21 @@ function TreeRowItem({
<Pressable onPress={handlePress} style={pressableStyle}>
{depth > 0 &&
Array.from({ length: depth }, (_, i) => (
<View
key={i}
style={[
styles.indentGuide,
{
left: theme.spacing[3] + i * INDENT_PER_LEVEL + 4,
},
]}
/>
<IndentGuide key={i} index={i} />
))}
<View style={styles.entryInfo}>
<View style={styles.entryIcon}>
{isDirectory ? (
loading ? (
<ActivityIndicator size="small" />
) : (
{(() => {
if (!isDirectory) {
return <SvgXml xml={getFileIconSvg(entry.name)} width={16} height={16} />;
}
if (loading) return <ActivityIndicator size="small" />;
return (
<View style={chevronStyle}>
<ChevronRight size={16} color={theme.colors.foregroundMuted} />
</View>
)
) : (
<SvgXml xml={getFileIconSvg(entry.name)} width={16} height={16} />
)}
);
})()}
</View>
<Text style={styles.entryName} numberOfLines={1}>
{entry.name}
@@ -540,77 +532,84 @@ export function FileExplorerPane({
);
}
return (
<View style={styles.container}>
{error ? (
<View style={styles.centerState}>
<Text style={styles.errorText}>{error}</Text>
<View style={styles.errorActions}>
{showBackFromError ? (
<Pressable style={styles.retryButton} onPress={handleBackFromError}>
<Text style={styles.retryButtonText}>Back</Text>
</Pressable>
) : null}
<Pressable style={styles.retryButton} onPress={handleRetry}>
<Text style={styles.retryButtonText}>Retry</Text>
let paneContent: ReactNode;
if (error) {
paneContent = (
<View style={styles.centerState}>
<Text style={styles.errorText}>{error}</Text>
<View style={styles.errorActions}>
{showBackFromError ? (
<Pressable style={styles.retryButton} onPress={handleBackFromError}>
<Text style={styles.retryButtonText}>Back</Text>
</Pressable>
</View>
) : null}
<Pressable style={styles.retryButton} onPress={handleRetry}>
<Text style={styles.retryButtonText}>Retry</Text>
</Pressable>
</View>
) : showInitialLoading ? (
<View style={styles.centerState}>
<ActivityIndicator size="small" />
<Text style={styles.loadingText}>Loading files</Text>
</View>
);
} else if (showInitialLoading) {
paneContent = (
<View style={styles.centerState}>
<ActivityIndicator size="small" />
<Text style={styles.loadingText}>Loading files</Text>
</View>
);
} else if (treeRows.length === 0) {
paneContent = (
<View style={styles.centerState}>
<Text style={styles.emptyText}>No files</Text>
</View>
);
} else {
paneContent = (
<View style={TREE_PANE_CONTAINER_STYLE}>
<View style={styles.paneHeader} testID="files-pane-header">
<Pressable onPress={handleSortCycle} style={sortTriggerStyle}>
<Text style={styles.sortTriggerText}>{currentSortLabel}</Text>
<ChevronDown size={12} color={theme.colors.foregroundMuted} />
</Pressable>
<Pressable
onPress={handleRefresh}
disabled={isRefreshFetching}
hitSlop={8}
style={iconButtonStyle}
accessibilityRole="button"
accessibilityLabel={isRefreshFetching ? "Refreshing files" : "Refresh files"}
>
<View style={styles.refreshIcon}>
{isRefreshFetching ? (
<LoadingSpinner size={theme.iconSize.sm} color={theme.colors.foregroundMuted} />
) : (
<RotateCw size={theme.iconSize.sm} color={theme.colors.foregroundMuted} />
)}
</View>
</Pressable>
</View>
) : treeRows.length === 0 ? (
<View style={styles.centerState}>
<Text style={styles.emptyText}>No files</Text>
</View>
) : (
<View style={[styles.treePane, styles.treePaneFill]}>
<View style={styles.paneHeader} testID="files-pane-header">
<Pressable onPress={handleSortCycle} style={sortTriggerStyle}>
<Text style={styles.sortTriggerText}>{currentSortLabel}</Text>
<ChevronDown size={12} color={theme.colors.foregroundMuted} />
</Pressable>
<Pressable
onPress={handleRefresh}
disabled={isRefreshFetching}
hitSlop={8}
style={iconButtonStyle}
accessibilityRole="button"
accessibilityLabel={isRefreshFetching ? "Refreshing files" : "Refresh files"}
>
<View style={styles.refreshIcon}>
{isRefreshFetching ? (
<LoadingSpinner size={theme.iconSize.sm} color={theme.colors.foregroundMuted} />
) : (
<RotateCw size={theme.iconSize.sm} color={theme.colors.foregroundMuted} />
)}
</View>
</Pressable>
</View>
<FlatList
ref={treeListRef}
style={styles.treeList}
data={treeRows}
renderItem={renderTreeRow}
keyExtractor={treeRowKeyExtractor}
testID="file-explorer-tree-scroll"
contentContainerStyle={styles.entriesContent}
onLayout={scrollbar.onLayout}
onScroll={scrollbar.onScroll}
onContentSizeChange={scrollbar.onContentSizeChange}
scrollEventThrottle={16}
showsVerticalScrollIndicator={!showDesktopWebScrollbar}
initialNumToRender={24}
maxToRenderPerBatch={40}
windowSize={12}
/>
{scrollbar.overlay}
</View>
)}
</View>
);
<FlatList
ref={treeListRef}
style={styles.treeList}
data={treeRows}
renderItem={renderTreeRow}
keyExtractor={treeRowKeyExtractor}
testID="file-explorer-tree-scroll"
contentContainerStyle={styles.entriesContent}
onLayout={scrollbar.onLayout}
onScroll={scrollbar.onScroll}
onContentSizeChange={scrollbar.onContentSizeChange}
scrollEventThrottle={16}
showsVerticalScrollIndicator={!showDesktopWebScrollbar}
initialNumToRender={24}
maxToRenderPerBatch={40}
windowSize={12}
/>
{scrollbar.overlay}
</View>
);
}
return <View style={styles.container}>{paneContent}</View>;
}
function sortEntries(entries: ExplorerEntry[], sortOption: SortOption): ExplorerEntry[] {
@@ -959,3 +958,21 @@ const styles = StyleSheet.create((theme) => ({
padding: theme.spacing[4],
},
}));
const TREE_PANE_CONTAINER_STYLE = [styles.treePane, styles.treePaneFill];
interface IndentGuideProps {
index: number;
}
function IndentGuide({ index }: IndentGuideProps) {
const { theme } = useUnistyles();
const guideStyle = useMemo(
() => [
styles.indentGuide,
{ left: theme.spacing[3] + index * INDENT_PER_LEVEL + 4 },
],
[index, theme.spacing],
);
return <View style={guideStyle} />;
}

View File

@@ -106,11 +106,13 @@ function ModifierButton({ modifier, active, onToggle }: ModifierButtonProps) {
],
[active],
);
const textStyle = useMemo(
() => [styles.keyButtonText, active && styles.keyButtonTextActive],
[active],
);
return (
<Pressable testID={`terminal-key-${modifier}`} onPress={handlePress} style={pressableStyle}>
<Text style={[styles.keyButtonText, active && styles.keyButtonTextActive]}>
{MODIFIER_LABELS[modifier]}
</Text>
<Text style={textStyle}>{MODIFIER_LABELS[modifier]}</Text>
</Pressable>
);
}
@@ -628,16 +630,7 @@ export function TerminalPane({
<View style={styles.terminalGestureContainer}>
<TerminalEmulator
ref={emulatorRef}
dom={{
style: { flex: 1 },
matchContents: false,
scrollEnabled: true,
nestedScrollEnabled: true,
overScrollMode: "never",
bounces: false,
automaticallyAdjustContentInsets: false,
contentInsetAdjustmentBehavior: "never",
}}
dom={TERMINAL_EMULATOR_DOM_PROPS}
streamKey={`${scopeKey}:${terminalId}`}
testId="terminal-surface"
xtermTheme={xtermTheme}
@@ -787,3 +780,14 @@ const styles = StyleSheet.create((theme) => ({
textAlign: "center",
},
}));
const TERMINAL_EMULATOR_DOM_PROPS = {
style: { flex: 1 },
matchContents: false,
scrollEnabled: true,
nestedScrollEnabled: true,
overScrollMode: "never" as const,
bounces: false,
automaticallyAdjustContentInsets: false,
contentInsetAdjustmentBehavior: "never" as const,
};

View File

@@ -283,27 +283,9 @@ export function WelcomeScreen({ onHostAdded }: WelcomeScreenProps) {
</View>
<View style={styles.actions}>
{actions.map((action) => {
const Icon = action.icon;
return (
<Pressable
key={action.key}
style={[styles.actionButton, action.primary ? styles.actionButtonPrimary : null]}
onPress={action.onPress}
testID={action.testID}
>
<Icon
size={18}
color={action.primary ? theme.colors.accentForeground : theme.colors.foreground}
/>
<Text
style={[styles.actionText, action.primary ? styles.actionTextPrimary : null]}
>
{action.label}
</Text>
</Pressable>
);
})}
{actions.map((action) => (
<WelcomeActionButton key={action.key} action={action} />
))}
</View>
<Button
@@ -334,3 +316,29 @@ export function WelcomeScreen({ onHostAdded }: WelcomeScreenProps) {
</View>
);
}
interface WelcomeActionButtonProps {
action: WelcomeAction;
}
function WelcomeActionButton({ action }: WelcomeActionButtonProps) {
const { theme } = useUnistyles();
const Icon = action.icon;
const buttonStyle = useMemo(
() => [styles.actionButton, action.primary ? styles.actionButtonPrimary : null],
[action.primary],
);
const textStyle = useMemo(
() => [styles.actionText, action.primary ? styles.actionTextPrimary : null],
[action.primary],
);
return (
<Pressable style={buttonStyle} onPress={action.onPress} testID={action.testID}>
<Icon
size={18}
color={action.primary ? theme.colors.accentForeground : theme.colors.foreground}
/>
<Text style={textStyle}>{action.label}</Text>
</Pressable>
);
}