mirror of
https://github.com/getpaseo/paseo.git
synced 2026-11-09 18:04:55 +00:00
* feat(app): streamline sidebar usage and stacked sheets * fix(app): restore compact sidebar navigation clicks The close button container emitted box-none through Unistyles as invalid CSS and intercepted New workspace clicks. Let React Native apply click-through behavior at the view boundary. Keep the import journey on its existing New workspace screen and assert only the visible transcript. * fix(app): cap menu sheets at eighty percent height Keep long sidebar usage reports recognizably sheet-sized while allowing the content to scroll. Short menu sheets retain dynamic sizing.
237 lines
6.2 KiB
TypeScript
237 lines
6.2 KiB
TypeScript
import React, { createContext, useContext, useCallback, useMemo } from "react";
|
|
import type { ReactNode } from "react";
|
|
import { View, Text, Pressable } from "react-native";
|
|
import { StyleSheet, withUnistyles } from "react-native-unistyles";
|
|
import { useTranslation } from "react-i18next";
|
|
import { BottomSheetScrollView } from "@gorhom/bottom-sheet";
|
|
import { X } from "lucide-react-native";
|
|
import type { ToolCallDetail } from "@getpaseo/protocol/agent-types";
|
|
import {
|
|
IsolatedBottomSheetModal,
|
|
type ContextBridge,
|
|
useIsolatedBottomSheetVisibility,
|
|
} from "@/components/ui/isolated-bottom-sheet-modal";
|
|
import type { ToolCallIconComponent } from "@/utils/tool-call-icon";
|
|
import { ToolCallDetailsContent } from "./tool-call-details";
|
|
|
|
// ----- Types -----
|
|
|
|
export interface ToolCallSheetData {
|
|
toolName: string;
|
|
displayName: string;
|
|
summary?: string;
|
|
detail?: ToolCallDetail;
|
|
errorText?: string;
|
|
icon: ToolCallIconComponent;
|
|
showLoadingSkeleton?: boolean;
|
|
}
|
|
|
|
interface ToolCallSheetContextValue {
|
|
openToolCall: (data: ToolCallSheetData) => void;
|
|
closeToolCall: () => void;
|
|
}
|
|
|
|
// ----- Context -----
|
|
|
|
const ToolCallSheetContext = createContext<ToolCallSheetContextValue | null>(null);
|
|
|
|
export function useToolCallSheet(): ToolCallSheetContextValue {
|
|
const context = useContext(ToolCallSheetContext);
|
|
if (!context) {
|
|
throw new Error("useToolCallSheet must be used within a ToolCallSheetProvider");
|
|
}
|
|
return context;
|
|
}
|
|
|
|
export function useToolCallSheetContextBridge(): ContextBridge {
|
|
const context = useToolCallSheet();
|
|
return useCallback(
|
|
(children) => (
|
|
<ToolCallSheetContext.Provider value={context}>{children}</ToolCallSheetContext.Provider>
|
|
),
|
|
[context],
|
|
);
|
|
}
|
|
|
|
interface ToolCallHeaderIconProps {
|
|
icon: ToolCallIconComponent;
|
|
size: number;
|
|
color?: string;
|
|
}
|
|
|
|
function ToolCallHeaderIcon({ icon: Icon, size, color }: ToolCallHeaderIconProps) {
|
|
return <Icon size={size} color={color} />;
|
|
}
|
|
|
|
const ThemedToolCallHeaderIcon = withUnistyles(ToolCallHeaderIcon, (theme) => ({
|
|
color: theme.colors.foreground,
|
|
}));
|
|
const ThemedCloseIcon = withUnistyles(X, (theme) => ({
|
|
color: theme.colors.foregroundMuted,
|
|
}));
|
|
|
|
export const ToolCallSheetModal = withUnistyles(IsolatedBottomSheetModal, (theme) => ({
|
|
backgroundStyle: {
|
|
backgroundColor: theme.colors.surface2,
|
|
borderRadius: 16,
|
|
},
|
|
handleIndicatorStyle: {
|
|
backgroundColor: theme.colors.palette.zinc[600],
|
|
},
|
|
}));
|
|
|
|
// ----- Provider Component -----
|
|
|
|
interface ToolCallSheetProviderProps {
|
|
children: ReactNode;
|
|
}
|
|
|
|
export function ToolCallSheetProvider({ children }: ToolCallSheetProviderProps) {
|
|
const [sheetData, setSheetData] = React.useState<ToolCallSheetData | null>(null);
|
|
const [isSheetOpen, setIsSheetOpen] = React.useState(false);
|
|
|
|
const snapPoints = useMemo(() => ["60%", "95%"], []);
|
|
|
|
const openToolCall = useCallback((data: ToolCallSheetData) => {
|
|
setSheetData(data);
|
|
setIsSheetOpen(true);
|
|
}, []);
|
|
|
|
const closeToolCall = useCallback(() => {
|
|
setIsSheetOpen(false);
|
|
}, []);
|
|
|
|
const {
|
|
sheetRef: bottomSheetRef,
|
|
handleSheetChange,
|
|
handleSheetDismiss,
|
|
} = useIsolatedBottomSheetVisibility({
|
|
visible: isSheetOpen,
|
|
onClose: closeToolCall,
|
|
});
|
|
|
|
const handleToolCallSheetDismiss = useCallback(() => {
|
|
handleSheetDismiss();
|
|
setSheetData(null);
|
|
}, [handleSheetDismiss]);
|
|
|
|
const contextValue = useMemo(
|
|
() => ({ openToolCall, closeToolCall }),
|
|
[openToolCall, closeToolCall],
|
|
);
|
|
|
|
return (
|
|
<ToolCallSheetContext.Provider value={contextValue}>
|
|
{children}
|
|
<ToolCallSheetModal
|
|
ref={bottomSheetRef}
|
|
contextBridge={null}
|
|
snapPoints={snapPoints}
|
|
index={0}
|
|
enableDynamicSizing={false}
|
|
onChange={handleSheetChange}
|
|
onDismiss={handleToolCallSheetDismiss}
|
|
backdropOpacity={0.5}
|
|
enablePanDownToClose
|
|
>
|
|
{sheetData && <ToolCallSheetContent data={sheetData} onClose={closeToolCall} />}
|
|
</ToolCallSheetModal>
|
|
</ToolCallSheetContext.Provider>
|
|
);
|
|
}
|
|
|
|
// ----- Sheet Content Component -----
|
|
|
|
interface ToolCallSheetContentProps {
|
|
data: ToolCallSheetData;
|
|
onClose: () => void;
|
|
}
|
|
|
|
function ToolCallSheetContent({ data, onClose }: ToolCallSheetContentProps) {
|
|
const { t } = useTranslation();
|
|
const {
|
|
toolName,
|
|
displayName,
|
|
detail,
|
|
errorText,
|
|
icon: IconComponent,
|
|
showLoadingSkeleton,
|
|
} = data;
|
|
|
|
return (
|
|
<View style={styles.container}>
|
|
{/* Header */}
|
|
<View style={styles.header}>
|
|
<View style={styles.headerLeft}>
|
|
<ThemedToolCallHeaderIcon icon={IconComponent} size={20} />
|
|
<Text style={styles.headerTitle} numberOfLines={1}>
|
|
{displayName}
|
|
</Text>
|
|
</View>
|
|
<Pressable
|
|
onPress={onClose}
|
|
style={styles.closeButton}
|
|
accessibilityRole="button"
|
|
accessibilityLabel={t("common.actions.close")}
|
|
testID="tool-call-sheet-close"
|
|
>
|
|
<ThemedCloseIcon size={20} />
|
|
</Pressable>
|
|
</View>
|
|
|
|
{/* Content */}
|
|
<BottomSheetScrollView style={styles.content} contentContainerStyle={styles.contentContainer}>
|
|
<ToolCallDetailsContent
|
|
toolName={toolName}
|
|
detail={detail}
|
|
errorText={errorText}
|
|
fillAvailableHeight
|
|
showLoadingSkeleton={showLoadingSkeleton}
|
|
/>
|
|
</BottomSheetScrollView>
|
|
</View>
|
|
);
|
|
}
|
|
|
|
// ----- Styles -----
|
|
|
|
const styles = StyleSheet.create((theme) => ({
|
|
container: {
|
|
flex: 1,
|
|
backgroundColor: theme.colors.surface2,
|
|
},
|
|
header: {
|
|
flexDirection: "row",
|
|
alignItems: "center",
|
|
justifyContent: "space-between",
|
|
paddingHorizontal: theme.spacing[4],
|
|
paddingVertical: theme.spacing[3],
|
|
borderBottomWidth: theme.borderWidth[1],
|
|
borderBottomColor: theme.colors.border,
|
|
},
|
|
headerLeft: {
|
|
flexDirection: "row",
|
|
alignItems: "center",
|
|
gap: theme.spacing[2],
|
|
flex: 1,
|
|
},
|
|
headerTitle: {
|
|
fontSize: theme.fontSize.base,
|
|
fontWeight: theme.fontWeight.semibold,
|
|
color: theme.colors.foreground,
|
|
flex: 1,
|
|
},
|
|
closeButton: {
|
|
padding: theme.spacing[2],
|
|
},
|
|
content: {
|
|
flex: 1,
|
|
minHeight: 0,
|
|
backgroundColor: theme.colors.surface2,
|
|
},
|
|
contentContainer: {
|
|
padding: 0,
|
|
flexGrow: 1,
|
|
},
|
|
}));
|