Fix reasoning timeline UI and type errors
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import { View, Text, Pressable, Animated } from "react-native";
|
||||
import { View, Text, Pressable, Animated, ScrollView } from "react-native";
|
||||
import { useState, useEffect, useRef, memo, useMemo, useCallback } from "react";
|
||||
import type { ReactNode, ComponentType } from "react";
|
||||
import type { AgentProvider } from "@server/server/agent/agent-sdk-types";
|
||||
import { getAgentProviderDefinition } from "@server/server/agent/provider-manifest";
|
||||
import Markdown from "react-native-markdown-display";
|
||||
@@ -18,14 +19,14 @@ import {
|
||||
Pencil,
|
||||
Eye,
|
||||
SquareTerminal,
|
||||
Brain,
|
||||
Search,
|
||||
Brain,
|
||||
} from "lucide-react-native";
|
||||
import { StyleSheet, useUnistyles } from "react-native-unistyles";
|
||||
import { baseColors, theme } from "@/styles/theme";
|
||||
import { Colors } from "@/constants/theme";
|
||||
import * as Clipboard from "expo-clipboard";
|
||||
import type { TodoEntry } from "@/types/stream";
|
||||
import type { TodoEntry, ThoughtStatus } from "@/types/stream";
|
||||
import type { CommandDetails, EditEntry, ReadEntry } from "@/utils/tool-call-parsers";
|
||||
import { DiffViewer } from "./diff-viewer";
|
||||
import { resolveToolCallPreview } from "./tool-call-preview";
|
||||
@@ -246,6 +247,55 @@ export const assistantMessageStylesheet = StyleSheet.create((theme) => ({
|
||||
},
|
||||
}));
|
||||
|
||||
const expandableBadgeStylesheet = StyleSheet.create((theme) => ({
|
||||
container: {
|
||||
marginHorizontal: theme.spacing[2],
|
||||
marginBottom: theme.spacing[2],
|
||||
},
|
||||
pressable: {
|
||||
borderRadius: theme.borderRadius.lg,
|
||||
borderWidth: theme.borderWidth[1],
|
||||
borderColor: theme.colors.border,
|
||||
backgroundColor: theme.colors.secondary,
|
||||
paddingHorizontal: theme.spacing[2],
|
||||
paddingVertical: theme.spacing[1],
|
||||
},
|
||||
pressablePressed: {
|
||||
opacity: 0.9,
|
||||
},
|
||||
headerRow: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
},
|
||||
iconBadge: {
|
||||
width: 22,
|
||||
height: 22,
|
||||
borderRadius: 11,
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
marginRight: theme.spacing[2],
|
||||
backgroundColor: "transparent",
|
||||
},
|
||||
label: {
|
||||
flex: 1,
|
||||
color: theme.colors.mutedForeground,
|
||||
fontSize: theme.fontSize.base,
|
||||
fontWeight: theme.fontWeight.normal,
|
||||
},
|
||||
chevron: {
|
||||
marginLeft: theme.spacing[1],
|
||||
},
|
||||
detailWrapper: {
|
||||
marginTop: theme.spacing[2],
|
||||
borderRadius: theme.borderRadius.md,
|
||||
borderWidth: theme.borderWidth[1],
|
||||
borderColor: theme.colors.border,
|
||||
backgroundColor: theme.colors.background,
|
||||
padding: theme.spacing[2],
|
||||
gap: theme.spacing[2],
|
||||
},
|
||||
}));
|
||||
|
||||
function isLikelyPathToken(value: string): boolean {
|
||||
if (!value || value.length > 300) {
|
||||
return false;
|
||||
@@ -848,74 +898,187 @@ export const TodoListCard = memo(function TodoListCard({
|
||||
|
||||
interface AgentThoughtMessageProps {
|
||||
message: string;
|
||||
status?: ThoughtStatus;
|
||||
}
|
||||
|
||||
interface ExpandableBadgeProps {
|
||||
label: string;
|
||||
icon?: ComponentType<{ size?: number; color?: string }>;
|
||||
isExpanded: boolean;
|
||||
onToggle: () => void;
|
||||
renderDetails?: () => ReactNode;
|
||||
isLoading?: boolean;
|
||||
isError?: boolean;
|
||||
}
|
||||
|
||||
const ExpandableBadge = memo(function ExpandableBadge({
|
||||
label,
|
||||
icon,
|
||||
isExpanded,
|
||||
onToggle,
|
||||
renderDetails,
|
||||
isLoading = false,
|
||||
isError = false,
|
||||
}: ExpandableBadgeProps) {
|
||||
const { theme } = useUnistyles();
|
||||
const hasDetails = Boolean(renderDetails);
|
||||
const detailContent = hasDetails && isExpanded ? renderDetails?.() : null;
|
||||
|
||||
const spinAnim = useRef(new Animated.Value(0)).current;
|
||||
|
||||
useEffect(() => {
|
||||
let loop: Animated.CompositeAnimation | null = null;
|
||||
if (isLoading) {
|
||||
spinAnim.setValue(0);
|
||||
loop = Animated.loop(
|
||||
Animated.timing(spinAnim, {
|
||||
toValue: 1,
|
||||
duration: 1400,
|
||||
useNativeDriver: true,
|
||||
easing: (t) => t,
|
||||
})
|
||||
);
|
||||
loop.start();
|
||||
} else {
|
||||
spinAnim.stopAnimation();
|
||||
}
|
||||
|
||||
return () => {
|
||||
loop?.stop();
|
||||
};
|
||||
}, [isLoading, spinAnim]);
|
||||
|
||||
const spin = spinAnim.interpolate({
|
||||
inputRange: [0, 1],
|
||||
outputRange: ["0deg", "360deg"],
|
||||
});
|
||||
|
||||
const IconComponent = icon;
|
||||
const iconColor = isError
|
||||
? theme.colors.destructive
|
||||
: theme.colors.foreground;
|
||||
|
||||
let iconNode: ReactNode = null;
|
||||
if (isLoading) {
|
||||
iconNode = (
|
||||
<Animated.View style={{ transform: [{ rotate: spin }] }}>
|
||||
<Loader2 size={12} color={iconColor} />
|
||||
</Animated.View>
|
||||
);
|
||||
} else if (isError) {
|
||||
iconNode = <X size={12} color={iconColor} />;
|
||||
} else if (IconComponent) {
|
||||
iconNode = <IconComponent size={12} color={iconColor} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={expandableBadgeStylesheet.container}>
|
||||
<Pressable
|
||||
onPress={hasDetails ? onToggle : undefined}
|
||||
disabled={!hasDetails}
|
||||
accessibilityRole={hasDetails ? "button" : undefined}
|
||||
accessibilityState={hasDetails ? { expanded: isExpanded } : undefined}
|
||||
style={({ pressed }) => [
|
||||
expandableBadgeStylesheet.pressable,
|
||||
pressed && hasDetails ? expandableBadgeStylesheet.pressablePressed : null,
|
||||
]}
|
||||
>
|
||||
<View style={expandableBadgeStylesheet.headerRow}>
|
||||
<View style={expandableBadgeStylesheet.iconBadge}>{iconNode}</View>
|
||||
<Text style={expandableBadgeStylesheet.label} numberOfLines={1}>
|
||||
{label}
|
||||
</Text>
|
||||
{hasDetails ? (
|
||||
<ChevronRight
|
||||
size={14}
|
||||
color={theme.colors.mutedForeground}
|
||||
style={[
|
||||
expandableBadgeStylesheet.chevron,
|
||||
{ transform: [{ rotate: isExpanded ? "90deg" : "0deg" }] },
|
||||
]}
|
||||
/>
|
||||
) : null}
|
||||
</View>
|
||||
{detailContent ? (
|
||||
<View style={expandableBadgeStylesheet.detailWrapper}>{detailContent}</View>
|
||||
) : null}
|
||||
</Pressable>
|
||||
</View>
|
||||
);
|
||||
});
|
||||
|
||||
const agentThoughtStylesheet = StyleSheet.create((theme) => ({
|
||||
container: {
|
||||
marginHorizontal: theme.spacing[2],
|
||||
marginBottom: theme.spacing[2],
|
||||
},
|
||||
card: {
|
||||
backgroundColor: theme.colors.card,
|
||||
borderRadius: theme.borderRadius.lg,
|
||||
borderWidth: theme.borderWidth[1],
|
||||
borderColor: theme.colors.border,
|
||||
paddingVertical: theme.spacing[2],
|
||||
paddingHorizontal: theme.spacing[3],
|
||||
flexDirection: "row",
|
||||
alignItems: "flex-start",
|
||||
gap: theme.spacing[3],
|
||||
},
|
||||
iconContainer: {
|
||||
width: 28,
|
||||
height: 28,
|
||||
borderRadius: 14,
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
flexShrink: 0,
|
||||
paddingTop: theme.spacing[1],
|
||||
},
|
||||
icon: {
|
||||
color: theme.colors.mutedForeground,
|
||||
opacity: 0.8,
|
||||
},
|
||||
textContainer: {
|
||||
flex: 1,
|
||||
paddingVertical: theme.spacing[1],
|
||||
},
|
||||
label: {
|
||||
emptyText: {
|
||||
color: theme.colors.mutedForeground,
|
||||
fontSize: theme.fontSize.xs,
|
||||
fontWeight: theme.fontWeight.semibold,
|
||||
textTransform: "uppercase",
|
||||
marginBottom: theme.spacing[1],
|
||||
},
|
||||
text: {
|
||||
color: theme.colors.mutedForeground,
|
||||
fontSize: theme.fontSize.base,
|
||||
lineHeight: 20,
|
||||
fontStyle: "italic" as const,
|
||||
},
|
||||
}));
|
||||
|
||||
export function AgentThoughtMessage({ message }: AgentThoughtMessageProps) {
|
||||
const messageText = useMemo(() => {
|
||||
return message
|
||||
.trim()
|
||||
.replace(/^\*\*|\*\*$/g, "")
|
||||
.trim();
|
||||
}, [message]);
|
||||
export function AgentThoughtMessage({ message, status = "ready" }: AgentThoughtMessageProps) {
|
||||
const { theme } = useUnistyles();
|
||||
const [isExpanded, setIsExpanded] = useState(false);
|
||||
const markdownContent = useMemo(() => message?.trim() ?? "", [message]);
|
||||
const markdownStyles = useMemo(
|
||||
() => ({
|
||||
body: {
|
||||
color: theme.colors.foreground,
|
||||
fontSize: theme.fontSize.sm,
|
||||
lineHeight: 20,
|
||||
},
|
||||
text: {
|
||||
color: theme.colors.foreground,
|
||||
},
|
||||
paragraph: {
|
||||
marginBottom: theme.spacing[2],
|
||||
},
|
||||
strong: {
|
||||
fontWeight: theme.fontWeight.semibold,
|
||||
},
|
||||
em: {
|
||||
fontStyle: "italic" as const,
|
||||
},
|
||||
code_inline: {
|
||||
backgroundColor: theme.colors.secondary,
|
||||
color: theme.colors.secondaryForeground,
|
||||
paddingHorizontal: theme.spacing[1],
|
||||
paddingVertical: 2,
|
||||
borderRadius: theme.borderRadius.sm,
|
||||
fontFamily: "monospace",
|
||||
fontSize: theme.fontSize.xs,
|
||||
},
|
||||
code_block: {
|
||||
backgroundColor: theme.colors.secondary,
|
||||
color: theme.colors.secondaryForeground,
|
||||
padding: theme.spacing[3],
|
||||
borderRadius: theme.borderRadius.md,
|
||||
fontFamily: "monospace",
|
||||
fontSize: theme.fontSize.sm,
|
||||
},
|
||||
}),
|
||||
[theme]
|
||||
);
|
||||
|
||||
const toggle = useCallback(() => {
|
||||
setIsExpanded((prev) => !prev);
|
||||
}, []);
|
||||
|
||||
const renderDetails = useCallback(() => {
|
||||
if (!markdownContent) {
|
||||
return <Text style={agentThoughtStylesheet.emptyText}>No captured thinking</Text>;
|
||||
}
|
||||
return <Markdown style={markdownStyles}>{markdownContent}</Markdown>;
|
||||
}, [markdownContent, markdownStyles]);
|
||||
|
||||
return (
|
||||
<View style={agentThoughtStylesheet.container}>
|
||||
<View style={agentThoughtStylesheet.card}>
|
||||
<View style={agentThoughtStylesheet.iconContainer}>
|
||||
<Brain size={18} style={agentThoughtStylesheet.icon} />
|
||||
</View>
|
||||
<View style={agentThoughtStylesheet.textContainer}>
|
||||
<Text style={agentThoughtStylesheet.text}>{messageText}</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
<ExpandableBadge
|
||||
label="Thinking"
|
||||
icon={status === "ready" ? Brain : undefined}
|
||||
isExpanded={isExpanded}
|
||||
onToggle={toggle}
|
||||
renderDetails={renderDetails}
|
||||
isLoading={status !== "ready"}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -929,77 +1092,14 @@ interface ToolCallProps {
|
||||
parsedEditEntries?: EditEntry[];
|
||||
parsedReadEntries?: ReadEntry[];
|
||||
parsedCommandDetails?: CommandDetails | null;
|
||||
onOpenDetails?: () => void;
|
||||
}
|
||||
|
||||
const toolCallStylesheet = StyleSheet.create((theme) => ({
|
||||
pressable: {
|
||||
marginHorizontal: theme.spacing[2],
|
||||
marginBottom: theme.spacing[2],
|
||||
backgroundColor: theme.colors.secondary,
|
||||
borderRadius: theme.borderRadius.lg,
|
||||
borderWidth: theme.borderWidth[1],
|
||||
borderColor: "transparent",
|
||||
overflow: "hidden",
|
||||
shadowColor: theme.colors.palette.black,
|
||||
shadowOpacity: 0.2,
|
||||
shadowOffset: { width: 0, height: 8 },
|
||||
shadowRadius: 16,
|
||||
elevation: 4,
|
||||
},
|
||||
pressableActive: {
|
||||
opacity: 0.8,
|
||||
},
|
||||
executingBorder: {
|
||||
borderColor: theme.colors.primary,
|
||||
},
|
||||
completedBorder: {
|
||||
borderColor: theme.colors.border,
|
||||
},
|
||||
failedBorder: {
|
||||
borderColor: theme.colors.destructive,
|
||||
},
|
||||
content: {
|
||||
padding: theme.spacing[3],
|
||||
},
|
||||
headerRow: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
gap: theme.spacing[2],
|
||||
},
|
||||
chevronContainer: {
|
||||
marginRight: theme.spacing[2],
|
||||
},
|
||||
toolName: {
|
||||
color: theme.colors.foreground,
|
||||
fontFamily: "monospace",
|
||||
fontWeight: theme.fontWeight.semibold,
|
||||
fontSize: theme.fontSize.sm,
|
||||
flex: 1,
|
||||
},
|
||||
statusBadge: {
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
padding: theme.spacing[1],
|
||||
borderRadius: theme.borderRadius.base,
|
||||
width: 28,
|
||||
height: 28,
|
||||
},
|
||||
executingBadgeBg: {
|
||||
backgroundColor: theme.colors.accent,
|
||||
},
|
||||
completedBadgeBg: {
|
||||
backgroundColor: "transparent",
|
||||
},
|
||||
failedBadgeBg: {
|
||||
backgroundColor: "rgba(239, 68, 68, 0.2)",
|
||||
},
|
||||
expandedContent: {
|
||||
marginTop: theme.spacing[3],
|
||||
gap: theme.spacing[2],
|
||||
detailContent: {
|
||||
gap: theme.spacing[3],
|
||||
},
|
||||
section: {
|
||||
// empty - just for grouping
|
||||
gap: theme.spacing[1],
|
||||
},
|
||||
sectionTitle: {
|
||||
color: theme.colors.mutedForeground,
|
||||
@@ -1007,26 +1107,78 @@ const toolCallStylesheet = StyleSheet.create((theme) => ({
|
||||
fontWeight: theme.fontWeight.semibold,
|
||||
textTransform: "uppercase",
|
||||
letterSpacing: 0.5,
|
||||
marginBottom: 6,
|
||||
},
|
||||
errorSectionTitle: {
|
||||
color: theme.colors.palette.red[300],
|
||||
},
|
||||
sectionContent: {
|
||||
backgroundColor: theme.colors.background,
|
||||
fileBadge: {
|
||||
alignSelf: "flex-start",
|
||||
paddingHorizontal: theme.spacing[2],
|
||||
paddingVertical: theme.spacing[1],
|
||||
borderRadius: theme.borderRadius.base,
|
||||
borderWidth: theme.borderWidth[1],
|
||||
borderColor: theme.colors.border,
|
||||
backgroundColor: theme.colors.card,
|
||||
},
|
||||
fileBadgeText: {
|
||||
color: theme.colors.foreground,
|
||||
fontFamily: "monospace",
|
||||
fontSize: theme.fontSize.xs,
|
||||
},
|
||||
diffContainer: {
|
||||
borderWidth: theme.borderWidth[1],
|
||||
borderColor: theme.colors.border,
|
||||
borderRadius: theme.borderRadius.base,
|
||||
overflow: "hidden",
|
||||
backgroundColor: theme.colors.card,
|
||||
},
|
||||
scrollArea: {
|
||||
borderWidth: theme.borderWidth[1],
|
||||
borderColor: theme.colors.border,
|
||||
borderRadius: theme.borderRadius.base,
|
||||
maxHeight: 260,
|
||||
},
|
||||
scrollContent: {
|
||||
padding: theme.spacing[2],
|
||||
},
|
||||
errorSectionContent: {
|
||||
borderColor: theme.colors.palette.red[800],
|
||||
},
|
||||
sectionText: {
|
||||
color: theme.colors.foreground,
|
||||
fontSize: theme.fontSize.xs,
|
||||
scrollText: {
|
||||
fontFamily: "monospace",
|
||||
lineHeight: 16,
|
||||
fontSize: theme.fontSize.xs,
|
||||
color: theme.colors.foreground,
|
||||
lineHeight: 18,
|
||||
},
|
||||
jsonScroll: {
|
||||
borderWidth: theme.borderWidth[1],
|
||||
borderColor: theme.colors.border,
|
||||
borderRadius: theme.borderRadius.base,
|
||||
},
|
||||
jsonScrollError: {
|
||||
borderColor: theme.colors.destructive,
|
||||
},
|
||||
jsonContent: {
|
||||
padding: theme.spacing[2],
|
||||
},
|
||||
errorText: {
|
||||
color: theme.colors.destructive,
|
||||
},
|
||||
emptyStateText: {
|
||||
color: theme.colors.mutedForeground,
|
||||
fontSize: theme.fontSize.sm,
|
||||
fontStyle: "italic",
|
||||
},
|
||||
metaRow: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
gap: theme.spacing[2],
|
||||
},
|
||||
metaLabel: {
|
||||
color: theme.colors.mutedForeground,
|
||||
fontSize: theme.fontSize.xs,
|
||||
textTransform: "uppercase",
|
||||
letterSpacing: 0.5,
|
||||
},
|
||||
metaValue: {
|
||||
color: theme.colors.foreground,
|
||||
fontFamily: "monospace",
|
||||
fontSize: theme.fontSize.xs,
|
||||
flex: 1,
|
||||
},
|
||||
}));
|
||||
|
||||
@@ -1039,24 +1191,18 @@ const toolKindIcons: Record<string, any> = {
|
||||
// Add more mappings as needed
|
||||
};
|
||||
|
||||
function formatPreviewValue(value: unknown, limit = 800): string {
|
||||
if (value === undefined || value === null) {
|
||||
function formatFullValue(value: unknown): string {
|
||||
if (value === undefined) {
|
||||
return "";
|
||||
}
|
||||
let text: string;
|
||||
if (typeof value === "string") {
|
||||
text = value.trim();
|
||||
} else {
|
||||
try {
|
||||
text = JSON.stringify(value, null, 2);
|
||||
} catch {
|
||||
text = String(value);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
if (text.length <= limit) {
|
||||
return text;
|
||||
try {
|
||||
return JSON.stringify(value, null, 2);
|
||||
} catch {
|
||||
return String(value);
|
||||
}
|
||||
return `${text.slice(0, limit)}…`;
|
||||
}
|
||||
|
||||
export const ToolCall = memo(function ToolCall({
|
||||
@@ -1069,7 +1215,6 @@ export const ToolCall = memo(function ToolCall({
|
||||
parsedEditEntries,
|
||||
parsedReadEntries,
|
||||
parsedCommandDetails,
|
||||
onOpenDetails,
|
||||
}: ToolCallProps) {
|
||||
const { editEntries, readEntries, commandDetails } = useMemo(
|
||||
() =>
|
||||
@@ -1083,227 +1228,216 @@ export const ToolCall = memo(function ToolCall({
|
||||
[args, result, parsedEditEntries, parsedReadEntries, parsedCommandDetails]
|
||||
);
|
||||
|
||||
const primaryEditEntry = editEntries[0];
|
||||
const primaryReadEntry = readEntries[0];
|
||||
const genericResult =
|
||||
result !== undefined &&
|
||||
!commandDetails?.output &&
|
||||
!primaryReadEntry &&
|
||||
!primaryEditEntry
|
||||
? formatPreviewValue(result)
|
||||
: null;
|
||||
const formattedError =
|
||||
error !== undefined ? formatPreviewValue(error ?? null, 600) : null;
|
||||
const [isExpanded, setIsExpanded] = useState(false);
|
||||
|
||||
const spinAnim = useRef(new Animated.Value(0)).current;
|
||||
const IconComponent = kind
|
||||
? toolKindIcons[kind.toLowerCase()] || Wrench
|
||||
: Wrench;
|
||||
|
||||
useEffect(() => {
|
||||
if (status === "executing") {
|
||||
// Reset to 0 before starting
|
||||
spinAnim.setValue(0);
|
||||
Animated.loop(
|
||||
Animated.timing(spinAnim, {
|
||||
toValue: 1,
|
||||
duration: 1500,
|
||||
useNativeDriver: true,
|
||||
easing: (t) => t, // Linear easing for smooth continuous rotation
|
||||
})
|
||||
).start();
|
||||
} else {
|
||||
spinAnim.stopAnimation();
|
||||
const serializedArgs = useMemo(
|
||||
() => (args !== undefined ? formatFullValue(args) : ""),
|
||||
[args]
|
||||
);
|
||||
const serializedResult = useMemo(
|
||||
() => (result !== undefined ? formatFullValue(result) : ""),
|
||||
[result]
|
||||
);
|
||||
const serializedError = useMemo(
|
||||
() => (error !== undefined ? formatFullValue(error) : ""),
|
||||
[error]
|
||||
);
|
||||
|
||||
const handleToggle = useCallback(() => {
|
||||
setIsExpanded((prev) => !prev);
|
||||
}, []);
|
||||
|
||||
const hasCommandDetails = Boolean(
|
||||
commandDetails &&
|
||||
(commandDetails.command ||
|
||||
commandDetails.cwd ||
|
||||
commandDetails.exitCode !== undefined ||
|
||||
commandDetails.output)
|
||||
);
|
||||
|
||||
function renderJsonSection(
|
||||
label: string,
|
||||
text: string,
|
||||
isError = false
|
||||
): ReactNode {
|
||||
const displayText = text.length ? text : "(empty)";
|
||||
|
||||
return (
|
||||
<View key={label} style={toolCallStylesheet.section}>
|
||||
<Text style={toolCallStylesheet.sectionTitle}>{label}</Text>
|
||||
<ScrollView
|
||||
horizontal
|
||||
nestedScrollEnabled
|
||||
style={[
|
||||
toolCallStylesheet.jsonScroll,
|
||||
isError ? toolCallStylesheet.jsonScrollError : null,
|
||||
]}
|
||||
contentContainerStyle={toolCallStylesheet.jsonContent}
|
||||
showsHorizontalScrollIndicator={true}
|
||||
>
|
||||
<Text
|
||||
style={[
|
||||
toolCallStylesheet.scrollText,
|
||||
isError ? toolCallStylesheet.errorText : null,
|
||||
]}
|
||||
>
|
||||
{displayText}
|
||||
</Text>
|
||||
</ScrollView>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const commandSection = useMemo(() => {
|
||||
if (!hasCommandDetails || !commandDetails) {
|
||||
return null;
|
||||
}
|
||||
}, [status, spinAnim]);
|
||||
return (
|
||||
<View style={toolCallStylesheet.section}>
|
||||
<Text style={toolCallStylesheet.sectionTitle}>Command</Text>
|
||||
{commandDetails.command ? (
|
||||
<ScrollView
|
||||
horizontal
|
||||
nestedScrollEnabled
|
||||
style={toolCallStylesheet.jsonScroll}
|
||||
contentContainerStyle={toolCallStylesheet.jsonContent}
|
||||
showsHorizontalScrollIndicator={true}
|
||||
>
|
||||
<Text style={toolCallStylesheet.scrollText}>
|
||||
{commandDetails.command}
|
||||
</Text>
|
||||
</ScrollView>
|
||||
) : null}
|
||||
{commandDetails.cwd ? (
|
||||
<View style={toolCallStylesheet.metaRow}>
|
||||
<Text style={toolCallStylesheet.metaLabel}>Directory</Text>
|
||||
<Text style={toolCallStylesheet.metaValue}>{commandDetails.cwd}</Text>
|
||||
</View>
|
||||
) : null}
|
||||
{commandDetails.exitCode !== undefined ? (
|
||||
<View style={toolCallStylesheet.metaRow}>
|
||||
<Text style={toolCallStylesheet.metaLabel}>Exit Code</Text>
|
||||
<Text style={toolCallStylesheet.metaValue}>
|
||||
{commandDetails.exitCode === null
|
||||
? "Unknown"
|
||||
: commandDetails.exitCode}
|
||||
</Text>
|
||||
</View>
|
||||
) : null}
|
||||
{commandDetails.output ? (
|
||||
<ScrollView
|
||||
style={toolCallStylesheet.scrollArea}
|
||||
contentContainerStyle={toolCallStylesheet.scrollContent}
|
||||
nestedScrollEnabled
|
||||
showsVerticalScrollIndicator={true}
|
||||
>
|
||||
<Text style={toolCallStylesheet.scrollText}>
|
||||
{commandDetails.output}
|
||||
</Text>
|
||||
</ScrollView>
|
||||
) : null}
|
||||
</View>
|
||||
);
|
||||
}, [commandDetails, hasCommandDetails]);
|
||||
|
||||
const spin = spinAnim.interpolate({
|
||||
inputRange: [0, 1],
|
||||
outputRange: ["0deg", "360deg"],
|
||||
});
|
||||
const editSections = useMemo(
|
||||
() =>
|
||||
editEntries.map((entry, index) => (
|
||||
<View
|
||||
key={`${entry.filePath ?? "diff"}-${index}`}
|
||||
style={toolCallStylesheet.section}
|
||||
>
|
||||
<Text style={toolCallStylesheet.sectionTitle}>Diff</Text>
|
||||
{entry.filePath ? (
|
||||
<View style={toolCallStylesheet.fileBadge}>
|
||||
<Text style={toolCallStylesheet.fileBadgeText}>{entry.filePath}</Text>
|
||||
</View>
|
||||
) : null}
|
||||
<View style={toolCallStylesheet.diffContainer}>
|
||||
<DiffViewer diffLines={entry.diffLines} maxHeight={240} />
|
||||
</View>
|
||||
</View>
|
||||
)),
|
||||
[editEntries]
|
||||
);
|
||||
|
||||
const statusConfig = {
|
||||
executing: {
|
||||
border: toolCallStylesheet.executingBorder,
|
||||
badgeBg: toolCallStylesheet.executingBadgeBg,
|
||||
color: "#fff",
|
||||
},
|
||||
completed: {
|
||||
border: toolCallStylesheet.completedBorder,
|
||||
badgeBg: toolCallStylesheet.completedBadgeBg,
|
||||
color: "#71717a",
|
||||
},
|
||||
failed: {
|
||||
border: toolCallStylesheet.failedBorder,
|
||||
badgeBg: toolCallStylesheet.failedBadgeBg,
|
||||
color: "#fca5a5",
|
||||
},
|
||||
};
|
||||
const readSections = useMemo(
|
||||
() =>
|
||||
readEntries.map((entry, index) => (
|
||||
<View
|
||||
key={`${entry.filePath ?? "read"}-${index}`}
|
||||
style={toolCallStylesheet.section}
|
||||
>
|
||||
<Text style={toolCallStylesheet.sectionTitle}>Read Result</Text>
|
||||
{entry.filePath ? (
|
||||
<View style={toolCallStylesheet.fileBadge}>
|
||||
<Text style={toolCallStylesheet.fileBadgeText}>{entry.filePath}</Text>
|
||||
</View>
|
||||
) : null}
|
||||
<ScrollView
|
||||
style={toolCallStylesheet.scrollArea}
|
||||
contentContainerStyle={toolCallStylesheet.scrollContent}
|
||||
nestedScrollEnabled
|
||||
showsVerticalScrollIndicator={true}
|
||||
>
|
||||
<Text style={toolCallStylesheet.scrollText}>{entry.content}</Text>
|
||||
</ScrollView>
|
||||
</View>
|
||||
)),
|
||||
[readEntries]
|
||||
);
|
||||
|
||||
const config = statusConfig[status];
|
||||
|
||||
// Get the appropriate icon for the tool kind
|
||||
const getToolIcon = () => {
|
||||
if (kind) {
|
||||
const IconComponent = toolKindIcons[kind.toLowerCase()] || Wrench;
|
||||
return IconComponent;
|
||||
const jsonSections = useMemo(() => {
|
||||
const sections: ReactNode[] = [];
|
||||
if (args !== undefined) {
|
||||
sections.push(renderJsonSection("Arguments", serializedArgs));
|
||||
}
|
||||
return Wrench; // Default icon
|
||||
};
|
||||
if (result !== undefined) {
|
||||
sections.push(renderJsonSection("Result", serializedResult));
|
||||
}
|
||||
if (error !== undefined) {
|
||||
sections.push(renderJsonSection("Error", serializedError, true));
|
||||
}
|
||||
return sections;
|
||||
}, [args, serializedArgs, result, serializedResult, error, serializedError]);
|
||||
|
||||
const renderDetails = useCallback(() => {
|
||||
if (
|
||||
!commandSection &&
|
||||
editSections.length === 0 &&
|
||||
readSections.length === 0 &&
|
||||
jsonSections.length === 0
|
||||
) {
|
||||
return (
|
||||
<Text style={toolCallStylesheet.emptyStateText}>
|
||||
No additional details available
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={toolCallStylesheet.detailContent}>
|
||||
{commandSection}
|
||||
{editSections}
|
||||
{readSections}
|
||||
{jsonSections}
|
||||
</View>
|
||||
);
|
||||
}, [commandSection, editSections, readSections, jsonSections]);
|
||||
|
||||
return (
|
||||
<Pressable
|
||||
onPress={onOpenDetails}
|
||||
style={[
|
||||
toolCallStylesheet.pressable,
|
||||
toolCallStylesheet.pressableActive,
|
||||
config.border,
|
||||
]}
|
||||
>
|
||||
<View style={toolCallStylesheet.content}>
|
||||
<View style={toolCallStylesheet.headerRow}>
|
||||
<View style={[toolCallStylesheet.statusBadge, config.badgeBg]}>
|
||||
{status === "executing" ? (
|
||||
<Animated.View style={{ transform: [{ rotate: spin }] }}>
|
||||
<Loader2 size={16} color={config.color} />
|
||||
</Animated.View>
|
||||
) : status === "completed" ? (
|
||||
(() => {
|
||||
const IconComponent = getToolIcon();
|
||||
return <IconComponent size={16} color={config.color} />;
|
||||
})()
|
||||
) : (
|
||||
<X size={16} color={config.color} />
|
||||
)}
|
||||
</View>
|
||||
<Text
|
||||
style={toolCallStylesheet.toolName}
|
||||
numberOfLines={1}
|
||||
ellipsizeMode="tail"
|
||||
>
|
||||
{toolName}
|
||||
</Text>
|
||||
</View>
|
||||
{(commandDetails ||
|
||||
primaryEditEntry ||
|
||||
primaryReadEntry ||
|
||||
genericResult ||
|
||||
formattedError) && (
|
||||
<View style={toolCallStylesheet.expandedContent}>
|
||||
{commandDetails &&
|
||||
(commandDetails.command ||
|
||||
commandDetails.cwd ||
|
||||
commandDetails.exitCode !== undefined ||
|
||||
commandDetails.output) && (
|
||||
<View style={toolCallStylesheet.section}>
|
||||
<Text style={toolCallStylesheet.sectionTitle}>Command</Text>
|
||||
<View style={toolCallStylesheet.sectionContent}>
|
||||
{commandDetails.command && (
|
||||
<Text
|
||||
style={toolCallStylesheet.sectionText}
|
||||
numberOfLines={2}
|
||||
>
|
||||
{commandDetails.command}
|
||||
</Text>
|
||||
)}
|
||||
{commandDetails.cwd && (
|
||||
<Text
|
||||
style={toolCallStylesheet.sectionText}
|
||||
numberOfLines={1}
|
||||
>
|
||||
{commandDetails.cwd}
|
||||
</Text>
|
||||
)}
|
||||
{commandDetails.exitCode !== undefined && (
|
||||
<Text style={toolCallStylesheet.sectionText}>
|
||||
Exit code:{" "}
|
||||
{commandDetails.exitCode === null
|
||||
? "Unknown"
|
||||
: commandDetails.exitCode}
|
||||
</Text>
|
||||
)}
|
||||
{commandDetails.output && (
|
||||
<Text
|
||||
style={toolCallStylesheet.sectionText}
|
||||
numberOfLines={6}
|
||||
>
|
||||
{formatPreviewValue(commandDetails.output)}
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{primaryReadEntry && (
|
||||
<View style={toolCallStylesheet.section}>
|
||||
<Text style={toolCallStylesheet.sectionTitle}>
|
||||
{primaryReadEntry.filePath
|
||||
? `Read: ${primaryReadEntry.filePath}`
|
||||
: "Read Output"}
|
||||
</Text>
|
||||
<View style={toolCallStylesheet.sectionContent}>
|
||||
<Text
|
||||
style={toolCallStylesheet.sectionText}
|
||||
numberOfLines={6}
|
||||
>
|
||||
{formatPreviewValue(primaryReadEntry.content)}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{primaryEditEntry && (
|
||||
<View style={toolCallStylesheet.section}>
|
||||
<Text style={toolCallStylesheet.sectionTitle}>
|
||||
{primaryEditEntry.filePath
|
||||
? `Diff: ${primaryEditEntry.filePath}`
|
||||
: "Diff"}
|
||||
</Text>
|
||||
<View style={toolCallStylesheet.sectionContent}>
|
||||
<DiffViewer diffLines={primaryEditEntry.diffLines} maxHeight={160} />
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{genericResult && (
|
||||
<View style={toolCallStylesheet.section}>
|
||||
<Text style={toolCallStylesheet.sectionTitle}>Result</Text>
|
||||
<View style={toolCallStylesheet.sectionContent}>
|
||||
<Text style={toolCallStylesheet.sectionText} numberOfLines={6}>
|
||||
{genericResult}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{formattedError && (
|
||||
<View style={toolCallStylesheet.section}>
|
||||
<Text
|
||||
style={[
|
||||
toolCallStylesheet.sectionTitle,
|
||||
toolCallStylesheet.errorSectionTitle,
|
||||
]}
|
||||
>
|
||||
Error
|
||||
</Text>
|
||||
<View
|
||||
style={[
|
||||
toolCallStylesheet.sectionContent,
|
||||
toolCallStylesheet.errorSectionContent,
|
||||
]}
|
||||
>
|
||||
<Text
|
||||
style={[
|
||||
toolCallStylesheet.sectionText,
|
||||
toolCallStylesheet.errorSectionTitle,
|
||||
]}
|
||||
numberOfLines={6}
|
||||
>
|
||||
{formattedError}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
</Pressable>
|
||||
<ExpandableBadge
|
||||
label={toolName}
|
||||
icon={IconComponent}
|
||||
isExpanded={isExpanded}
|
||||
onToggle={handleToggle}
|
||||
renderDetails={renderDetails}
|
||||
isLoading={status === "executing"}
|
||||
isError={status === "failed"}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user