mirror of
https://github.com/getpaseo/paseo.git
synced 2026-07-29 12:01:31 +00:00
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:
@@ -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" />
|
||||
|
||||
@@ -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} />;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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} />;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
) : (
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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 />
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
|
||||
@@ -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"
|
||||
/>
|
||||
|
||||
@@ -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" />;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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}>
|
||||
|
||||
@@ -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) => (
|
||||
|
||||
@@ -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" }];
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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"
|
||||
>
|
||||
|
||||
@@ -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],
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
) : (
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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} />
|
||||
|
||||
@@ -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."
|
||||
|
||||
@@ -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}>
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -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
|
||||
>
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user