Skip to content

Commit 9a4158c

Browse files
committed
feat: implement tool call/result mapping with collapsible UI
1 parent 7434e18 commit 9a4158c

6 files changed

Lines changed: 468 additions & 76 deletions

File tree

src/components/AgentExecution.tsx

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import { listen, type UnlistenFn } from "@tauri-apps/api/event";
2424
import { StreamMessage } from "./StreamMessage";
2525
import { ExecutionControlBar } from "./ExecutionControlBar";
2626
import { ErrorBoundary } from "./ErrorBoundary";
27+
import { enhanceMessages, type EnhancedMessage } from "@/types/enhanced-messages";
2728

2829
interface AgentExecutionProps {
2930
/**
@@ -73,6 +74,7 @@ export const AgentExecution: React.FC<AgentExecutionProps> = ({
7374
const [model, setModel] = useState(agent.model || "sonnet");
7475
const [isRunning, setIsRunning] = useState(false);
7576
const [messages, setMessages] = useState<ClaudeStreamMessage[]>([]);
77+
const [enhancedMessages, setEnhancedMessages] = useState<EnhancedMessage[]>([]);
7678
const [rawJsonlOutput, setRawJsonlOutput] = useState<string[]>([]);
7779
const [error, setError] = useState<string | null>(null);
7880
const [copyPopoverOpen, setCopyPopoverOpen] = useState(false);
@@ -159,6 +161,12 @@ export const AgentExecution: React.FC<AgentExecutionProps> = ({
159161
setTotalTokens(tokens);
160162
}, [messages]);
161163

164+
// Enhance messages whenever they change
165+
useEffect(() => {
166+
const enhanced = enhanceMessages(messages);
167+
setEnhancedMessages(enhanced);
168+
}, [messages]);
169+
162170
const handleSelectPath = async () => {
163171
try {
164172
const selected = await open({
@@ -594,7 +602,7 @@ export const AgentExecution: React.FC<AgentExecutionProps> = ({
594602
}}
595603
>
596604
<div ref={messagesContainerRef}>
597-
{messages.length === 0 && !isRunning && (
605+
{enhancedMessages.length === 0 && !isRunning && (
598606
<div className="flex flex-col items-center justify-center h-full text-center">
599607
<Terminal className="h-16 w-16 text-muted-foreground mb-4" />
600608
<h3 className="text-lg font-medium mb-2">Ready to Execute</h3>
@@ -604,7 +612,7 @@ export const AgentExecution: React.FC<AgentExecutionProps> = ({
604612
</div>
605613
)}
606614

607-
{isRunning && messages.length === 0 && (
615+
{isRunning && enhancedMessages.length === 0 && (
608616
<div className="flex items-center justify-center h-full">
609617
<div className="flex items-center gap-3">
610618
<Loader2 className="h-6 w-6 animate-spin" />
@@ -614,7 +622,7 @@ export const AgentExecution: React.FC<AgentExecutionProps> = ({
614622
)}
615623

616624
<AnimatePresence>
617-
{messages.map((message, index) => (
625+
{enhancedMessages.map((message, index) => (
618626
<motion.div
619627
key={index}
620628
initial={{ opacity: 0, y: 10 }}
@@ -623,7 +631,7 @@ export const AgentExecution: React.FC<AgentExecutionProps> = ({
623631
className="mb-4"
624632
>
625633
<ErrorBoundary>
626-
<StreamMessage message={message} streamMessages={messages} />
634+
<StreamMessage message={message} streamMessages={enhancedMessages} />
627635
</ErrorBoundary>
628636
</motion.div>
629637
))}
@@ -724,7 +732,7 @@ export const AgentExecution: React.FC<AgentExecutionProps> = ({
724732
}
725733
}}
726734
>
727-
{messages.length === 0 && !isRunning && (
735+
{enhancedMessages.length === 0 && !isRunning && (
728736
<div className="flex flex-col items-center justify-center h-full text-center">
729737
<Terminal className="h-16 w-16 text-muted-foreground mb-4" />
730738
<h3 className="text-lg font-medium mb-2">Ready to Execute</h3>
@@ -734,7 +742,7 @@ export const AgentExecution: React.FC<AgentExecutionProps> = ({
734742
</div>
735743
)}
736744

737-
{isRunning && messages.length === 0 && (
745+
{isRunning && enhancedMessages.length === 0 && (
738746
<div className="flex items-center justify-center h-full">
739747
<div className="flex items-center gap-3">
740748
<Loader2 className="h-6 w-6 animate-spin" />
@@ -744,7 +752,7 @@ export const AgentExecution: React.FC<AgentExecutionProps> = ({
744752
)}
745753

746754
<AnimatePresence>
747-
{messages.map((message, index) => (
755+
{enhancedMessages.map((message, index) => (
748756
<motion.div
749757
key={index}
750758
initial={{ opacity: 0, y: 10 }}
@@ -753,7 +761,7 @@ export const AgentExecution: React.FC<AgentExecutionProps> = ({
753761
className="mb-4"
754762
>
755763
<ErrorBoundary>
756-
<StreamMessage message={message} streamMessages={messages} />
764+
<StreamMessage message={message} streamMessages={enhancedMessages} />
757765
</ErrorBoundary>
758766
</motion.div>
759767
))}

src/components/ClaudeCodeSession.tsx

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import { TimelineNavigator } from "./TimelineNavigator";
2626
import { CheckpointSettings } from "./CheckpointSettings";
2727
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter } from "@/components/ui/dialog";
2828
import type { ClaudeStreamMessage } from "./AgentExecution";
29+
import { enhanceMessages, type EnhancedMessage } from "@/types/enhanced-messages";
2930

3031
interface ClaudeCodeSessionProps {
3132
/**
@@ -60,6 +61,7 @@ export const ClaudeCodeSession: React.FC<ClaudeCodeSessionProps> = ({
6061
}) => {
6162
const [projectPath, setProjectPath] = useState(initialProjectPath || session?.project_path || "");
6263
const [messages, setMessages] = useState<ClaudeStreamMessage[]>([]);
64+
const [enhancedMessages, setEnhancedMessages] = useState<EnhancedMessage[]>([]);
6365
const [isLoading, setIsLoading] = useState(false);
6466
const [error, setError] = useState<string | null>(null);
6567
const [rawJsonlOutput, setRawJsonlOutput] = useState<string[]>([]);
@@ -115,10 +117,16 @@ export const ClaudeCodeSession: React.FC<ClaudeCodeSessionProps> = ({
115117
}
116118
}, [session]);
117119

120+
// Enhance messages whenever they change
121+
useEffect(() => {
122+
const enhanced = enhanceMessages(messages);
123+
setEnhancedMessages(enhanced);
124+
}, [messages]);
125+
118126
// Auto-scroll to bottom when new messages arrive
119127
useEffect(() => {
120128
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
121-
}, [messages]);
129+
}, [enhancedMessages]);
122130

123131
// Calculate total tokens from messages
124132
useEffect(() => {
@@ -513,7 +521,7 @@ export const ClaudeCodeSession: React.FC<ClaudeCodeSessionProps> = ({
513521
</>
514522
)}
515523

516-
{messages.length > 0 && (
524+
{enhancedMessages.length > 0 && (
517525
<Popover
518526
trigger={
519527
<Button
@@ -611,7 +619,7 @@ export const ClaudeCodeSession: React.FC<ClaudeCodeSessionProps> = ({
611619

612620
{/* Messages Display */}
613621
<div className="flex-1 overflow-y-auto p-4 space-y-2 pb-40">
614-
{messages.length === 0 && !isLoading && (
622+
{enhancedMessages.length === 0 && !isLoading && (
615623
<div className="flex flex-col items-center justify-center h-full text-center">
616624
<Terminal className="h-16 w-16 text-muted-foreground mb-4" />
617625
<h3 className="text-lg font-medium mb-2">Ready to Start</h3>
@@ -624,7 +632,7 @@ export const ClaudeCodeSession: React.FC<ClaudeCodeSessionProps> = ({
624632
</div>
625633
)}
626634

627-
{isLoading && messages.length === 0 && (
635+
{isLoading && enhancedMessages.length === 0 && (
628636
<div className="flex items-center justify-center h-full">
629637
<div className="flex items-center gap-3">
630638
<Loader2 className="h-6 w-6 animate-spin" />
@@ -636,22 +644,22 @@ export const ClaudeCodeSession: React.FC<ClaudeCodeSessionProps> = ({
636644
)}
637645

638646
<AnimatePresence>
639-
{messages.map((message, index) => (
647+
{enhancedMessages.map((message, index) => (
640648
<motion.div
641649
key={index}
642650
initial={{ opacity: 0, y: 10 }}
643651
animate={{ opacity: 1, y: 0 }}
644652
transition={{ duration: 0.2 }}
645653
>
646654
<ErrorBoundary>
647-
<StreamMessage message={message} streamMessages={messages} />
655+
<StreamMessage message={message} streamMessages={enhancedMessages} />
648656
</ErrorBoundary>
649657
</motion.div>
650658
))}
651659
</AnimatePresence>
652660

653661
{/* Show loading indicator when processing, even if there are messages */}
654-
{isLoading && messages.length > 0 && (
662+
{isLoading && enhancedMessages.length > 0 && (
655663
<div className="flex items-center gap-2 p-4">
656664
<Loader2 className="h-4 w-4 animate-spin" />
657665
<span className="text-sm text-muted-foreground">Processing...</span>
Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
import React, { useState } from "react";
2+
import { motion, AnimatePresence } from "framer-motion";
3+
import {
4+
ChevronDown,
5+
ChevronRight,
6+
Loader2,
7+
CheckCircle2,
8+
AlertCircle,
9+
Terminal,
10+
FileText,
11+
Search,
12+
Edit,
13+
FolderOpen,
14+
Code
15+
} from "lucide-react";
16+
import { cn } from "@/lib/utils";
17+
import type { ToolCall, ToolResult } from "@/types/enhanced-messages";
18+
19+
interface CollapsibleToolResultProps {
20+
toolCall: ToolCall;
21+
toolResult?: ToolResult;
22+
className?: string;
23+
children?: React.ReactNode;
24+
}
25+
26+
// Map tool names to icons
27+
const toolIcons: Record<string, React.ReactNode> = {
28+
read: <FileText className="h-4 w-4" />,
29+
write: <Edit className="h-4 w-4" />,
30+
edit: <Edit className="h-4 w-4" />,
31+
multiedit: <Edit className="h-4 w-4" />,
32+
bash: <Terminal className="h-4 w-4" />,
33+
ls: <FolderOpen className="h-4 w-4" />,
34+
glob: <Search className="h-4 w-4" />,
35+
grep: <Search className="h-4 w-4" />,
36+
task: <Code className="h-4 w-4" />,
37+
default: <Terminal className="h-4 w-4" />
38+
};
39+
40+
// Get tool icon based on tool name
41+
function getToolIcon(toolName: string): React.ReactNode {
42+
const lowerName = toolName.toLowerCase();
43+
return toolIcons[lowerName] || toolIcons.default;
44+
}
45+
46+
// Get display name for tools
47+
function getToolDisplayName(toolName: string): string {
48+
const displayNames: Record<string, string> = {
49+
ls: "List directory",
50+
read: "Read file",
51+
write: "Write file",
52+
edit: "Edit file",
53+
multiedit: "Multi-edit file",
54+
bash: "Run command",
55+
glob: "Find files",
56+
grep: "Search files",
57+
task: "Run task",
58+
todowrite: "Update todos",
59+
todoread: "Read todos",
60+
websearch: "Search web",
61+
webfetch: "Fetch webpage"
62+
};
63+
64+
const lowerName = toolName.toLowerCase();
65+
return displayNames[lowerName] || toolName;
66+
}
67+
68+
// Get a brief description of the tool call
69+
function getToolDescription(toolCall: ToolCall): string {
70+
const name = toolCall.name.toLowerCase();
71+
const input = toolCall.input;
72+
73+
switch (name) {
74+
case "read":
75+
return input?.file_path ? `${input.file_path}` : "Reading file";
76+
case "write":
77+
return input?.file_path ? `${input.file_path}` : "Writing file";
78+
case "edit":
79+
case "multiedit":
80+
return input?.file_path ? `${input.file_path}` : "Editing file";
81+
case "bash":
82+
return input?.command ? `${input.command}` : "Running command";
83+
case "ls":
84+
return input?.path ? `${input.path}` : "Listing directory";
85+
case "glob":
86+
return input?.pattern ? `${input.pattern}` : "Finding files";
87+
case "grep":
88+
return input?.pattern ? `${input.pattern}` : "Searching files";
89+
case "task":
90+
return input?.description || "Running task";
91+
default:
92+
return toolCall.name;
93+
}
94+
}
95+
96+
export const CollapsibleToolResult: React.FC<CollapsibleToolResultProps> = ({
97+
toolCall,
98+
toolResult,
99+
className,
100+
children
101+
}) => {
102+
const [isExpanded, setIsExpanded] = useState(false);
103+
const isPending = !toolResult;
104+
const isError = toolResult?.isError;
105+
106+
return (
107+
<div className={cn("space-y-2", className)}>
108+
{/* Tool Call Header */}
109+
<div
110+
className={cn(
111+
"flex items-center gap-2 p-2 rounded-md border cursor-pointer transition-colors",
112+
"hover:bg-muted/50",
113+
isPending && "border-muted-foreground/20",
114+
!isPending && !isError && "border-green-500/20",
115+
isError && "border-destructive/20"
116+
)}
117+
onClick={() => setIsExpanded(!isExpanded)}
118+
>
119+
{/* Expand/Collapse Icon */}
120+
<motion.div
121+
animate={{ rotate: isExpanded ? 90 : 0 }}
122+
transition={{ duration: 0.2 }}
123+
>
124+
<ChevronRight className="h-3 w-3 text-muted-foreground" />
125+
</motion.div>
126+
127+
{/* Tool Icon */}
128+
<div className="text-muted-foreground">
129+
{getToolIcon(toolCall.name)}
130+
</div>
131+
132+
{/* Tool Name */}
133+
<span className="text-sm font-medium">
134+
{getToolDisplayName(toolCall.name)}
135+
</span>
136+
137+
{/* Tool Description */}
138+
<span className="text-xs text-muted-foreground flex-1 truncate">
139+
{getToolDescription(toolCall)}
140+
</span>
141+
142+
{/* Status Icon */}
143+
<div className="ml-auto">
144+
{isPending ? (
145+
<Loader2 className="h-4 w-4 animate-spin text-muted-foreground" />
146+
) : isError ? (
147+
<AlertCircle className="h-4 w-4 text-destructive" />
148+
) : (
149+
<CheckCircle2 className="h-4 w-4 text-green-500" />
150+
)}
151+
</div>
152+
</div>
153+
154+
{/* Tool Result (collapsible) */}
155+
<AnimatePresence>
156+
{isExpanded && toolResult && (
157+
<motion.div
158+
initial={{ height: 0, opacity: 0 }}
159+
animate={{ height: "auto", opacity: 1 }}
160+
exit={{ height: 0, opacity: 0 }}
161+
transition={{ duration: 0.2 }}
162+
className="overflow-hidden"
163+
>
164+
<div className={cn(
165+
"ml-6 p-2 rounded-md border",
166+
isError ? "border-destructive/20 bg-destructive/5" : "border-green-500/20 bg-green-500/5"
167+
)}>
168+
<div className="flex items-center gap-2 mb-2">
169+
{isError ? (
170+
<AlertCircle className="h-4 w-4 text-destructive" />
171+
) : (
172+
<CheckCircle2 className="h-4 w-4 text-green-500" />
173+
)}
174+
<span className="text-sm font-medium">
175+
{isError ? "Tool Error" : "Tool Result"}
176+
</span>
177+
</div>
178+
179+
{/* Result Content */}
180+
<div className="text-xs font-mono overflow-x-auto whitespace-pre-wrap">
181+
{typeof toolResult.content === 'string'
182+
? toolResult.content
183+
: JSON.stringify(toolResult.content, null, 2)}
184+
</div>
185+
</div>
186+
</motion.div>
187+
)}
188+
</AnimatePresence>
189+
</div>
190+
);
191+
};

0 commit comments

Comments
 (0)