Open summarized tool calls in mobile sheets (#3619)
* feat(app): open summarized tool calls in mobile sheets Keep compact timelines readable by moving summarized tool-call groups into a live sheet while preserving the existing inline desktop interaction. Individual calls reuse the current detail sheet and stack above the group. * fix(app): keep mobile tool call sheets current Restore live group-sheet auto-follow, replace the mocked component coverage with real compact and desktop browser journeys, and remove the touched theme subscriptions.
This commit is contained in:
@@ -1,17 +1,14 @@
|
||||
import React, { createContext, useContext, useCallback, useMemo } from "react";
|
||||
import type { ReactNode } from "react";
|
||||
import { View, Text, Pressable } from "react-native";
|
||||
import { StyleSheet, useUnistyles } from "react-native-unistyles";
|
||||
import Animated from "react-native-reanimated";
|
||||
import {
|
||||
BottomSheetScrollView,
|
||||
BottomSheetBackdrop,
|
||||
BottomSheetBackgroundProps,
|
||||
} from "@gorhom/bottom-sheet";
|
||||
import { StyleSheet, withUnistyles } from "react-native-unistyles";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { BottomSheetScrollView, BottomSheetBackdrop } 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";
|
||||
@@ -45,17 +42,43 @@ export function useToolCallSheet(): ToolCallSheetContextValue {
|
||||
return context;
|
||||
}
|
||||
|
||||
// ----- Custom Background Component -----
|
||||
|
||||
function CustomSheetBackground({ style }: BottomSheetBackgroundProps) {
|
||||
const { theme } = useUnistyles();
|
||||
const containerStyle = useMemo(
|
||||
() => [style, { backgroundColor: theme.colors.surface2, borderRadius: 16 }],
|
||||
[style, theme.colors.surface2],
|
||||
export function useToolCallSheetContextBridge(): ContextBridge {
|
||||
const context = useToolCallSheet();
|
||||
return useCallback(
|
||||
(children) => (
|
||||
<ToolCallSheetContext.Provider value={context}>{children}</ToolCallSheetContext.Provider>
|
||||
),
|
||||
[context],
|
||||
);
|
||||
return <Animated.View pointerEvents="none" style={containerStyle} />;
|
||||
}
|
||||
|
||||
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 {
|
||||
@@ -63,7 +86,6 @@ interface ToolCallSheetProviderProps {
|
||||
}
|
||||
|
||||
export function ToolCallSheetProvider({ children }: ToolCallSheetProviderProps) {
|
||||
const { theme } = useUnistyles();
|
||||
const [sheetData, setSheetData] = React.useState<ToolCallSheetData | null>(null);
|
||||
const [isSheetOpen, setIsSheetOpen] = React.useState(false);
|
||||
|
||||
@@ -104,15 +126,10 @@ 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}
|
||||
<IsolatedBottomSheetModal
|
||||
<ToolCallSheetModal
|
||||
ref={bottomSheetRef}
|
||||
contextBridge={null}
|
||||
snapPoints={snapPoints}
|
||||
@@ -122,11 +139,9 @@ export function ToolCallSheetProvider({ children }: ToolCallSheetProviderProps)
|
||||
onDismiss={handleToolCallSheetDismiss}
|
||||
backdropComponent={renderBackdrop}
|
||||
enablePanDownToClose
|
||||
backgroundComponent={CustomSheetBackground}
|
||||
handleIndicatorStyle={handleIndicatorStyle}
|
||||
>
|
||||
{sheetData && <ToolCallSheetContent data={sheetData} onClose={closeToolCall} />}
|
||||
</IsolatedBottomSheetModal>
|
||||
</ToolCallSheetModal>
|
||||
</ToolCallSheetContext.Provider>
|
||||
);
|
||||
}
|
||||
@@ -139,7 +154,7 @@ interface ToolCallSheetContentProps {
|
||||
}
|
||||
|
||||
function ToolCallSheetContent({ data, onClose }: ToolCallSheetContentProps) {
|
||||
const { theme } = useUnistyles();
|
||||
const { t } = useTranslation();
|
||||
const { displayName, detail, errorText, icon: IconComponent, showLoadingSkeleton } = data;
|
||||
|
||||
return (
|
||||
@@ -147,13 +162,19 @@ function ToolCallSheetContent({ data, onClose }: ToolCallSheetContentProps) {
|
||||
{/* Header */}
|
||||
<View style={styles.header}>
|
||||
<View style={styles.headerLeft}>
|
||||
<IconComponent size={20} color={theme.colors.foreground} />
|
||||
<ThemedToolCallHeaderIcon icon={IconComponent} size={20} />
|
||||
<Text style={styles.headerTitle} numberOfLines={1}>
|
||||
{displayName}
|
||||
</Text>
|
||||
</View>
|
||||
<Pressable onPress={onClose} style={styles.closeButton}>
|
||||
<X size={20} color={theme.colors.foregroundMuted} />
|
||||
<Pressable
|
||||
onPress={onClose}
|
||||
style={styles.closeButton}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={t("common.actions.close")}
|
||||
testID="tool-call-sheet-close"
|
||||
>
|
||||
<ThemedCloseIcon size={20} />
|
||||
</Pressable>
|
||||
</View>
|
||||
|
||||
|
||||
@@ -0,0 +1,138 @@
|
||||
import { useCallback, useEffect, useRef, type ReactNode } from "react";
|
||||
import { Pressable, Text, View } from "react-native";
|
||||
import {
|
||||
BottomSheetBackdrop,
|
||||
BottomSheetScrollView,
|
||||
type BottomSheetScrollViewMethods,
|
||||
} from "@gorhom/bottom-sheet";
|
||||
import { Wrench, X } from "lucide-react-native";
|
||||
import { StyleSheet, withUnistyles } from "react-native-unistyles";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useIsolatedBottomSheetVisibility } from "@/components/ui/isolated-bottom-sheet-modal";
|
||||
import { ToolCallSheetModal, useToolCallSheetContextBridge } from "@/components/tool-call-sheet";
|
||||
import type { Theme } from "@/styles/theme";
|
||||
|
||||
interface OverviewToolCallGroupSheetProps {
|
||||
visible: boolean;
|
||||
summary: string;
|
||||
children: ReactNode;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
const SNAP_POINTS = ["60%", "95%"];
|
||||
const ThemedWrench = withUnistyles(Wrench);
|
||||
const ThemedX = withUnistyles(X);
|
||||
const foregroundMapping = (theme: Theme) => ({ color: theme.colors.foreground });
|
||||
const foregroundMutedMapping = (theme: Theme) => ({ color: theme.colors.foregroundMuted });
|
||||
|
||||
export function OverviewToolCallGroupSheet({
|
||||
visible,
|
||||
summary,
|
||||
children,
|
||||
onClose,
|
||||
}: OverviewToolCallGroupSheetProps) {
|
||||
const { t } = useTranslation();
|
||||
const scrollRef = useRef<BottomSheetScrollViewMethods>(null);
|
||||
const contextBridge = useToolCallSheetContextBridge();
|
||||
const { sheetRef, handleSheetChange, handleSheetDismiss } = useIsolatedBottomSheetVisibility({
|
||||
visible,
|
||||
onClose,
|
||||
});
|
||||
const renderBackdrop = useCallback(
|
||||
(props: React.ComponentProps<typeof BottomSheetBackdrop>) => (
|
||||
<BottomSheetBackdrop {...props} disappearsOnIndex={-1} appearsOnIndex={0} opacity={0.5} />
|
||||
),
|
||||
[],
|
||||
);
|
||||
const scrollToLatest = useCallback(() => {
|
||||
scrollRef.current?.scrollToEnd({ animated: false });
|
||||
}, []);
|
||||
useEffect(() => {
|
||||
if (!visible) {
|
||||
return;
|
||||
}
|
||||
const frame = requestAnimationFrame(scrollToLatest);
|
||||
return () => cancelAnimationFrame(frame);
|
||||
}, [children, scrollToLatest, visible]);
|
||||
|
||||
return (
|
||||
<ToolCallSheetModal
|
||||
ref={sheetRef}
|
||||
contextBridge={contextBridge}
|
||||
snapPoints={SNAP_POINTS}
|
||||
index={0}
|
||||
enableDynamicSizing={false}
|
||||
onChange={handleSheetChange}
|
||||
onDismiss={handleSheetDismiss}
|
||||
backdropComponent={renderBackdrop}
|
||||
enablePanDownToClose
|
||||
>
|
||||
<View style={styles.container} testID="tool-call-group-sheet">
|
||||
<View style={styles.header}>
|
||||
<View style={styles.headerLeft}>
|
||||
<ThemedWrench size={20} uniProps={foregroundMapping} />
|
||||
<Text
|
||||
style={styles.headerTitle}
|
||||
numberOfLines={2}
|
||||
testID="tool-call-group-sheet-summary"
|
||||
>
|
||||
{summary}
|
||||
</Text>
|
||||
</View>
|
||||
<Pressable
|
||||
onPress={onClose}
|
||||
style={styles.closeButton}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={t("common.actions.close")}
|
||||
testID="tool-call-group-sheet-close"
|
||||
>
|
||||
<ThemedX size={20} uniProps={foregroundMutedMapping} />
|
||||
</Pressable>
|
||||
</View>
|
||||
<BottomSheetScrollView ref={scrollRef} style={styles.content}>
|
||||
<View style={styles.contentContainer}>{children}</View>
|
||||
</BottomSheetScrollView>
|
||||
</View>
|
||||
</ToolCallSheetModal>
|
||||
);
|
||||
}
|
||||
|
||||
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.medium,
|
||||
color: theme.colors.foreground,
|
||||
flex: 1,
|
||||
},
|
||||
closeButton: {
|
||||
padding: theme.spacing[2],
|
||||
},
|
||||
content: {
|
||||
flex: 1,
|
||||
minHeight: 0,
|
||||
backgroundColor: theme.colors.surface2,
|
||||
},
|
||||
contentContainer: {
|
||||
paddingHorizontal: 13,
|
||||
paddingVertical: 8,
|
||||
},
|
||||
}));
|
||||
@@ -1,10 +1,12 @@
|
||||
import { memo, useCallback, useMemo, useRef, type ReactNode } from "react";
|
||||
import React, { memo, useCallback, useMemo, useRef, type ReactNode } from "react";
|
||||
import { ScrollView } from "react-native";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Wrench } from "lucide-react-native";
|
||||
import { StyleSheet } from "react-native-unistyles";
|
||||
import { ExpandableBadge } from "@/components/message";
|
||||
import { useIsCompactFormFactor } from "@/constants/layout";
|
||||
import { type OverviewSummary, type OverviewToolCallGroup } from "./model";
|
||||
import { OverviewToolCallGroupSheet } from "./sheet";
|
||||
|
||||
interface OverviewGroupProps {
|
||||
group: OverviewToolCallGroup;
|
||||
@@ -59,6 +61,7 @@ export const OverviewToolCallGroupView = memo(function OverviewToolCallGroupView
|
||||
children,
|
||||
}: OverviewGroupProps) {
|
||||
const scrollRef = useRef<ScrollView>(null);
|
||||
const isCompact = useIsCompactFormFactor();
|
||||
const aggregateSummary = useOverviewSummary(group.summary);
|
||||
const scrollToLatest = useCallback(() => {
|
||||
scrollRef.current?.scrollToEnd({ animated: false });
|
||||
@@ -66,6 +69,9 @@ export const OverviewToolCallGroupView = memo(function OverviewToolCallGroupView
|
||||
const toggle = useCallback(() => {
|
||||
onExpandedChange(group.run.id, !expanded);
|
||||
}, [expanded, group.run.id, onExpandedChange]);
|
||||
const close = useCallback(() => {
|
||||
onExpandedChange(group.run.id, false);
|
||||
}, [group.run.id, onExpandedChange]);
|
||||
const renderDetails = useCallback(
|
||||
() => (
|
||||
<ScrollView
|
||||
@@ -82,6 +88,25 @@ export const OverviewToolCallGroupView = memo(function OverviewToolCallGroupView
|
||||
[children, scrollToLatest],
|
||||
);
|
||||
|
||||
if (isCompact) {
|
||||
return (
|
||||
<>
|
||||
<ExpandableBadge
|
||||
testID="tool-call-group"
|
||||
label={aggregateSummary}
|
||||
icon={Wrench}
|
||||
isLoading={group.isLoading}
|
||||
isExpanded={false}
|
||||
isLastInSequence={isLastInSequence}
|
||||
onToggle={toggle}
|
||||
/>
|
||||
<OverviewToolCallGroupSheet visible={expanded} summary={aggregateSummary} onClose={close}>
|
||||
{children}
|
||||
</OverviewToolCallGroupSheet>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<ExpandableBadge
|
||||
testID="tool-call-group"
|
||||
|
||||
Reference in New Issue
Block a user