Skip to content

Commit c52c29e

Browse files
committed
feat: non-collapsible widgets with tool call/result mapping
1 parent 670630f commit c52c29e

7 files changed

Lines changed: 415 additions & 470 deletions

File tree

src/components/AgentExecution.tsx

Lines changed: 73 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@ 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";
2827
import { useVirtualizer } from "@tanstack/react-virtual";
2928

3029
interface AgentExecutionProps {
@@ -75,7 +74,6 @@ export const AgentExecution: React.FC<AgentExecutionProps> = ({
7574
const [model, setModel] = useState(agent.model || "sonnet");
7675
const [isRunning, setIsRunning] = useState(false);
7776
const [messages, setMessages] = useState<ClaudeStreamMessage[]>([]);
78-
const [enhancedMessages, setEnhancedMessages] = useState<EnhancedMessage[]>([]);
7977
const [rawJsonlOutput, setRawJsonlOutput] = useState<string[]>([]);
8078
const [error, setError] = useState<string | null>(null);
8179
const [copyPopoverOpen, setCopyPopoverOpen] = useState(false);
@@ -95,16 +93,74 @@ export const AgentExecution: React.FC<AgentExecutionProps> = ({
9593
const unlistenRefs = useRef<UnlistenFn[]>([]);
9694
const elapsedTimeIntervalRef = useRef<NodeJS.Timeout | null>(null);
9795

96+
// Filter out messages that shouldn't be displayed
97+
const displayableMessages = React.useMemo(() => {
98+
return messages.filter((message, index) => {
99+
// Skip meta messages that don't have meaningful content
100+
if (message.isMeta && !message.leafUuid && !message.summary) {
101+
return false;
102+
}
103+
104+
// Skip empty user messages
105+
if (message.type === "user" && message.message) {
106+
const msg = message.message;
107+
if (!msg.content || (Array.isArray(msg.content) && msg.content.length === 0)) {
108+
return false;
109+
}
110+
111+
// Check if this is a user message with only tool results that are already displayed
112+
if (Array.isArray(msg.content)) {
113+
const hasOnlyHiddenToolResults = msg.content.every((content: any) => {
114+
if (content.type !== "tool_result") return false;
115+
116+
// Check if this tool result should be hidden
117+
let hasCorrespondingWidget = false;
118+
if (content.tool_use_id) {
119+
// Look for the matching tool_use in previous assistant messages
120+
for (let i = index - 1; i >= 0; i--) {
121+
const prevMsg = messages[i];
122+
if (prevMsg.type === 'assistant' && prevMsg.message?.content && Array.isArray(prevMsg.message.content)) {
123+
const toolUse = prevMsg.message.content.find((c: any) =>
124+
c.type === 'tool_use' && c.id === content.tool_use_id
125+
);
126+
if (toolUse) {
127+
const toolName = toolUse.name?.toLowerCase();
128+
const toolsWithWidgets = [
129+
'task', 'edit', 'multiedit', 'todowrite', 'ls', 'read',
130+
'glob', 'bash', 'write', 'grep'
131+
];
132+
if (toolsWithWidgets.includes(toolName) || toolUse.name?.startsWith('mcp__')) {
133+
hasCorrespondingWidget = true;
134+
}
135+
break;
136+
}
137+
}
138+
}
139+
}
140+
141+
return hasCorrespondingWidget && !content.is_error;
142+
});
143+
144+
if (hasOnlyHiddenToolResults) {
145+
return false;
146+
}
147+
}
148+
}
149+
150+
return true;
151+
});
152+
}, [messages]);
153+
98154
// Virtualizers for efficient, smooth scrolling of potentially very long outputs
99155
const rowVirtualizer = useVirtualizer({
100-
count: enhancedMessages.length,
156+
count: displayableMessages.length,
101157
getScrollElement: () => scrollContainerRef.current,
102158
estimateSize: () => 150, // fallback estimate; dynamically measured afterwards
103159
overscan: 5,
104160
});
105161

106162
const fullscreenRowVirtualizer = useVirtualizer({
107-
count: enhancedMessages.length,
163+
count: displayableMessages.length,
108164
getScrollElement: () => fullscreenScrollRef.current,
109165
estimateSize: () => 150,
110166
overscan: 5,
@@ -132,19 +188,19 @@ export const AgentExecution: React.FC<AgentExecutionProps> = ({
132188
};
133189

134190
useEffect(() => {
135-
if (enhancedMessages.length === 0) return;
191+
if (displayableMessages.length === 0) return;
136192

137193
// Auto-scroll only if the user has not manually scrolled OR they are still at the bottom
138194
const shouldAutoScroll = !hasUserScrolled || isAtBottom();
139195

140196
if (shouldAutoScroll) {
141197
if (isFullscreenModalOpen) {
142-
fullscreenRowVirtualizer.scrollToIndex(enhancedMessages.length - 1, { align: "end", behavior: "smooth" });
198+
fullscreenRowVirtualizer.scrollToIndex(displayableMessages.length - 1, { align: "end", behavior: "smooth" });
143199
} else {
144-
rowVirtualizer.scrollToIndex(enhancedMessages.length - 1, { align: "end", behavior: "smooth" });
200+
rowVirtualizer.scrollToIndex(displayableMessages.length - 1, { align: "end", behavior: "smooth" });
145201
}
146202
}
147-
}, [enhancedMessages.length, hasUserScrolled, isFullscreenModalOpen, rowVirtualizer, fullscreenRowVirtualizer]);
203+
}, [displayableMessages.length, hasUserScrolled, isFullscreenModalOpen, rowVirtualizer, fullscreenRowVirtualizer]);
148204

149205
// Update elapsed time while running
150206
useEffect(() => {
@@ -179,11 +235,6 @@ export const AgentExecution: React.FC<AgentExecutionProps> = ({
179235
setTotalTokens(tokens);
180236
}, [messages]);
181237

182-
// Enhance messages whenever they change
183-
useEffect(() => {
184-
const enhanced = enhanceMessages(messages);
185-
setEnhancedMessages(enhanced);
186-
}, [messages]);
187238

188239
const handleSelectPath = async () => {
189240
try {
@@ -620,7 +671,7 @@ export const AgentExecution: React.FC<AgentExecutionProps> = ({
620671
}}
621672
>
622673
<div ref={messagesContainerRef}>
623-
{enhancedMessages.length === 0 && !isRunning && (
674+
{messages.length === 0 && !isRunning && (
624675
<div className="flex flex-col items-center justify-center h-full text-center">
625676
<Terminal className="h-16 w-16 text-muted-foreground mb-4" />
626677
<h3 className="text-lg font-medium mb-2">Ready to Execute</h3>
@@ -630,7 +681,7 @@ export const AgentExecution: React.FC<AgentExecutionProps> = ({
630681
</div>
631682
)}
632683

633-
{isRunning && enhancedMessages.length === 0 && (
684+
{isRunning && messages.length === 0 && (
634685
<div className="flex items-center justify-center h-full">
635686
<div className="flex items-center gap-3">
636687
<Loader2 className="h-6 w-6 animate-spin" />
@@ -645,7 +696,7 @@ export const AgentExecution: React.FC<AgentExecutionProps> = ({
645696
>
646697
<AnimatePresence>
647698
{rowVirtualizer.getVirtualItems().map((virtualItem) => {
648-
const message = enhancedMessages[virtualItem.index];
699+
const message = displayableMessages[virtualItem.index];
649700
return (
650701
<motion.div
651702
key={virtualItem.key}
@@ -658,7 +709,7 @@ export const AgentExecution: React.FC<AgentExecutionProps> = ({
658709
style={{ top: virtualItem.start }}
659710
>
660711
<ErrorBoundary>
661-
<StreamMessage message={message} streamMessages={enhancedMessages} />
712+
<StreamMessage message={message} streamMessages={messages} />
662713
</ErrorBoundary>
663714
</motion.div>
664715
);
@@ -761,7 +812,7 @@ export const AgentExecution: React.FC<AgentExecutionProps> = ({
761812
}
762813
}}
763814
>
764-
{enhancedMessages.length === 0 && !isRunning && (
815+
{messages.length === 0 && !isRunning && (
765816
<div className="flex flex-col items-center justify-center h-full text-center">
766817
<Terminal className="h-16 w-16 text-muted-foreground mb-4" />
767818
<h3 className="text-lg font-medium mb-2">Ready to Execute</h3>
@@ -771,7 +822,7 @@ export const AgentExecution: React.FC<AgentExecutionProps> = ({
771822
</div>
772823
)}
773824

774-
{isRunning && enhancedMessages.length === 0 && (
825+
{isRunning && messages.length === 0 && (
775826
<div className="flex items-center justify-center h-full">
776827
<div className="flex items-center gap-3">
777828
<Loader2 className="h-6 w-6 animate-spin" />
@@ -786,7 +837,7 @@ export const AgentExecution: React.FC<AgentExecutionProps> = ({
786837
>
787838
<AnimatePresence>
788839
{fullscreenRowVirtualizer.getVirtualItems().map((virtualItem) => {
789-
const message = enhancedMessages[virtualItem.index];
840+
const message = displayableMessages[virtualItem.index];
790841
return (
791842
<motion.div
792843
key={virtualItem.key}
@@ -799,7 +850,7 @@ export const AgentExecution: React.FC<AgentExecutionProps> = ({
799850
style={{ top: virtualItem.start }}
800851
>
801852
<ErrorBoundary>
802-
<StreamMessage message={message} streamMessages={enhancedMessages} />
853+
<StreamMessage message={message} streamMessages={messages} />
803854
</ErrorBoundary>
804855
</motion.div>
805856
);
@@ -817,4 +868,4 @@ export const AgentExecution: React.FC<AgentExecutionProps> = ({
817868
};
818869

819870
// Import AGENT_ICONS for icon rendering
820-
import { AGENT_ICONS } from "./CCAgents";
871+
import { AGENT_ICONS } from "./CCAgents";

src/components/ClaudeCodeSession.tsx

Lines changed: 66 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,6 @@ import { SplitPane } from "@/components/ui/split-pane";
3232
import { WebviewPreview } from "./WebviewPreview";
3333
import { PreviewPromptDialog } from "./PreviewPromptDialog";
3434
import type { ClaudeStreamMessage } from "./AgentExecution";
35-
import { enhanceMessages, type EnhancedMessage } from "@/types/enhanced-messages";
3635
import { useVirtualizer } from "@tanstack/react-virtual";
3736

3837
interface ClaudeCodeSessionProps {
@@ -68,7 +67,6 @@ export const ClaudeCodeSession: React.FC<ClaudeCodeSessionProps> = ({
6867
}) => {
6968
const [projectPath, setProjectPath] = useState(initialProjectPath || session?.project_path || "");
7069
const [messages, setMessages] = useState<ClaudeStreamMessage[]>([]);
71-
const [enhancedMessages, setEnhancedMessages] = useState<EnhancedMessage[]>([]);
7270
const [isLoading, setIsLoading] = useState(false);
7371
const [error, setError] = useState<string | null>(null);
7472
const [rawJsonlOutput, setRawJsonlOutput] = useState<string[]>([]);
@@ -114,8 +112,66 @@ export const ClaudeCodeSession: React.FC<ClaudeCodeSessionProps> = ({
114112
return null;
115113
}, [session, extractedSessionInfo, projectPath]);
116114

115+
// Filter out messages that shouldn't be displayed
116+
const displayableMessages = useMemo(() => {
117+
return messages.filter((message, index) => {
118+
// Skip meta messages that don't have meaningful content
119+
if (message.isMeta && !message.leafUuid && !message.summary) {
120+
return false;
121+
}
122+
123+
// Skip empty user messages
124+
if (message.type === "user" && message.message) {
125+
const msg = message.message;
126+
if (!msg.content || (Array.isArray(msg.content) && msg.content.length === 0)) {
127+
return false;
128+
}
129+
130+
// Check if this is a user message with only tool results that are already displayed
131+
if (Array.isArray(msg.content)) {
132+
const hasOnlyHiddenToolResults = msg.content.every((content: any) => {
133+
if (content.type !== "tool_result") return false;
134+
135+
// Check if this tool result should be hidden
136+
let hasCorrespondingWidget = false;
137+
if (content.tool_use_id) {
138+
// Look for the matching tool_use in previous assistant messages
139+
for (let i = index - 1; i >= 0; i--) {
140+
const prevMsg = messages[i];
141+
if (prevMsg.type === 'assistant' && prevMsg.message?.content && Array.isArray(prevMsg.message.content)) {
142+
const toolUse = prevMsg.message.content.find((c: any) =>
143+
c.type === 'tool_use' && c.id === content.tool_use_id
144+
);
145+
if (toolUse) {
146+
const toolName = toolUse.name?.toLowerCase();
147+
const toolsWithWidgets = [
148+
'task', 'edit', 'multiedit', 'todowrite', 'ls', 'read',
149+
'glob', 'bash', 'write', 'grep'
150+
];
151+
if (toolsWithWidgets.includes(toolName) || toolUse.name?.startsWith('mcp__')) {
152+
hasCorrespondingWidget = true;
153+
}
154+
break;
155+
}
156+
}
157+
}
158+
}
159+
160+
return hasCorrespondingWidget && !content.is_error;
161+
});
162+
163+
if (hasOnlyHiddenToolResults) {
164+
return false;
165+
}
166+
}
167+
}
168+
169+
return true;
170+
});
171+
}, [messages]);
172+
117173
const rowVirtualizer = useVirtualizer({
118-
count: enhancedMessages.length,
174+
count: displayableMessages.length,
119175
getScrollElement: () => parentRef.current,
120176
estimateSize: () => 150, // Estimate, will be dynamically measured
121177
overscan: 5,
@@ -140,18 +196,13 @@ export const ClaudeCodeSession: React.FC<ClaudeCodeSessionProps> = ({
140196
}
141197
}, [session]);
142198

143-
// Enhance messages whenever they change
144-
useEffect(() => {
145-
const enhanced = enhanceMessages(messages);
146-
setEnhancedMessages(enhanced);
147-
}, [messages]);
148199

149200
// Auto-scroll to bottom when new messages arrive
150201
useEffect(() => {
151-
if (enhancedMessages.length > 0) {
152-
rowVirtualizer.scrollToIndex(enhancedMessages.length - 1, { align: 'end', behavior: 'smooth' });
202+
if (displayableMessages.length > 0) {
203+
rowVirtualizer.scrollToIndex(displayableMessages.length - 1, { align: 'end', behavior: 'smooth' });
153204
}
154-
}, [enhancedMessages.length, rowVirtualizer]);
205+
}, [displayableMessages.length, rowVirtualizer]);
155206

156207
// Calculate total tokens from messages
157208
useEffect(() => {
@@ -586,7 +637,7 @@ export const ClaudeCodeSession: React.FC<ClaudeCodeSessionProps> = ({
586637
>
587638
<AnimatePresence>
588639
{rowVirtualizer.getVirtualItems().map((virtualItem) => {
589-
const message = enhancedMessages[virtualItem.index];
640+
const message = displayableMessages[virtualItem.index];
590641
return (
591642
<motion.div
592643
key={virtualItem.key}
@@ -603,7 +654,7 @@ export const ClaudeCodeSession: React.FC<ClaudeCodeSessionProps> = ({
603654
>
604655
<StreamMessage
605656
message={message}
606-
streamMessages={enhancedMessages}
657+
streamMessages={messages}
607658
onLinkDetected={handleLinkDetected}
608659
/>
609660
</motion.div>
@@ -778,7 +829,7 @@ export const ClaudeCodeSession: React.FC<ClaudeCodeSessionProps> = ({
778829
</Tooltip>
779830
</TooltipProvider>
780831

781-
{enhancedMessages.length > 0 && (
832+
{messages.length > 0 && (
782833
<Popover
783834
trigger={
784835
<Button
@@ -855,7 +906,7 @@ export const ClaudeCodeSession: React.FC<ClaudeCodeSessionProps> = ({
855906
</div>
856907
)}
857908

858-
{isLoading && enhancedMessages.length === 0 && (
909+
{isLoading && messages.length === 0 && (
859910
<div className="flex items-center justify-center h-full">
860911
<div className="flex items-center gap-3">
861912
<Loader2 className="h-6 w-6 animate-spin" />
@@ -865,31 +916,6 @@ export const ClaudeCodeSession: React.FC<ClaudeCodeSessionProps> = ({
865916
</div>
866917
</div>
867918
)}
868-
869-
<AnimatePresence>
870-
{enhancedMessages.map((message, index) => (
871-
<motion.div
872-
key={index}
873-
initial={{ opacity: 0, y: 10 }}
874-
animate={{ opacity: 1, y: 0 }}
875-
transition={{ duration: 0.2 }}
876-
>
877-
<ErrorBoundary>
878-
<StreamMessage message={message} streamMessages={enhancedMessages} />
879-
</ErrorBoundary>
880-
</motion.div>
881-
))}
882-
</AnimatePresence>
883-
884-
{/* Show loading indicator when processing, even if there are messages */}
885-
{isLoading && enhancedMessages.length > 0 && (
886-
<div className="flex items-center gap-2 p-4">
887-
<Loader2 className="h-4 w-4 animate-spin" />
888-
<span className="text-sm text-muted-foreground">
889-
{isCancelling ? "Cancelling..." : "Processing..."}
890-
</span>
891-
</div>
892-
)}
893919
</div>
894920

895921
{/* Floating Prompt Input - Always visible */}

0 commit comments

Comments
 (0)