diff --git a/src/components/AgentExecution.tsx b/src/components/AgentExecution.tsx index 115fb42..a5f1b64 100644 --- a/src/components/AgentExecution.tsx +++ b/src/components/AgentExecution.tsx @@ -24,7 +24,6 @@ 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"; import { useVirtualizer } from "@tanstack/react-virtual"; interface AgentExecutionProps { @@ -75,7 +74,6 @@ 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); @@ -95,16 +93,74 @@ export const AgentExecution: React.FC = ({ const unlistenRefs = useRef([]); const elapsedTimeIntervalRef = useRef(null); + // Filter out messages that shouldn't be displayed + const displayableMessages = React.useMemo(() => { + return messages.filter((message, index) => { + // Skip meta messages that don't have meaningful content + if (message.isMeta && !message.leafUuid && !message.summary) { + return false; + } + + // Skip empty user messages + if (message.type === "user" && message.message) { + const msg = message.message; + if (!msg.content || (Array.isArray(msg.content) && msg.content.length === 0)) { + return false; + } + + // Check if this is a user message with only tool results that are already displayed + if (Array.isArray(msg.content)) { + const hasOnlyHiddenToolResults = msg.content.every((content: any) => { + if (content.type !== "tool_result") return false; + + // Check if this tool result should be hidden + let hasCorrespondingWidget = false; + if (content.tool_use_id) { + // Look for the matching tool_use in previous assistant messages + for (let i = index - 1; i >= 0; i--) { + const prevMsg = messages[i]; + if (prevMsg.type === 'assistant' && prevMsg.message?.content && Array.isArray(prevMsg.message.content)) { + const toolUse = prevMsg.message.content.find((c: any) => + c.type === 'tool_use' && c.id === content.tool_use_id + ); + if (toolUse) { + const toolName = toolUse.name?.toLowerCase(); + const toolsWithWidgets = [ + 'task', 'edit', 'multiedit', 'todowrite', 'ls', 'read', + 'glob', 'bash', 'write', 'grep' + ]; + if (toolsWithWidgets.includes(toolName) || toolUse.name?.startsWith('mcp__')) { + hasCorrespondingWidget = true; + } + break; + } + } + } + } + + return hasCorrespondingWidget && !content.is_error; + }); + + if (hasOnlyHiddenToolResults) { + return false; + } + } + } + + return true; + }); + }, [messages]); + // Virtualizers for efficient, smooth scrolling of potentially very long outputs const rowVirtualizer = useVirtualizer({ - count: enhancedMessages.length, + count: displayableMessages.length, getScrollElement: () => scrollContainerRef.current, estimateSize: () => 150, // fallback estimate; dynamically measured afterwards overscan: 5, }); const fullscreenRowVirtualizer = useVirtualizer({ - count: enhancedMessages.length, + count: displayableMessages.length, getScrollElement: () => fullscreenScrollRef.current, estimateSize: () => 150, overscan: 5, @@ -132,19 +188,19 @@ export const AgentExecution: React.FC = ({ }; useEffect(() => { - if (enhancedMessages.length === 0) return; + if (displayableMessages.length === 0) return; // Auto-scroll only if the user has not manually scrolled OR they are still at the bottom const shouldAutoScroll = !hasUserScrolled || isAtBottom(); if (shouldAutoScroll) { if (isFullscreenModalOpen) { - fullscreenRowVirtualizer.scrollToIndex(enhancedMessages.length - 1, { align: "end", behavior: "smooth" }); + fullscreenRowVirtualizer.scrollToIndex(displayableMessages.length - 1, { align: "end", behavior: "smooth" }); } else { - rowVirtualizer.scrollToIndex(enhancedMessages.length - 1, { align: "end", behavior: "smooth" }); + rowVirtualizer.scrollToIndex(displayableMessages.length - 1, { align: "end", behavior: "smooth" }); } } - }, [enhancedMessages.length, hasUserScrolled, isFullscreenModalOpen, rowVirtualizer, fullscreenRowVirtualizer]); + }, [displayableMessages.length, hasUserScrolled, isFullscreenModalOpen, rowVirtualizer, fullscreenRowVirtualizer]); // Update elapsed time while running useEffect(() => { @@ -179,11 +235,6 @@ 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 { @@ -620,7 +671,7 @@ export const AgentExecution: React.FC = ({ }} >
- {enhancedMessages.length === 0 && !isRunning && ( + {messages.length === 0 && !isRunning && (

Ready to Execute

@@ -630,7 +681,7 @@ export const AgentExecution: React.FC = ({
)} - {isRunning && enhancedMessages.length === 0 && ( + {isRunning && messages.length === 0 && (
@@ -645,7 +696,7 @@ export const AgentExecution: React.FC = ({ > {rowVirtualizer.getVirtualItems().map((virtualItem) => { - const message = enhancedMessages[virtualItem.index]; + const message = displayableMessages[virtualItem.index]; return ( = ({ style={{ top: virtualItem.start }} > - + ); @@ -761,7 +812,7 @@ export const AgentExecution: React.FC = ({ } }} > - {enhancedMessages.length === 0 && !isRunning && ( + {messages.length === 0 && !isRunning && (

Ready to Execute

@@ -771,7 +822,7 @@ export const AgentExecution: React.FC = ({
)} - {isRunning && enhancedMessages.length === 0 && ( + {isRunning && messages.length === 0 && (
@@ -786,7 +837,7 @@ export const AgentExecution: React.FC = ({ > {fullscreenRowVirtualizer.getVirtualItems().map((virtualItem) => { - const message = enhancedMessages[virtualItem.index]; + const message = displayableMessages[virtualItem.index]; return ( = ({ style={{ top: virtualItem.start }} > - + ); @@ -817,4 +868,4 @@ export const AgentExecution: React.FC = ({ }; // Import AGENT_ICONS for icon rendering -import { AGENT_ICONS } from "./CCAgents"; +import { AGENT_ICONS } from "./CCAgents"; diff --git a/src/components/ClaudeCodeSession.tsx b/src/components/ClaudeCodeSession.tsx index 6f17ba5..8a9369e 100644 --- a/src/components/ClaudeCodeSession.tsx +++ b/src/components/ClaudeCodeSession.tsx @@ -32,7 +32,6 @@ import { SplitPane } from "@/components/ui/split-pane"; import { WebviewPreview } from "./WebviewPreview"; import { PreviewPromptDialog } from "./PreviewPromptDialog"; import type { ClaudeStreamMessage } from "./AgentExecution"; -import { enhanceMessages, type EnhancedMessage } from "@/types/enhanced-messages"; import { useVirtualizer } from "@tanstack/react-virtual"; interface ClaudeCodeSessionProps { @@ -68,7 +67,6 @@ 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([]); @@ -114,8 +112,66 @@ export const ClaudeCodeSession: React.FC = ({ return null; }, [session, extractedSessionInfo, projectPath]); + // Filter out messages that shouldn't be displayed + const displayableMessages = useMemo(() => { + return messages.filter((message, index) => { + // Skip meta messages that don't have meaningful content + if (message.isMeta && !message.leafUuid && !message.summary) { + return false; + } + + // Skip empty user messages + if (message.type === "user" && message.message) { + const msg = message.message; + if (!msg.content || (Array.isArray(msg.content) && msg.content.length === 0)) { + return false; + } + + // Check if this is a user message with only tool results that are already displayed + if (Array.isArray(msg.content)) { + const hasOnlyHiddenToolResults = msg.content.every((content: any) => { + if (content.type !== "tool_result") return false; + + // Check if this tool result should be hidden + let hasCorrespondingWidget = false; + if (content.tool_use_id) { + // Look for the matching tool_use in previous assistant messages + for (let i = index - 1; i >= 0; i--) { + const prevMsg = messages[i]; + if (prevMsg.type === 'assistant' && prevMsg.message?.content && Array.isArray(prevMsg.message.content)) { + const toolUse = prevMsg.message.content.find((c: any) => + c.type === 'tool_use' && c.id === content.tool_use_id + ); + if (toolUse) { + const toolName = toolUse.name?.toLowerCase(); + const toolsWithWidgets = [ + 'task', 'edit', 'multiedit', 'todowrite', 'ls', 'read', + 'glob', 'bash', 'write', 'grep' + ]; + if (toolsWithWidgets.includes(toolName) || toolUse.name?.startsWith('mcp__')) { + hasCorrespondingWidget = true; + } + break; + } + } + } + } + + return hasCorrespondingWidget && !content.is_error; + }); + + if (hasOnlyHiddenToolResults) { + return false; + } + } + } + + return true; + }); + }, [messages]); + const rowVirtualizer = useVirtualizer({ - count: enhancedMessages.length, + count: displayableMessages.length, getScrollElement: () => parentRef.current, estimateSize: () => 150, // Estimate, will be dynamically measured overscan: 5, @@ -140,18 +196,13 @@ 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(() => { - if (enhancedMessages.length > 0) { - rowVirtualizer.scrollToIndex(enhancedMessages.length - 1, { align: 'end', behavior: 'smooth' }); + if (displayableMessages.length > 0) { + rowVirtualizer.scrollToIndex(displayableMessages.length - 1, { align: 'end', behavior: 'smooth' }); } - }, [enhancedMessages.length, rowVirtualizer]); + }, [displayableMessages.length, rowVirtualizer]); // Calculate total tokens from messages useEffect(() => { @@ -586,7 +637,7 @@ export const ClaudeCodeSession: React.FC = ({ > {rowVirtualizer.getVirtualItems().map((virtualItem) => { - const message = enhancedMessages[virtualItem.index]; + const message = displayableMessages[virtualItem.index]; return ( = ({ > @@ -778,7 +829,7 @@ export const ClaudeCodeSession: React.FC = ({ - {enhancedMessages.length > 0 && ( + {messages.length > 0 && ( = ({
)} - {isLoading && enhancedMessages.length === 0 && ( + {isLoading && messages.length === 0 && (
@@ -865,31 +916,6 @@ export const ClaudeCodeSession: React.FC = ({
)} - - - {enhancedMessages.map((message, index) => ( - - - - - - ))} - - - {/* Show loading indicator when processing, even if there are messages */} - {isLoading && enhancedMessages.length > 0 && ( -
- - - {isCancelling ? "Cancelling..." : "Processing..."} - -
- )}
{/* Floating Prompt Input - Always visible */} diff --git a/src/components/CollapsibleToolResult.tsx b/src/components/CollapsibleToolResult.tsx deleted file mode 100644 index d066345..0000000 --- a/src/components/CollapsibleToolResult.tsx +++ /dev/null @@ -1,189 +0,0 @@ -import React, { useState } from "react"; -import { motion, AnimatePresence } from "framer-motion"; -import { - 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 -}) => { - 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 ff601fd..b42fc08 100644 --- a/src/components/SessionOutputViewer.tsx +++ b/src/components/SessionOutputViewer.tsx @@ -12,7 +12,6 @@ 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; @@ -40,7 +39,6 @@ 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); @@ -91,11 +89,6 @@ 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; @@ -317,13 +310,13 @@ export function SessionOutputViewer({ session, onClose, className }: SessionOutp )} - {enhancedMessages.length} messages + {messages.length} messages
- {enhancedMessages.length > 0 && ( + {messages.length > 0 && ( <>