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
View File
@@ -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",
+1 -11
View File
@@ -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
+33
View File
@@ -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';
}
+58 -57
View File
@@ -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)