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 { 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 type { AgentStatus } from '@server/server/acp/types';
|
|
||||||
import { AssistantMessage, UserMessage, ActivityLog, ToolCall } from './message';
|
import { AssistantMessage, UserMessage, ActivityLog, ToolCall } from './message';
|
||||||
import { ToolCallBottomSheet } from './tool-call-bottom-sheet';
|
import { ToolCallBottomSheet } from './tool-call-bottom-sheet';
|
||||||
import type { StreamItem } from '@/types/stream';
|
import type { StreamItem } from '@/types/stream';
|
||||||
|
import type { SelectedToolCall, PendingPermission, AgentInfo } from '@/types/shared';
|
||||||
|
|
||||||
export interface AgentStreamViewProps {
|
export interface AgentStreamViewProps {
|
||||||
agentId: string;
|
agentId: string;
|
||||||
agent: {
|
agent: AgentInfo;
|
||||||
id: string;
|
|
||||||
status: AgentStatus;
|
|
||||||
createdAt: Date;
|
|
||||||
type: 'claude';
|
|
||||||
};
|
|
||||||
streamItems: StreamItem[];
|
streamItems: StreamItem[];
|
||||||
pendingPermissions: Map<
|
pendingPermissions: Map<string, PendingPermission>;
|
||||||
string,
|
|
||||||
{
|
|
||||||
agentId: string;
|
|
||||||
requestId: string;
|
|
||||||
sessionId: string;
|
|
||||||
toolCall: any;
|
|
||||||
options: Array<{
|
|
||||||
kind: string;
|
|
||||||
name: string;
|
|
||||||
optionId: string;
|
|
||||||
}>;
|
|
||||||
}
|
|
||||||
>;
|
|
||||||
onPermissionResponse: (requestId: string, optionId: string) => void;
|
onPermissionResponse: (requestId: string, optionId: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -44,26 +26,14 @@ 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<{
|
const [selectedToolCall, setSelectedToolCall] = useState<SelectedToolCall | null>(null);
|
||||||
toolName: string;
|
|
||||||
status: 'executing' | 'completed' | 'failed';
|
|
||||||
args: any;
|
|
||||||
result?: any;
|
|
||||||
error?: any;
|
|
||||||
} | null>(null);
|
|
||||||
|
|
||||||
// Auto-scroll to bottom when new items arrive
|
// Auto-scroll to bottom when new items arrive
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
scrollViewRef.current?.scrollToEnd({ animated: true });
|
scrollViewRef.current?.scrollToEnd({ animated: true });
|
||||||
}, [streamItems]);
|
}, [streamItems]);
|
||||||
|
|
||||||
function handleOpenToolCallDetails(toolCall: {
|
function handleOpenToolCallDetails(toolCall: SelectedToolCall) {
|
||||||
toolName: string;
|
|
||||||
status: 'executing' | 'completed' | 'failed';
|
|
||||||
args: any;
|
|
||||||
result?: any;
|
|
||||||
error?: any;
|
|
||||||
}) {
|
|
||||||
setSelectedToolCall(toolCall);
|
setSelectedToolCall(toolCall);
|
||||||
bottomSheetRef.current?.present();
|
bottomSheetRef.current?.present();
|
||||||
}
|
}
|
||||||
@@ -114,28 +84,42 @@ export function AgentStreamView({
|
|||||||
);
|
);
|
||||||
|
|
||||||
case 'tool_call': {
|
case 'tool_call': {
|
||||||
// Map status: pending/in_progress -> executing, completed -> completed, failed -> failed
|
const { payload } = item;
|
||||||
const toolStatus = item.status === 'pending' || item.status === 'in_progress'
|
|
||||||
? 'executing' as const
|
|
||||||
: item.status === 'completed'
|
|
||||||
? 'completed' as const
|
|
||||||
: 'failed' as const;
|
|
||||||
|
|
||||||
return (
|
// Extract data based on source
|
||||||
<ToolCall
|
if (payload.source === 'acp') {
|
||||||
key={item.id}
|
const data = payload.data;
|
||||||
toolName={item.title}
|
// Map ACP status to display status
|
||||||
args={item.rawInput}
|
const toolStatus = data.status === 'pending' || data.status === 'in_progress'
|
||||||
result={item.rawOutput}
|
? 'executing' as const
|
||||||
status={toolStatus}
|
: data.status === 'completed'
|
||||||
onOpenDetails={() => handleOpenToolCallDetails({
|
? 'completed' as const
|
||||||
toolName: item.title,
|
: 'failed' as const;
|
||||||
status: toolStatus,
|
|
||||||
args: item.rawInput,
|
return (
|
||||||
result: item.rawOutput,
|
<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':
|
case 'plan':
|
||||||
@@ -167,11 +151,7 @@ export function AgentStreamView({
|
|||||||
|
|
||||||
<ToolCallBottomSheet
|
<ToolCallBottomSheet
|
||||||
bottomSheetRef={bottomSheetRef}
|
bottomSheetRef={bottomSheetRef}
|
||||||
toolName={selectedToolCall?.toolName}
|
selectedToolCall={selectedToolCall}
|
||||||
status={selectedToolCall?.status}
|
|
||||||
args={selectedToolCall?.args}
|
|
||||||
result={selectedToolCall?.result}
|
|
||||||
error={selectedToolCall?.error}
|
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
} from "@/components/message";
|
} from "@/components/message";
|
||||||
import { ToolCallBottomSheet } from "./tool-call-bottom-sheet";
|
import { ToolCallBottomSheet } from "./tool-call-bottom-sheet";
|
||||||
import type { MessageEntry } from "@/contexts/session-context";
|
import type { MessageEntry } from "@/contexts/session-context";
|
||||||
|
import type { SelectedToolCall } from "@/types/shared";
|
||||||
|
|
||||||
interface OrchestratorMessagesViewProps {
|
interface OrchestratorMessagesViewProps {
|
||||||
messages: MessageEntry[];
|
messages: MessageEntry[];
|
||||||
@@ -21,13 +22,7 @@ export const OrchestratorMessagesView = forwardRef<ScrollView, OrchestratorMessa
|
|||||||
function OrchestratorMessagesView({ messages, currentAssistantMessage, onArtifactClick }, ref) {
|
function OrchestratorMessagesView({ messages, currentAssistantMessage, onArtifactClick }, ref) {
|
||||||
const { theme } = useUnistyles();
|
const { theme } = useUnistyles();
|
||||||
const bottomSheetRef = useRef<BottomSheetModal | null>(null);
|
const bottomSheetRef = useRef<BottomSheetModal | null>(null);
|
||||||
const [selectedToolCall, setSelectedToolCall] = useState<{
|
const [selectedToolCall, setSelectedToolCall] = useState<SelectedToolCall | null>(null);
|
||||||
toolName: string;
|
|
||||||
status: 'executing' | 'completed' | 'failed';
|
|
||||||
args: any;
|
|
||||||
result?: any;
|
|
||||||
error?: any;
|
|
||||||
} | null>(null);
|
|
||||||
|
|
||||||
function handleOpenToolCallDetails(toolCall: {
|
function handleOpenToolCallDetails(toolCall: {
|
||||||
toolName: string;
|
toolName: string;
|
||||||
@@ -36,7 +31,20 @@ export const OrchestratorMessagesView = forwardRef<ScrollView, OrchestratorMessa
|
|||||||
result?: any;
|
result?: any;
|
||||||
error?: 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();
|
bottomSheetRef.current?.present();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -133,11 +141,7 @@ export const OrchestratorMessagesView = forwardRef<ScrollView, OrchestratorMessa
|
|||||||
|
|
||||||
<ToolCallBottomSheet
|
<ToolCallBottomSheet
|
||||||
bottomSheetRef={bottomSheetRef}
|
bottomSheetRef={bottomSheetRef}
|
||||||
toolName={selectedToolCall?.toolName}
|
selectedToolCall={selectedToolCall}
|
||||||
status={selectedToolCall?.status}
|
|
||||||
args={selectedToolCall?.args}
|
|
||||||
result={selectedToolCall?.result}
|
|
||||||
error={selectedToolCall?.error}
|
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import React, { useCallback, useMemo } from "react";
|
import React, { useCallback, useMemo } from "react";
|
||||||
import { View, Text, StyleSheet } from "react-native";
|
import { View, Text, StyleSheet } from "react-native";
|
||||||
|
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||||
|
import { ScrollView } from "react-native-gesture-handler";
|
||||||
import {
|
import {
|
||||||
BottomSheetModal,
|
BottomSheetModal,
|
||||||
BottomSheetView,
|
BottomSheetView,
|
||||||
@@ -7,24 +9,18 @@ import {
|
|||||||
BottomSheetBackdrop,
|
BottomSheetBackdrop,
|
||||||
BottomSheetBackdropProps,
|
BottomSheetBackdropProps,
|
||||||
} from "@gorhom/bottom-sheet";
|
} from "@gorhom/bottom-sheet";
|
||||||
|
import type { SelectedToolCall } from "@/types/shared";
|
||||||
|
|
||||||
interface ToolCallBottomSheetProps {
|
interface ToolCallBottomSheetProps {
|
||||||
bottomSheetRef: React.RefObject<BottomSheetModal | null>;
|
bottomSheetRef: React.RefObject<BottomSheetModal | null>;
|
||||||
toolName?: string;
|
selectedToolCall: SelectedToolCall | null;
|
||||||
status?: "pending" | "in_progress" | "executing" | "completed" | "failed";
|
|
||||||
args?: any;
|
|
||||||
result?: any;
|
|
||||||
error?: any;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ToolCallBottomSheet({
|
export function ToolCallBottomSheet({
|
||||||
bottomSheetRef,
|
bottomSheetRef,
|
||||||
toolName,
|
selectedToolCall,
|
||||||
status,
|
|
||||||
args,
|
|
||||||
result,
|
|
||||||
error,
|
|
||||||
}: ToolCallBottomSheetProps) {
|
}: ToolCallBottomSheetProps) {
|
||||||
|
const insets = useSafeAreaInsets();
|
||||||
const snapPoints = useMemo(() => ["80%"], []);
|
const snapPoints = useMemo(() => ["80%"], []);
|
||||||
|
|
||||||
const renderBackdrop = useCallback(
|
const renderBackdrop = useCallback(
|
||||||
@@ -39,35 +35,48 @@ export function ToolCallBottomSheet({
|
|||||||
[]
|
[]
|
||||||
);
|
);
|
||||||
|
|
||||||
const statusColor = useMemo(() => {
|
// Extract data based on source
|
||||||
switch (status) {
|
const { toolName, args, result, error } = useMemo(() => {
|
||||||
case "completed":
|
if (!selectedToolCall) {
|
||||||
return "#22c55e";
|
return {
|
||||||
case "failed":
|
toolName: "Tool Call",
|
||||||
return "#ef4444";
|
args: undefined,
|
||||||
case "executing":
|
result: undefined,
|
||||||
case "in_progress":
|
error: undefined,
|
||||||
return "#3b82f6";
|
};
|
||||||
case "pending":
|
|
||||||
default:
|
|
||||||
return "#6b7280";
|
|
||||||
}
|
}
|
||||||
}, [status]);
|
|
||||||
|
|
||||||
const statusLabel = useMemo(() => {
|
const { payload } = selectedToolCall;
|
||||||
switch (status) {
|
|
||||||
case "in_progress":
|
if (payload.source === "acp") {
|
||||||
case "executing":
|
const data = payload.data;
|
||||||
return "Executing";
|
|
||||||
case "completed":
|
const content = data.content
|
||||||
return "Completed";
|
?.flatMap((item) => {
|
||||||
case "failed":
|
if (item.type === "content" && item.content.type === "text") {
|
||||||
return "Failed";
|
return [item.content.text];
|
||||||
case "pending":
|
}
|
||||||
default:
|
return [];
|
||||||
return "Pending";
|
})
|
||||||
|
.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 (
|
return (
|
||||||
<BottomSheetModal
|
<BottomSheetModal
|
||||||
@@ -77,62 +86,77 @@ export function ToolCallBottomSheet({
|
|||||||
enablePanDownToClose={true}
|
enablePanDownToClose={true}
|
||||||
handleIndicatorStyle={styles.handleIndicator}
|
handleIndicatorStyle={styles.handleIndicator}
|
||||||
backgroundStyle={styles.background}
|
backgroundStyle={styles.background}
|
||||||
|
topInset={insets.top}
|
||||||
>
|
>
|
||||||
<BottomSheetView style={styles.container}>
|
{/* Header */}
|
||||||
<View style={styles.header}>
|
<View style={styles.header}>
|
||||||
<Text style={styles.toolName}>{toolName || "Tool Call"}</Text>
|
<Text style={styles.toolName}>{toolName || "Tool Call"}</Text>
|
||||||
<View style={[styles.statusBadge, { backgroundColor: statusColor }]}>
|
</View>
|
||||||
<Text style={styles.statusText}>{statusLabel}</Text>
|
|
||||||
|
{/* 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>
|
||||||
</View>
|
)}
|
||||||
|
|
||||||
<BottomSheetScrollView
|
{result !== undefined && (
|
||||||
style={styles.scrollView}
|
<View style={styles.section}>
|
||||||
contentContainerStyle={styles.scrollContent}
|
<Text style={styles.sectionTitle}>Result</Text>
|
||||||
>
|
<ScrollView
|
||||||
{args !== undefined && (
|
horizontal
|
||||||
<View style={styles.section}>
|
style={styles.jsonContainer}
|
||||||
<Text style={styles.sectionTitle}>Arguments</Text>
|
contentContainerStyle={styles.jsonContent}
|
||||||
<View style={styles.jsonContainer}>
|
showsHorizontalScrollIndicator={true}
|
||||||
<Text style={styles.jsonText}>
|
nestedScrollEnabled={true}
|
||||||
{JSON.stringify(args, null, 2)}
|
>
|
||||||
</Text>
|
<Text style={styles.jsonText}>
|
||||||
</View>
|
{typeof result === "string"
|
||||||
</View>
|
? result
|
||||||
)}
|
: JSON.stringify(result, null, 2)}
|
||||||
|
</Text>
|
||||||
|
</ScrollView>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
|
||||||
{result !== undefined && (
|
{error !== undefined && (
|
||||||
<View style={styles.section}>
|
<View style={styles.section}>
|
||||||
<Text style={styles.sectionTitle}>Result</Text>
|
<Text style={styles.sectionTitle}>Error</Text>
|
||||||
<View style={styles.jsonContainer}>
|
<ScrollView
|
||||||
<Text style={styles.jsonText}>
|
horizontal
|
||||||
{JSON.stringify(result, null, 2)}
|
style={[styles.jsonContainer, styles.errorContainer]}
|
||||||
</Text>
|
contentContainerStyle={styles.jsonContent}
|
||||||
</View>
|
showsHorizontalScrollIndicator={true}
|
||||||
</View>
|
nestedScrollEnabled={true}
|
||||||
)}
|
>
|
||||||
|
<Text style={[styles.jsonText, styles.errorText]}>
|
||||||
{error !== undefined && (
|
{JSON.stringify(error, null, 2)}
|
||||||
<View style={styles.section}>
|
</Text>
|
||||||
<Text style={styles.sectionTitle}>Error</Text>
|
</ScrollView>
|
||||||
<View style={[styles.jsonContainer, styles.errorContainer]}>
|
</View>
|
||||||
<Text style={[styles.jsonText, styles.errorText]}>
|
)}
|
||||||
{JSON.stringify(error, null, 2)}
|
</BottomSheetScrollView>
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
)}
|
|
||||||
</BottomSheetScrollView>
|
|
||||||
</BottomSheetView>
|
|
||||||
</BottomSheetModal>
|
</BottomSheetModal>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
container: {
|
|
||||||
flex: 1,
|
|
||||||
backgroundColor: "#1f2937",
|
|
||||||
},
|
|
||||||
handleIndicator: {
|
handleIndicator: {
|
||||||
backgroundColor: "#4b5563",
|
backgroundColor: "#4b5563",
|
||||||
},
|
},
|
||||||
@@ -140,41 +164,25 @@ const styles = StyleSheet.create({
|
|||||||
backgroundColor: "#1f2937",
|
backgroundColor: "#1f2937",
|
||||||
},
|
},
|
||||||
header: {
|
header: {
|
||||||
flexDirection: "row",
|
|
||||||
alignItems: "center",
|
|
||||||
justifyContent: "space-between",
|
|
||||||
paddingHorizontal: 20,
|
paddingHorizontal: 20,
|
||||||
paddingVertical: 16,
|
paddingTop: 4,
|
||||||
|
paddingBottom: 16,
|
||||||
borderBottomWidth: 1,
|
borderBottomWidth: 1,
|
||||||
borderBottomColor: "#374151",
|
borderBottomColor: "#374151",
|
||||||
|
backgroundColor: "#1f2937",
|
||||||
},
|
},
|
||||||
toolName: {
|
toolName: {
|
||||||
fontSize: 18,
|
fontSize: 18,
|
||||||
fontWeight: "600",
|
fontWeight: "600",
|
||||||
color: "#f9fafb",
|
color: "#f9fafb",
|
||||||
flex: 1,
|
|
||||||
},
|
},
|
||||||
statusBadge: {
|
sheetContent: {
|
||||||
paddingHorizontal: 8,
|
paddingTop: 20,
|
||||||
paddingVertical: 4,
|
paddingBottom: 20,
|
||||||
borderRadius: 4,
|
|
||||||
marginLeft: 12,
|
|
||||||
},
|
|
||||||
statusText: {
|
|
||||||
fontSize: 12,
|
|
||||||
fontWeight: "600",
|
|
||||||
color: "#fff",
|
|
||||||
textTransform: "capitalize",
|
|
||||||
},
|
|
||||||
scrollView: {
|
|
||||||
flex: 1,
|
|
||||||
},
|
|
||||||
scrollContent: {
|
|
||||||
paddingHorizontal: 20,
|
|
||||||
paddingVertical: 16,
|
|
||||||
},
|
},
|
||||||
section: {
|
section: {
|
||||||
marginBottom: 20,
|
marginBottom: 20,
|
||||||
|
paddingHorizontal: 20,
|
||||||
},
|
},
|
||||||
sectionTitle: {
|
sectionTitle: {
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
@@ -187,15 +195,19 @@ const styles = StyleSheet.create({
|
|||||||
jsonContainer: {
|
jsonContainer: {
|
||||||
backgroundColor: "#111827",
|
backgroundColor: "#111827",
|
||||||
borderRadius: 8,
|
borderRadius: 8,
|
||||||
padding: 12,
|
|
||||||
borderWidth: 1,
|
borderWidth: 1,
|
||||||
borderColor: "#374151",
|
borderColor: "#374151",
|
||||||
|
// Natural height based on content
|
||||||
|
},
|
||||||
|
jsonContent: {
|
||||||
|
padding: 12,
|
||||||
},
|
},
|
||||||
jsonText: {
|
jsonText: {
|
||||||
fontFamily: "monospace",
|
fontFamily: "monospace",
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
color: "#e5e7eb",
|
color: "#e5e7eb",
|
||||||
lineHeight: 18,
|
lineHeight: 18,
|
||||||
|
// Text maintains whitespace and formatting
|
||||||
},
|
},
|
||||||
errorContainer: {
|
errorContainer: {
|
||||||
borderColor: "#ef4444",
|
borderColor: "#ef4444",
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { createContext, useContext, useState, useRef, ReactNode, useCallback, us
|
|||||||
import { useWebSocket, type UseWebSocketReturn } from "@/hooks/use-websocket";
|
import { useWebSocket, type UseWebSocketReturn } from "@/hooks/use-websocket";
|
||||||
import { useAudioPlayer } from "@/hooks/use-audio-player";
|
import { useAudioPlayer } from "@/hooks/use-audio-player";
|
||||||
import { reduceStreamUpdate, generateMessageId, type StreamItem } from "@/types/stream";
|
import { reduceStreamUpdate, generateMessageId, type StreamItem } from "@/types/stream";
|
||||||
|
import type { PendingPermission } from "@/types/shared";
|
||||||
import type {
|
import type {
|
||||||
ActivityLogPayload,
|
ActivityLogPayload,
|
||||||
SessionInboundMessage,
|
SessionInboundMessage,
|
||||||
@@ -77,17 +78,6 @@ export interface Command {
|
|||||||
exitCode: number | null;
|
exitCode: number | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PendingPermission {
|
|
||||||
agentId: string;
|
|
||||||
requestId: string;
|
|
||||||
sessionId: string;
|
|
||||||
toolCall: any;
|
|
||||||
options: Array<{
|
|
||||||
kind: string;
|
|
||||||
name: string;
|
|
||||||
optionId: string;
|
|
||||||
}>;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface SessionContextValue {
|
interface SessionContextValue {
|
||||||
// WebSocket
|
// WebSocket
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
import type { AgentStatus } from '@server/server/acp/types';
|
||||||
|
import type { RequestPermissionRequest } from '@agentclientprotocol/sdk';
|
||||||
|
import type { ToolCallPayload } from './stream';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generic tool call selection for bottom sheet
|
||||||
|
* Reuses ToolCallPayload so we don't duplicate shapes
|
||||||
|
*/
|
||||||
|
export interface SelectedToolCall {
|
||||||
|
payload: ToolCallPayload;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pending permission structure
|
||||||
|
* Uses actual ACP types instead of any
|
||||||
|
*/
|
||||||
|
export interface PendingPermission {
|
||||||
|
agentId: string;
|
||||||
|
requestId: string;
|
||||||
|
sessionId: string;
|
||||||
|
toolCall: RequestPermissionRequest['toolCall'];
|
||||||
|
options: RequestPermissionRequest['options'];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Agent info for UI display
|
||||||
|
*/
|
||||||
|
export interface AgentInfo {
|
||||||
|
id: string;
|
||||||
|
status: AgentStatus;
|
||||||
|
createdAt: Date;
|
||||||
|
type: 'claude';
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import type { AgentNotification } from '@server/server/acp/types';
|
import type { AgentNotification } from '@server/server/acp/types';
|
||||||
|
import type { SessionNotification } from '@agentclientprotocol/sdk';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Simple hash function for deterministic ID generation
|
* Simple hash function for deterministic ID generation
|
||||||
@@ -89,18 +90,30 @@ export interface ThoughtItem {
|
|||||||
timestamp: Date;
|
timestamp: Date;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Extract the actual ACP tool_call type from SDK
|
||||||
|
type ACPToolCall = Extract<SessionNotification['update'], { sessionUpdate: 'tool_call' }>;
|
||||||
|
|
||||||
|
// Orchestrator tool call data (from activity_log messages)
|
||||||
|
interface OrchestratorToolCallData {
|
||||||
|
toolCallId: string;
|
||||||
|
toolName: string;
|
||||||
|
arguments: unknown;
|
||||||
|
result?: unknown;
|
||||||
|
error?: unknown;
|
||||||
|
status: 'executing' | 'completed' | 'failed';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tagged union for tool call sources
|
||||||
|
export type ToolCallPayload =
|
||||||
|
| { source: 'acp'; data: ACPToolCall }
|
||||||
|
| { source: 'orchestrator'; data: OrchestratorToolCallData };
|
||||||
|
|
||||||
|
// Simplified ToolCallItem with payload
|
||||||
export interface ToolCallItem {
|
export interface ToolCallItem {
|
||||||
kind: 'tool_call';
|
kind: 'tool_call';
|
||||||
id: string;
|
id: string;
|
||||||
toolCallId: string;
|
|
||||||
title: string;
|
|
||||||
status: 'pending' | 'in_progress' | 'completed' | 'failed';
|
|
||||||
toolKind?: 'read' | 'edit' | 'delete' | 'move' | 'search' | 'execute' | 'think' | 'fetch' | 'switch_mode' | 'other';
|
|
||||||
rawInput?: Record<string, unknown>;
|
|
||||||
rawOutput?: Record<string, unknown>;
|
|
||||||
content?: unknown[];
|
|
||||||
locations?: unknown[];
|
|
||||||
timestamp: Date;
|
timestamp: Date;
|
||||||
|
payload: ToolCallPayload;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PlanItem {
|
export interface PlanItem {
|
||||||
@@ -132,6 +145,9 @@ export interface ArtifactItem {
|
|||||||
timestamp: Date;
|
timestamp: Date;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Extract ACP types for tool calls
|
||||||
|
type ACPToolCallUpdate = Extract<SessionNotification['update'], { sessionUpdate: 'tool_call_update' }>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parsed notification types (internal representation before converting to StreamItem)
|
* Parsed notification types (internal representation before converting to StreamItem)
|
||||||
*/
|
*/
|
||||||
@@ -139,8 +155,8 @@ type ParsedNotification =
|
|||||||
| { kind: 'user_message_chunk'; text: string; messageId?: string }
|
| { kind: 'user_message_chunk'; text: string; messageId?: string }
|
||||||
| { kind: 'agent_message_chunk'; text: string; messageId?: string }
|
| { kind: 'agent_message_chunk'; text: string; messageId?: string }
|
||||||
| { kind: 'agent_thought_chunk'; text: string; messageId?: string }
|
| { kind: 'agent_thought_chunk'; text: string; messageId?: string }
|
||||||
| { kind: 'tool_call'; toolCallId: string; title: string; status?: string; toolKind?: string; rawInput?: any; rawOutput?: any; content?: any[]; locations?: any[] }
|
| { kind: 'tool_call'; data: ACPToolCall }
|
||||||
| { kind: 'tool_call_update'; toolCallId: string; title?: string | null; status?: string | null; toolKind?: string | null; rawInput?: any; rawOutput?: any; content?: any[] | null; locations?: any[] | null }
|
| { kind: 'tool_call_update'; data: ACPToolCallUpdate }
|
||||||
| { kind: 'plan'; entries: Array<{ content: string; status: string; priority: string }> }
|
| { kind: 'plan'; entries: Array<{ content: string; status: string; priority: string }> }
|
||||||
| { kind: 'current_mode_update'; currentModeId: string }
|
| { kind: 'current_mode_update'; currentModeId: string }
|
||||||
| { kind: 'available_commands_update'; availableCommands: Array<{ name: string; description: string }> }
|
| { kind: 'available_commands_update'; availableCommands: Array<{ name: string; description: string }> }
|
||||||
@@ -198,31 +214,19 @@ function parseNotification(notification: AgentNotification): ParsedNotification
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
case 'tool_call':
|
case 'tool_call': {
|
||||||
return {
|
return {
|
||||||
kind: 'tool_call',
|
kind: 'tool_call' as const,
|
||||||
toolCallId: update.toolCallId,
|
data: update as ACPToolCall,
|
||||||
title: update.title,
|
|
||||||
status: update.status,
|
|
||||||
toolKind: update.kind,
|
|
||||||
rawInput: update.rawInput,
|
|
||||||
rawOutput: update.rawOutput,
|
|
||||||
content: update.content,
|
|
||||||
locations: update.locations,
|
|
||||||
};
|
};
|
||||||
|
}
|
||||||
|
|
||||||
case 'tool_call_update':
|
case 'tool_call_update': {
|
||||||
return {
|
return {
|
||||||
kind: 'tool_call_update',
|
kind: 'tool_call_update' as const,
|
||||||
toolCallId: update.toolCallId,
|
data: update as ACPToolCallUpdate,
|
||||||
title: update.title,
|
|
||||||
status: update.status,
|
|
||||||
toolKind: update.kind,
|
|
||||||
rawInput: update.rawInput,
|
|
||||||
rawOutput: update.rawOutput,
|
|
||||||
content: update.content,
|
|
||||||
locations: update.locations,
|
|
||||||
};
|
};
|
||||||
|
}
|
||||||
|
|
||||||
case 'plan':
|
case 'plan':
|
||||||
return {
|
return {
|
||||||
@@ -346,16 +350,22 @@ export function reduceStreamUpdate(
|
|||||||
// Tool call updates - find existing and merge
|
// Tool call updates - find existing and merge
|
||||||
if (parsed.kind === 'tool_call_update') {
|
if (parsed.kind === 'tool_call_update') {
|
||||||
return state.map(item => {
|
return state.map(item => {
|
||||||
if (item.kind === 'tool_call' && item.toolCallId === parsed.toolCallId) {
|
if (item.kind === 'tool_call' &&
|
||||||
|
item.payload.source === 'acp' &&
|
||||||
|
item.payload.data.toolCallId === parsed.data.toolCallId) {
|
||||||
|
// Merge the update data with existing ACP data
|
||||||
|
const updatedData = { ...item.payload.data };
|
||||||
|
|
||||||
|
// Apply non-null updates from parsed.data
|
||||||
|
Object.entries(parsed.data).forEach(([key, value]) => {
|
||||||
|
if (value !== undefined && value !== null) {
|
||||||
|
(updatedData as any)[key] = value;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...item,
|
...item,
|
||||||
...(parsed.title !== undefined && parsed.title !== null ? { title: parsed.title } : {}),
|
payload: { source: 'acp' as const, data: updatedData },
|
||||||
...(parsed.status !== undefined && parsed.status !== null ? { status: parsed.status as any } : {}),
|
|
||||||
...(parsed.toolKind !== undefined && parsed.toolKind !== null ? { toolKind: parsed.toolKind as any } : {}),
|
|
||||||
...(parsed.rawInput ? { rawInput: { ...item.rawInput, ...parsed.rawInput } } : {}),
|
|
||||||
...(parsed.rawOutput ? { rawOutput: { ...item.rawOutput, ...parsed.rawOutput } } : {}),
|
|
||||||
...(parsed.content !== undefined && parsed.content !== null ? { content: parsed.content } : {}),
|
|
||||||
...(parsed.locations !== undefined && parsed.locations !== null ? { locations: parsed.locations } : {}),
|
|
||||||
timestamp,
|
timestamp,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -366,7 +376,9 @@ export function reduceStreamUpdate(
|
|||||||
// New tool call - check if exists first (server may send multiple notifications for same tool)
|
// New tool call - check if exists first (server may send multiple notifications for same tool)
|
||||||
if (parsed.kind === 'tool_call') {
|
if (parsed.kind === 'tool_call') {
|
||||||
const existingIndex = state.findIndex(
|
const existingIndex = state.findIndex(
|
||||||
item => item.kind === 'tool_call' && item.toolCallId === parsed.toolCallId
|
item => item.kind === 'tool_call' &&
|
||||||
|
item.payload.source === 'acp' &&
|
||||||
|
item.payload.data.toolCallId === parsed.data.toolCallId
|
||||||
);
|
);
|
||||||
|
|
||||||
if (existingIndex >= 0) {
|
if (existingIndex >= 0) {
|
||||||
@@ -375,13 +387,7 @@ export function reduceStreamUpdate(
|
|||||||
if (idx === existingIndex && item.kind === 'tool_call') {
|
if (idx === existingIndex && item.kind === 'tool_call') {
|
||||||
return {
|
return {
|
||||||
...item,
|
...item,
|
||||||
title: parsed.title,
|
payload: { source: 'acp' as const, data: parsed.data },
|
||||||
status: (parsed.status as any) || item.status,
|
|
||||||
toolKind: (parsed.toolKind as any) || item.toolKind,
|
|
||||||
rawInput: parsed.rawInput ? { ...item.rawInput, ...parsed.rawInput } : item.rawInput,
|
|
||||||
rawOutput: parsed.rawOutput ? { ...item.rawOutput, ...parsed.rawOutput } : item.rawOutput,
|
|
||||||
content: parsed.content || item.content,
|
|
||||||
locations: parsed.locations || item.locations,
|
|
||||||
timestamp,
|
timestamp,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -390,19 +396,14 @@ export function reduceStreamUpdate(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Create new tool call
|
// Create new tool call
|
||||||
return [...state, {
|
const newToolCall: ToolCallItem = {
|
||||||
kind: 'tool_call',
|
kind: 'tool_call',
|
||||||
id: parsed.toolCallId,
|
id: parsed.data.toolCallId,
|
||||||
toolCallId: parsed.toolCallId,
|
|
||||||
title: parsed.title,
|
|
||||||
status: (parsed.status as any) || 'pending',
|
|
||||||
toolKind: parsed.toolKind as any,
|
|
||||||
rawInput: parsed.rawInput,
|
|
||||||
rawOutput: parsed.rawOutput,
|
|
||||||
content: parsed.content,
|
|
||||||
locations: parsed.locations,
|
|
||||||
timestamp,
|
timestamp,
|
||||||
}];
|
payload: { source: 'acp', data: parsed.data },
|
||||||
|
};
|
||||||
|
|
||||||
|
return [...state, newToolCall];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Plan - replace existing or create new (uses constant ID since only one plan exists)
|
// Plan - replace existing or create new (uses constant ID since only one plan exists)
|
||||||
|
|||||||
Reference in New Issue
Block a user