fix: show tool outputs in embedded tool calls and improve light mode styling

- Build toolCallId -> output map from TOOL messages
- Pass outputs to embedded tool calls in ASSISTANT messages
- Skip rendering separate TOOL messages (now shown with their calls)
- Improve light mode contrast for tool cards:
  - Use slate-50 background instead of gray
  - Better status badge colors (green-700, blue-700 in light mode)
  - Better error colors (red-600 in light mode)
  - Better output text colors (emerald-700, red-600 in light mode)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Ajax Davis 2026-01-11 02:16:56 +10:00
parent 49af625254
commit 5c0b9c5ae9

View file

@ -86,10 +86,14 @@ function ToolCallCard({
const effectiveStatus = hasError ? 'error' : toolCall.status; const effectiveStatus = hasError ? 'error' : toolCall.status;
const statusColors = { const statusColors = {
pending: 'bg-yellow-500/20 text-yellow-400 border-yellow-500/30', pending:
running: 'bg-blue-500/20 text-blue-400 border-blue-500/30', 'bg-yellow-100 dark:bg-yellow-500/20 text-yellow-700 dark:text-yellow-400 border-yellow-300 dark:border-yellow-500/30',
success: 'bg-green-500/20 text-green-400 border-green-500/30', running:
error: 'bg-red-500/20 text-red-400 border-red-500/30', 'bg-blue-100 dark:bg-blue-500/20 text-blue-700 dark:text-blue-400 border-blue-300 dark:border-blue-500/30',
success:
'bg-green-100 dark:bg-green-500/20 text-green-700 dark:text-green-400 border-green-300 dark:border-green-500/30',
error:
'bg-red-100 dark:bg-red-500/20 text-red-700 dark:text-red-400 border-red-300 dark:border-red-500/30',
}; };
const statusIcons: Record<ToolCall['status'], 'loader' | 'check' | 'alertCircle' | 'info'> = { const statusIcons: Record<ToolCall['status'], 'loader' | 'check' | 'alertCircle' | 'info'> = {
@ -109,13 +113,13 @@ function ToolCallCard({
return ( return (
<div <div
className={`rounded-lg border overflow-hidden font-mono text-xs ${hasError ? 'border-red-500/50 bg-red-500/5' : 'border-border bg-surface-secondary/50'}`} className={`rounded-lg border overflow-hidden font-mono text-xs ${hasError ? 'border-red-500/50 bg-red-50 dark:bg-red-500/5' : 'border-border bg-slate-50 dark:bg-surface-secondary/50'}`}
> >
{/* Header */} {/* Header */}
<button <button
type="button" type="button"
onClick={onToggle} onClick={onToggle}
className="w-full flex items-center gap-3 p-3 hover:bg-surface-secondary/80 transition-colors" className="w-full flex items-center gap-3 p-3 hover:bg-slate-100 dark:hover:bg-surface-secondary/80 transition-colors"
> >
<div className={`p-1.5 rounded ${statusColors[effectiveStatus]}`}> <div className={`p-1.5 rounded ${statusColors[effectiveStatus]}`}>
<Icon <Icon
@ -131,14 +135,14 @@ function ToolCallCard({
{toolCall.toolCallId.slice(0, 8)}... {toolCall.toolCallId.slice(0, 8)}...
</span> </span>
{hasError && ( {hasError && (
<span className="text-[10px] px-1.5 py-0.5 rounded bg-red-500/20 text-red-400"> <span className="text-[10px] px-1.5 py-0.5 rounded bg-red-100 dark:bg-red-500/20 text-red-600 dark:text-red-400">
ERROR ERROR
</span> </span>
)} )}
</div> </div>
{/* Show error message preview in header */} {/* Show error message preview in header */}
{hasError && errorMessage && !isExpanded && ( {hasError && errorMessage && !isExpanded && (
<div className="text-red-400 text-[10px] mt-1 truncate max-w-[300px]"> <div className="text-red-600 dark:text-red-400 text-[10px] mt-1 truncate max-w-[300px]">
{errorMessage} {errorMessage}
</div> </div>
)} )}
@ -170,14 +174,14 @@ function ToolCallCard({
{/* Error Message Section */} {/* Error Message Section */}
{hasError && errorMessage && ( {hasError && errorMessage && (
<div className="p-3 bg-red-500/10 border-b border-red-500/20"> <div className="p-3 bg-red-100 dark:bg-red-500/10 border-b border-red-200 dark:border-red-500/20">
<div className="flex items-center gap-2 mb-2"> <div className="flex items-center gap-2 mb-2">
<Icon icon="alertCircle" size="xs" className="text-red-400" /> <Icon icon="alertCircle" size="xs" className="text-red-600 dark:text-red-400" />
<span className="text-[10px] uppercase tracking-wider text-red-400 font-semibold"> <span className="text-[10px] uppercase tracking-wider text-red-600 dark:text-red-400 font-semibold">
Error Error
</span> </span>
</div> </div>
<p className="text-[11px] text-red-400">{errorMessage}</p> <p className="text-[11px] text-red-600 dark:text-red-400">{errorMessage}</p>
</div> </div>
)} )}
@ -191,7 +195,7 @@ function ToolCallCard({
<div className="flex-1 h-px bg-border/50" /> <div className="flex-1 h-px bg-border/50" />
</div> </div>
<pre <pre
className={`text-[11px] overflow-x-auto whitespace-pre-wrap break-all max-h-48 overflow-y-auto ${hasError ? 'text-red-300' : 'text-green-400'}`} className={`text-[11px] overflow-x-auto whitespace-pre-wrap break-all max-h-48 overflow-y-auto ${hasError ? 'text-red-600 dark:text-red-300' : 'text-emerald-700 dark:text-green-400'}`}
> >
{formatJson(toolCall.output)} {formatJson(toolCall.output)}
</pre> </pre>
@ -604,80 +608,84 @@ export default function AgentChatPage(): React.ReactElement {
</div> </div>
)} )}
{messages.map((message) => { {(() => {
// Parse tool output safely // Build a map of toolCallId -> tool result from TOOL messages
const getToolOutput = () => { const toolResultsMap = new Map<string, unknown>();
if (message.toolResult) return message.toolResult; for (const msg of messages) {
try { if (msg.role === 'TOOL' && msg.toolCallId) {
return JSON.parse(message.content || '{}'); let output: unknown;
} catch { if (msg.toolResult) {
return { result: message.content }; output = msg.toolResult;
} else {
try {
output = JSON.parse(msg.content || '{}');
} catch {
output = { result: msg.content };
}
}
toolResultsMap.set(msg.toolCallId, output);
} }
}; }
// Check if this ASSISTANT message has embedded tool calls return messages.map((message) => {
const hasEmbeddedToolCalls = // Check if this ASSISTANT message has embedded tool calls
message.role === 'ASSISTANT' && const hasEmbeddedToolCalls =
message.toolCalls && message.role === 'ASSISTANT' &&
Array.isArray(message.toolCalls) && message.toolCalls &&
message.toolCalls.length > 0; Array.isArray(message.toolCalls) &&
message.toolCalls.length > 0;
return ( return (
<div key={message.id} className="space-y-2"> <div key={message.id} className="space-y-2">
{/* Render embedded tool calls from ASSISTANT messages */} {/* Render embedded tool calls from ASSISTANT messages */}
{hasEmbeddedToolCalls && ( {hasEmbeddedToolCalls && (
<div className="space-y-2"> <div className="space-y-2">
{message.toolCalls!.map((tc) => ( {message.toolCalls?.map((tc) => {
<div key={tc.toolCallId} className="flex justify-start"> // Look up the output for this tool call
<div className="max-w-[80%]"> const toolOutput = toolResultsMap.get(tc.toolCallId);
<ToolCallCard const hasOutput = toolOutput !== undefined;
toolCall={{
toolCallId: tc.toolCallId,
toolName: tc.toolName,
input: tc.args,
status: 'success',
}}
isExpanded={expandedToolCalls.has(tc.toolCallId)}
onToggle={() => toggleToolCall(tc.toolCallId)}
/>
</div>
</div>
))}
</div>
)}
{/* Render the message content */} return (
<div <div key={tc.toolCallId} className="flex justify-start">
className={`flex ${message.role === 'USER' ? 'justify-end' : 'justify-start'}`} <div className="max-w-[80%]">
> <ToolCallCard
{message.role === 'TOOL' ? ( toolCall={{
<div className="max-w-[80%]"> toolCallId: tc.toolCallId,
<ToolCallCard toolName: tc.toolName,
toolCall={{ input: tc.args,
toolCallId: message.toolCallId || message.id, output: toolOutput,
toolName: message.toolName || 'Unknown Tool', status: hasOutput ? 'success' : 'pending',
output: getToolOutput(), }}
status: 'success', isExpanded={expandedToolCalls.has(tc.toolCallId)}
}} onToggle={() => toggleToolCall(tc.toolCallId)}
isExpanded={expandedToolCalls.has(message.toolCallId || message.id)} />
onToggle={() => toggleToolCall(message.toolCallId || message.id)} </div>
/> </div>
);
})}
</div> </div>
) : ( )}
{/* Render the message content - skip TOOL messages as they're shown with their calls */}
{message.role !== 'TOOL' && (
<div <div
className={`max-w-[80%] rounded-lg p-4 ${ className={`flex ${message.role === 'USER' ? 'justify-end' : 'justify-start'}`}
message.role === 'USER'
? 'bg-primary text-primary-foreground'
: 'bg-surface-secondary'
}`}
> >
<p className="whitespace-pre-wrap text-sm">{message.content}</p> <div
className={`max-w-[80%] rounded-lg p-4 ${
message.role === 'USER'
? 'bg-primary text-primary-foreground'
: 'bg-surface-secondary'
}`}
>
<p className="whitespace-pre-wrap text-sm">{message.content}</p>
</div>
</div> </div>
)} )}
</div> </div>
</div> );
); });
})} })()}
{/* Live tool calls during streaming */} {/* Live tool calls during streaming */}
{toolCalls.length > 0 && ( {toolCalls.length > 0 && (