mirror of
https://github.com/getpaseo/paseo.git
synced 2026-07-29 12:01:31 +00:00
chore(lint): hoist inline callbacks in app (jsx-no-new-function-as-prop)
Work in progress: 30 of 369 warnings fixed across 23 files.
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { useMemo, useState } from "react";
|
||||
import { useCallback, useMemo, useState } from "react";
|
||||
import { View, Text } from "react-native";
|
||||
import { StyleSheet } from "react-native-unistyles";
|
||||
import Animated from "react-native-reanimated";
|
||||
@@ -27,7 +27,7 @@ export function ArchivedAgentCallout({ serverId, agentId }: ArchivedAgentCallout
|
||||
[insets.bottom, keyboardAnimatedStyle],
|
||||
);
|
||||
|
||||
async function handleUnarchive() {
|
||||
const handleUnarchive = useCallback(async () => {
|
||||
if (!client || !isConnected || isUnarchiving) return;
|
||||
setIsUnarchiving(true);
|
||||
try {
|
||||
@@ -36,7 +36,7 @@ export function ArchivedAgentCallout({ serverId, agentId }: ArchivedAgentCallout
|
||||
console.error("[ArchivedAgentCallout] Failed to unarchive agent:", error);
|
||||
setIsUnarchiving(false);
|
||||
}
|
||||
}
|
||||
}, [client, isConnected, isUnarchiving, agentId]);
|
||||
|
||||
return (
|
||||
<Animated.View style={containerStyle}>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { Modal, Pressable, Text, View } from "react-native";
|
||||
import { StyleSheet, useUnistyles } from "react-native-unistyles";
|
||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||
@@ -47,6 +47,9 @@ export function AttachmentLightbox({ metadata, onClose }: AttachmentLightboxProp
|
||||
[insets.top, insets.right, theme.spacing],
|
||||
);
|
||||
|
||||
const handleImageError = useCallback(() => setErrored(true), []);
|
||||
const noopPress = useCallback(() => {}, []);
|
||||
|
||||
if (!metadata) {
|
||||
return null;
|
||||
}
|
||||
@@ -68,12 +71,12 @@ export function AttachmentLightbox({ metadata, onClose }: AttachmentLightboxProp
|
||||
{hasError ? (
|
||||
<Text style={styles.errorText}>Couldn't load image</Text>
|
||||
) : (
|
||||
<Pressable onPress={() => {}} style={styles.imagePressable}>
|
||||
<Pressable onPress={noopPress} style={styles.imagePressable}>
|
||||
<ExpoImage
|
||||
testID="attachment-lightbox-image"
|
||||
source={{ uri: url }}
|
||||
contentFit="contain"
|
||||
onError={() => setErrored(true)}
|
||||
onError={handleImageError}
|
||||
style={imageFillStyle}
|
||||
/>
|
||||
</Pressable>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { X } from "lucide-react-native";
|
||||
import { useMemo, type ReactNode } from "react";
|
||||
import { Pressable, Text, View } from "react-native";
|
||||
import { useCallback, useMemo, type ReactNode } from "react";
|
||||
import { Pressable, Text, View, type PressableStateCallbackType } from "react-native";
|
||||
import { StyleSheet, useUnistyles } from "react-native-unistyles";
|
||||
|
||||
export type CalloutActionVariant = "primary" | "secondary";
|
||||
@@ -113,18 +113,22 @@ function CalloutActionButton({ action, testID }: { action: CalloutAction; testID
|
||||
() => [styles.actionLabel, isPrimary ? styles.actionLabelPrimary : styles.actionLabelSecondary],
|
||||
[isPrimary],
|
||||
);
|
||||
const pressableStyle = useCallback(
|
||||
({ pressed }: PressableStateCallbackType) => [
|
||||
styles.actionButton,
|
||||
isPrimary ? styles.actionButtonPrimary : styles.actionButtonSecondary,
|
||||
pressed ? styles.actionButtonPressed : null,
|
||||
action.disabled ? styles.actionButtonDisabled : null,
|
||||
],
|
||||
[action.disabled, isPrimary],
|
||||
);
|
||||
return (
|
||||
<Pressable
|
||||
onPress={action.onPress}
|
||||
disabled={action.disabled}
|
||||
testID={testID}
|
||||
accessibilityRole="button"
|
||||
style={({ pressed }) => [
|
||||
styles.actionButton,
|
||||
isPrimary ? styles.actionButtonPrimary : styles.actionButtonSecondary,
|
||||
pressed ? styles.actionButtonPressed : null,
|
||||
action.disabled ? styles.actionButtonDisabled : null,
|
||||
]}
|
||||
style={pressableStyle}
|
||||
>
|
||||
<Text style={labelStyle} numberOfLines={1}>
|
||||
{action.label}
|
||||
|
||||
@@ -61,6 +61,11 @@ export function DiffScroll({
|
||||
[horizontalScroll, scrollId],
|
||||
);
|
||||
|
||||
const handleLayout = useCallback(
|
||||
(e: LayoutChangeEvent) => onScrollViewWidthChange(e.nativeEvent.layout.width),
|
||||
[onScrollViewWidthChange],
|
||||
);
|
||||
|
||||
return (
|
||||
<ScrollView
|
||||
ref={scrollViewRef}
|
||||
@@ -72,7 +77,7 @@ export function DiffScroll({
|
||||
contentContainerStyle={contentContainerStyle}
|
||||
onScroll={handleScroll}
|
||||
scrollEventThrottle={16}
|
||||
onLayout={(e: LayoutChangeEvent) => onScrollViewWidthChange(e.nativeEvent.layout.width)}
|
||||
onLayout={handleLayout}
|
||||
// When at left edge, wait for close gesture to fail before scrolling.
|
||||
// The close gesture fails quickly on leftward swipes (failOffsetX=-10),
|
||||
// so scrolling left works normally. On rightward swipes, close gesture
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useMemo } from "react";
|
||||
import { useCallback, useMemo } from "react";
|
||||
import { ScrollView, type LayoutChangeEvent, type StyleProp, type ViewStyle } from "react-native";
|
||||
import { useWebScrollbarStyle } from "@/hooks/use-web-scrollbar-style";
|
||||
|
||||
@@ -18,6 +18,10 @@ export function DiffScroll({
|
||||
}: DiffScrollProps) {
|
||||
const webScrollbarStyle = useWebScrollbarStyle();
|
||||
const combinedStyle = useMemo(() => [style, webScrollbarStyle], [style, webScrollbarStyle]);
|
||||
const handleLayout = useCallback(
|
||||
(e: LayoutChangeEvent) => onScrollViewWidthChange(e.nativeEvent.layout.width),
|
||||
[onScrollViewWidthChange],
|
||||
);
|
||||
|
||||
return (
|
||||
<ScrollView
|
||||
@@ -26,7 +30,7 @@ export function DiffScroll({
|
||||
showsHorizontalScrollIndicator
|
||||
style={combinedStyle}
|
||||
contentContainerStyle={contentContainerStyle}
|
||||
onLayout={(e: LayoutChangeEvent) => onScrollViewWidthChange(e.nativeEvent.layout.width)}
|
||||
onLayout={handleLayout}
|
||||
>
|
||||
{children}
|
||||
</ScrollView>
|
||||
|
||||
@@ -25,6 +25,11 @@ export function DiffViewer({
|
||||
}: DiffViewerProps) {
|
||||
const [scrollViewWidth, setScrollViewWidth] = React.useState(0);
|
||||
const webScrollbarStyle = useWebScrollbarStyle();
|
||||
const handleInnerLayout = React.useCallback(
|
||||
(e: { nativeEvent: { layout: { width: number } } }) =>
|
||||
setScrollViewWidth(e.nativeEvent.layout.width),
|
||||
[],
|
||||
);
|
||||
|
||||
if (!diffLines.length) {
|
||||
return (
|
||||
@@ -52,7 +57,7 @@ export function DiffViewer({
|
||||
showsHorizontalScrollIndicator
|
||||
style={webScrollbarStyle}
|
||||
contentContainerStyle={styles.horizontalContent}
|
||||
onLayout={(e) => setScrollViewWidth(e.nativeEvent.layout.width)}
|
||||
onLayout={handleInnerLayout}
|
||||
>
|
||||
<View style={[styles.linesContainer, scrollViewWidth > 0 && { minWidth: scrollViewWidth }]}>
|
||||
{diffLines.map((line, index) => (
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useMemo, useRef } from "react";
|
||||
import { useCallback, useEffect, useMemo, useRef } from "react";
|
||||
import { ActivityIndicator, Pressable, Text, View } from "react-native";
|
||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||
import { StyleSheet, useUnistyles } from "react-native-unistyles";
|
||||
@@ -41,6 +41,12 @@ export function DownloadToast() {
|
||||
[theme.spacing, insets.bottom],
|
||||
);
|
||||
|
||||
const handleDismiss = useCallback(() => {
|
||||
if (activeDownload) {
|
||||
dismissDownload(activeDownload.id);
|
||||
}
|
||||
}, [activeDownload, dismissDownload]);
|
||||
|
||||
if (!activeDownload) {
|
||||
return null;
|
||||
}
|
||||
@@ -75,11 +81,7 @@ export function DownloadToast() {
|
||||
)}
|
||||
</View>
|
||||
{activeDownload.status !== "downloading" && (
|
||||
<Pressable
|
||||
onPress={() => dismissDownload(activeDownload.id)}
|
||||
hitSlop={8}
|
||||
style={styles.dismiss}
|
||||
>
|
||||
<Pressable onPress={handleDismiss} hitSlop={8} style={styles.dismiss}>
|
||||
<X size={16} color={theme.colors.foregroundMuted} />
|
||||
</Pressable>
|
||||
)}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { ReactNode } from "react";
|
||||
import { useCallback, type ReactNode } from "react";
|
||||
import { Pressable } from "react-native";
|
||||
import { router } from "expo-router";
|
||||
import { StyleSheet, useUnistyles } from "react-native-unistyles";
|
||||
@@ -13,15 +13,26 @@ interface BackHeaderProps {
|
||||
onBack?: () => void;
|
||||
}
|
||||
|
||||
function goBack(): void {
|
||||
router.back();
|
||||
}
|
||||
|
||||
export function BackHeader({ title, titleAccessory, rightContent, onBack }: BackHeaderProps) {
|
||||
const { theme } = useUnistyles();
|
||||
const handleBack = useCallback(() => {
|
||||
if (onBack) {
|
||||
onBack();
|
||||
return;
|
||||
}
|
||||
goBack();
|
||||
}, [onBack]);
|
||||
|
||||
return (
|
||||
<ScreenHeader
|
||||
left={
|
||||
<>
|
||||
<Pressable
|
||||
onPress={onBack ?? (() => router.back())}
|
||||
onPress={handleBack}
|
||||
style={styles.backButton}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel="Back"
|
||||
|
||||
@@ -50,9 +50,7 @@ export function HeaderToggleButton({
|
||||
{...props}
|
||||
{...ariaExpandedProps}
|
||||
disabled={disabled}
|
||||
onPress={(e) => {
|
||||
onPress(e);
|
||||
}}
|
||||
onPress={onPress}
|
||||
style={combinedStyle}
|
||||
>
|
||||
{typeof children === "function"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { ReactNode } from "react";
|
||||
import { useCallback, type ReactNode } from "react";
|
||||
import { View, type StyleProp, type ViewStyle } from "react-native";
|
||||
import { StyleSheet, useUnistyles } from "react-native-unistyles";
|
||||
import { PanelLeft } from "lucide-react-native";
|
||||
@@ -53,9 +53,13 @@ export function SidebarMenuToggle({
|
||||
const menuIconColor =
|
||||
!isMobile && isOpen ? theme.colors.foreground : theme.colors.foregroundMuted;
|
||||
|
||||
const handlePress = useCallback(() => {
|
||||
toggleAgentListForLayout({ isCompact: isMobile });
|
||||
}, [toggleAgentListForLayout, isMobile]);
|
||||
|
||||
return (
|
||||
<HeaderToggleButton
|
||||
onPress={() => toggleAgentListForLayout({ isCompact: isMobile })}
|
||||
onPress={handlePress}
|
||||
tooltipLabel="Toggle sidebar"
|
||||
tooltipKeys={toggleShortcutKeys}
|
||||
tooltipSide={tooltipSide}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useMemo } from "react";
|
||||
import { useCallback, useMemo } from "react";
|
||||
import { Text, View } from "react-native";
|
||||
import { StyleSheet } from "react-native-unistyles";
|
||||
import { getIsElectronRuntime } from "@/constants/layout";
|
||||
@@ -21,11 +21,13 @@ export function KeyboardShortcutsDialog() {
|
||||
[isDesktopApp, isMac],
|
||||
);
|
||||
|
||||
const handleClose = useCallback(() => setOpen(false), [setOpen]);
|
||||
|
||||
return (
|
||||
<AdaptiveModalSheet
|
||||
title="Shortcuts"
|
||||
visible={open}
|
||||
onClose={() => setOpen(false)}
|
||||
onClose={handleClose}
|
||||
testID="keyboard-shortcuts-dialog"
|
||||
snapPoints={SNAP_POINTS}
|
||||
>
|
||||
|
||||
@@ -1,6 +1,13 @@
|
||||
import { AlertCircle, RotateCw, Search } from "lucide-react-native";
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { ActivityIndicator, Pressable, ScrollView, Text, View } from "react-native";
|
||||
import {
|
||||
ActivityIndicator,
|
||||
Pressable,
|
||||
type PressableStateCallbackType,
|
||||
ScrollView,
|
||||
Text,
|
||||
View,
|
||||
} from "react-native";
|
||||
import { StyleSheet, useUnistyles } from "react-native-unistyles";
|
||||
import { AdaptiveModalSheet, AdaptiveTextInput } from "@/components/adaptive-modal-sheet";
|
||||
import { LoadingSpinner } from "@/components/ui/loading-spinner";
|
||||
@@ -81,6 +88,15 @@ export function ProviderDiagnosticSheet({
|
||||
[client, provider],
|
||||
);
|
||||
|
||||
const refreshButtonStyle = useCallback(
|
||||
({ hovered, pressed }: PressableStateCallbackType & { hovered?: boolean }) => [
|
||||
sheetStyles.iconButton,
|
||||
(Boolean(hovered) || pressed) && sheetStyles.iconButtonHovered,
|
||||
refreshInFlight ? sheetStyles.disabled : null,
|
||||
],
|
||||
[refreshInFlight],
|
||||
);
|
||||
|
||||
const handleRefresh = useCallback(() => {
|
||||
if (!provider) {
|
||||
return;
|
||||
@@ -155,11 +171,7 @@ export function ProviderDiagnosticSheet({
|
||||
onPress={handleRefresh}
|
||||
disabled={refreshInFlight}
|
||||
hitSlop={8}
|
||||
style={({ hovered, pressed }) => [
|
||||
sheetStyles.iconButton,
|
||||
(hovered || pressed) && sheetStyles.iconButtonHovered,
|
||||
refreshInFlight ? sheetStyles.disabled : null,
|
||||
]}
|
||||
style={refreshButtonStyle}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={
|
||||
refreshInFlight ? `Refreshing ${providerLabel}` : `Refresh ${providerLabel}`
|
||||
|
||||
@@ -93,6 +93,20 @@ export function ResizeHandle({
|
||||
[direction, groupId, index, onResizeSplit, sizes],
|
||||
);
|
||||
|
||||
const handlePointerEnter = useCallback(() => {
|
||||
hoverTimerRef.current = setTimeout(() => {
|
||||
setActive(true);
|
||||
}, 150);
|
||||
}, []);
|
||||
|
||||
const handlePointerLeave = useCallback(() => {
|
||||
if (hoverTimerRef.current) {
|
||||
clearTimeout(hoverTimerRef.current);
|
||||
hoverTimerRef.current = null;
|
||||
}
|
||||
setActive(false);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<View
|
||||
style={[
|
||||
@@ -122,18 +136,8 @@ export function ResizeHandle({
|
||||
} as any,
|
||||
]}
|
||||
onPointerDown={handlePointerDown}
|
||||
onPointerEnter={() => {
|
||||
hoverTimerRef.current = setTimeout(() => {
|
||||
setActive(true);
|
||||
}, 150);
|
||||
}}
|
||||
onPointerLeave={() => {
|
||||
if (hoverTimerRef.current) {
|
||||
clearTimeout(hoverTimerRef.current);
|
||||
hoverTimerRef.current = null;
|
||||
}
|
||||
setActive(false);
|
||||
}}
|
||||
onPointerEnter={handlePointerEnter}
|
||||
onPointerLeave={handlePointerLeave}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useMemo } from "react";
|
||||
import { Pressable, Text, View } from "react-native";
|
||||
import { useCallback, useMemo } from "react";
|
||||
import { Pressable, Text, View, type PressableStateCallbackType } from "react-native";
|
||||
import { StyleSheet, useUnistyles } from "react-native-unistyles";
|
||||
import type { LucideIcon } from "lucide-react-native";
|
||||
import { HEADER_INNER_HEIGHT, HEADER_INNER_HEIGHT_MOBILE } from "@/constants/layout";
|
||||
@@ -31,6 +31,35 @@ export function SidebarHeaderRow({
|
||||
}: SidebarHeaderRowProps) {
|
||||
const { theme } = useUnistyles();
|
||||
|
||||
const buttonStyle = useCallback(
|
||||
({ hovered }: PressableStateCallbackType & { hovered?: boolean }) => [
|
||||
styles.button,
|
||||
(Boolean(hovered) || isActive) && styles.buttonHovered,
|
||||
],
|
||||
[isActive],
|
||||
);
|
||||
|
||||
const renderChildren = useCallback(
|
||||
(state: PressableStateCallbackType & { hovered?: boolean }) => {
|
||||
const isHighlighted = Boolean(state.hovered) || isActive;
|
||||
const iconColor = isHighlighted ? theme.colors.foreground : theme.colors.foregroundMuted;
|
||||
return (
|
||||
<>
|
||||
<Icon size={theme.iconSize.md} color={iconColor} />
|
||||
<SidebarHeaderRowLabel label={label} isHighlighted={isHighlighted} />
|
||||
</>
|
||||
);
|
||||
},
|
||||
[
|
||||
Icon,
|
||||
isActive,
|
||||
label,
|
||||
theme.colors.foreground,
|
||||
theme.colors.foregroundMuted,
|
||||
theme.iconSize.md,
|
||||
],
|
||||
);
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<Pressable
|
||||
@@ -40,18 +69,9 @@ export function SidebarHeaderRow({
|
||||
accessible
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={accessibilityLabel ?? label}
|
||||
style={({ hovered }) => [styles.button, (hovered || isActive) && styles.buttonHovered]}
|
||||
style={buttonStyle}
|
||||
>
|
||||
{({ hovered }) => {
|
||||
const isHighlighted = hovered || isActive;
|
||||
const iconColor = isHighlighted ? theme.colors.foreground : theme.colors.foregroundMuted;
|
||||
return (
|
||||
<>
|
||||
<Icon size={theme.iconSize.md} color={iconColor} />
|
||||
<SidebarHeaderRowLabel label={label} isHighlighted={isHighlighted} />
|
||||
</>
|
||||
);
|
||||
}}
|
||||
{renderChildren}
|
||||
</Pressable>
|
||||
</View>
|
||||
);
|
||||
|
||||
@@ -22,6 +22,10 @@ const DEFAULT_MAINTAIN_VISIBLE_CONTENT_POSITION = Object.freeze({
|
||||
autoscrollToTopThreshold: 0,
|
||||
});
|
||||
|
||||
function keyExtractor(item: { id: string }): string {
|
||||
return item.id;
|
||||
}
|
||||
|
||||
function NativeStreamViewport(props: StreamRenderInput & { strategy: StreamStrategy }) {
|
||||
const {
|
||||
agentId,
|
||||
@@ -298,7 +302,7 @@ function NativeStreamViewport(props: StreamRenderInput & { strategy: StreamStrat
|
||||
ref={flatListRef}
|
||||
data={historyRows}
|
||||
renderItem={renderItem}
|
||||
keyExtractor={(item) => item.id}
|
||||
keyExtractor={keyExtractor}
|
||||
testID="agent-chat-scroll"
|
||||
nativeID="agent-chat-scroll-native-virtualized"
|
||||
ListHeaderComponent={liveHeaderContent ?? undefined}
|
||||
|
||||
@@ -107,6 +107,12 @@ function WebStreamViewport(props: StreamRenderInput & { isMobileBreakpoint: bool
|
||||
} = props;
|
||||
const scrollContainerRef = useRef<HTMLElement | null>(null);
|
||||
const contentRef = useRef<HTMLElement | null>(null);
|
||||
const handleScrollContainerRef = useCallback((node: HTMLElement | null) => {
|
||||
scrollContainerRef.current = node;
|
||||
}, []);
|
||||
const handleContentRef = useCallback((node: HTMLElement | null) => {
|
||||
contentRef.current = node;
|
||||
}, []);
|
||||
const [followOutput, setFollowOutputr] = useState(true);
|
||||
const setFollowOutput = (value: boolean) => {
|
||||
setFollowOutputr(value);
|
||||
@@ -705,19 +711,12 @@ function WebStreamViewport(props: StreamRenderInput & { isMobileBreakpoint: bool
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
ref={(node) => {
|
||||
scrollContainerRef.current = node;
|
||||
}}
|
||||
ref={handleScrollContainerRef}
|
||||
data-testid="agent-chat-scroll"
|
||||
id={`agent-chat-scroll-${shouldUseVirtualizer ? "web-dom-virtualized" : "web-dom-scroll"}`}
|
||||
style={scrollContainerStyle}
|
||||
>
|
||||
<div
|
||||
ref={(node) => {
|
||||
contentRef.current = node;
|
||||
}}
|
||||
style={contentContainerStyle}
|
||||
>
|
||||
<div ref={handleContentRef} style={contentContainerStyle}>
|
||||
{shouldUseVirtualizer ? (
|
||||
<div style={virtualRowsContainerStyle}>
|
||||
{virtualRows.map((virtualRow) => {
|
||||
|
||||
@@ -12,6 +12,8 @@ const SNAP_POINTS_50: (string | number)[] = ["50%"];
|
||||
const SNAP_POINTS_60: (string | number)[] = ["60%"];
|
||||
const SNAP_POINTS_90: (string | number)[] = ["90%"];
|
||||
|
||||
function noop(): void {}
|
||||
|
||||
const { modalMethods, modalProps } = vi.hoisted(() => ({
|
||||
modalMethods: {
|
||||
present: vi.fn(),
|
||||
@@ -117,7 +119,7 @@ describe("IsolatedBottomSheetModal", () => {
|
||||
it("allows nested sheets inside a parent sheet without creating a sibling provider", () => {
|
||||
const { getAllByTestId } = render(
|
||||
<IsolatedBottomSheetModal index={0} snapPoints={SNAP_POINTS_90}>
|
||||
<IsolatedBottomSheetModal index={0} snapPoints={SNAP_POINTS_60} onChange={() => {}}>
|
||||
<IsolatedBottomSheetModal index={0} snapPoints={SNAP_POINTS_60} onChange={noop}>
|
||||
<div>Nested model picker</div>
|
||||
</IsolatedBottomSheetModal>
|
||||
</IsolatedBottomSheetModal>,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useMemo, type ReactNode } from "react";
|
||||
import { Pressable, Text, View } from "react-native";
|
||||
import { useCallback, useMemo, type ReactNode } from "react";
|
||||
import { Pressable, Text, View, type PressableStateCallbackType } from "react-native";
|
||||
import type { StyleProp, TextStyle, ViewStyle } from "react-native";
|
||||
import { StyleSheet, useUnistyles } from "react-native-unistyles";
|
||||
|
||||
@@ -61,11 +61,8 @@ export function SegmentedControl<T extends string>({
|
||||
hideLabels={hideLabels}
|
||||
segmentSizeStyle={segmentSizeStyle}
|
||||
labelSizeStyle={labelSizeStyle}
|
||||
onPress={() => {
|
||||
if (!option.disabled && option.value !== value) {
|
||||
onValueChange(option.value);
|
||||
}
|
||||
}}
|
||||
currentValue={value}
|
||||
onValueChange={onValueChange}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
@@ -81,7 +78,8 @@ function SegmentItem<T extends string>({
|
||||
hideLabels,
|
||||
segmentSizeStyle,
|
||||
labelSizeStyle,
|
||||
onPress,
|
||||
currentValue,
|
||||
onValueChange,
|
||||
}: {
|
||||
option: SegmentedControlOption<T>;
|
||||
isSelected: boolean;
|
||||
@@ -90,27 +88,37 @@ function SegmentItem<T extends string>({
|
||||
hideLabels: boolean;
|
||||
segmentSizeStyle: StyleProp<ViewStyle>;
|
||||
labelSizeStyle: StyleProp<TextStyle>;
|
||||
onPress: () => void;
|
||||
currentValue: T;
|
||||
onValueChange: (value: T) => void;
|
||||
}) {
|
||||
const labelStyle = useMemo(
|
||||
() => [styles.label, labelSizeStyle, isSelected && styles.labelSelected],
|
||||
[labelSizeStyle, isSelected],
|
||||
);
|
||||
const handlePress = useCallback(() => {
|
||||
if (!option.disabled && option.value !== currentValue) {
|
||||
onValueChange(option.value);
|
||||
}
|
||||
}, [option.disabled, option.value, currentValue, onValueChange]);
|
||||
const pressableStyle = useCallback(
|
||||
({ hovered, pressed }: PressableStateCallbackType & { hovered?: boolean }) => [
|
||||
styles.segment,
|
||||
segmentSizeStyle,
|
||||
isSelected && styles.segmentSelected,
|
||||
Boolean(hovered) && !isSelected && styles.segmentHover,
|
||||
pressed && !isSelected && styles.segmentPressed,
|
||||
option.disabled && styles.segmentDisabled,
|
||||
],
|
||||
[isSelected, option.disabled, segmentSizeStyle],
|
||||
);
|
||||
return (
|
||||
<Pressable
|
||||
accessibilityRole="button"
|
||||
accessibilityState={{ selected: isSelected, disabled: option.disabled }}
|
||||
disabled={option.disabled}
|
||||
testID={option.testID}
|
||||
onPress={onPress}
|
||||
style={({ hovered, pressed }) => [
|
||||
styles.segment,
|
||||
segmentSizeStyle,
|
||||
isSelected && styles.segmentSelected,
|
||||
hovered && !isSelected && styles.segmentHover,
|
||||
pressed && !isSelected && styles.segmentPressed,
|
||||
option.disabled && styles.segmentDisabled,
|
||||
]}
|
||||
onPress={handlePress}
|
||||
style={pressableStyle}
|
||||
>
|
||||
{option.icon ? (
|
||||
<View style={styles.iconContainer}>
|
||||
|
||||
@@ -494,6 +494,8 @@ export function TooltipContent({
|
||||
[maxWidth, style, position?.x, position?.y],
|
||||
);
|
||||
|
||||
const handleDismiss = useCallback(() => ctx.setOpen(false), [ctx]);
|
||||
|
||||
if (!ctx.open || !ctx.enabled) return null;
|
||||
|
||||
// On web, avoid React Native's <Modal/> implementation (it uses <dialog> and can
|
||||
@@ -525,9 +527,9 @@ export function TooltipContent({
|
||||
transparent
|
||||
animationType="none"
|
||||
statusBarTranslucent={Platform.OS === "android"}
|
||||
onRequestClose={() => ctx.setOpen(false)}
|
||||
onRequestClose={handleDismiss}
|
||||
>
|
||||
<Pressable style={styles.overlay} onPress={() => ctx.setOpen(false)}>
|
||||
<Pressable style={styles.overlay} onPress={handleDismiss}>
|
||||
<Animated.View
|
||||
pointerEvents="none"
|
||||
entering={FadeIn.duration(80)}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { SquarePen } from "lucide-react-native";
|
||||
import { useCallback } from "react";
|
||||
import invariant from "tiny-invariant";
|
||||
import { WorkspaceDraftAgentTab } from "@/screens/workspace/workspace-draft-agent-tab";
|
||||
import { usePaneContext, usePaneFocus } from "@/panels/pane-context";
|
||||
@@ -22,6 +23,26 @@ function DraftPanel() {
|
||||
const { isInteractive } = usePaneFocus();
|
||||
invariant(target.kind === "draft", "DraftPanel requires draft target");
|
||||
|
||||
const handleOpenWorkspaceFile = useCallback(
|
||||
({ filePath }: { filePath: string }) => {
|
||||
openFileInWorkspace(filePath);
|
||||
},
|
||||
[openFileInWorkspace],
|
||||
);
|
||||
|
||||
const handleCreated = useCallback(
|
||||
(agentSnapshot: Parameters<typeof normalizeAgentSnapshot>[0]) => {
|
||||
const normalized = normalizeAgentSnapshot(agentSnapshot, serverId);
|
||||
retargetCurrentTab({ kind: "agent", agentId: agentSnapshot.id });
|
||||
useSessionStore.getState().setAgents(serverId, (prev) => {
|
||||
const next = new Map(prev);
|
||||
next.set(agentSnapshot.id, normalized);
|
||||
return next;
|
||||
});
|
||||
},
|
||||
[retargetCurrentTab, serverId],
|
||||
);
|
||||
|
||||
return (
|
||||
<WorkspaceDraftAgentTab
|
||||
serverId={serverId}
|
||||
@@ -29,18 +50,8 @@ function DraftPanel() {
|
||||
tabId={tabId}
|
||||
draftId={target.draftId}
|
||||
isPaneFocused={isInteractive}
|
||||
onOpenWorkspaceFile={({ filePath }) => {
|
||||
openFileInWorkspace(filePath);
|
||||
}}
|
||||
onCreated={(agentSnapshot) => {
|
||||
const normalized = normalizeAgentSnapshot(agentSnapshot, serverId);
|
||||
retargetCurrentTab({ kind: "agent", agentId: agentSnapshot.id });
|
||||
useSessionStore.getState().setAgents(serverId, (prev) => {
|
||||
const next = new Map(prev);
|
||||
next.set(agentSnapshot.id, normalized);
|
||||
return next;
|
||||
});
|
||||
}}
|
||||
onOpenWorkspaceFile={handleOpenWorkspaceFile}
|
||||
onCreated={handleCreated}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,13 +1,23 @@
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { CheckCircle2, ChevronRight, CircleAlert, SquareTerminal } from "lucide-react-native";
|
||||
import { ActivityIndicator, Pressable, ScrollView, Text, View } from "react-native";
|
||||
import {
|
||||
ActivityIndicator,
|
||||
Pressable,
|
||||
type PressableStateCallbackType,
|
||||
ScrollView,
|
||||
Text,
|
||||
View,
|
||||
} from "react-native";
|
||||
import invariant from "tiny-invariant";
|
||||
import { StyleSheet, useUnistyles } from "react-native-unistyles";
|
||||
import { Fonts } from "@/constants/theme";
|
||||
import { usePaneContext } from "@/panels/pane-context";
|
||||
import type { PanelDescriptor, PanelRegistration } from "@/panels/panel-registry";
|
||||
import { buildWorkspaceTabPersistenceKey } from "@/stores/workspace-tabs-store";
|
||||
import { useWorkspaceSetupStore } from "@/stores/workspace-setup-store";
|
||||
import {
|
||||
useWorkspaceSetupStore,
|
||||
type WorkspaceSetupSnapshot,
|
||||
} from "@/stores/workspace-setup-store";
|
||||
import { useHostRuntimeClient } from "@/runtime/host-runtime";
|
||||
|
||||
function useSetupPanelDescriptor(
|
||||
@@ -223,67 +233,19 @@ function SetupPanel() {
|
||||
const processedLog = hasLog ? processCarriageReturns(commandLog) : "";
|
||||
|
||||
return (
|
||||
<View key={`${command.index}:${command.command}`} style={styles.commandItem}>
|
||||
<Pressable
|
||||
onPress={() => toggleExpanded(command.index, isAutoExpanded)}
|
||||
style={({ pressed }) => [
|
||||
styles.commandRow,
|
||||
showDetail && styles.commandRowExpanded,
|
||||
pressed && styles.commandRowPressed,
|
||||
]}
|
||||
accessibilityRole="button"
|
||||
accessibilityState={{ expanded: showDetail }}
|
||||
>
|
||||
<View style={styles.commandStatusIcon}>
|
||||
<CommandStatusIcon status={command.status} />
|
||||
</View>
|
||||
<Text style={styles.commandText} numberOfLines={1}>
|
||||
{command.command}
|
||||
</Text>
|
||||
{command.durationMs != null ? (
|
||||
<Text style={styles.commandDuration}>{formatDuration(command.durationMs)}</Text>
|
||||
) : null}
|
||||
<SetupCommandChevron
|
||||
showDetail={showDetail}
|
||||
color={theme.colors.foregroundMuted}
|
||||
/>
|
||||
</Pressable>
|
||||
{showDetail ? (
|
||||
<View style={styles.commandDetail}>
|
||||
{hasLog ? (
|
||||
<ScrollView
|
||||
style={styles.logScroll}
|
||||
contentContainerStyle={styles.logScrollContent}
|
||||
horizontal={false}
|
||||
showsVerticalScrollIndicator
|
||||
testID="workspace-setup-log"
|
||||
accessible
|
||||
accessibilityLabel="Workspace setup log"
|
||||
>
|
||||
<Text selectable style={styles.logText}>
|
||||
{processedLog}
|
||||
</Text>
|
||||
</ScrollView>
|
||||
) : (
|
||||
<View
|
||||
style={styles.logScrollContent}
|
||||
testID="workspace-setup-log"
|
||||
accessible
|
||||
accessibilityLabel="Workspace setup log"
|
||||
>
|
||||
<Text style={styles.emptyLogText}>No output</Text>
|
||||
</View>
|
||||
)}
|
||||
{hasError ? (
|
||||
<View style={styles.errorCard}>
|
||||
<Text selectable style={styles.errorText}>
|
||||
{snapshot.error}
|
||||
</Text>
|
||||
</View>
|
||||
) : null}
|
||||
</View>
|
||||
) : null}
|
||||
</View>
|
||||
<SetupCommandRow
|
||||
key={`${command.index}:${command.command}`}
|
||||
command={command}
|
||||
showDetail={showDetail}
|
||||
isAutoExpanded={isAutoExpanded}
|
||||
isExpandable={isExpandable}
|
||||
hasLog={hasLog}
|
||||
hasError={!!hasError}
|
||||
processedLog={processedLog}
|
||||
errorMessage={snapshot?.error ?? null}
|
||||
foregroundMutedColor={theme.colors.foregroundMuted}
|
||||
onToggle={toggleExpanded}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
||||
@@ -317,6 +279,105 @@ function SetupPanel() {
|
||||
);
|
||||
}
|
||||
|
||||
type SetupCommand = WorkspaceSetupSnapshot["detail"]["commands"][number];
|
||||
|
||||
interface SetupCommandRowProps {
|
||||
command: SetupCommand;
|
||||
showDetail: boolean;
|
||||
isAutoExpanded: boolean;
|
||||
isExpandable: boolean;
|
||||
hasLog: boolean;
|
||||
hasError: boolean;
|
||||
processedLog: string;
|
||||
errorMessage: string | null;
|
||||
foregroundMutedColor: string;
|
||||
onToggle: (index: number, isAutoExpanded: boolean) => void;
|
||||
}
|
||||
|
||||
function SetupCommandRow({
|
||||
command,
|
||||
showDetail,
|
||||
isAutoExpanded,
|
||||
isExpandable,
|
||||
hasLog,
|
||||
hasError,
|
||||
processedLog,
|
||||
errorMessage,
|
||||
foregroundMutedColor,
|
||||
onToggle,
|
||||
}: SetupCommandRowProps) {
|
||||
const handlePress = useCallback(() => {
|
||||
if (!isExpandable) return;
|
||||
onToggle(command.index, isAutoExpanded);
|
||||
}, [command.index, isAutoExpanded, isExpandable, onToggle]);
|
||||
|
||||
const pressableStyle = useCallback(
|
||||
({ pressed }: PressableStateCallbackType) => [
|
||||
styles.commandRow,
|
||||
showDetail && styles.commandRowExpanded,
|
||||
pressed && styles.commandRowPressed,
|
||||
],
|
||||
[showDetail],
|
||||
);
|
||||
|
||||
return (
|
||||
<View style={styles.commandItem}>
|
||||
<Pressable
|
||||
onPress={handlePress}
|
||||
style={pressableStyle}
|
||||
accessibilityRole="button"
|
||||
accessibilityState={{ expanded: showDetail }}
|
||||
>
|
||||
<View style={styles.commandStatusIcon}>
|
||||
<CommandStatusIcon status={command.status} />
|
||||
</View>
|
||||
<Text style={styles.commandText} numberOfLines={1}>
|
||||
{command.command}
|
||||
</Text>
|
||||
{command.durationMs != null ? (
|
||||
<Text style={styles.commandDuration}>{formatDuration(command.durationMs)}</Text>
|
||||
) : null}
|
||||
<SetupCommandChevron showDetail={showDetail} color={foregroundMutedColor} />
|
||||
</Pressable>
|
||||
{showDetail ? (
|
||||
<View style={styles.commandDetail}>
|
||||
{hasLog ? (
|
||||
<ScrollView
|
||||
style={styles.logScroll}
|
||||
contentContainerStyle={styles.logScrollContent}
|
||||
horizontal={false}
|
||||
showsVerticalScrollIndicator
|
||||
testID="workspace-setup-log"
|
||||
accessible
|
||||
accessibilityLabel="Workspace setup log"
|
||||
>
|
||||
<Text selectable style={styles.logText}>
|
||||
{processedLog}
|
||||
</Text>
|
||||
</ScrollView>
|
||||
) : (
|
||||
<View
|
||||
style={styles.logScrollContent}
|
||||
testID="workspace-setup-log"
|
||||
accessible
|
||||
accessibilityLabel="Workspace setup log"
|
||||
>
|
||||
<Text style={styles.emptyLogText}>No output</Text>
|
||||
</View>
|
||||
)}
|
||||
{hasError && errorMessage ? (
|
||||
<View style={styles.errorCard}>
|
||||
<Text selectable style={styles.errorText}>
|
||||
{errorMessage}
|
||||
</Text>
|
||||
</View>
|
||||
) : null}
|
||||
</View>
|
||||
) : null}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
export const setupPanelRegistration: PanelRegistration<"setup"> = {
|
||||
kind: "setup",
|
||||
component: SetupPanel,
|
||||
|
||||
@@ -47,6 +47,10 @@ function SessionsScreenContent({ serverId }: { serverId: string }) {
|
||||
return [...agents].sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime());
|
||||
}, [agents]);
|
||||
|
||||
const handleBack = useCallback(() => {
|
||||
router.navigate(buildHostOpenProjectRoute(serverId));
|
||||
}, [serverId]);
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<MenuHeader title="Sessions" />
|
||||
@@ -57,11 +61,7 @@ function SessionsScreenContent({ serverId }: { serverId: string }) {
|
||||
) : sortedAgents.length === 0 ? (
|
||||
<View style={styles.emptyContainer}>
|
||||
<Text style={styles.emptyText}>No sessions yet</Text>
|
||||
<Button
|
||||
variant="ghost"
|
||||
leftIcon={ChevronLeft}
|
||||
onPress={() => router.navigate(buildHostOpenProjectRoute(serverId))}
|
||||
>
|
||||
<Button variant="ghost" leftIcon={ChevronLeft} onPress={handleBack}>
|
||||
Back
|
||||
</Button>
|
||||
</View>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useMemo, type ReactElement, type ReactNode } from "react";
|
||||
import { Pressable, Text, View } from "react-native";
|
||||
import { useCallback, useMemo, type ReactElement, type ReactNode } from "react";
|
||||
import { Pressable, Text, View, type PressableStateCallbackType } from "react-native";
|
||||
import { Check } from "lucide-react-native";
|
||||
import { StyleSheet, useUnistyles } from "react-native-unistyles";
|
||||
import invariant from "tiny-invariant";
|
||||
@@ -184,15 +184,16 @@ export function WorkspaceTabOptionRow({
|
||||
trailingAccessory,
|
||||
}: WorkspaceTabOptionRowProps): ReactElement {
|
||||
const { theme } = useUnistyles();
|
||||
const pressableStyle = useCallback(
|
||||
({ hovered, pressed }: PressableStateCallbackType & { hovered?: boolean }) => [
|
||||
styles.optionMainPressable,
|
||||
(Boolean(hovered) || pressed || active) && styles.optionRowActive,
|
||||
],
|
||||
[active],
|
||||
);
|
||||
return (
|
||||
<View style={[styles.optionRow, active && styles.optionRowActive]}>
|
||||
<Pressable
|
||||
onPress={onPress}
|
||||
style={({ hovered = false, pressed }) => [
|
||||
styles.optionMainPressable,
|
||||
(hovered || pressed || active) && styles.optionRowActive,
|
||||
]}
|
||||
>
|
||||
<Pressable onPress={onPress} style={pressableStyle}>
|
||||
<View style={styles.optionLeadingSlot}>
|
||||
<WorkspaceTabIcon presentation={presentation} active={selected || active} />
|
||||
</View>
|
||||
|
||||
Reference in New Issue
Block a user