import { useEffect, useRef, useState } from "react"; 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 { 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: AgentInfo; streamItems: StreamItem[]; pendingPermissions: Map; onPermissionResponse: (requestId: string, optionId: string) => void; } export function AgentStreamView({ agentId, agent, streamItems, pendingPermissions, onPermissionResponse, }: AgentStreamViewProps) { const scrollViewRef = useRef(null); const bottomSheetRef = useRef(null); const insets = useSafeAreaInsets(); const [selectedToolCall, setSelectedToolCall] = useState(null); // Auto-scroll to bottom when new items arrive useEffect(() => { scrollViewRef.current?.scrollToEnd({ animated: true }); }, [streamItems]); function handleOpenToolCallDetails(toolCall: SelectedToolCall) { setSelectedToolCall(toolCall); // Delay present to next frame to ensure component is mounted setTimeout(() => { bottomSheetRef.current?.present(); }, 0); } function handleBottomSheetDismiss() { setSelectedToolCall(null); } return ( {/* Content list */} {streamItems.length === 0 ? ( Start chatting with this agent... ) : ( streamItems.map((item) => { switch (item.kind) { case "user_message": return ( ); case "assistant_message": return ( ); case "thought": return ( ); case "tool_call": { const { payload } = item; // 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 ( handleOpenToolCallDetails({ payload }) } /> ); } else { // Orchestrator tool call const data = payload.data; return ( handleOpenToolCallDetails({ payload }) } /> ); } } case "plan": // TODO: Render plan component return null; case "activity_log": case "artifact": // These are orchestrator-only, skip for now return null; default: return null; } }) )} {/* Render pending permissions for this agent */} {Array.from(pendingPermissions.values()) .filter((perm) => perm.agentId === agentId) .map((permission) => ( ))} ); } // Permission Request Card Component function PermissionRequestCard({ permission, onResponse, }: { permission: { requestId: string; toolCall: any; options: Array<{ kind: string; name: string; optionId: string; }>; }; onResponse: (requestId: string, optionId: string) => void; }) { const { theme } = useUnistyles(); // Determine permission type and content based on toolCall const getPermissionInfo = () => { const rawInput = permission.toolCall?.rawInput || {}; const toolCallId = permission.toolCall?.toolCallId || ""; console.log("[PermissionCard] Tool call details:", { toolCallId, rawInputKeys: Object.keys(rawInput), rawInput, }); // Check if this is a plan (ExitPlanMode) if (rawInput.plan) { return { title: "Plan Ready for Review", content: rawInput.plan, type: "plan" as const, }; } // Check if this is a file operation (Write, Edit, etc.) if (rawInput.file_path) { const operation = toolCallId.includes("Write") ? "Create" : "Edit"; const fileContent = rawInput.content || rawInput.new_string || ""; const preview = fileContent.length > 500 ? fileContent.slice(0, 500) + "\n\n... (truncated)" : fileContent; return { title: `${operation} File Permission`, content: `File: ${rawInput.file_path}\n\n${preview || "(empty file)"}`, type: "file" as const, }; } // Check if this is a command (Bash) if (rawInput.command) { return { title: "Run Command Permission", content: `Command: ${rawInput.command}\n\nDescription: ${ rawInput.description || "No description" }`, type: "command" as const, }; } // Fallback - show whatever is in rawInput return { title: "Permission Required", content: JSON.stringify(rawInput, null, 2), type: "unknown" as const, }; }; const permissionInfo = getPermissionInfo(); return ( {permissionInfo.title} {permissionInfo.content && ( {permissionInfo.content} )} How would you like to proceed? {permission.options.map((option) => ( onResponse(permission.requestId, option.optionId)} > {option.name} ))} ); } const stylesheet = StyleSheet.create((theme) => ({ container: { flex: 1, backgroundColor: theme.colors.background, }, scrollView: { flex: 1, paddingHorizontal: theme.spacing[2], }, emptyState: { flex: 1, alignItems: "center", justifyContent: "center", paddingVertical: theme.spacing[12], }, emptyStateText: { color: theme.colors.mutedForeground, fontSize: theme.fontSize.sm, textAlign: "center", }, })); const permissionStyles = StyleSheet.create((theme) => ({ container: { marginVertical: theme.spacing[3], padding: theme.spacing[4], borderRadius: theme.spacing[2], borderWidth: 1, }, title: { fontSize: 16, fontWeight: "600", marginBottom: theme.spacing[3], }, planContainer: { padding: theme.spacing[3], borderRadius: theme.spacing[1], marginBottom: theme.spacing[3], }, planText: { fontSize: 14, lineHeight: 20, }, question: { fontSize: 14, marginBottom: theme.spacing[3], }, optionsContainer: { gap: theme.spacing[2], }, optionButton: { padding: theme.spacing[3], borderRadius: theme.spacing[1], alignItems: "center", }, optionText: { fontSize: 14, fontWeight: "600", }, }));