feat: non-collapsible widgets with tool call/result mapping

This commit is contained in:
Vivek R
2025-06-23 23:25:25 +05:30
parent 670630fb63
commit c52c29ebad
7 changed files with 415 additions and 470 deletions

View File

@@ -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<ClaudeCodeSessionProps> = ({
}) => {
const [projectPath, setProjectPath] = useState(initialProjectPath || session?.project_path || "");
const [messages, setMessages] = useState<ClaudeStreamMessage[]>([]);
const [enhancedMessages, setEnhancedMessages] = useState<EnhancedMessage[]>([]);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [rawJsonlOutput, setRawJsonlOutput] = useState<string[]>([]);
@@ -114,8 +112,66 @@ export const ClaudeCodeSession: React.FC<ClaudeCodeSessionProps> = ({
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<ClaudeCodeSessionProps> = ({
}
}, [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<ClaudeCodeSessionProps> = ({
>
<AnimatePresence>
{rowVirtualizer.getVirtualItems().map((virtualItem) => {
const message = enhancedMessages[virtualItem.index];
const message = displayableMessages[virtualItem.index];
return (
<motion.div
key={virtualItem.key}
@@ -603,7 +654,7 @@ export const ClaudeCodeSession: React.FC<ClaudeCodeSessionProps> = ({
>
<StreamMessage
message={message}
streamMessages={enhancedMessages}
streamMessages={messages}
onLinkDetected={handleLinkDetected}
/>
</motion.div>
@@ -778,7 +829,7 @@ export const ClaudeCodeSession: React.FC<ClaudeCodeSessionProps> = ({
</Tooltip>
</TooltipProvider>
{enhancedMessages.length > 0 && (
{messages.length > 0 && (
<Popover
trigger={
<Button
@@ -855,7 +906,7 @@ export const ClaudeCodeSession: React.FC<ClaudeCodeSessionProps> = ({
</div>
)}
{isLoading && enhancedMessages.length === 0 && (
{isLoading && messages.length === 0 && (
<div className="flex items-center justify-center h-full">
<div className="flex items-center gap-3">
<Loader2 className="h-6 w-6 animate-spin" />
@@ -865,31 +916,6 @@ export const ClaudeCodeSession: React.FC<ClaudeCodeSessionProps> = ({
</div>
</div>
)}
<AnimatePresence>
{enhancedMessages.map((message, index) => (
<motion.div
key={index}
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.2 }}
>
<ErrorBoundary>
<StreamMessage message={message} streamMessages={enhancedMessages} />
</ErrorBoundary>
</motion.div>
))}
</AnimatePresence>
{/* Show loading indicator when processing, even if there are messages */}
{isLoading && enhancedMessages.length > 0 && (
<div className="flex items-center gap-2 p-4">
<Loader2 className="h-4 w-4 animate-spin" />
<span className="text-sm text-muted-foreground">
{isCancelling ? "Cancelling..." : "Processing..."}
</span>
</div>
)}
</div>
{/* Floating Prompt Input - Always visible */}