From 9a4158c649d922deb4aced4c65ac63546e5743e8 Mon Sep 17 00:00:00 2001 From: Vivek R <123vivekr@gmail.com> Date: Sun, 22 Jun 2025 22:39:53 +0530 Subject: [PATCH] feat: implement tool call/result mapping with collapsible UI --- src/components/AgentExecution.tsx | 24 ++- src/components/ClaudeCodeSession.tsx | 22 ++- src/components/CollapsibleToolResult.tsx | 191 +++++++++++++++++++++++ src/components/SessionOutputViewer.tsx | 24 ++- src/components/StreamMessage.tsx | 142 ++++++++++------- src/types/enhanced-messages.ts | 143 +++++++++++++++++ 6 files changed, 469 insertions(+), 77 deletions(-) create mode 100644 src/components/CollapsibleToolResult.tsx create mode 100644 src/types/enhanced-messages.ts diff --git a/src/components/AgentExecution.tsx b/src/components/AgentExecution.tsx index 1655486..e057640 100644 --- a/src/components/AgentExecution.tsx +++ b/src/components/AgentExecution.tsx @@ -24,6 +24,7 @@ import { listen, type UnlistenFn } from "@tauri-apps/api/event"; import { StreamMessage } from "./StreamMessage"; import { ExecutionControlBar } from "./ExecutionControlBar"; import { ErrorBoundary } from "./ErrorBoundary"; +import { enhanceMessages, type EnhancedMessage } from "@/types/enhanced-messages"; interface AgentExecutionProps { /** @@ -73,6 +74,7 @@ export const AgentExecution: React.FC = ({ const [model, setModel] = useState(agent.model || "sonnet"); const [isRunning, setIsRunning] = useState(false); const [messages, setMessages] = useState([]); + const [enhancedMessages, setEnhancedMessages] = useState([]); const [rawJsonlOutput, setRawJsonlOutput] = useState([]); const [error, setError] = useState(null); const [copyPopoverOpen, setCopyPopoverOpen] = useState(false); @@ -159,6 +161,12 @@ export const AgentExecution: React.FC = ({ setTotalTokens(tokens); }, [messages]); + // Enhance messages whenever they change + useEffect(() => { + const enhanced = enhanceMessages(messages); + setEnhancedMessages(enhanced); + }, [messages]); + const handleSelectPath = async () => { try { const selected = await open({ @@ -594,7 +602,7 @@ export const AgentExecution: React.FC = ({ }} >
- {messages.length === 0 && !isRunning && ( + {enhancedMessages.length === 0 && !isRunning && (

Ready to Execute

@@ -604,7 +612,7 @@ export const AgentExecution: React.FC = ({
)} - {isRunning && messages.length === 0 && ( + {isRunning && enhancedMessages.length === 0 && (
@@ -614,7 +622,7 @@ export const AgentExecution: React.FC = ({ )} - {messages.map((message, index) => ( + {enhancedMessages.map((message, index) => ( = ({ className="mb-4" > - + ))} @@ -724,7 +732,7 @@ export const AgentExecution: React.FC = ({ } }} > - {messages.length === 0 && !isRunning && ( + {enhancedMessages.length === 0 && !isRunning && (

Ready to Execute

@@ -734,7 +742,7 @@ export const AgentExecution: React.FC = ({
)} - {isRunning && messages.length === 0 && ( + {isRunning && enhancedMessages.length === 0 && (
@@ -744,7 +752,7 @@ export const AgentExecution: React.FC = ({ )} - {messages.map((message, index) => ( + {enhancedMessages.map((message, index) => ( = ({ className="mb-4" > - + ))} diff --git a/src/components/ClaudeCodeSession.tsx b/src/components/ClaudeCodeSession.tsx index ce16785..a81cb52 100644 --- a/src/components/ClaudeCodeSession.tsx +++ b/src/components/ClaudeCodeSession.tsx @@ -26,6 +26,7 @@ import { TimelineNavigator } from "./TimelineNavigator"; import { CheckpointSettings } from "./CheckpointSettings"; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter } from "@/components/ui/dialog"; import type { ClaudeStreamMessage } from "./AgentExecution"; +import { enhanceMessages, type EnhancedMessage } from "@/types/enhanced-messages"; interface ClaudeCodeSessionProps { /** @@ -60,6 +61,7 @@ export const ClaudeCodeSession: React.FC = ({ }) => { const [projectPath, setProjectPath] = useState(initialProjectPath || session?.project_path || ""); const [messages, setMessages] = useState([]); + const [enhancedMessages, setEnhancedMessages] = useState([]); const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(null); const [rawJsonlOutput, setRawJsonlOutput] = useState([]); @@ -115,10 +117,16 @@ export const ClaudeCodeSession: React.FC = ({ } }, [session]); + // Enhance messages whenever they change + useEffect(() => { + const enhanced = enhanceMessages(messages); + setEnhancedMessages(enhanced); + }, [messages]); + // Auto-scroll to bottom when new messages arrive useEffect(() => { messagesEndRef.current?.scrollIntoView({ behavior: "smooth" }); - }, [messages]); + }, [enhancedMessages]); // Calculate total tokens from messages useEffect(() => { @@ -513,7 +521,7 @@ export const ClaudeCodeSession: React.FC = ({ )} - {messages.length > 0 && ( + {enhancedMessages.length > 0 && ( = ({ {/* Messages Display */}
- {messages.length === 0 && !isLoading && ( + {enhancedMessages.length === 0 && !isLoading && (

Ready to Start

@@ -624,7 +632,7 @@ export const ClaudeCodeSession: React.FC = ({
)} - {isLoading && messages.length === 0 && ( + {isLoading && enhancedMessages.length === 0 && (
@@ -636,7 +644,7 @@ export const ClaudeCodeSession: React.FC = ({ )} - {messages.map((message, index) => ( + {enhancedMessages.map((message, index) => ( = ({ transition={{ duration: 0.2 }} > - + ))} {/* Show loading indicator when processing, even if there are messages */} - {isLoading && messages.length > 0 && ( + {isLoading && enhancedMessages.length > 0 && (
Processing... diff --git a/src/components/CollapsibleToolResult.tsx b/src/components/CollapsibleToolResult.tsx new file mode 100644 index 0000000..c904c96 --- /dev/null +++ b/src/components/CollapsibleToolResult.tsx @@ -0,0 +1,191 @@ +import React, { useState } from "react"; +import { motion, AnimatePresence } from "framer-motion"; +import { + ChevronDown, + ChevronRight, + Loader2, + CheckCircle2, + AlertCircle, + Terminal, + FileText, + Search, + Edit, + FolderOpen, + Code +} from "lucide-react"; +import { cn } from "@/lib/utils"; +import type { ToolCall, ToolResult } from "@/types/enhanced-messages"; + +interface CollapsibleToolResultProps { + toolCall: ToolCall; + toolResult?: ToolResult; + className?: string; + children?: React.ReactNode; +} + +// Map tool names to icons +const toolIcons: Record = { + read: , + write: , + edit: , + multiedit: , + bash: , + ls: , + glob: , + grep: , + task: , + default: +}; + +// Get tool icon based on tool name +function getToolIcon(toolName: string): React.ReactNode { + const lowerName = toolName.toLowerCase(); + return toolIcons[lowerName] || toolIcons.default; +} + +// Get display name for tools +function getToolDisplayName(toolName: string): string { + const displayNames: Record = { + ls: "List directory", + read: "Read file", + write: "Write file", + edit: "Edit file", + multiedit: "Multi-edit file", + bash: "Run command", + glob: "Find files", + grep: "Search files", + task: "Run task", + todowrite: "Update todos", + todoread: "Read todos", + websearch: "Search web", + webfetch: "Fetch webpage" + }; + + const lowerName = toolName.toLowerCase(); + return displayNames[lowerName] || toolName; +} + +// Get a brief description of the tool call +function getToolDescription(toolCall: ToolCall): string { + const name = toolCall.name.toLowerCase(); + const input = toolCall.input; + + switch (name) { + case "read": + return input?.file_path ? `${input.file_path}` : "Reading file"; + case "write": + return input?.file_path ? `${input.file_path}` : "Writing file"; + case "edit": + case "multiedit": + return input?.file_path ? `${input.file_path}` : "Editing file"; + case "bash": + return input?.command ? `${input.command}` : "Running command"; + case "ls": + return input?.path ? `${input.path}` : "Listing directory"; + case "glob": + return input?.pattern ? `${input.pattern}` : "Finding files"; + case "grep": + return input?.pattern ? `${input.pattern}` : "Searching files"; + case "task": + return input?.description || "Running task"; + default: + return toolCall.name; + } +} + +export const CollapsibleToolResult: React.FC = ({ + toolCall, + toolResult, + className, + children +}) => { + const [isExpanded, setIsExpanded] = useState(false); + const isPending = !toolResult; + const isError = toolResult?.isError; + + return ( +
+ {/* Tool Call Header */} +
setIsExpanded(!isExpanded)} + > + {/* Expand/Collapse Icon */} + + + + + {/* Tool Icon */} +
+ {getToolIcon(toolCall.name)} +
+ + {/* Tool Name */} + + {getToolDisplayName(toolCall.name)} + + + {/* Tool Description */} + + {getToolDescription(toolCall)} + + + {/* Status Icon */} +
+ {isPending ? ( + + ) : isError ? ( + + ) : ( + + )} +
+
+ + {/* Tool Result (collapsible) */} + + {isExpanded && toolResult && ( + +
+
+ {isError ? ( + + ) : ( + + )} + + {isError ? "Tool Error" : "Tool Result"} + +
+ + {/* Result Content */} +
+ {typeof toolResult.content === 'string' + ? toolResult.content + : JSON.stringify(toolResult.content, null, 2)} +
+
+
+ )} +
+
+ ); +}; \ No newline at end of file diff --git a/src/components/SessionOutputViewer.tsx b/src/components/SessionOutputViewer.tsx index d36d0e8..ff601fd 100644 --- a/src/components/SessionOutputViewer.tsx +++ b/src/components/SessionOutputViewer.tsx @@ -12,6 +12,7 @@ import type { AgentRun } from '@/lib/api'; import { listen, type UnlistenFn } from '@tauri-apps/api/event'; import { StreamMessage } from './StreamMessage'; import { ErrorBoundary } from './ErrorBoundary'; +import { enhanceMessages, type EnhancedMessage } from '@/types/enhanced-messages'; interface SessionOutputViewerProps { session: AgentRun; @@ -39,6 +40,7 @@ export interface ClaudeStreamMessage { export function SessionOutputViewer({ session, onClose, className }: SessionOutputViewerProps) { const [messages, setMessages] = useState([]); + const [enhancedMessages, setEnhancedMessages] = useState([]); const [rawJsonlOutput, setRawJsonlOutput] = useState([]); const [loading, setLoading] = useState(false); const [isFullscreen, setIsFullscreen] = useState(false); @@ -89,6 +91,12 @@ export function SessionOutputViewer({ session, onClose, className }: SessionOutp } }, [messages, hasUserScrolled, isFullscreen]); + // Enhance messages whenever they change + useEffect(() => { + const enhanced = enhanceMessages(messages); + setEnhancedMessages(enhanced); + }, [messages]); + const loadOutput = async (skipCache = false) => { if (!session.id) return; @@ -309,13 +317,13 @@ export function SessionOutputViewer({ session, onClose, className }: SessionOutp )} - {messages.length} messages + {enhancedMessages.length} messages
- {messages.length > 0 && ( + {enhancedMessages.length > 0 && ( <>