refactor: improve UI consistency across message components and theme styles

This commit is contained in:
Mohamed Boudra
2025-10-23 22:46:09 +02:00
parent 29e6da8536
commit d24bf65fe9
4 changed files with 360 additions and 223 deletions
@@ -1,12 +1,21 @@
import { useEffect, useRef, useState } from 'react'; import { useEffect, useRef, useState } from "react";
import { View, Text, ScrollView, Pressable } from 'react-native'; import { View, Text, ScrollView, Pressable } from "react-native";
import { useSafeAreaInsets } from 'react-native-safe-area-context'; import { useSafeAreaInsets } from "react-native-safe-area-context";
import { StyleSheet, useUnistyles } from 'react-native-unistyles'; import { StyleSheet, useUnistyles } from "react-native-unistyles";
import { BottomSheetModal } from '@gorhom/bottom-sheet'; import { BottomSheetModal } from "@gorhom/bottom-sheet";
import { AssistantMessage, UserMessage, ActivityLog, ToolCall } from './message'; import {
import { ToolCallBottomSheet } from './tool-call-bottom-sheet'; AssistantMessage,
import type { StreamItem } from '@/types/stream'; UserMessage,
import type { SelectedToolCall, PendingPermission, AgentInfo } from '@/types/shared'; ActivityLog,
ToolCall,
} from "./message";
import { ToolCallBottomSheet } from "./tool-call-bottom-sheet";
import type { StreamItem } from "@/types/stream";
import type {
SelectedToolCall,
PendingPermission,
AgentInfo,
} from "@/types/shared";
export interface AgentStreamViewProps { export interface AgentStreamViewProps {
agentId: string; agentId: string;
@@ -26,7 +35,8 @@ export function AgentStreamView({
const scrollViewRef = useRef<ScrollView>(null); const scrollViewRef = useRef<ScrollView>(null);
const bottomSheetRef = useRef<BottomSheetModal | null>(null); const bottomSheetRef = useRef<BottomSheetModal | null>(null);
const insets = useSafeAreaInsets(); const insets = useSafeAreaInsets();
const [selectedToolCall, setSelectedToolCall] = useState<SelectedToolCall | null>(null); const [selectedToolCall, setSelectedToolCall] =
useState<SelectedToolCall | null>(null);
// Auto-scroll to bottom when new items arrive // Auto-scroll to bottom when new items arrive
useEffect(() => { useEffect(() => {
@@ -44,7 +54,10 @@ export function AgentStreamView({
<ScrollView <ScrollView
ref={scrollViewRef} ref={scrollViewRef}
style={stylesheet.scrollView} style={stylesheet.scrollView}
contentContainerStyle={{ paddingTop: 24, paddingBottom: Math.max(insets.bottom, 32) }} contentContainerStyle={{
paddingTop: 24,
paddingBottom: Math.max(insets.bottom, 32),
}}
> >
{streamItems.length === 0 ? ( {streamItems.length === 0 ? (
<View style={stylesheet.emptyState}> <View style={stylesheet.emptyState}>
@@ -55,7 +68,7 @@ export function AgentStreamView({
) : ( ) : (
streamItems.map((item) => { streamItems.map((item) => {
switch (item.kind) { switch (item.kind) {
case 'user_message': case "user_message":
return ( return (
<UserMessage <UserMessage
key={item.id} key={item.id}
@@ -64,7 +77,7 @@ export function AgentStreamView({
/> />
); );
case 'assistant_message': case "assistant_message":
return ( return (
<AssistantMessage <AssistantMessage
key={item.id} key={item.id}
@@ -73,7 +86,7 @@ export function AgentStreamView({
/> />
); );
case 'thought': case "thought":
return ( return (
<ActivityLog <ActivityLog
key={item.id} key={item.id}
@@ -83,27 +96,31 @@ export function AgentStreamView({
/> />
); );
case 'tool_call': { case "tool_call": {
const { payload } = item; const { payload } = item;
// Extract data based on source // Extract data based on source
if (payload.source === 'acp') { if (payload.source === "acp") {
const data = payload.data; const data = payload.data;
// Map ACP status to display status // Map ACP status to display status
const toolStatus = data.status === 'pending' || data.status === 'in_progress' const toolStatus =
? 'executing' as const data.status === "pending" || data.status === "in_progress"
: data.status === 'completed' ? ("executing" as const)
? 'completed' as const : data.status === "completed"
: 'failed' as const; ? ("completed" as const)
: ("failed" as const);
return ( return (
<ToolCall <ToolCall
key={item.id} key={item.id}
toolName={data.title ?? 'Unknown Tool'} toolName={data.title ?? "Unknown Tool"}
kind={data.kind}
args={data.rawInput} args={data.rawInput}
result={data.rawOutput} result={data.rawOutput}
status={toolStatus} status={toolStatus}
onOpenDetails={() => handleOpenToolCallDetails({ payload })} onOpenDetails={() =>
handleOpenToolCallDetails({ payload })
}
/> />
); );
} else { } else {
@@ -116,18 +133,20 @@ export function AgentStreamView({
args={data.arguments} args={data.arguments}
result={data.result} result={data.result}
status={data.status} status={data.status}
onOpenDetails={() => handleOpenToolCallDetails({ payload })} onOpenDetails={() =>
handleOpenToolCallDetails({ payload })
}
/> />
); );
} }
} }
case 'plan': case "plan":
// TODO: Render plan component // TODO: Render plan component
return null; return null;
case 'activity_log': case "activity_log":
case 'artifact': case "artifact":
// These are orchestrator-only, skip for now // These are orchestrator-only, skip for now
return null; return null;
@@ -178,9 +197,9 @@ function PermissionRequestCard({
// Determine permission type and content based on toolCall // Determine permission type and content based on toolCall
const getPermissionInfo = () => { const getPermissionInfo = () => {
const rawInput = permission.toolCall?.rawInput || {}; const rawInput = permission.toolCall?.rawInput || {};
const toolCallId = permission.toolCall?.toolCallId || ''; const toolCallId = permission.toolCall?.toolCallId || "";
console.log('[PermissionCard] Tool call details:', { console.log("[PermissionCard] Tool call details:", {
toolCallId, toolCallId,
rawInputKeys: Object.keys(rawInput), rawInputKeys: Object.keys(rawInput),
rawInput, rawInput,
@@ -189,41 +208,44 @@ function PermissionRequestCard({
// Check if this is a plan (ExitPlanMode) // Check if this is a plan (ExitPlanMode)
if (rawInput.plan) { if (rawInput.plan) {
return { return {
title: 'Plan Ready for Review', title: "Plan Ready for Review",
content: rawInput.plan, content: rawInput.plan,
type: 'plan' as const, type: "plan" as const,
}; };
} }
// Check if this is a file operation (Write, Edit, etc.) // Check if this is a file operation (Write, Edit, etc.)
if (rawInput.file_path) { if (rawInput.file_path) {
const operation = toolCallId.includes('Write') ? 'Create' : 'Edit'; const operation = toolCallId.includes("Write") ? "Create" : "Edit";
const fileContent = rawInput.content || rawInput.new_string || ''; const fileContent = rawInput.content || rawInput.new_string || "";
const preview = fileContent.length > 500 const preview =
? fileContent.slice(0, 500) + '\n\n... (truncated)' fileContent.length > 500
: fileContent; ? fileContent.slice(0, 500) + "\n\n... (truncated)"
: fileContent;
return { return {
title: `${operation} File Permission`, title: `${operation} File Permission`,
content: `File: ${rawInput.file_path}\n\n${preview || '(empty file)'}`, content: `File: ${rawInput.file_path}\n\n${preview || "(empty file)"}`,
type: 'file' as const, type: "file" as const,
}; };
} }
// Check if this is a command (Bash) // Check if this is a command (Bash)
if (rawInput.command) { if (rawInput.command) {
return { return {
title: 'Run Command Permission', title: "Run Command Permission",
content: `Command: ${rawInput.command}\n\nDescription: ${rawInput.description || 'No description'}`, content: `Command: ${rawInput.command}\n\nDescription: ${
type: 'command' as const, rawInput.description || "No description"
}`,
type: "command" as const,
}; };
} }
// Fallback - show whatever is in rawInput // Fallback - show whatever is in rawInput
return { return {
title: 'Permission Required', title: "Permission Required",
content: JSON.stringify(rawInput, null, 2), content: JSON.stringify(rawInput, null, 2),
type: 'unknown' as const, type: "unknown" as const,
}; };
}; };
@@ -233,22 +255,42 @@ function PermissionRequestCard({
<View <View
style={[ style={[
permissionStyles.container, permissionStyles.container,
{ backgroundColor: theme.colors.secondary, borderColor: theme.colors.border }, {
backgroundColor: theme.colors.secondary,
borderColor: theme.colors.border,
},
]} ]}
> >
<Text style={[permissionStyles.title, { color: theme.colors.foreground }]}> <Text
style={[permissionStyles.title, { color: theme.colors.foreground }]}
>
{permissionInfo.title} {permissionInfo.title}
</Text> </Text>
{permissionInfo.content && ( {permissionInfo.content && (
<View style={[permissionStyles.planContainer, { backgroundColor: theme.colors.background }]}> <View
<Text style={[permissionStyles.planText, { color: theme.colors.foreground }]}> style={[
permissionStyles.planContainer,
{ backgroundColor: theme.colors.background },
]}
>
<Text
style={[
permissionStyles.planText,
{ color: theme.colors.foreground },
]}
>
{permissionInfo.content} {permissionInfo.content}
</Text> </Text>
</View> </View>
)} )}
<Text style={[permissionStyles.question, { color: theme.colors.mutedForeground }]}> <Text
style={[
permissionStyles.question,
{ color: theme.colors.mutedForeground },
]}
>
How would you like to proceed? How would you like to proceed?
</Text> </Text>
@@ -259,14 +301,19 @@ function PermissionRequestCard({
style={[ style={[
permissionStyles.optionButton, permissionStyles.optionButton,
{ {
backgroundColor: option.kind.includes('reject') backgroundColor: option.kind.includes("reject")
? theme.colors.destructive ? theme.colors.destructive
: theme.colors.primary, : theme.colors.primary,
}, },
]} ]}
onPress={() => onResponse(permission.requestId, option.optionId)} onPress={() => onResponse(permission.requestId, option.optionId)}
> >
<Text style={[permissionStyles.optionText, { color: theme.colors.primaryForeground }]}> <Text
style={[
permissionStyles.optionText,
{ color: theme.colors.primaryForeground },
]}
>
{option.name} {option.name}
</Text> </Text>
</Pressable> </Pressable>
@@ -283,7 +330,7 @@ const stylesheet = StyleSheet.create((theme) => ({
}, },
scrollView: { scrollView: {
flex: 1, flex: 1,
paddingHorizontal: theme.spacing[4], paddingHorizontal: theme.spacing[2],
}, },
emptyState: { emptyState: {
flex: 1, flex: 1,
+226 -137
View File
@@ -1,8 +1,25 @@
import { View, Text, Pressable, Animated } from 'react-native'; import { View, Text, Pressable, Animated } from "react-native";
import { useState, useEffect, useRef } from 'react'; import { useState, useEffect, useRef } from "react";
import Markdown from 'react-native-markdown-display'; import Markdown from "react-native-markdown-display";
import { Circle, Info, CheckCircle, XCircle, FileText, ChevronRight, ChevronDown, RefreshCw } from 'lucide-react-native'; import {
import { StyleSheet } from 'react-native-unistyles'; Circle,
Info,
CheckCircle,
XCircle,
FileText,
ChevronRight,
ChevronDown,
Loader2,
Check,
X,
Wrench,
Pencil,
Eye,
SquareTerminal,
} from "lucide-react-native";
import { StyleSheet } from "react-native-unistyles";
import { baseColors, theme } from "@/styles/theme";
import { Colors } from "@/constants/theme";
interface UserMessageProps { interface UserMessageProps {
message: string; message: string;
@@ -11,18 +28,18 @@ interface UserMessageProps {
const userMessageStylesheet = StyleSheet.create((theme) => ({ const userMessageStylesheet = StyleSheet.create((theme) => ({
container: { container: {
flexDirection: 'row', flexDirection: "row",
justifyContent: 'flex-end', justifyContent: "flex-end",
marginBottom: theme.spacing[3], marginBottom: theme.spacing[3],
paddingHorizontal: theme.spacing[4], paddingHorizontal: theme.spacing[4],
}, },
bubble: { bubble: {
backgroundColor: theme.colors.primary, backgroundColor: theme.colors.primary,
borderRadius: theme.borderRadius['2xl'], borderRadius: theme.borderRadius["2xl"],
borderTopRightRadius: theme.borderRadius.sm, borderTopRightRadius: theme.borderRadius.sm,
paddingHorizontal: theme.spacing[4], paddingHorizontal: theme.spacing[4],
paddingVertical: theme.spacing[3], paddingVertical: theme.spacing[3],
maxWidth: '80%', maxWidth: "80%",
}, },
text: { text: {
color: theme.colors.primaryForeground, color: theme.colors.primaryForeground,
@@ -61,9 +78,66 @@ const assistantMessageStylesheet = StyleSheet.create((theme) => ({
fontSize: theme.fontSize.xs, fontSize: theme.fontSize.xs,
fontWeight: theme.fontWeight.bold, fontWeight: theme.fontWeight.bold,
}, },
// Markdown styles
markdownBody: {
color: theme.colors.foreground,
fontSize: theme.fontSize.base,
lineHeight: 24,
},
markdownParagraph: {
marginTop: 0,
marginBottom: theme.spacing[2],
},
markdownStrong: {
fontWeight: theme.fontWeight.bold,
},
markdownEm: {
fontStyle: "italic" as const,
},
markdownCodeInline: {
backgroundColor: theme.colors.secondary,
color: theme.colors.secondaryForeground,
paddingHorizontal: theme.spacing[2],
paddingVertical: 2,
borderRadius: theme.borderRadius.sm,
fontFamily: "monospace",
fontSize: 13,
},
markdownCodeBlock: {
backgroundColor: theme.colors.secondary,
color: theme.colors.secondaryForeground,
padding: theme.spacing[3],
borderRadius: theme.borderRadius.md,
fontFamily: "monospace",
fontSize: 13,
},
markdownFence: {
backgroundColor: theme.colors.secondary,
borderColor: theme.colors.border,
color: theme.colors.secondaryForeground,
padding: theme.spacing[3],
borderRadius: theme.borderRadius.md,
marginVertical: theme.spacing[2],
fontFamily: "monospace",
fontSize: 13,
},
markdownLink: {
color: theme.colors.primary,
textDecorationLine: "underline" as const,
},
markdownList: {
marginBottom: theme.spacing[2],
},
markdownListItem: {
marginBottom: theme.spacing[1],
},
})); }));
export function AssistantMessage({ message, timestamp, isStreaming = false }: AssistantMessageProps) { export function AssistantMessage({
message,
timestamp,
isStreaming = false,
}: AssistantMessageProps) {
const fadeAnim = useRef(new Animated.Value(0.3)).current; const fadeAnim = useRef(new Animated.Value(0.3)).current;
useEffect(() => { useEffect(() => {
@@ -89,65 +163,29 @@ export function AssistantMessage({ message, timestamp, isStreaming = false }: As
}, [isStreaming, fadeAnim]); }, [isStreaming, fadeAnim]);
const markdownStyles = { const markdownStyles = {
body: { body: assistantMessageStylesheet.markdownBody,
color: '#f0fdfa', paragraph: assistantMessageStylesheet.markdownParagraph,
fontSize: 16, strong: assistantMessageStylesheet.markdownStrong,
lineHeight: 24, em: assistantMessageStylesheet.markdownEm,
}, code_inline: assistantMessageStylesheet.markdownCodeInline,
paragraph: { code_block: assistantMessageStylesheet.markdownCodeBlock,
marginTop: 0, fence: assistantMessageStylesheet.markdownFence,
marginBottom: 8, link: assistantMessageStylesheet.markdownLink,
}, bullet_list: assistantMessageStylesheet.markdownList,
strong: { ordered_list: assistantMessageStylesheet.markdownList,
fontWeight: '700' as const, list_item: assistantMessageStylesheet.markdownListItem,
},
em: {
fontStyle: 'italic' as const,
},
code_inline: {
backgroundColor: '#134e4a',
color: '#ccfbf1',
paddingHorizontal: 4,
paddingVertical: 2,
borderRadius: 3,
fontFamily: 'monospace',
},
code_block: {
backgroundColor: '#134e4a',
color: '#ccfbf1',
padding: 12,
borderRadius: 6,
fontFamily: 'monospace',
fontSize: 14,
},
fence: {
backgroundColor: '#134e4a',
color: '#ccfbf1',
padding: 12,
borderRadius: 6,
fontFamily: 'monospace',
fontSize: 14,
},
link: {
color: '#5eead4',
textDecorationLine: 'underline' as const,
},
bullet_list: {
marginBottom: 8,
},
ordered_list: {
marginBottom: 8,
},
list_item: {
marginBottom: 4,
},
}; };
return ( return (
<View style={assistantMessageStylesheet.container}> <View style={assistantMessageStylesheet.container}>
<Markdown style={markdownStyles}>{message}</Markdown> <Markdown style={markdownStyles}>{message}</Markdown>
{isStreaming && ( {isStreaming && (
<Animated.View style={[assistantMessageStylesheet.streamingIndicator, { opacity: fadeAnim }]}> <Animated.View
style={[
assistantMessageStylesheet.streamingIndicator,
{ opacity: fadeAnim },
]}
>
<Text style={assistantMessageStylesheet.streamingText}>...</Text> <Text style={assistantMessageStylesheet.streamingText}>...</Text>
</Animated.View> </Animated.View>
)} )}
@@ -156,7 +194,7 @@ export function AssistantMessage({ message, timestamp, isStreaming = false }: As
} }
interface ActivityLogProps { interface ActivityLogProps {
type: 'system' | 'info' | 'success' | 'error' | 'artifact'; type: "system" | "info" | "success" | "error" | "artifact";
message: string; message: string;
timestamp: number; timestamp: number;
metadata?: Record<string, unknown>; metadata?: Record<string, unknown>;
@@ -171,33 +209,33 @@ const activityLogStylesheet = StyleSheet.create((theme) => ({
marginHorizontal: theme.spacing[2], marginHorizontal: theme.spacing[2],
marginBottom: theme.spacing[1], marginBottom: theme.spacing[1],
borderRadius: theme.borderRadius.md, borderRadius: theme.borderRadius.md,
overflow: 'hidden', overflow: "hidden",
}, },
pressableActive: { pressableActive: {
opacity: 0.7, opacity: 0.7,
}, },
systemBg: { systemBg: {
backgroundColor: 'rgba(39, 39, 42, 0.5)', backgroundColor: "rgba(39, 39, 42, 0.5)",
}, },
infoBg: { infoBg: {
backgroundColor: 'rgba(30, 58, 138, 0.3)', backgroundColor: "rgba(30, 58, 138, 0.3)",
}, },
successBg: { successBg: {
backgroundColor: 'rgba(20, 83, 45, 0.3)', backgroundColor: "rgba(20, 83, 45, 0.3)",
}, },
errorBg: { errorBg: {
backgroundColor: 'rgba(127, 29, 29, 0.3)', backgroundColor: "rgba(127, 29, 29, 0.3)",
}, },
artifactBg: { artifactBg: {
backgroundColor: 'rgba(30, 58, 138, 0.4)', backgroundColor: "rgba(30, 58, 138, 0.4)",
}, },
content: { content: {
paddingHorizontal: theme.spacing[3], paddingHorizontal: theme.spacing[3],
paddingVertical: 10, paddingVertical: 10,
}, },
row: { row: {
flexDirection: 'row', flexDirection: "row",
alignItems: 'flex-start', alignItems: "flex-start",
gap: theme.spacing[2], gap: theme.spacing[2],
}, },
iconContainer: { iconContainer: {
@@ -211,8 +249,8 @@ const activityLogStylesheet = StyleSheet.create((theme) => ({
lineHeight: 20, lineHeight: 20,
}, },
detailsRow: { detailsRow: {
flexDirection: 'row', flexDirection: "row",
alignItems: 'center', alignItems: "center",
marginTop: theme.spacing[1], marginTop: theme.spacing[1],
}, },
detailsText: { detailsText: {
@@ -222,7 +260,7 @@ const activityLogStylesheet = StyleSheet.create((theme) => ({
}, },
metadataContainer: { metadataContainer: {
marginTop: theme.spacing[2], marginTop: theme.spacing[2],
backgroundColor: 'rgba(0, 0, 0, 0.5)', backgroundColor: "rgba(0, 0, 0, 0.5)",
borderRadius: theme.borderRadius.base, borderRadius: theme.borderRadius.base,
padding: theme.spacing[2], padding: theme.spacing[2],
borderWidth: theme.borderWidth[1], borderWidth: theme.borderWidth[1],
@@ -231,7 +269,7 @@ const activityLogStylesheet = StyleSheet.create((theme) => ({
metadataText: { metadataText: {
color: theme.colors.foreground, color: theme.colors.foreground,
fontSize: theme.fontSize.xs, fontSize: theme.fontSize.xs,
fontFamily: 'monospace', fontFamily: "monospace",
lineHeight: 16, lineHeight: 16,
}, },
})); }));
@@ -244,34 +282,51 @@ export function ActivityLog({
artifactId, artifactId,
artifactType, artifactType,
title, title,
onArtifactClick onArtifactClick,
}: ActivityLogProps) { }: ActivityLogProps) {
const [isExpanded, setIsExpanded] = useState(false); const [isExpanded, setIsExpanded] = useState(false);
const typeConfig = { const typeConfig = {
system: { bg: activityLogStylesheet.systemBg, color: '#a1a1aa', Icon: Circle }, system: {
info: { bg: activityLogStylesheet.infoBg, color: '#60a5fa', Icon: Info }, bg: activityLogStylesheet.systemBg,
success: { bg: activityLogStylesheet.successBg, color: '#4ade80', Icon: CheckCircle }, color: "#a1a1aa",
error: { bg: activityLogStylesheet.errorBg, color: '#f87171', Icon: XCircle }, Icon: Circle,
artifact: { bg: activityLogStylesheet.artifactBg, color: '#93c5fd', Icon: FileText }, },
info: { bg: activityLogStylesheet.infoBg, color: "#60a5fa", Icon: Info },
success: {
bg: activityLogStylesheet.successBg,
color: "#4ade80",
Icon: CheckCircle,
},
error: {
bg: activityLogStylesheet.errorBg,
color: "#f87171",
Icon: XCircle,
},
artifact: {
bg: activityLogStylesheet.artifactBg,
color: "#93c5fd",
Icon: FileText,
},
}; };
const config = typeConfig[type]; const config = typeConfig[type];
const IconComponent = config.Icon; const IconComponent = config.Icon;
const handlePress = () => { const handlePress = () => {
if (type === 'artifact' && artifactId && onArtifactClick) { if (type === "artifact" && artifactId && onArtifactClick) {
onArtifactClick(artifactId); onArtifactClick(artifactId);
} else if (metadata) { } else if (metadata) {
setIsExpanded(!isExpanded); setIsExpanded(!isExpanded);
} }
}; };
const displayMessage = type === 'artifact' && artifactType && title const displayMessage =
? `${artifactType}: ${title}` type === "artifact" && artifactType && title
: message; ? `${artifactType}: ${title}`
: message;
const isInteractive = type === 'artifact' || metadata; const isInteractive = type === "artifact" || metadata;
return ( return (
<Pressable <Pressable
@@ -289,7 +344,12 @@ export function ActivityLog({
<IconComponent size={16} color={config.color} /> <IconComponent size={16} color={config.color} />
</View> </View>
<View style={activityLogStylesheet.textContainer}> <View style={activityLogStylesheet.textContainer}>
<Text style={[activityLogStylesheet.messageText, { color: config.color }]}> <Text
style={[
activityLogStylesheet.messageText,
{ color: config.color },
]}
>
{displayMessage} {displayMessage}
</Text> </Text>
{metadata && ( {metadata && (
@@ -318,10 +378,11 @@ export function ActivityLog({
interface ToolCallProps { interface ToolCallProps {
toolName: string; toolName: string;
kind?: string; // Optional kind for ACP tool calls
args: any; args: any;
result?: any; result?: any;
error?: any; error?: any;
status: 'executing' | 'completed' | 'failed'; status: "executing" | "completed" | "failed";
onOpenDetails?: () => void; onOpenDetails?: () => void;
} }
@@ -332,16 +393,16 @@ const toolCallStylesheet = StyleSheet.create((theme) => ({
backgroundColor: theme.colors.card, backgroundColor: theme.colors.card,
borderRadius: theme.borderRadius.lg, borderRadius: theme.borderRadius.lg,
borderWidth: theme.borderWidth[1], borderWidth: theme.borderWidth[1],
overflow: 'hidden', overflow: "hidden",
}, },
pressableActive: { pressableActive: {
opacity: 0.8, opacity: 0.8,
}, },
executingBorder: { executingBorder: {
borderColor: theme.colors.palette.amber[500], borderColor: theme.colors.primary,
}, },
completedBorder: { completedBorder: {
borderColor: theme.colors.palette.green[500], borderColor: theme.colors.border,
}, },
failedBorder: { failedBorder: {
borderColor: theme.colors.destructive, borderColor: theme.colors.destructive,
@@ -350,40 +411,36 @@ const toolCallStylesheet = StyleSheet.create((theme) => ({
padding: theme.spacing[3], padding: theme.spacing[3],
}, },
headerRow: { headerRow: {
flexDirection: 'row', flexDirection: "row",
alignItems: 'center', alignItems: "center",
gap: theme.spacing[2],
}, },
chevronContainer: { chevronContainer: {
marginRight: theme.spacing[2], marginRight: theme.spacing[2],
}, },
toolName: { toolName: {
color: theme.colors.foreground, color: theme.colors.foreground,
fontFamily: 'monospace', fontFamily: "monospace",
fontWeight: theme.fontWeight.semibold, fontWeight: theme.fontWeight.semibold,
fontSize: theme.fontSize.sm, fontSize: theme.fontSize.sm,
flex: 1, flex: 1,
}, },
statusBadge: { statusBadge: {
flexDirection: 'row', alignItems: "center",
alignItems: 'center', justifyContent: "center",
gap: 6, padding: theme.spacing[1],
paddingHorizontal: theme.spacing[2],
paddingVertical: theme.spacing[1],
borderRadius: theme.borderRadius.base, borderRadius: theme.borderRadius.base,
width: 28,
height: 28,
}, },
executingBadgeBg: { executingBadgeBg: {
backgroundColor: 'rgba(245, 158, 11, 0.2)', backgroundColor: theme.colors.accent,
}, },
completedBadgeBg: { completedBadgeBg: {
backgroundColor: 'rgba(34, 197, 94, 0.2)', backgroundColor: "transparent",
}, },
failedBadgeBg: { failedBadgeBg: {
backgroundColor: 'rgba(239, 68, 68, 0.2)', backgroundColor: "rgba(239, 68, 68, 0.2)",
},
statusText: {
fontSize: theme.fontSize.xs,
fontWeight: theme.fontWeight.semibold,
textTransform: 'uppercase',
}, },
expandedContent: { expandedContent: {
marginTop: theme.spacing[3], marginTop: theme.spacing[3],
@@ -396,7 +453,7 @@ const toolCallStylesheet = StyleSheet.create((theme) => ({
color: theme.colors.mutedForeground, color: theme.colors.mutedForeground,
fontSize: theme.fontSize.xs, fontSize: theme.fontSize.xs,
fontWeight: theme.fontWeight.semibold, fontWeight: theme.fontWeight.semibold,
textTransform: 'uppercase', textTransform: "uppercase",
letterSpacing: 0.5, letterSpacing: 0.5,
marginBottom: 6, marginBottom: 6,
}, },
@@ -416,81 +473,113 @@ const toolCallStylesheet = StyleSheet.create((theme) => ({
sectionText: { sectionText: {
color: theme.colors.foreground, color: theme.colors.foreground,
fontSize: theme.fontSize.xs, fontSize: theme.fontSize.xs,
fontFamily: 'monospace', fontFamily: "monospace",
lineHeight: 16, lineHeight: 16,
}, },
})); }));
export function ToolCall({ toolName, args, result, error, status, onOpenDetails }: ToolCallProps) { // Icon mapping for tool kinds
const toolKindIcons: Record<string, any> = {
edit: Pencil,
read: Eye,
execute: SquareTerminal,
// Add more mappings as needed
};
export function ToolCall({
toolName,
kind,
args,
result,
error,
status,
onOpenDetails,
}: ToolCallProps) {
const spinAnim = useRef(new Animated.Value(0)).current; const spinAnim = useRef(new Animated.Value(0)).current;
useEffect(() => { useEffect(() => {
if (status === 'executing') { if (status === "executing") {
// Reset to 0 before starting
spinAnim.setValue(0);
Animated.loop( Animated.loop(
Animated.timing(spinAnim, { Animated.timing(spinAnim, {
toValue: 1, toValue: 1,
duration: 1000, duration: 1500,
useNativeDriver: true, useNativeDriver: true,
easing: (t) => t, // Linear easing for smooth continuous rotation
}) })
).start(); ).start();
} else { } else {
spinAnim.stopAnimation(); spinAnim.stopAnimation();
spinAnim.setValue(0);
} }
}, [status, spinAnim]); }, [status, spinAnim]);
const spin = spinAnim.interpolate({ const spin = spinAnim.interpolate({
inputRange: [0, 1], inputRange: [0, 1],
outputRange: ['0deg', '360deg'], outputRange: ["0deg", "360deg"],
}); });
const statusConfig = { const statusConfig = {
executing: { executing: {
border: toolCallStylesheet.executingBorder, border: toolCallStylesheet.executingBorder,
badgeBg: toolCallStylesheet.executingBadgeBg, badgeBg: toolCallStylesheet.executingBadgeBg,
color: '#fcd34d', color: "#fff",
label: 'executing',
}, },
completed: { completed: {
border: toolCallStylesheet.completedBorder, border: toolCallStylesheet.completedBorder,
badgeBg: toolCallStylesheet.completedBadgeBg, badgeBg: toolCallStylesheet.completedBadgeBg,
color: '#86efac', color: "#71717a",
label: 'completed',
}, },
failed: { failed: {
border: toolCallStylesheet.failedBorder, border: toolCallStylesheet.failedBorder,
badgeBg: toolCallStylesheet.failedBadgeBg, badgeBg: toolCallStylesheet.failedBadgeBg,
color: '#fca5a5', color: "#fca5a5",
label: 'failed',
}, },
}; };
const config = statusConfig[status]; const config = statusConfig[status];
// Get the appropriate icon for the tool kind
const getToolIcon = () => {
if (kind) {
const IconComponent = toolKindIcons[kind.toLowerCase()] || Wrench;
return IconComponent;
}
return Wrench; // Default icon
};
return ( return (
<Pressable <Pressable
onPress={onOpenDetails} onPress={onOpenDetails}
style={[toolCallStylesheet.pressable, toolCallStylesheet.pressableActive, config.border]} style={[
toolCallStylesheet.pressable,
toolCallStylesheet.pressableActive,
config.border,
]}
> >
<View style={toolCallStylesheet.content}> <View style={toolCallStylesheet.content}>
<View style={toolCallStylesheet.headerRow}> <View style={toolCallStylesheet.headerRow}>
<Text style={toolCallStylesheet.toolName}> <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} {toolName}
</Text> </Text>
<View style={[toolCallStylesheet.statusBadge, config.badgeBg]}>
{status === 'executing' ? (
<Animated.View style={{ transform: [{ rotate: spin }] }}>
<RefreshCw size={14} color={config.color} />
</Animated.View>
) : status === 'completed' ? (
<CheckCircle size={14} color={config.color} />
) : (
<XCircle size={14} color={config.color} />
)}
<Text style={[toolCallStylesheet.statusText, { color: config.color }]}>
{config.label}
</Text>
</View>
</View> </View>
</View> </View>
</Pressable> </Pressable>
@@ -1,7 +1,8 @@
import React, { useCallback, useMemo } from "react"; import React, { useCallback, useMemo } from "react";
import { View, Text, StyleSheet } from "react-native"; import { View, Text } from "react-native";
import { useSafeAreaInsets } from "react-native-safe-area-context"; import { useSafeAreaInsets } from "react-native-safe-area-context";
import { ScrollView } from "react-native-gesture-handler"; import { ScrollView } from "react-native-gesture-handler";
import { StyleSheet } from "react-native-unistyles";
import { import {
BottomSheetModal, BottomSheetModal,
BottomSheetView, BottomSheetView,
@@ -156,64 +157,64 @@ export function ToolCallBottomSheet({
); );
} }
const styles = StyleSheet.create({ const styles = StyleSheet.create((theme) => ({
handleIndicator: { handleIndicator: {
backgroundColor: "#4b5563", backgroundColor: theme.colors.border,
}, },
background: { background: {
backgroundColor: "#1f2937", backgroundColor: theme.colors.popover,
}, },
header: { header: {
paddingHorizontal: 20, paddingHorizontal: theme.spacing[6],
paddingTop: 4, paddingTop: theme.spacing[1],
paddingBottom: 16, paddingBottom: theme.spacing[4],
borderBottomWidth: 1, borderBottomWidth: theme.borderWidth[1],
borderBottomColor: "#374151", borderBottomColor: theme.colors.border,
backgroundColor: "#1f2937", backgroundColor: theme.colors.popover,
}, },
toolName: { toolName: {
fontSize: 18, fontSize: theme.fontSize.xl,
fontWeight: "600", fontWeight: theme.fontWeight.semibold,
color: "#f9fafb", color: theme.colors.popoverForeground,
}, },
sheetContent: { sheetContent: {
paddingTop: 20, paddingTop: theme.spacing[6],
paddingBottom: 20, paddingBottom: theme.spacing[6],
}, },
section: { section: {
marginBottom: 20, marginBottom: theme.spacing[6],
paddingHorizontal: 20, paddingHorizontal: theme.spacing[6],
}, },
sectionTitle: { sectionTitle: {
fontSize: 14, fontSize: theme.fontSize.sm,
fontWeight: "600", fontWeight: theme.fontWeight.semibold,
color: "#9ca3af", color: theme.colors.mutedForeground,
marginBottom: 8, marginBottom: theme.spacing[2],
textTransform: "uppercase", textTransform: "uppercase" as const,
letterSpacing: 0.5, letterSpacing: 0.5,
}, },
jsonContainer: { jsonContainer: {
backgroundColor: "#111827", backgroundColor: theme.colors.background,
borderRadius: 8, borderRadius: theme.borderRadius.lg,
borderWidth: 1, borderWidth: theme.borderWidth[1],
borderColor: "#374151", borderColor: theme.colors.border,
// Natural height based on content // Natural height based on content
}, },
jsonContent: { jsonContent: {
padding: 12, padding: theme.spacing[3],
}, },
jsonText: { jsonText: {
fontFamily: "monospace", fontFamily: "monospace",
fontSize: 12, fontSize: theme.fontSize.sm,
color: "#e5e7eb", color: theme.colors.foreground,
lineHeight: 18, lineHeight: 20,
// Text maintains whitespace and formatting // Text maintains whitespace and formatting
}, },
errorContainer: { errorContainer: {
borderColor: "#ef4444", borderColor: theme.colors.destructive,
backgroundColor: "#1f1416", backgroundColor: theme.colors.background,
}, },
errorText: { errorText: {
color: "#fca5a5", color: theme.colors.destructive,
}, },
}); }));
+1 -1
View File
@@ -1,4 +1,4 @@
const baseColors = { export const baseColors = {
// Base colors // Base colors
white: "#ffffff", white: "#ffffff",
black: "#000000", black: "#000000",