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:
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user