feat: add screenshot and settings for Claude, refactor agent stream and orchestrator views

- Introduced a new screenshot image for the app.
- Added local settings for Claude permissions.
- Refactored AgentStreamView and OrchestratorMessagesView to utilize new types for tool calls and permissions.
- Updated ToolCallBottomSheet to handle selected tool calls more efficiently.
- Enhanced type definitions for shared data structures related to tool calls and permissions.
This commit is contained in:
Mohamed Boudra
2025-10-23 22:12:02 +02:00
parent a0f45f2cfc
commit 29e6da8536
7 changed files with 269 additions and 249 deletions
@@ -3,34 +3,16 @@ import { View, Text, ScrollView, Pressable } from 'react-native';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { StyleSheet, useUnistyles } from 'react-native-unistyles';
import { BottomSheetModal } from '@gorhom/bottom-sheet';
import type { AgentStatus } from '@server/server/acp/types';
import { AssistantMessage, UserMessage, 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 {
agentId: string;
agent: {
id: string;
status: AgentStatus;
createdAt: Date;
type: 'claude';
};
agent: AgentInfo;
streamItems: StreamItem[];
pendingPermissions: Map<
string,
{
agentId: string;
requestId: string;
sessionId: string;
toolCall: any;
options: Array<{
kind: string;
name: string;
optionId: string;
}>;
}
>;
pendingPermissions: Map<string, PendingPermission>;
onPermissionResponse: (requestId: string, optionId: string) => void;
}
@@ -44,26 +26,14 @@ export function AgentStreamView({
const scrollViewRef = useRef<ScrollView>(null);
const bottomSheetRef = useRef<BottomSheetModal | null>(null);
const insets = useSafeAreaInsets();
const [selectedToolCall, setSelectedToolCall] = useState<{
toolName: string;
status: 'executing' | 'completed' | 'failed';
args: any;
result?: any;
error?: any;
} | null>(null);
const [selectedToolCall, setSelectedToolCall] = useState<SelectedToolCall | null>(null);
// Auto-scroll to bottom when new items arrive
useEffect(() => {
scrollViewRef.current?.scrollToEnd({ animated: true });
}, [streamItems]);
function handleOpenToolCallDetails(toolCall: {
toolName: string;
status: 'executing' | 'completed' | 'failed';
args: any;
result?: any;
error?: any;
}) {
function handleOpenToolCallDetails(toolCall: SelectedToolCall) {
setSelectedToolCall(toolCall);
bottomSheetRef.current?.present();
}
@@ -114,28 +84,42 @@ export function AgentStreamView({
);
case 'tool_call': {
// Map status: pending/in_progress -> executing, completed -> completed, failed -> failed
const toolStatus = item.status === 'pending' || item.status === 'in_progress'
? 'executing' as const
: item.status === 'completed'
? 'completed' as const
: 'failed' as const;
const { payload } = item;
return (
<ToolCall
key={item.id}
toolName={item.title}
args={item.rawInput}
result={item.rawOutput}
status={toolStatus}
onOpenDetails={() => handleOpenToolCallDetails({
toolName: item.title,
status: toolStatus,
args: item.rawInput,
result: item.rawOutput,
})}
/>
);
// Extract data based on source
if (payload.source === 'acp') {
const data = payload.data;
// Map ACP status to display status
const toolStatus = data.status === 'pending' || data.status === 'in_progress'
? 'executing' as const
: data.status === 'completed'
? 'completed' as const
: 'failed' as const;
return (
<ToolCall
key={item.id}
toolName={data.title ?? 'Unknown Tool'}
args={data.rawInput}
result={data.rawOutput}
status={toolStatus}
onOpenDetails={() => handleOpenToolCallDetails({ payload })}
/>
);
} else {
// Orchestrator tool call
const data = payload.data;
return (
<ToolCall
key={item.id}
toolName={data.toolName}
args={data.arguments}
result={data.result}
status={data.status}
onOpenDetails={() => handleOpenToolCallDetails({ payload })}
/>
);
}
}
case 'plan':
@@ -167,11 +151,7 @@ export function AgentStreamView({
<ToolCallBottomSheet
bottomSheetRef={bottomSheetRef}
toolName={selectedToolCall?.toolName}
status={selectedToolCall?.status}
args={selectedToolCall?.args}
result={selectedToolCall?.result}
error={selectedToolCall?.error}
selectedToolCall={selectedToolCall}
/>
</View>
);
@@ -10,6 +10,7 @@ import {
} from "@/components/message";
import { ToolCallBottomSheet } from "./tool-call-bottom-sheet";
import type { MessageEntry } from "@/contexts/session-context";
import type { SelectedToolCall } from "@/types/shared";
interface OrchestratorMessagesViewProps {
messages: MessageEntry[];
@@ -21,13 +22,7 @@ export const OrchestratorMessagesView = forwardRef<ScrollView, OrchestratorMessa
function OrchestratorMessagesView({ messages, currentAssistantMessage, onArtifactClick }, ref) {
const { theme } = useUnistyles();
const bottomSheetRef = useRef<BottomSheetModal | null>(null);
const [selectedToolCall, setSelectedToolCall] = useState<{
toolName: string;
status: 'executing' | 'completed' | 'failed';
args: any;
result?: any;
error?: any;
} | null>(null);
const [selectedToolCall, setSelectedToolCall] = useState<SelectedToolCall | null>(null);
function handleOpenToolCallDetails(toolCall: {
toolName: string;
@@ -36,7 +31,20 @@ export const OrchestratorMessagesView = forwardRef<ScrollView, OrchestratorMessa
result?: any;
error?: any;
}) {
setSelectedToolCall(toolCall);
// Wrap orchestrator tool call in payload structure
setSelectedToolCall({
payload: {
source: 'orchestrator',
data: {
toolCallId: `orchestrator_${Date.now()}`, // Generate a simple ID
toolName: toolCall.toolName,
arguments: toolCall.args,
result: toolCall.result,
error: toolCall.error,
status: toolCall.status,
},
},
});
bottomSheetRef.current?.present();
}
@@ -133,11 +141,7 @@ export const OrchestratorMessagesView = forwardRef<ScrollView, OrchestratorMessa
<ToolCallBottomSheet
bottomSheetRef={bottomSheetRef}
toolName={selectedToolCall?.toolName}
status={selectedToolCall?.status}
args={selectedToolCall?.args}
result={selectedToolCall?.result}
error={selectedToolCall?.error}
selectedToolCall={selectedToolCall}
/>
</>
);
@@ -1,5 +1,7 @@
import React, { useCallback, useMemo } from "react";
import { View, Text, StyleSheet } from "react-native";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import { ScrollView } from "react-native-gesture-handler";
import {
BottomSheetModal,
BottomSheetView,
@@ -7,24 +9,18 @@ import {
BottomSheetBackdrop,
BottomSheetBackdropProps,
} from "@gorhom/bottom-sheet";
import type { SelectedToolCall } from "@/types/shared";
interface ToolCallBottomSheetProps {
bottomSheetRef: React.RefObject<BottomSheetModal | null>;
toolName?: string;
status?: "pending" | "in_progress" | "executing" | "completed" | "failed";
args?: any;
result?: any;
error?: any;
selectedToolCall: SelectedToolCall | null;
}
export function ToolCallBottomSheet({
bottomSheetRef,
toolName,
status,
args,
result,
error,
selectedToolCall,
}: ToolCallBottomSheetProps) {
const insets = useSafeAreaInsets();
const snapPoints = useMemo(() => ["80%"], []);
const renderBackdrop = useCallback(
@@ -39,35 +35,48 @@ export function ToolCallBottomSheet({
[]
);
const statusColor = useMemo(() => {
switch (status) {
case "completed":
return "#22c55e";
case "failed":
return "#ef4444";
case "executing":
case "in_progress":
return "#3b82f6";
case "pending":
default:
return "#6b7280";
// Extract data based on source
const { toolName, args, result, error } = useMemo(() => {
if (!selectedToolCall) {
return {
toolName: "Tool Call",
args: undefined,
result: undefined,
error: undefined,
};
}
}, [status]);
const statusLabel = useMemo(() => {
switch (status) {
case "in_progress":
case "executing":
return "Executing";
case "completed":
return "Completed";
case "failed":
return "Failed";
case "pending":
default:
return "Pending";
const { payload } = selectedToolCall;
if (payload.source === "acp") {
const data = payload.data;
const content = data.content
?.flatMap((item) => {
if (item.type === "content" && item.content.type === "text") {
return [item.content.text];
}
return [];
})
.join("\n");
return {
toolName: data.kind ?? "Unknown Tool",
args: data.rawInput,
result: content,
error: undefined, // ACP doesn't have a separate error field
};
} else {
// Orchestrator tool call
const data = payload.data;
return {
toolName: data.toolName,
args: data.arguments,
result: data.result,
error: data.error,
};
}
}, [status]);
}, [selectedToolCall]);
return (
<BottomSheetModal
@@ -77,62 +86,77 @@ export function ToolCallBottomSheet({
enablePanDownToClose={true}
handleIndicatorStyle={styles.handleIndicator}
backgroundStyle={styles.background}
topInset={insets.top}
>
<BottomSheetView style={styles.container}>
<View style={styles.header}>
<Text style={styles.toolName}>{toolName || "Tool Call"}</Text>
<View style={[styles.statusBadge, { backgroundColor: statusColor }]}>
<Text style={styles.statusText}>{statusLabel}</Text>
{/* Header */}
<View style={styles.header}>
<Text style={styles.toolName}>{toolName || "Tool Call"}</Text>
</View>
{/* Scrollable content */}
<BottomSheetScrollView
contentContainerStyle={styles.sheetContent}
showsVerticalScrollIndicator={true}
>
{/* Content sections */}
{args !== undefined && (
<View style={styles.section}>
<Text style={styles.sectionTitle}>Arguments</Text>
<ScrollView
horizontal
style={styles.jsonContainer}
contentContainerStyle={styles.jsonContent}
showsHorizontalScrollIndicator={true}
nestedScrollEnabled={true}
>
<Text style={styles.jsonText}>
{JSON.stringify(args, null, 2)}
</Text>
</ScrollView>
</View>
</View>
)}
<BottomSheetScrollView
style={styles.scrollView}
contentContainerStyle={styles.scrollContent}
>
{args !== undefined && (
<View style={styles.section}>
<Text style={styles.sectionTitle}>Arguments</Text>
<View style={styles.jsonContainer}>
<Text style={styles.jsonText}>
{JSON.stringify(args, null, 2)}
</Text>
</View>
</View>
)}
{result !== undefined && (
<View style={styles.section}>
<Text style={styles.sectionTitle}>Result</Text>
<ScrollView
horizontal
style={styles.jsonContainer}
contentContainerStyle={styles.jsonContent}
showsHorizontalScrollIndicator={true}
nestedScrollEnabled={true}
>
<Text style={styles.jsonText}>
{typeof result === "string"
? result
: JSON.stringify(result, null, 2)}
</Text>
</ScrollView>
</View>
)}
{result !== undefined && (
<View style={styles.section}>
<Text style={styles.sectionTitle}>Result</Text>
<View style={styles.jsonContainer}>
<Text style={styles.jsonText}>
{JSON.stringify(result, null, 2)}
</Text>
</View>
</View>
)}
{error !== undefined && (
<View style={styles.section}>
<Text style={styles.sectionTitle}>Error</Text>
<View style={[styles.jsonContainer, styles.errorContainer]}>
<Text style={[styles.jsonText, styles.errorText]}>
{JSON.stringify(error, null, 2)}
</Text>
</View>
</View>
)}
</BottomSheetScrollView>
</BottomSheetView>
{error !== undefined && (
<View style={styles.section}>
<Text style={styles.sectionTitle}>Error</Text>
<ScrollView
horizontal
style={[styles.jsonContainer, styles.errorContainer]}
contentContainerStyle={styles.jsonContent}
showsHorizontalScrollIndicator={true}
nestedScrollEnabled={true}
>
<Text style={[styles.jsonText, styles.errorText]}>
{JSON.stringify(error, null, 2)}
</Text>
</ScrollView>
</View>
)}
</BottomSheetScrollView>
</BottomSheetModal>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: "#1f2937",
},
handleIndicator: {
backgroundColor: "#4b5563",
},
@@ -140,41 +164,25 @@ const styles = StyleSheet.create({
backgroundColor: "#1f2937",
},
header: {
flexDirection: "row",
alignItems: "center",
justifyContent: "space-between",
paddingHorizontal: 20,
paddingVertical: 16,
paddingTop: 4,
paddingBottom: 16,
borderBottomWidth: 1,
borderBottomColor: "#374151",
backgroundColor: "#1f2937",
},
toolName: {
fontSize: 18,
fontWeight: "600",
color: "#f9fafb",
flex: 1,
},
statusBadge: {
paddingHorizontal: 8,
paddingVertical: 4,
borderRadius: 4,
marginLeft: 12,
},
statusText: {
fontSize: 12,
fontWeight: "600",
color: "#fff",
textTransform: "capitalize",
},
scrollView: {
flex: 1,
},
scrollContent: {
paddingHorizontal: 20,
paddingVertical: 16,
sheetContent: {
paddingTop: 20,
paddingBottom: 20,
},
section: {
marginBottom: 20,
paddingHorizontal: 20,
},
sectionTitle: {
fontSize: 14,
@@ -187,15 +195,19 @@ const styles = StyleSheet.create({
jsonContainer: {
backgroundColor: "#111827",
borderRadius: 8,
padding: 12,
borderWidth: 1,
borderColor: "#374151",
// Natural height based on content
},
jsonContent: {
padding: 12,
},
jsonText: {
fontFamily: "monospace",
fontSize: 12,
color: "#e5e7eb",
lineHeight: 18,
// Text maintains whitespace and formatting
},
errorContainer: {
borderColor: "#ef4444",