chore(lint): hoist inline arrays and objects in app (145 warnings)

- Hoist static style arrays/objects to module-level consts
- Memoize dynamic ones with useMemo and correct deps
- jsx-no-new-array-as-prop: 145 -> 43
- jsx-no-new-object-as-prop: 64 -> 21
This commit is contained in:
Mohamed Boudra
2026-04-24 02:40:14 +07:00
parent 58d92f501b
commit 8a3129ff15
42 changed files with 719 additions and 490 deletions

View File

@@ -834,19 +834,23 @@ function FaviconStatusSync() {
return null;
}
const AGENT_SCREEN_OPTIONS = { gestureEnabled: false };
function RootStack() {
const storeReady = useStoreReady();
const { theme } = useUnistyles();
const stackScreenOptions = useMemo(
() => ({
headerShown: false,
animation: "none" as const,
contentStyle: {
backgroundColor: theme.colors.surface0,
},
}),
[theme.colors.surface0],
);
return (
<Stack
screenOptions={{
headerShown: false,
animation: "none",
contentStyle: {
backgroundColor: theme.colors.surface0,
},
}}
>
<Stack screenOptions={stackScreenOptions}>
<Stack.Screen name="index" />
<Stack.Protected guard={storeReady}>
<Stack.Screen name="welcome" />
@@ -862,7 +866,7 @@ function RootStack() {
outside this route-level native-stack API.
*/}
<Stack.Screen name="h/[serverId]/workspace/[workspaceId]" />
<Stack.Screen name="h/[serverId]/agent/[agentId]" options={{ gestureEnabled: false }} />
<Stack.Screen name="h/[serverId]/agent/[agentId]" options={AGENT_SCREEN_OPTIONS} />
<Stack.Screen name="h/[serverId]/index" />
<Stack.Screen name="h/[serverId]/sessions" />
<Stack.Screen name="h/[serverId]/open-project" />

View File

@@ -1,4 +1,5 @@
import { useLocalSearchParams } from "expo-router";
import { useMemo } from "react";
import SettingsScreen from "@/screens/settings-screen";
import { isSettingsSectionSlug, type SettingsSectionSlug } from "@/utils/host-routes";
@@ -6,6 +7,7 @@ export default function SettingsSectionRoute() {
const params = useLocalSearchParams<{ section?: string }>();
const rawSection = typeof params.section === "string" ? params.section : "";
const section: SettingsSectionSlug = isSettingsSectionSlug(rawSection) ? rawSection : "general";
const view = useMemo(() => ({ kind: "section" as const, section }), [section]);
return <SettingsScreen view={{ kind: "section", section }} />;
return <SettingsScreen view={view} />;
}

View File

@@ -1,14 +1,16 @@
import { useLocalSearchParams } from "expo-router";
import { useMemo } from "react";
import { HostRouteBootstrapBoundary } from "@/components/host-route-bootstrap-boundary";
import SettingsScreen from "@/screens/settings-screen";
export default function SettingsHostRoute() {
const params = useLocalSearchParams<{ serverId?: string }>();
const serverId = typeof params.serverId === "string" ? params.serverId.trim() : "";
const view = useMemo(() => ({ kind: "host" as const, serverId }), [serverId]);
return (
<HostRouteBootstrapBoundary>
<SettingsScreen view={{ kind: "host", serverId }} />
<SettingsScreen view={view} />
</HostRouteBootstrapBoundary>
);
}

View File

@@ -3,6 +3,8 @@ import { useIsCompactFormFactor } from "@/constants/layout";
import SettingsScreen from "@/screens/settings-screen";
import { buildSettingsSectionRoute } from "@/utils/host-routes";
const ROOT_VIEW = { kind: "root" as const };
export default function SettingsIndexRoute() {
const isCompactLayout = useIsCompactFormFactor();
@@ -10,5 +12,5 @@ export default function SettingsIndexRoute() {
return <Redirect href={buildSettingsSectionRoute("general")} />;
}
return <SettingsScreen view={{ kind: "root" }} />;
return <SettingsScreen view={ROOT_VIEW} />;
}

View File

@@ -24,6 +24,7 @@ import { isWeb } from "@/constants/platform";
type EscHandler = () => void;
const escStack: EscHandler[] = [];
let escListenerAttached = false;
const ABSOLUTE_FILL_STYLE = { ...StyleSheet.absoluteFillObject };
function handleEscKeyDown(event: KeyboardEvent) {
if (event.key !== "Escape") return;
@@ -191,6 +192,10 @@ export function AdaptiveModalSheet({
const isMobile = useIsCompactFormFactor();
const titleColor = theme.colors.foreground;
const resolvedSnapPoints = useMemo(() => snapPoints ?? ["65%", "90%"], [snapPoints]);
const handleIndicatorStyle = useMemo(
() => ({ backgroundColor: theme.colors.surface2 }),
[theme.colors.surface2],
);
const { sheetRef, handleSheetChange } = useIsolatedBottomSheetVisibility({
visible,
isEnabled: isMobile,
@@ -204,6 +209,12 @@ export function AdaptiveModalSheet({
[],
);
const titleStyle = useMemo(() => [styles.title, { color: titleColor }], [titleColor]);
const desktopCardStyle = useMemo(
() => [styles.desktopCard, desktopMaxWidth != null && { maxWidth: desktopMaxWidth }],
[desktopMaxWidth],
);
useEffect(() => {
if (!isWeb || isMobile || !visible) return;
return pushEscHandler(onClose);
@@ -220,14 +231,14 @@ export function AdaptiveModalSheet({
backdropComponent={renderBackdrop}
enablePanDownToClose
backgroundComponent={SheetBackground}
handleIndicatorStyle={{ backgroundColor: theme.colors.surface2 }}
handleIndicatorStyle={handleIndicatorStyle}
keyboardBehavior="extend"
keyboardBlurBehavior="restore"
accessible={false}
>
<View style={styles.bottomSheetHeader} testID={testID}>
<View style={styles.headerTitleGroup}>
<Text key={titleColor} style={[styles.title, { color: titleColor }]} numberOfLines={1}>
<Text key={titleColor} style={titleStyle} numberOfLines={1}>
{title}
</Text>
{subtitle}
@@ -256,7 +267,7 @@ export function AdaptiveModalSheet({
<>
<View style={styles.header}>
<View style={styles.headerTitleGroup}>
<Text key={titleColor} style={[styles.title, { color: titleColor }]} numberOfLines={1}>
<Text key={titleColor} style={titleStyle} numberOfLines={1}>
{title}
</Text>
{subtitle}
@@ -283,12 +294,8 @@ export function AdaptiveModalSheet({
const desktopContent = (
<View style={styles.desktopOverlay} testID={testID}>
<Pressable
accessibilityLabel="Dismiss"
style={{ ...StyleSheet.absoluteFillObject }}
onPress={onClose}
/>
<View style={[styles.desktopCard, desktopMaxWidth != null && { maxWidth: desktopMaxWidth }]}>
<Pressable accessibilityLabel="Dismiss" style={ABSOLUTE_FILL_STYLE} onPress={onClose} />
<View style={desktopCardStyle}>
{onFilesDropped ? (
<FileDropZone onFilesDropped={onFilesDropped}>{cardInner}</FileDropZone>
) : (

View File

@@ -10,6 +10,8 @@ import { DaemonConnectionTestError, connectToDaemon } from "@/utils/test-daemon-
import { AdaptiveModalSheet, AdaptiveTextInput } from "./adaptive-modal-sheet";
import { Button } from "@/components/ui/button";
const FLEX_ONE_STYLE = { flex: 1 } as const;
const styles = StyleSheet.create((theme) => ({
field: {
gap: theme.spacing[2],
@@ -274,11 +276,16 @@ export function AddHostModal({ visible, onClose, onCancel, onSaved }: AddHostMod
</View>
<View style={styles.actions}>
<Button style={{ flex: 1 }} variant="secondary" onPress={handleCancel} disabled={isSaving}>
<Button
style={FLEX_ONE_STYLE}
variant="secondary"
onPress={handleCancel}
disabled={isSaving}
>
Cancel
</Button>
<Button
style={{ flex: 1 }}
style={FLEX_ONE_STYLE}
variant="default"
onPress={handleSavePress}
disabled={isSaving}

View File

@@ -593,6 +593,7 @@ const AgentStreamViewComponent = forwardRef<AgentStreamViewHandle, AgentStreamVi
};
}, [baseRenderModel, getGapBetween, pendingPermissionsNode, workingIndicatorNode]);
const emptyStateStyle = useMemo(() => [stylesheet.emptyState, stylesheet.contentWrapper], []);
const listEmptyComponent = useMemo(() => {
if (
renderModel.boundary.hasVirtualizedHistory ||
@@ -605,11 +606,11 @@ const AgentStreamViewComponent = forwardRef<AgentStreamViewHandle, AgentStreamVi
}
return (
<View style={[stylesheet.emptyState, stylesheet.contentWrapper]}>
<View style={emptyStateStyle}>
<Text style={stylesheet.emptyStateText}>Start chatting with this agent...</Text>
</View>
);
}, [renderModel]);
}, [renderModel, emptyStateStyle]);
const historyItems = renderModel.history;
const liveHeadItems = renderModel.segments.liveHead;
@@ -647,34 +648,30 @@ const AgentStreamViewComponent = forwardRef<AgentStreamViewHandle, AgentStreamVi
renderStreamItem(item, index, items, index === 0 ? lastHistoryItem : null),
[lastHistoryItem, renderStreamItem],
);
const liveAuxiliaryHeaderStyle = useMemo(
() => [
stylesheet.listHeaderContent,
boundary.hasLiveHead
? streamRenderStrategy.getFlatListInverted()
? { paddingBottom: looseGap }
: { paddingTop: looseGap }
: null,
],
[boundary.hasLiveHead, streamRenderStrategy, looseGap],
);
const renderLiveAuxiliary = useCallback<StreamSegmentRenderers["renderLiveAuxiliary"]>(() => {
if (!auxiliary.pendingPermissions && !auxiliary.workingIndicator) {
return null;
}
return (
<View style={stylesheet.contentWrapper}>
<View
style={[
stylesheet.listHeaderContent,
boundary.hasLiveHead
? streamRenderStrategy.getFlatListInverted()
? { paddingBottom: looseGap }
: { paddingTop: looseGap }
: null,
]}
>
<View style={liveAuxiliaryHeaderStyle}>
{auxiliary.pendingPermissions}
{auxiliary.workingIndicator}
</View>
</View>
);
}, [
auxiliary.pendingPermissions,
auxiliary.workingIndicator,
boundary.hasLiveHead,
looseGap,
streamRenderStrategy,
]);
}, [auxiliary.pendingPermissions, auxiliary.workingIndicator, liveAuxiliaryHeaderStyle]);
const renderers = useMemo<StreamSegmentRenderers>(
() => ({
@@ -788,12 +785,19 @@ function WorkingIndicator() {
};
});
const dotOneCombinedStyle = useMemo(() => [stylesheet.workingDot, dotOneStyle], [dotOneStyle]);
const dotTwoCombinedStyle = useMemo(() => [stylesheet.workingDot, dotTwoStyle], [dotTwoStyle]);
const dotThreeCombinedStyle = useMemo(
() => [stylesheet.workingDot, dotThreeStyle],
[dotThreeStyle],
);
return (
<View style={stylesheet.workingIndicatorBubble}>
<View style={stylesheet.workingDotsRow}>
<Animated.View style={[stylesheet.workingDot, dotOneStyle]} />
<Animated.View style={[stylesheet.workingDot, dotTwoStyle]} />
<Animated.View style={[stylesheet.workingDot, dotThreeStyle]} />
<Animated.View style={dotOneCombinedStyle} />
<Animated.View style={dotTwoCombinedStyle} />
<Animated.View style={dotThreeCombinedStyle} />
</View>
</View>
);
@@ -880,6 +884,10 @@ function PermissionActionButton({
],
[theme.colors.surface2, theme.colors.surface1, theme.colors.borderAccent, isDanger],
);
const optionTextStyle = useMemo(
() => [permissionStyles.optionText, { color: textColor }],
[textColor],
);
return (
<Pressable testID={testID} style={pressableStyle} onPress={handlePress} disabled={isResponding}>
{isRespondingAction ? (
@@ -887,7 +895,7 @@ function PermissionActionButton({
) : (
<View style={permissionStyles.optionContent}>
<Icon size={14} color={iconColor} />
<Text style={[permissionStyles.optionText, { color: textColor }]}>{action.label}</Text>
<Text style={optionTextStyle}>{action.label}</Text>
</View>
)}
</Pressable>
@@ -1018,21 +1026,43 @@ function PermissionRequestCard({
);
}
const questionTextStyle = useMemo(
() => [permissionStyles.question, { color: theme.colors.foregroundMuted }],
[theme.colors.foregroundMuted],
);
const optionsContainerStyle = useMemo(
() => [
permissionStyles.optionsContainer,
!isMobile && permissionStyles.optionsContainerDesktop,
],
[isMobile],
);
const cardContainerStyle = useMemo(
() => [
permissionStyles.container,
{
backgroundColor: theme.colors.surface1,
borderColor: theme.colors.border,
},
],
[theme.colors.surface1, theme.colors.border],
);
const cardTitleStyle = useMemo(
() => [permissionStyles.title, { color: theme.colors.foreground }],
[theme.colors.foreground],
);
const cardDescriptionStyle = useMemo(
() => [permissionStyles.description, { color: theme.colors.foregroundMuted }],
[theme.colors.foregroundMuted],
);
const footer = (
<>
<Text
testID="permission-request-question"
style={[permissionStyles.question, { color: theme.colors.foregroundMuted }]}
>
<Text testID="permission-request-question" style={questionTextStyle}>
How would you like to proceed?
</Text>
<View
style={[
permissionStyles.optionsContainer,
!isMobile && permissionStyles.optionsContainerDesktop,
]}
>
<View style={optionsContainerStyle}>
{resolvedActions.map((action) => {
const isDanger = action.variant === "danger" || action.behavior === "deny";
const isPrimary = action.variant === "primary";
@@ -1080,22 +1110,10 @@ function PermissionRequestCard({
}
return (
<View
style={[
permissionStyles.container,
{
backgroundColor: theme.colors.surface1,
borderColor: theme.colors.border,
},
]}
>
<Text style={[permissionStyles.title, { color: theme.colors.foreground }]}>{title}</Text>
<View style={cardContainerStyle}>
<Text style={cardTitleStyle}>{title}</Text>
{description ? (
<Text style={[permissionStyles.description, { color: theme.colors.foregroundMuted }]}>
{description}
</Text>
) : null}
{description ? <Text style={cardDescriptionStyle}>{description}</Text> : null}
{planMarkdown ? (
<PlanCard title="Proposed plan" text={planMarkdown} disableOuterSpacing />

View File

@@ -49,6 +49,7 @@ export function AttachmentLightbox({ metadata, onClose }: AttachmentLightboxProp
const handleImageError = useCallback(() => setErrored(true), []);
const noopPress = useCallback(() => {}, []);
const imageSource = useMemo(() => ({ uri: url ?? "" }), [url]);
if (!metadata) {
return null;
@@ -74,7 +75,7 @@ export function AttachmentLightbox({ metadata, onClose }: AttachmentLightboxProp
<Pressable onPress={noopPress} style={styles.imagePressable}>
<ExpoImage
testID="attachment-lightbox-image"
source={{ uri: url }}
source={imageSource}
contentFit="contain"
onError={handleImageError}
style={imageFillStyle}

View File

@@ -113,10 +113,7 @@ function QueuedMessageRow({ item, onEdit, onSendNow }: QueuedMessageRowProps) {
<Pressable onPress={handleEdit} style={styles.queueActionButton}>
<Pencil size={theme.iconSize.sm} color={theme.colors.foreground} />
</Pressable>
<Pressable
onPress={handleSendNow}
style={QUEUE_SEND_BUTTON_STYLE}
>
<Pressable onPress={handleSendNow} style={QUEUE_SEND_BUTTON_STYLE}>
<ArrowUp size={theme.iconSize.sm} color="white" />
</Pressable>
</View>

View File

@@ -17,6 +17,26 @@ import { isNative } from "@/constants/platform";
* On macOS, Electron handles edge resize natively.
*/
const DRAG_OVERLAY_STYLE: React.CSSProperties = {
top: 0,
left: 0,
display: "block",
position: "absolute",
width: "100%",
height: "100%",
// @ts-expect-error — WebkitAppRegion is not in CSSProperties
WebkitAppRegion: "drag",
};
const TOP_RESIZER_STYLE: React.CSSProperties = {
position: "absolute",
top: 0,
width: "100%",
height: 4,
// @ts-expect-error — WebkitAppRegion is not in CSSProperties
WebkitAppRegion: "no-drag",
};
/**
* Static drag overlay and top-edge resizer. Returns null on non-Electron.
* Place as FIRST child of any positioned container that should be draggable.
@@ -29,29 +49,9 @@ export function TitlebarDragRegion() {
return (
<>
{/* Drag overlay — VS Code .titlebar-drag-region (titlebarpart.css:57-64) */}
<div
style={{
top: 0,
left: 0,
display: "block",
position: "absolute",
width: "100%",
height: "100%",
// @ts-expect-error — WebkitAppRegion is not in CSSProperties
WebkitAppRegion: "drag",
}}
/>
<div style={DRAG_OVERLAY_STYLE} />
{/* Top-edge resizer — VS Code .resizer (titlebarpart.css:249-256) */}
<div
style={{
position: "absolute",
top: 0,
width: "100%",
height: 4,
// @ts-expect-error — WebkitAppRegion is not in CSSProperties
WebkitAppRegion: "no-drag",
}}
/>
<div style={TOP_RESIZER_STYLE} />
</>
);
}

View File

@@ -17,6 +17,63 @@ interface DiffViewerProps {
fillAvailableHeight?: boolean;
}
function DiffLineRow({ line }: { line: DiffLine }) {
const lineContainerStyle = React.useMemo(
() => [
styles.line,
line.type === "header" && styles.headerLine,
line.type === "add" && styles.addLine,
line.type === "remove" && styles.removeLine,
line.type === "context" && styles.contextLine,
],
[line.type],
);
const plainLineTextStyle = React.useMemo(
() => [
styles.lineText,
line.type === "header" && styles.headerText,
line.type === "add" && styles.addText,
line.type === "remove" && styles.removeText,
line.type === "context" && styles.contextText,
],
[line.type],
);
return (
<View style={lineContainerStyle}>
{line.segments ? (
<Text style={styles.lineText}>
<Text style={line.type === "add" ? styles.addText : styles.removeText}>
{line.content[0]}
</Text>
{line.segments.map((segment, segIdx) => (
<DiffSegment key={segIdx} segment={segment} lineType={line.type} />
))}
</Text>
) : (
<Text style={plainLineTextStyle}>{line.content}</Text>
)}
</View>
);
}
function DiffSegment({
segment,
lineType,
}: {
segment: NonNullable<DiffLine["segments"]>[number];
lineType: DiffLine["type"];
}) {
const segmentStyle = React.useMemo(
() => [
lineType === "add" ? styles.addText : styles.removeText,
segment.changed && (lineType === "add" ? styles.addHighlight : styles.removeHighlight),
],
[lineType, segment.changed],
);
return <Text style={segmentStyle}>{segment.text}</Text>;
}
export function DiffViewer({
diffLines,
maxHeight,
@@ -70,48 +127,7 @@ export function DiffViewer({
>
<View style={linesContainerStyle}>
{diffLines.map((line, index) => (
<View
key={`${line.type}-${index}`}
style={[
styles.line,
line.type === "header" && styles.headerLine,
line.type === "add" && styles.addLine,
line.type === "remove" && styles.removeLine,
line.type === "context" && styles.contextLine,
]}
>
{line.segments ? (
<Text style={styles.lineText}>
<Text style={line.type === "add" ? styles.addText : styles.removeText}>
{line.content[0]}
</Text>
{line.segments.map((segment, segIdx) => (
<Text
key={segIdx}
style={[
line.type === "add" ? styles.addText : styles.removeText,
segment.changed &&
(line.type === "add" ? styles.addHighlight : styles.removeHighlight),
]}
>
{segment.text}
</Text>
))}
</Text>
) : (
<Text
style={[
styles.lineText,
line.type === "header" && styles.headerText,
line.type === "add" && styles.addText,
line.type === "remove" && styles.removeText,
line.type === "context" && styles.contextText,
]}
>
{line.content}
</Text>
)}
</View>
<DiffLineRow key={`${line.type}-${index}`} line={line} />
))}
</View>
</ScrollView>

View File

@@ -1,4 +1,4 @@
import { useCallback, useRef, useState, type ReactElement } from "react";
import { useCallback, useMemo, useRef, useState, type ReactElement } from "react";
import { ScrollView, View } from "react-native";
import {
DndContext,
@@ -29,6 +29,8 @@ const restrictToVerticalAxis: Modifier = ({ transform }) => ({
x: 0,
});
const DND_MODIFIERS = [restrictToVerticalAxis];
interface SortableItemProps<T> {
id: string;
item: T;
@@ -183,12 +185,18 @@ export function DraggableList<T>({
[clearDragState, items, keyExtractor, onDragEnd],
);
const ids = items.map((item, index) => keyExtractor(item, index));
const wrapperStyle = [
{ position: "relative" as const },
scrollEnabled ? { flex: 1, minHeight: 0 } : null,
containerStyle,
];
const ids = useMemo(
() => items.map((item, index) => keyExtractor(item, index)),
[items, keyExtractor],
);
const wrapperStyle = useMemo(
() => [
{ position: "relative" as const },
scrollEnabled ? { flex: 1, minHeight: 0 } : null,
containerStyle,
],
[scrollEnabled, containerStyle],
);
return (
<View style={wrapperStyle}>
@@ -209,7 +217,7 @@ export function DraggableList<T>({
<DndContext
sensors={sensors}
collisionDetection={closestCenter}
modifiers={[restrictToVerticalAxis]}
modifiers={DND_MODIFIERS}
onDragStart={handleDragStart}
onDragCancel={clearDragState}
onDragEnd={handleDragEnd}
@@ -240,7 +248,7 @@ export function DraggableList<T>({
<DndContext
sensors={sensors}
collisionDetection={closestCenter}
modifiers={[restrictToVerticalAxis]}
modifiers={DND_MODIFIERS}
onDragStart={handleDragStart}
onDragCancel={clearDragState}
onDragEnd={handleDragEnd}

View File

@@ -137,6 +137,11 @@ function TreeRowItem({
onDownloadEntry(entry);
}, [onDownloadEntry, entry]);
const chevronStyle = useMemo(
() => [styles.chevron, isExpanded && styles.chevronExpanded],
[isExpanded],
);
return (
<Pressable onPress={handlePress} style={pressableStyle}>
{depth > 0 &&
@@ -157,7 +162,7 @@ function TreeRowItem({
loading ? (
<ActivityIndicator size="small" />
) : (
<View style={[styles.chevron, isExpanded && styles.chevronExpanded]}>
<View style={chevronStyle}>
<ChevronRight size={16} color={theme.colors.foregroundMuted} />
</View>
)

View File

@@ -183,6 +183,11 @@ function FilePreviewBody({
return lineNumberGutterWidth(highlightedLines.length);
}, [highlightedLines]);
const imageSource = useMemo(
() => (imagePreviewUri ? { uri: imagePreviewUri } : null),
[imagePreviewUri],
);
if (isLoading && !preview) {
return (
<View style={styles.centerState}>
@@ -292,9 +297,7 @@ function FilePreviewBody({
showsVerticalScrollIndicator={!showDesktopWebScrollbar}
>
<RNImage
source={{
uri: imagePreviewUri,
}}
source={imageSource ?? undefined}
style={styles.previewImage}
resizeMode="contain"
/>

View File

@@ -1,4 +1,5 @@
import { SquareTerminal } from "lucide-react-native";
import { useMemo } from "react";
import { Image, type ImageSourcePropType } from "react-native";
import {
isKnownEditorTargetId,
@@ -29,15 +30,10 @@ export function hasBundledEditorAppIcon(editorId: EditorTargetId): editorId is K
}
export function EditorAppIcon({ editorId, size = 16, color }: EditorAppIconProps) {
const imageStyle = useMemo(() => ({ width: size, height: size }), [size]);
if (!hasBundledEditorAppIcon(editorId)) {
return <SquareTerminal size={size} color={color} />;
}
return (
<Image
source={EDITOR_APP_IMAGES[editorId]}
style={{ width: size, height: size }}
resizeMode="contain"
/>
);
return <Image source={EDITOR_APP_IMAGES[editorId]} style={imageStyle} resizeMode="contain" />;
}

View File

@@ -525,20 +525,26 @@ function MobileSidebar({
const overlayPointerEvents = isWeb ? (isOpen ? "auto" : "none") : "box-none";
const backdropStyle = useMemo(
() => [staticStyles.backdrop, backdropAnimatedStyle],
[backdropAnimatedStyle],
);
const mobileSidebarStyle = useMemo(
() => [
staticStyles.mobileSidebar,
mobileSidebarInsetStyle,
sidebarAnimatedStyle,
{ backgroundColor: theme.colors.surfaceSidebar },
],
[mobileSidebarInsetStyle, sidebarAnimatedStyle, theme.colors.surfaceSidebar],
);
return (
<View style={StyleSheet.absoluteFillObject} pointerEvents={overlayPointerEvents}>
<Animated.View style={[staticStyles.backdrop, backdropAnimatedStyle]} />
<Animated.View style={backdropStyle} />
<GestureDetector gesture={closeGesture} touchAction="pan-y">
<Animated.View
style={[
staticStyles.mobileSidebar,
mobileSidebarInsetStyle,
sidebarAnimatedStyle,
{ backgroundColor: theme.colors.surfaceSidebar },
]}
pointerEvents="auto"
>
<Animated.View style={mobileSidebarStyle} pointerEvents="auto">
<View style={styles.sidebarContent} pointerEvents="auto">
<SidebarHeaderRow
icon={MessagesSquare}
@@ -713,18 +719,27 @@ function DesktopSidebar({
width: resizeWidth.value,
}));
const paddingTopSpacerStyle = useMemo(() => ({ height: padding.top }), [padding.top]);
const desktopSidebarStyle = useMemo(
() => [staticStyles.desktopSidebar, resizeAnimatedStyle, { paddingTop: insetsTop }],
[resizeAnimatedStyle, insetsTop],
);
const desktopSidebarBorderStyle = useMemo(() => [styles.desktopSidebarBorder, { flex: 1 }], []);
const resizeHandleStyle = useMemo(
() => [styles.resizeHandle, isWeb && ({ cursor: "col-resize" } as any)],
[],
);
if (!isOpen) {
return null;
}
return (
<Animated.View
style={[staticStyles.desktopSidebar, resizeAnimatedStyle, { paddingTop: insetsTop }]}
>
<View style={[styles.desktopSidebarBorder, { flex: 1 }]}>
<Animated.View style={desktopSidebarStyle}>
<View style={desktopSidebarBorderStyle}>
<View style={styles.sidebarDragArea}>
<TitlebarDragRegion />
{padding.top > 0 ? <View style={{ height: padding.top }} /> : null}
{padding.top > 0 ? <View style={paddingTopSpacerStyle} /> : null}
<SidebarHeaderRow
icon={MessagesSquare}
label="Sessions"
@@ -823,7 +838,7 @@ function DesktopSidebar({
{/* Resize handle - absolutely positioned over right border */}
<GestureDetector gesture={resizeGesture}>
<View style={[styles.resizeHandle, isWeb && ({ cursor: "col-resize" } as any)]} />
<View style={resizeHandleStyle} />
</GestureDetector>
</View>
</Animated.View>

View File

@@ -379,17 +379,36 @@ export const UserMessage = memo(function UserMessage({
const handleHoverOut = useCallback(() => setMessageHovered(false), []);
const getMessageContent = useCallback(() => message, [message]);
const containerStyle = useMemo(
() => [
userMessageStylesheet.container,
!resolvedDisableOuterSpacing && [
isFirstInGroup && { marginTop: theme.spacing[4] },
isLastInGroup && { marginBottom: theme.spacing[4] },
!isFirstInGroup || !isLastInGroup ? { marginBottom: theme.spacing[1] } : undefined,
],
],
[resolvedDisableOuterSpacing, isFirstInGroup, isLastInGroup, theme.spacing],
);
const imagePreviewContainerStyle = useMemo(
() => [
userMessageStylesheet.imagePreviewContainer,
hasText ? userMessageStylesheet.imagePreviewSpacing : undefined,
],
[hasText],
);
const copyButtonStyle = useMemo(
() => [
userMessageStylesheet.copyButton,
showCopyButton
? userMessageStylesheet.copyButtonVisible
: userMessageStylesheet.copyButtonHidden,
],
[showCopyButton],
);
return (
<View
style={[
userMessageStylesheet.container,
!resolvedDisableOuterSpacing && [
isFirstInGroup && { marginTop: theme.spacing[4] },
isLastInGroup && { marginBottom: theme.spacing[4] },
!isFirstInGroup || !isLastInGroup ? { marginBottom: theme.spacing[1] } : undefined,
],
]}
>
<View style={containerStyle}>
<Pressable
style={userMessageStylesheet.content}
onHoverIn={handleHoverIn}
@@ -397,12 +416,7 @@ export const UserMessage = memo(function UserMessage({
>
<View style={userMessageStylesheet.bubble}>
{hasImages ? (
<View
style={[
userMessageStylesheet.imagePreviewContainer,
hasText ? userMessageStylesheet.imagePreviewSpacing : undefined,
]}
>
<View style={imagePreviewContainerStyle}>
{images.map((image, index) => (
<View key={`${image.id}-${index}`} style={userMessageStylesheet.imagePill}>
<UserMessageAttachmentThumbnail image={image} />
@@ -419,12 +433,7 @@ export const UserMessage = memo(function UserMessage({
{hasText ? (
<TurnCopyButton
getContent={getMessageContent}
containerStyle={[
userMessageStylesheet.copyButton,
showCopyButton
? userMessageStylesheet.copyButtonVisible
: userMessageStylesheet.copyButtonHidden,
]}
containerStyle={copyButtonStyle}
accessibilityLabel="Copy message"
onHoverChange={setCopyButtonHovered}
/>
@@ -569,9 +578,13 @@ const AssistantMarkdownResolvedImage = memo(function AssistantMarkdownResolvedIm
],
[aspectRatio],
);
const frameStyle = useMemo<StyleProp<ViewStyle>>(
() => [assistantMessageStylesheet.imageFrame, containerStyle],
[containerStyle],
);
return (
<View style={[assistantMessageStylesheet.imageFrame, containerStyle]}>
<View style={frameStyle}>
<View style={surfaceStyle}>
<Image
source={{ uri }}
@@ -675,6 +688,15 @@ function AssistantMarkdownImage({
const directUri = resolution?.kind === "direct" && !dataImage ? resolution.uri : null;
const resolvedUri = directUri ?? dataImageAssetUri ?? fileAssetUri ?? null;
const stateFrameStyle = useMemo<StyleProp<ViewStyle>>(
() => [
assistantMessageStylesheet.imageFrame,
containerStyle,
assistantMessageStylesheet.imageState,
],
[containerStyle],
);
if (resolvedUri) {
return (
<AssistantMarkdownResolvedImage
@@ -690,26 +712,14 @@ function AssistantMarkdownImage({
if (query.isLoading || dataImageQuery.isLoading) {
return (
<View
style={[
assistantMessageStylesheet.imageFrame,
containerStyle,
assistantMessageStylesheet.imageState,
]}
>
<View style={stateFrameStyle}>
<ActivityIndicator size="small" />
</View>
);
}
return (
<View
style={[
assistantMessageStylesheet.imageFrame,
containerStyle,
assistantMessageStylesheet.imageState,
]}
>
<View style={stateFrameStyle}>
<Text style={assistantMessageStylesheet.imageErrorText}>
{query.error instanceof Error
? query.error.message
@@ -727,13 +737,18 @@ interface InlinePathChipProps {
onPress: (target: InlinePathTarget) => void;
}
const INLINE_PATH_CHIP_STYLE = [
assistantMessageStylesheet.pathChip,
assistantMessageStylesheet.pathChipText,
];
function InlinePathChip({ content, parsed, onPress }: InlinePathChipProps) {
const handlePress = useCallback(() => onPress(parsed), [onPress, parsed]);
return (
<Text
onPress={handlePress}
selectable={isWeb ? undefined : false}
style={[assistantMessageStylesheet.pathChip, assistantMessageStylesheet.pathChipText]}
style={INLINE_PATH_CHIP_STYLE}
>
{content}
</Text>
@@ -755,6 +770,10 @@ function MarkdownLink({
const handlePress = useCallback(() => onPress(href), [onPress, href]);
const handleHoverIn = useCallback(() => setHovered(true), []);
const handleHoverOut = useCallback(() => setHovered(false), []);
const hoveredTextStyle = useMemo(
() => [style, hovered && { textDecorationLine: "underline" }],
[style, hovered],
);
if (isNative) {
return (
<Text accessibilityRole="link" onPress={handlePress} style={style}>
@@ -770,7 +789,7 @@ function MarkdownLink({
onHoverIn={handleHoverIn}
onHoverOut={handleHoverOut}
>
<Text style={[style, hovered && { textDecorationLine: "underline" }]}>{children}</Text>
<Text style={hoveredTextStyle}>{children}</Text>
</Pressable>
);
}
@@ -876,13 +895,17 @@ export const TurnCopyButton = memo(function TurnCopyButton({
const handleHoverIn = useCallback(() => onHoverChange?.(true), [onHoverChange]);
const handleHoverOut = useCallback(() => onHoverChange?.(false), [onHoverChange]);
const pressableStyle = useMemo(
() => [turnCopyButtonStylesheet.container, containerStyle],
[containerStyle],
);
return (
<Pressable
onPress={handleCopy}
onHoverIn={handleHoverIn}
onHoverOut={handleHoverOut}
style={[turnCopyButtonStylesheet.container, containerStyle]}
style={pressableStyle}
accessibilityRole="button"
accessibilityLabel={
copied ? (copiedAccessibilityLabel ?? "Copied") : (accessibilityLabel ?? "Copy turn")
@@ -1377,18 +1400,20 @@ export const AssistantMessage = memo(function AssistantMessage({
const blocks = useMemo(() => splitMarkdownBlocks(message), [message]);
const assistantContainerStyle = useMemo(
() => [
assistantMessageStylesheet.container,
(spacing === "compactTop" || spacing === "compactBoth") &&
assistantMessageStylesheet.containerCompactTop,
(spacing === "compactBottom" || spacing === "compactBoth") &&
assistantMessageStylesheet.containerCompactBottom,
!resolvedDisableOuterSpacing && assistantMessageStylesheet.containerSpacing,
],
[spacing, resolvedDisableOuterSpacing],
);
return (
<View
testID="assistant-message"
style={[
assistantMessageStylesheet.container,
(spacing === "compactTop" || spacing === "compactBoth") &&
assistantMessageStylesheet.containerCompactTop,
(spacing === "compactBottom" || spacing === "compactBoth") &&
assistantMessageStylesheet.containerCompactBottom,
!resolvedDisableOuterSpacing && assistantMessageStylesheet.containerSpacing,
]}
>
<View testID="assistant-message" style={assistantContainerStyle}>
{blocks.map((block, index) => (
<View
key={index}
@@ -1448,15 +1473,16 @@ export const SpeakMessage = memo(function SpeakMessage({
}: SpeakMessageProps) {
const { theme } = useUnistyles();
const resolvedDisableOuterSpacing = useDisableOuterSpacing(disableOuterSpacing);
const containerStyle = useMemo(
() => [
speakMessageStylesheet.container,
!resolvedDisableOuterSpacing && speakMessageStylesheet.containerSpacing,
],
[resolvedDisableOuterSpacing],
);
return (
<View
testID="speak-message"
style={[
speakMessageStylesheet.container,
!resolvedDisableOuterSpacing && speakMessageStylesheet.containerSpacing,
]}
>
<View testID="speak-message" style={containerStyle}>
<View style={speakMessageStylesheet.header}>
<MicVocal size={14} color={theme.colors.foregroundMuted} />
<Text style={speakMessageStylesheet.headerLabel}>Spoke</Text>
@@ -1603,27 +1629,29 @@ export const ActivityLog = memo(function ActivityLog({
type === "artifact" && artifactType && title ? `${artifactType}: ${title}` : message;
const isInteractive = type === "artifact" || metadata;
const pressableStyle = useMemo(
() => [
activityLogStylesheet.pressable,
!resolvedDisableOuterSpacing && activityLogStylesheet.pressableSpacing,
config.bg,
isInteractive && activityLogStylesheet.pressableActive,
],
[resolvedDisableOuterSpacing, config.bg, isInteractive],
);
const messageTextStyle = useMemo(
() => [activityLogStylesheet.messageText, { color: config.color }],
[config.color],
);
return (
<Pressable
onPress={handlePress}
disabled={!isInteractive}
style={[
activityLogStylesheet.pressable,
!resolvedDisableOuterSpacing && activityLogStylesheet.pressableSpacing,
config.bg,
isInteractive && activityLogStylesheet.pressableActive,
]}
>
<Pressable onPress={handlePress} disabled={!isInteractive} style={pressableStyle}>
<View style={activityLogStylesheet.content}>
<View style={activityLogStylesheet.row}>
<View style={activityLogStylesheet.iconContainer}>
<IconComponent size={16} color={config.color} />
</View>
<View style={activityLogStylesheet.textContainer}>
<Text style={[activityLogStylesheet.messageText, { color: config.color }]}>
{displayMessage}
</Text>
<Text style={messageTextStyle}>{displayMessage}</Text>
{metadata && (
<View style={activityLogStylesheet.detailsRow}>
<Text style={activityLogStylesheet.detailsText}>Details</Text>

View File

@@ -11,6 +11,8 @@ import { ConnectionOfferSchema } from "@server/shared/connection-offer";
import { AdaptiveModalSheet, AdaptiveTextInput } from "./adaptive-modal-sheet";
import { Button } from "@/components/ui/button";
const FLEX_ONE_STYLE = { flex: 1 } as const;
const styles = StyleSheet.create((theme) => ({
helper: {
color: theme.colors.foregroundMuted,
@@ -189,7 +191,7 @@ export function PairLinkModal({ visible, onClose, onCancel, onSaved }: PairLinkM
<View style={styles.actions}>
<Button
style={{ flex: 1 }}
style={FLEX_ONE_STYLE}
variant="secondary"
onPress={handleCancel}
disabled={isSaving}
@@ -200,7 +202,7 @@ export function PairLinkModal({ visible, onClose, onCancel, onSaved }: PairLinkM
Cancel
</Button>
<Button
style={{ flex: 1 }}
style={FLEX_ONE_STYLE}
variant="default"
onPress={handleSavePress}
disabled={isSaving}

View File

@@ -1,4 +1,4 @@
import { useCallback, useState } from "react";
import { useCallback, useMemo, useState } from "react";
import { Pressable, ScrollView, Text, View } from "react-native";
import { StyleSheet, useUnistyles } from "react-native-unistyles";
import {
@@ -66,6 +66,7 @@ export function PrPane({ data }: { data: PrPaneData }) {
const stateColor = getStateColor(data.state, theme);
const StateIcon = getStateIcon(data.state);
const stateLabel = getStateLabel(data.state);
const stateLabelStyle = useMemo(() => [styles.stateLabel, { color: stateColor }], [stateColor]);
return (
<View style={styles.root}>
@@ -74,7 +75,7 @@ export function PrPane({ data }: { data: PrPaneData }) {
<>
<View style={styles.stateLine}>
<StateIcon size={14} color={stateColor} />
<Text style={[styles.stateLabel, { color: stateColor }]}>{stateLabel}</Text>
<Text style={stateLabelStyle}>{stateLabel}</Text>
</View>
<Text style={styles.title} numberOfLines={3}>
{data.title}
@@ -197,11 +198,12 @@ function SummaryPill({
color: string;
icon: React.ReactNode;
}) {
const textStyle = useMemo(() => [styles.summaryPillText, { color }], [color]);
if (count === 0) return null;
return (
<View style={styles.summaryPill}>
{icon}
<Text style={[styles.summaryPillText, { color }]}>{count}</Text>
<Text style={textStyle}>{count}</Text>
</View>
);
}
@@ -239,9 +241,13 @@ function ActivityRow({ item }: { item: PrPaneActivity }) {
const handlePress = useCallback(() => {
void openExternalUrl(item.url);
}, [item.url]);
const avatarStyle = useMemo(
() => [styles.avatar, { backgroundColor: item.avatarColor }],
[item.avatarColor],
);
return (
<Pressable onPress={handlePress} style={activityPressableStyle}>
<View style={[styles.avatar, { backgroundColor: item.avatarColor }]}>
<View style={avatarStyle}>
<Text style={styles.avatarText}>{item.author.slice(0, 1).toUpperCase()}</Text>
</View>
<View style={styles.activityMain}>

View File

@@ -40,13 +40,17 @@ function PathRow({ path, active, onSelect }: PathRowProps) {
],
[active, theme.colors.surface1],
);
const rowTextStyle = useMemo(
() => [styles.rowText, { color: theme.colors.foreground }],
[theme.colors.foreground],
);
return (
<Pressable style={pressableStyle} onPress={handlePress}>
<View style={styles.rowContent}>
<View style={styles.iconSlot}>
<Folder size={16} strokeWidth={2.2} color={theme.colors.foregroundMuted} />
</View>
<Text style={[styles.rowText, { color: theme.colors.foreground }]} numberOfLines={1}>
<Text style={rowTextStyle} numberOfLines={1}>
{shortenPath(path)}
</Text>
</View>
@@ -192,6 +196,29 @@ export function ProjectPickerModal() {
return () => window.removeEventListener("keydown", handler, true);
}, [activeIndex, handleSelectPath, handleSubmitCustom, open, options, query, setOpen]);
const panelStyle = useMemo(
() => [
styles.panel,
{
borderColor: theme.colors.border,
backgroundColor: theme.colors.surface0,
},
],
[theme.colors.border, theme.colors.surface0],
);
const headerStyle = useMemo(
() => [styles.header, { borderBottomColor: theme.colors.border }],
[theme.colors.border],
);
const inputStyle = useMemo(
() => [styles.input, { color: theme.colors.foreground }],
[theme.colors.foreground],
);
const emptyTextStyle = useMemo(
() => [styles.emptyText, { color: theme.colors.foregroundMuted }],
[theme.colors.foregroundMuted],
);
if (!serverId) return null;
return (
@@ -199,23 +226,15 @@ export function ProjectPickerModal() {
<View style={styles.overlay}>
<Pressable style={styles.backdrop} onPress={handleClose} />
<View
style={[
styles.panel,
{
borderColor: theme.colors.border,
backgroundColor: theme.colors.surface0,
},
]}
>
<View style={[styles.header, { borderBottomColor: theme.colors.border }]}>
<View style={panelStyle}>
<View style={headerStyle}>
<TextInput
ref={inputRef}
value={query}
onChangeText={handleChangeQuery}
placeholder="Type a directory path..."
placeholderTextColor={theme.colors.foregroundMuted}
style={[styles.input, { color: theme.colors.foreground }]}
style={inputStyle}
autoCapitalize="none"
autoCorrect={false}
autoFocus
@@ -230,13 +249,9 @@ export function ProjectPickerModal() {
showsVerticalScrollIndicator={false}
>
{isSubmitting ? (
<Text style={[styles.emptyText, { color: theme.colors.foregroundMuted }]}>
Opening project...
</Text>
<Text style={emptyTextStyle}>Opening project...</Text>
) : options.length === 0 && !query.trim() ? (
<Text style={[styles.emptyText, { color: theme.colors.foregroundMuted }]}>
Start typing a path
</Text>
<Text style={emptyTextStyle}>Start typing a path</Text>
) : (
<>
{options.map((path, index) => (

View File

@@ -26,6 +26,23 @@ interface ProviderDiagnosticSheetProps {
serverId: string;
}
function ModelRow({ model, isFirst }: { model: AgentModelDefinition; isFirst: boolean }) {
const rowStyle = useMemo(
() => [sheetStyles.modelRow, !isFirst && sheetStyles.modelRowBorder],
[isFirst],
);
return (
<View style={rowStyle}>
<Text style={sheetStyles.modelLabel} numberOfLines={1}>
{model.label}
</Text>
<Text style={sheetStyles.modelId} numberOfLines={1} selectable>
{model.id}
</Text>
</View>
);
}
export function ProviderDiagnosticSheet({
provider,
visible,
@@ -148,14 +165,7 @@ export function ProviderDiagnosticSheet({
);
}
return filteredModels.map((model: AgentModelDefinition, index) => (
<View key={model.id} style={[sheetStyles.modelRow, index > 0 && sheetStyles.modelRowBorder]}>
<Text style={sheetStyles.modelLabel} numberOfLines={1}>
{model.label}
</Text>
<Text style={sheetStyles.modelId} numberOfLines={1} selectable>
{model.id}
</Text>
</View>
<ModelRow key={model.id} model={model} isFirst={index === 0} />
));
}
@@ -164,7 +174,7 @@ export function ProviderDiagnosticSheet({
title={providerLabel}
visible={visible}
onClose={onClose}
snapPoints={["50%", "85%"]}
snapPoints={DIAGNOSTIC_SHEET_SNAP_POINTS}
scrollable={false}
headerActions={
<Pressable
@@ -237,7 +247,7 @@ export function ProviderDiagnosticSheet({
autoCapitalize="none"
autoCorrect={false}
// @ts-expect-error - outlineStyle is web-only
style={[sheetStyles.searchInput, isWeb && { outlineStyle: "none" }]}
style={DIAGNOSTIC_SEARCH_INPUT_STYLE}
/>
</View>
) : null}
@@ -375,3 +385,6 @@ const sheetStyles = StyleSheet.create((theme) => ({
gap: theme.spacing[2],
},
}));
const DIAGNOSTIC_SHEET_SNAP_POINTS = ["50%", "85%"];
const DIAGNOSTIC_SEARCH_INPUT_STYLE = [sheetStyles.searchInput, isWeb && { outlineStyle: "none" }];

View File

@@ -1,4 +1,4 @@
import { useCallback, useRef, useState } from "react";
import { useCallback, useMemo, useRef, useState } from "react";
import { View } from "react-native";
import { StyleSheet, useUnistyles } from "react-native-unistyles";
@@ -107,34 +107,40 @@ export function ResizeHandle({
setActive(false);
}, []);
const handleStyle = useMemo(
() => [
styles.handle,
direction === "horizontal" ? styles.handleHorizontal : styles.handleVertical,
{ backgroundColor: theme.colors.border },
],
[direction, theme.colors.border],
);
const highlightStyle = useMemo(
() => [
styles.highlight,
direction === "horizontal" ? styles.highlightHorizontal : styles.highlightVertical,
{ backgroundColor: theme.colors.accent },
],
[direction, theme.colors.accent],
);
const hitAreaStyle = useMemo(
() => [
styles.hitArea,
direction === "horizontal" ? styles.hitAreaHorizontal : styles.hitAreaVertical,
{
cursor: direction === "horizontal" ? "col-resize" : "row-resize",
} as any,
],
[direction],
);
return (
<View
style={[
styles.handle,
direction === "horizontal" ? styles.handleHorizontal : styles.handleVertical,
{ backgroundColor: theme.colors.border },
]}
>
{highlighted && (
<View
pointerEvents="none"
style={[
styles.highlight,
direction === "horizontal" ? styles.highlightHorizontal : styles.highlightVertical,
{ backgroundColor: theme.colors.accent },
]}
/>
)}
<View style={handleStyle}>
{highlighted && <View pointerEvents="none" style={highlightStyle} />}
<View
role="separator"
aria-orientation={direction === "horizontal" ? "vertical" : "horizontal"}
style={[
styles.hitArea,
direction === "horizontal" ? styles.hitAreaHorizontal : styles.hitAreaVertical,
{
cursor: direction === "horizontal" ? "col-resize" : "row-resize",
} as any,
]}
style={hitAreaStyle}
onPointerDown={handlePointerDown}
onPointerEnter={handlePointerEnter}
onPointerLeave={handlePointerLeave}

View File

@@ -71,15 +71,19 @@ export function SyncedLoader({ size = 10, color }: { size?: number; color: strin
[animatedStyle, gridWidth, gridHeight],
);
return (
<View
style={{
const containerStyle = useMemo(
() =>
({
width: size,
height: size,
alignItems: "center",
justifyContent: "center",
}}
>
}) as const,
[size],
);
return (
<View style={containerStyle}>
<Animated.View style={gridStyle}>
{Array.from({ length: DOT_COUNT }).map((_, dotIndex) => {
const rowIndex = Math.floor(dotIndex / GRID_COLUMNS);

View File

@@ -6,6 +6,7 @@ import {
useMemo,
useRef,
useState,
type CSSProperties,
type MouseEvent as ReactMouseEvent,
type PointerEvent as ReactPointerEvent,
type Ref,
@@ -44,6 +45,33 @@ const SCROLLBAR_HANDLE_SCROLL_VISIBILITY_MS = 1_200;
const SCROLLBAR_HANDLE_SCROLL_ACTIVE_MS = 110;
const WEBKIT_SCROLLBAR_STYLE_ID = "terminal-emulator-webkit-scrollbar-style";
const HOST_DIV_STYLE: CSSProperties = {
flex: 1,
minHeight: 0,
minWidth: 0,
width: "100%",
height: "100%",
overflow: "hidden",
overscrollBehavior: "none",
paddingTop: 0,
paddingBottom: 0,
paddingLeft: 0,
paddingRight: 0,
};
const SCROLLBAR_CONTAINER_STYLE: CSSProperties = {
position: "absolute",
top: 0,
right: 0,
bottom: 0,
width: 12,
display: "flex",
alignItems: "center",
justifyContent: "flex-start",
zIndex: 10,
pointerEvents: "none",
};
interface ViewportMetrics {
offset: number;
viewportSize: number;
@@ -625,91 +653,80 @@ export default function TerminalEmulator({
setIsHandleHovered(false);
}, []);
const rootDivStyle = useMemo<CSSProperties>(
() => ({
position: "relative",
display: "flex",
width: "100%",
height: "100%",
minHeight: 0,
minWidth: 0,
backgroundColor: xtermTheme.background ?? "#0b0b0b",
overflow: "hidden",
overscrollBehavior: "none",
touchAction: "pan-y",
}),
[xtermTheme.background],
);
const handleContainerStyle = useMemo<CSSProperties>(
() => ({
position: "absolute",
top: 0,
right: -3,
width: SCROLLBAR_HANDLE_GRAB_WIDTH,
height: thumbRegionHeight,
transform: `translateY(${thumbRegionOffset}px)`,
cursor: isDraggingScrollbar ? "grabbing" : "grab",
touchAction: "none",
userSelect: "none",
transitionProperty: "transform",
transitionDuration: `${handleTravelDurationMs}ms`,
transitionTimingFunction: "linear",
pointerEvents: handleVisible ? "auto" : "none",
}),
[
thumbRegionHeight,
thumbRegionOffset,
isDraggingScrollbar,
handleTravelDurationMs,
handleVisible,
],
);
const handleInnerStyle = useMemo<CSSProperties>(
() => ({
marginTop: handleInsetTop,
height: scrollbarGeometry.handleSize,
width: handleWidth,
borderRadius: 999,
alignSelf: "center",
backgroundColor: "rgba(113, 113, 122, 1)",
opacity: handleOpacity,
transitionProperty: "opacity, width, background-color",
transitionDuration: `${SCROLLBAR_HANDLE_FADE_DURATION_MS}ms, ${SCROLLBAR_HANDLE_WIDTH_TRANSITION_DURATION_MS}ms, ${SCROLLBAR_HANDLE_FADE_DURATION_MS}ms`,
transitionTimingFunction: "ease-out, cubic-bezier(0.22, 0.75, 0.2, 1), ease-out",
}),
[handleInsetTop, scrollbarGeometry.handleSize, handleWidth, handleOpacity],
);
return (
<div
ref={rootRef}
data-testid={testId}
data-terminal-scrollbar-root="true"
style={{
position: "relative",
display: "flex",
width: "100%",
height: "100%",
minHeight: 0,
minWidth: 0,
backgroundColor: xtermTheme.background ?? "#0b0b0b",
overflow: "hidden",
overscrollBehavior: "none",
touchAction: "pan-y",
}}
style={rootDivStyle}
onPointerDown={handleRootPointerDown}
onContextMenu={handleRootContextMenu}
>
<div
ref={hostRef}
style={{
flex: 1,
minHeight: 0,
minWidth: 0,
width: "100%",
height: "100%",
overflow: "hidden",
overscrollBehavior: "none",
paddingTop: 0,
paddingBottom: 0,
paddingLeft: 0,
paddingRight: 0,
}}
/>
<div ref={hostRef} style={HOST_DIV_STYLE} />
{scrollbarGeometry.isVisible ? (
<div
style={{
position: "absolute",
top: 0,
right: 0,
bottom: 0,
width: 12,
display: "flex",
alignItems: "center",
justifyContent: "flex-start",
zIndex: 10,
pointerEvents: "none",
}}
>
<div style={SCROLLBAR_CONTAINER_STYLE}>
<div
style={{
position: "absolute",
top: 0,
right: -3,
width: SCROLLBAR_HANDLE_GRAB_WIDTH,
height: thumbRegionHeight,
transform: `translateY(${thumbRegionOffset}px)`,
cursor: isDraggingScrollbar ? "grabbing" : "grab",
touchAction: "none",
userSelect: "none",
transitionProperty: "transform",
transitionDuration: `${handleTravelDurationMs}ms`,
transitionTimingFunction: "linear",
pointerEvents: handleVisible ? "auto" : "none",
}}
style={handleContainerStyle}
onPointerDown={handleScrollbarPointerDown}
onPointerEnter={handleScrollbarPointerEnter}
onPointerLeave={handleScrollbarPointerLeave}
>
<div
style={{
marginTop: handleInsetTop,
height: scrollbarGeometry.handleSize,
width: handleWidth,
borderRadius: 999,
alignSelf: "center",
backgroundColor: "rgba(113, 113, 122, 1)",
opacity: handleOpacity,
transitionProperty: "opacity, width, background-color",
transitionDuration: `${SCROLLBAR_HANDLE_FADE_DURATION_MS}ms, ${SCROLLBAR_HANDLE_WIDTH_TRANSITION_DURATION_MS}ms, ${SCROLLBAR_HANDLE_FADE_DURATION_MS}ms`,
transitionTimingFunction: "ease-out, cubic-bezier(0.22, 0.75, 0.2, 1), ease-out",
}}
/>
<div style={handleInnerStyle} />
</div>
</div>
) : null}

View File

@@ -102,6 +102,11 @@ export function ToolCallSheetProvider({ children }: ToolCallSheetProviderProps)
[openToolCall, closeToolCall],
);
const handleIndicatorStyle = useMemo(
() => ({ backgroundColor: theme.colors.palette.zinc[600] }),
[theme.colors.palette.zinc],
);
return (
<ToolCallSheetContext.Provider value={contextValue}>
{children}
@@ -114,7 +119,7 @@ export function ToolCallSheetProvider({ children }: ToolCallSheetProviderProps)
backdropComponent={renderBackdrop}
enablePanDownToClose
backgroundComponent={CustomSheetBackground}
handleIndicatorStyle={{ backgroundColor: theme.colors.palette.zinc[600] }}
handleIndicatorStyle={handleIndicatorStyle}
>
{sheetData && <ToolCallSheetContent data={sheetData} onClose={closeToolCall} />}
</IsolatedBottomSheetModal>

View File

@@ -206,10 +206,7 @@ export function Autocomplete({
[ensureActiveItemVisible],
);
const containerStyle = useMemo(
() => [styles.container, { maxHeight }],
[maxHeight],
);
const containerStyle = useMemo(() => [styles.container, { maxHeight }], [maxHeight]);
if (isLoading) {
return (

View File

@@ -111,10 +111,14 @@ function SegmentItem<T extends string>({
],
[isSelected, option.disabled, segmentSizeStyle],
);
const accessibilityState = useMemo(
() => ({ selected: isSelected, disabled: option.disabled }),
[isSelected, option.disabled],
);
return (
<Pressable
accessibilityRole="button"
accessibilityState={{ selected: isSelected, disabled: option.disabled }}
accessibilityState={accessibilityState}
disabled={option.disabled}
testID={option.testID}
onPress={handlePress}

View File

@@ -1,4 +1,4 @@
import { useCallback, useEffect, useState, useSyncExternalStore } from "react";
import { useCallback, useEffect, useMemo, useState, useSyncExternalStore } from "react";
import { Pressable, Text, View, ScrollView } from "react-native";
import { useRouter } from "expo-router";
import { StyleSheet, useUnistyles } from "react-native-unistyles";
@@ -250,14 +250,16 @@ export function WelcomeScreen({ onHostAdded }: WelcomeScreenProps) {
const isConnectingToSavedHosts = hosts.length > 0 && !anyOnlineServerId;
const scrollContentContainerStyle = useMemo(
() => [styles.container, { paddingBottom: theme.spacing[6] + insets.bottom }],
[theme.spacing, insets.bottom],
);
return (
<View style={styles.root}>
<ScrollView
style={styles.scrollView}
contentContainerStyle={[
styles.container,
{ paddingBottom: theme.spacing[6] + insets.bottom },
]}
contentContainerStyle={scrollContentContainerStyle}
showsVerticalScrollIndicator={false}
testID="welcome-screen"
>

View File

@@ -335,10 +335,7 @@ function ChecksSummaryContent({
() => [styles.checksSummaryLabel, hovered && styles.checksSummaryLabelHovered],
[hovered],
);
const dotStyle = useMemo(
() => [styles.checksDot, { backgroundColor: badgeColor }],
[badgeColor],
);
const dotStyle = useMemo(() => [styles.checksDot, { backgroundColor: badgeColor }], [badgeColor]);
const statusTextStyle = useMemo(
() => [styles.checksStatusText, { color: badgeColor }],
[badgeColor],

View File

@@ -260,6 +260,17 @@ export function WorkspaceSetupDialog() {
() => ({ backgroundColor: theme.colors.surface2 }),
[theme.colors.surface2],
);
const iconSource = useMemo(() => (iconDataUri ? { uri: iconDataUri } : null), [iconDataUri]);
const statusControlsWithDisabled = useMemo(
() =>
composerState
? {
...composerState.statusControls,
disabled: pendingAction !== null,
}
: undefined,
[composerState, pendingAction],
);
if (!pendingWorkspaceSetup || !sourceDirectory) {
return null;
@@ -267,8 +278,8 @@ export function WorkspaceSetupDialog() {
const subtitleContent = (
<View style={styles.subtitleRow}>
{iconDataUri ? (
<Image source={{ uri: iconDataUri }} style={styles.projectIcon} />
{iconSource ? (
<Image source={iconSource} style={styles.projectIcon} />
) : (
<View style={styles.projectIconFallback}>
<Text style={styles.projectIconFallbackText}>{placeholderInitial}</Text>
@@ -307,14 +318,7 @@ export function WorkspaceSetupDialog() {
clearDraft={chatDraft.clear}
autoFocus
commandDraftConfig={composerState?.commandDraftConfig}
statusControls={
composerState
? {
...composerState.statusControls,
disabled: pendingAction !== null,
}
: undefined
}
statusControls={statusControlsWithDisabled}
inputWrapperStyle={composerInputWrapperStyle}
onAddImages={handleAddImagesCallback}
/>

View File

@@ -1,4 +1,4 @@
import { createContext, useContext, useCallback, useRef, type ReactNode } from "react";
import { createContext, useContext, useCallback, useMemo, useRef, type ReactNode } from "react";
import { useSharedValue, type SharedValue } from "react-native-reanimated";
interface HorizontalScrollContextValue {
@@ -42,14 +42,17 @@ export function HorizontalScrollProvider({ children }: { children: ReactNode })
[updateIsAnyScrolled],
);
const contextValue = useMemo(
() => ({
isAnyScrolledRight,
registerScrollOffset,
unregisterScrollOffset,
}),
[isAnyScrolledRight, registerScrollOffset, unregisterScrollOffset],
);
return (
<HorizontalScrollContext.Provider
value={{
isAnyScrolledRight,
registerScrollOffset,
unregisterScrollOffset,
}}
>
<HorizontalScrollContext.Provider value={contextValue}>
{children}
</HorizontalScrollContext.Provider>
);

View File

@@ -1,4 +1,4 @@
import { useCallback } from "react";
import { useCallback, useMemo } from "react";
import { ActivityIndicator, Image, Text, TextInput, View } from "react-native";
import * as Clipboard from "expo-clipboard";
import * as QRCode from "qrcode";
@@ -50,6 +50,11 @@ export function PairDeviceSection() {
void handleCopyLink();
}, [handleCopyLink]);
const qrImageSource = useMemo(
() => (qrQuery.data ? { uri: qrQuery.data } : null),
[qrQuery.data],
);
if (!showSection) return null;
return (
@@ -98,8 +103,8 @@ export function PairDeviceSection() {
Scan this QR code with Paseo on your phone, or copy the link below.
</Text>
<View style={styles.qrContainer}>
{qrQuery.data ? (
<Image source={{ uri: qrQuery.data }} style={styles.qrImage} resizeMode="contain" />
{qrImageSource ? (
<Image source={qrImageSource} style={styles.qrImage} resizeMode="contain" />
) : qrQuery.isError ? (
<Text style={styles.hint}>QR code unavailable.</Text>
) : (

View File

@@ -6,6 +6,13 @@ import { usePaneContext } from "@/panels/pane-context";
import type { PanelRegistration } from "@/panels/panel-registry";
import { useWorkspaceExecutionAuthority } from "@/stores/session-store-hooks";
const CENTERED_PADDED_STYLE = {
flex: 1,
alignItems: "center",
justifyContent: "center",
padding: 16,
} as const;
function useFilePanelDescriptor(target: { kind: "file"; path: string }) {
const fileName = target.path.split("/").filter(Boolean).pop() ?? target.path;
return {
@@ -26,7 +33,7 @@ function FilePanel() {
invariant(target.kind === "file", "FilePanel requires file target");
if (!workspaceDirectory) {
return (
<View style={{ flex: 1, alignItems: "center", justifyContent: "center", padding: 16 }}>
<View style={CENTERED_PADDED_STYLE}>
<Text>Workspace execution directory not found.</Text>
</View>
);

View File

@@ -320,13 +320,15 @@ function SetupCommandRow({
[showDetail],
);
const accessibilityState = useMemo(() => ({ expanded: showDetail }), [showDetail]);
return (
<View style={styles.commandItem}>
<Pressable
onPress={handlePress}
style={pressableStyle}
accessibilityRole="button"
accessibilityState={{ expanded: showDetail }}
accessibilityState={accessibilityState}
>
<View style={styles.commandStatusIcon}>
<CommandStatusIcon status={command.status} />

View File

@@ -14,6 +14,14 @@ import { useWorkspaceExecutionAuthority } from "@/stores/session-store-hooks";
type ListTerminalsPayload = ListTerminalsResponse["payload"];
const FLEX_FILL_STYLE = { flex: 1 } as const;
const CENTERED_PADDED_STYLE = {
flex: 1,
alignItems: "center",
justifyContent: "center",
padding: 16,
} as const;
function trimNonEmpty(value: string | null | undefined): string | null {
if (typeof value !== "string") {
return null;
@@ -84,12 +92,12 @@ function TerminalPanel() {
invariant(target.kind === "terminal", "TerminalPanel requires terminal target");
if (!isWorkspaceFocused) {
return <View style={{ flex: 1 }} />;
return <View style={FLEX_FILL_STYLE} />;
}
if (!workspaceDirectory) {
return (
<View style={{ flex: 1, alignItems: "center", justifyContent: "center", padding: 16 }}>
<View style={CENTERED_PADDED_STYLE}>
<Text>
{workspaceAuthority.ok
? "Workspace execution directory not found."

View File

@@ -480,6 +480,17 @@ export function NewWorkspaceScreen({
[keyboardAnimatedStyle],
);
const statusControlsWithDisabled = useMemo(
() =>
composerState
? {
...composerState.statusControls,
disabled: isPending,
}
: undefined,
[composerState, isPending],
);
return (
<View style={styles.container}>
<ScreenHeader
@@ -521,14 +532,7 @@ export function NewWorkspaceScreen({
clearDraft={handleClearDraft}
autoFocus
commandDraftConfig={composerState?.commandDraftConfig}
statusControls={
composerState
? {
...composerState.statusControls,
disabled: isPending,
}
: undefined
}
statusControls={statusControlsWithDisabled}
onAddImages={handleAddImagesCallback}
/>
<Animated.View testID="new-workspace-ref-picker-row" style={optionsRowStyle}>

View File

@@ -1,4 +1,4 @@
import { useCallback, useState } from "react";
import { useCallback, useMemo, useState } from "react";
import { Pressable, Text, View } from "react-native";
import { StyleSheet, useUnistyles } from "react-native-unistyles";
import { settingsStyles } from "@/styles/settings";
@@ -47,12 +47,13 @@ function ProviderRow({ def, entry, isFirst, onPress }: ProviderRowProps) {
const handlePress = useCallback(() => onPress(def.id), [def.id, onPress]);
const rowStyle = useMemo(
() => [settingsStyles.row, !isFirst && settingsStyles.rowBorder],
[isFirst],
);
return (
<Pressable
style={[settingsStyles.row, !isFirst && settingsStyles.rowBorder]}
onPress={handlePress}
accessibilityRole="button"
>
<Pressable style={rowStyle} onPress={handlePress} accessibilityRole="button">
<View style={settingsStyles.rowContent}>
<View style={styles.titleRow}>
<ProviderIcon size={theme.iconSize.sm} color={theme.colors.foreground} />
@@ -124,11 +125,11 @@ export function ProvidersSection({ serverId }: ProvidersSectionProps) {
style={styles.sectionSpacing}
>
{!hasServer || !isConnected ? (
<View style={[settingsStyles.card, styles.emptyCard]}>
<View style={EMPTY_CARD_STYLE}>
<Text style={styles.emptyText}>Connect to this host to see providers</Text>
</View>
) : isLoading ? (
<View style={[settingsStyles.card, styles.emptyCard]}>
<View style={EMPTY_CARD_STYLE}>
<Text style={styles.emptyText}>Loading...</Text>
</View>
) : (
@@ -181,3 +182,5 @@ const styles = StyleSheet.create((theme) => ({
marginTop: theme.spacing[1],
},
}));
const EMPTY_CARD_STYLE = [settingsStyles.card, styles.emptyCard];

View File

@@ -169,9 +169,19 @@ const styles = StyleSheet.create((theme) => ({
},
}));
const TITLE_ERROR_STYLE = [styles.title, styles.titleError];
export function StartupSplashScreen({ bootstrapState }: StartupSplashScreenProps) {
const { theme } = useUnistyles();
const webScrollbarStyle = useWebScrollbarStyle();
const errorScrollViewStyle = useMemo(
() => [styles.errorScrollView, webScrollbarStyle],
[webScrollbarStyle],
);
const logsScrollStyle = useMemo(
() => [styles.logsScroll, webScrollbarStyle],
[webScrollbarStyle],
);
const [daemonLogs, setDaemonLogs] = useState<DesktopDaemonLogs | null>(null);
const [logsError, setLogsError] = useState<string | null>(null);
const [isLoadingLogs, setIsLoadingLogs] = useState(false);
@@ -293,14 +303,14 @@ export function StartupSplashScreen({ bootstrapState }: StartupSplashScreenProps
<View style={styles.errorScreen}>
<TitlebarDragRegion />
<ScrollView
style={[styles.errorScrollView, webScrollbarStyle]}
style={errorScrollViewStyle}
contentContainerStyle={styles.errorScrollContent}
showsVerticalScrollIndicator
>
<View style={styles.errorContent}>
<View style={styles.errorHeader}>
<PaseoLogo size={64} />
<Text style={[styles.title, styles.titleError]}>Something went wrong</Text>
<Text style={TITLE_ERROR_STYLE}>Something went wrong</Text>
</View>
<Text style={styles.errorDescription}>
@@ -314,7 +324,7 @@ export function StartupSplashScreen({ bootstrapState }: StartupSplashScreenProps
<View style={styles.logsContainer}>
<ScrollView
style={[styles.logsScroll, webScrollbarStyle]}
style={logsScrollStyle}
contentContainerStyle={styles.logsContent}
showsVerticalScrollIndicator
>

View File

@@ -259,10 +259,7 @@ function TabChip({
[isFocused],
);
const tabLabelSkeletonStyle = useMemo(
() => [
styles.tabLabelSkeleton,
showCloseButton && styles.tabLabelSkeletonWithCloseButton,
],
() => [styles.tabLabelSkeleton, showCloseButton && styles.tabLabelSkeletonWithCloseButton],
[showCloseButton],
);
const tabLabelStyle = useMemo(
@@ -801,9 +798,7 @@ function ResolvedDesktopTabChip({
return (
<View style={styles.tabSlot}>
{showDropIndicatorBefore ? (
<View style={TAB_DROP_INDICATOR_BEFORE_STYLE} />
) : null}
{showDropIndicatorBefore ? <View style={TAB_DROP_INDICATOR_BEFORE_STYLE} /> : null}
<TabChip
tab={item.tab}
isActive={item.isActive}
@@ -823,9 +818,7 @@ function ResolvedDesktopTabChip({
onCloseTab={onCloseTab}
dragHandleProps={dragHandleProps}
/>
{showDropIndicatorAfter ? (
<View style={TAB_DROP_INDICATOR_AFTER_STYLE} />
) : null}
{showDropIndicatorAfter ? <View style={TAB_DROP_INDICATOR_AFTER_STYLE} /> : null}
</View>
);
}}
@@ -996,11 +989,5 @@ const styles = StyleSheet.create((theme) => ({
},
}));
const TAB_DROP_INDICATOR_BEFORE_STYLE = [
styles.tabDropIndicator,
styles.tabDropIndicatorBefore,
];
const TAB_DROP_INDICATOR_AFTER_STYLE = [
styles.tabDropIndicator,
styles.tabDropIndicatorAfter,
];
const TAB_DROP_INDICATOR_BEFORE_STYLE = [styles.tabDropIndicator, styles.tabDropIndicatorBefore];
const TAB_DROP_INDICATOR_AFTER_STYLE = [styles.tabDropIndicator, styles.tabDropIndicatorAfter];

View File

@@ -342,6 +342,34 @@ export function WorkspaceDraftAgentTab({
[insets.bottom],
);
const handleDropdownCloseFocus = useCallback(() => {
focusInputRef.current?.();
}, []);
const composerStatusControls = useMemo(
() => ({
...composerState.statusControls,
onSelectProvider: handleProviderSelectWithFocus,
onSelectMode: handleModeSelectWithFocus,
onSelectModel: handleModelSelectWithFocus,
onSelectProviderAndModel: handleProviderAndModelSelectWithFocus,
onSelectThinkingOption: handleThinkingOptionSelectWithFocus,
onSetFeature: handleSetFeatureWithFocus,
onDropdownClose: handleDropdownCloseFocus,
disabled: isSubmitting,
}),
[
composerState.statusControls,
handleProviderSelectWithFocus,
handleModeSelectWithFocus,
handleModelSelectWithFocus,
handleProviderAndModelSelectWithFocus,
handleThinkingOptionSelectWithFocus,
handleSetFeatureWithFocus,
handleDropdownCloseFocus,
isSubmitting,
],
);
return (
<FileDropZone onFilesDropped={handleFilesDropped}>
<View style={styles.container}>
@@ -391,17 +419,7 @@ export function WorkspaceDraftAgentTab({
onAddImages={handleAddImagesCallback}
onFocusInput={handleFocusInputCallback}
commandDraftConfig={composerState.commandDraftConfig}
statusControls={{
...composerState.statusControls,
onSelectProvider: handleProviderSelectWithFocus,
onSelectMode: handleModeSelectWithFocus,
onSelectModel: handleModelSelectWithFocus,
onSelectProviderAndModel: handleProviderAndModelSelectWithFocus,
onSelectThinkingOption: handleThinkingOptionSelectWithFocus,
onSetFeature: handleSetFeatureWithFocus,
onDropdownClose: () => focusInputRef.current?.(),
disabled: isSubmitting,
}}
statusControls={composerStatusControls}
/>
</View>
</View>

View File

@@ -60,10 +60,11 @@ function ScriptActionButtonChildren({
) : (
<Play {...iconProps} fill="transparent" />
);
const labelStyle = useMemo(() => [styles.actionButtonLabel, { color }], [color]);
return (
<>
{iconElement}
<Text style={[styles.actionButtonLabel, { color }]}>{label}</Text>
<Text style={labelStyle}>{label}</Text>
</>
);
}
@@ -133,9 +134,10 @@ function HostLinkChildren({
}: HostLinkChildrenProps): ReactElement {
const showIcon = !disabled && (hovered || isNative);
const color = hovered && !disabled ? theme.colors.foreground : theme.colors.foregroundMuted;
const hostLabelStyle = useMemo(() => [styles.hostLabel, { color }], [color]);
return (
<>
<Text style={[styles.hostLabel, { color }]} numberOfLines={1}>
<Text style={hostLabelStyle} numberOfLines={1}>
{label}
</Text>
<View style={styles.hostIconSlot}>
@@ -181,9 +183,10 @@ function HostLinkRow({ label, url, scriptName }: HostLinkProps): ReactElement {
function ExitCodeBadge({ code }: { code: number }): ReactElement {
const { theme } = useUnistyles();
const color = code === 0 ? theme.colors.foregroundMuted : theme.colors.palette.red[300];
const exitTextStyle = useMemo(() => [styles.exitBadgeText, { color }], [color]);
return (
<View style={styles.exitBadge}>
<Text style={[styles.exitBadgeText, { color }]}>exit {code}</Text>
<Text style={exitTextStyle}>exit {code}</Text>
</View>
);
}
@@ -272,6 +275,16 @@ function ScriptRow({
onStartScript(script.scriptName);
}, [onStartScript, script.scriptName]);
const scriptNameStyle = useMemo(
() => [
styles.scriptName,
{
color: isRunning ? theme.colors.foreground : theme.colors.foregroundMuted,
},
],
[isRunning, theme.colors.foreground, theme.colors.foregroundMuted],
);
let primaryAction: ReactElement | null = null;
if (isRunning && liveTerminalId) {
primaryAction = (
@@ -304,15 +317,7 @@ function ScriptRow({
>
<View style={styles.scriptHeader}>
<ScriptIcon size={14} color={iconColor} style={styles.scriptIcon} />
<Text
style={[
styles.scriptName,
{
color: isRunning ? theme.colors.foreground : theme.colors.foregroundMuted,
},
]}
numberOfLines={1}
>
<Text style={scriptNameStyle} numberOfLines={1}>
{script.scriptName}
</Text>
{showExitBadge ? <ExitCodeBadge code={exitCode} /> : null}

View File

@@ -146,13 +146,7 @@ export function WorkspaceTabIcon({
bottom: statusDotOffset,
},
],
[
statusDotColor,
statusDotBorderColor,
theme.colors.surface0,
statusDotSize,
statusDotOffset,
],
[statusDotColor, statusDotBorderColor, theme.colors.surface0, statusDotSize, statusDotOffset],
);
if (shouldShowLoader) {