feat: Add intelligent auto-router and enhanced integrations

- Add intelligent-router.sh hook for automatic agent routing
- Add AUTO-TRIGGER-SUMMARY.md documentation
- Add FINAL-INTEGRATION-SUMMARY.md documentation
- Complete Prometheus integration (6 commands + 4 tools)
- Complete Dexto integration (12 commands + 5 tools)
- Enhanced Ralph with access to all agents
- Fix /clawd command (removed disable-model-invocation)
- Update hooks.json to v5 with intelligent routing
- 291 total skills now available
- All 21 commands with automatic routing

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
admin
2026-01-28 00:27:56 +04:00
Unverified
parent 3b128ba3bd
commit b52318eeae
1724 changed files with 351216 additions and 0 deletions

View File

@@ -0,0 +1,936 @@
import { useState, useEffect } from 'react';
import {
ChevronRight,
CheckCircle2,
XCircle,
Loader2,
AlertCircle,
Shield,
FileText,
FileEdit,
FilePlus,
Trash2,
Terminal,
Search,
Copy,
Check,
} from 'lucide-react';
import { cn } from '@/lib/utils';
import { Button } from './ui/button';
import { CodePreview } from './CodePreview';
import type { ToolDisplayData } from '@dexto/core';
/**
* Sub-agent progress data for spawn_agent tool calls
*/
export interface SubAgentProgress {
task: string;
agentId: string;
toolsCalled: number;
currentTool: string;
currentArgs?: Record<string, unknown>;
}
export interface ToolCallTimelineProps {
toolName: string;
toolArgs?: Record<string, unknown>;
toolResult?: unknown;
success?: boolean;
requireApproval?: boolean;
approvalStatus?: 'pending' | 'approved' | 'rejected';
displayData?: ToolDisplayData;
subAgentProgress?: SubAgentProgress;
onApprove?: (formData?: Record<string, unknown>, rememberChoice?: boolean) => void;
onReject?: () => void;
}
// =============================================================================
// Helpers
// =============================================================================
function stripToolPrefix(toolName: string): { displayName: string; source: string } {
if (toolName.startsWith('internal--')) {
return { displayName: toolName.replace('internal--', ''), source: '' };
}
if (toolName.startsWith('custom--')) {
return { displayName: toolName.replace('custom--', ''), source: '' };
}
if (toolName.startsWith('mcp--')) {
const parts = toolName.split('--');
if (parts.length >= 3) {
return { displayName: parts.slice(2).join('--'), source: parts[1] ?? '' };
}
return { displayName: toolName.replace('mcp--', ''), source: 'mcp' };
}
if (toolName.startsWith('mcp__')) {
const parts = toolName.substring(5).split('__');
if (parts.length >= 2) {
return { displayName: parts.slice(1).join('__'), source: parts[0] ?? '' };
}
return { displayName: toolName.substring(5), source: 'mcp' };
}
if (toolName.startsWith('internal__')) {
return { displayName: toolName.substring(10), source: '' };
}
return { displayName: toolName, source: '' };
}
function getShortPath(path: string): string {
const parts = path.split('/').filter(Boolean);
if (parts.length <= 2) return path;
return `.../${parts.slice(-2).join('/')}`;
}
function truncate(text: string, maxLength: number): string {
if (text.length <= maxLength) return text;
return text.substring(0, maxLength) + '...';
}
function getSummary(
displayName: string,
toolArgs?: Record<string, unknown>
): { name: string; detail?: string } {
const args = toolArgs || {};
const filePath = (args.file_path || args.path || args.file) as string | undefined;
const command = args.command as string | undefined;
const pattern = (args.pattern || args.query) as string | undefined;
if (command) {
return { name: displayName, detail: truncate(command, 40) };
}
if (filePath) {
return { name: displayName, detail: getShortPath(filePath) };
}
if (pattern) {
return { name: displayName, detail: `"${truncate(pattern, 25)}"` };
}
return { name: displayName };
}
// =============================================================================
// Main Component
// =============================================================================
export function ToolCallTimeline({
toolName,
toolArgs,
toolResult,
success,
requireApproval = false,
approvalStatus,
displayData,
subAgentProgress,
onApprove,
onReject,
}: ToolCallTimelineProps) {
const hasResult = toolResult !== undefined;
const isPendingApproval = requireApproval && approvalStatus === 'pending';
const isFailed = success === false;
const isRejected = approvalStatus === 'rejected';
// Tool is processing only if: no result yet, not pending approval, and not marked as failed
// The `success === false` check handles incomplete tool calls from history (never got a result)
const isProcessing = !hasResult && !isPendingApproval && !isFailed;
const hasSubAgentProgress = !!subAgentProgress;
// Determine if there's meaningful content to show
const hasExpandableContent = Boolean(
displayData ||
toolArgs?.content ||
(toolArgs?.old_string && toolArgs?.new_string) ||
(toolArgs?.command && hasResult)
);
// Determine if this tool has rich UI that should be shown by default
// Rich UI includes: displayData, file content previews, and diff views
// Exclude bash commands as they're more variable in visual value
const hasRichUI = Boolean(
displayData || toolArgs?.content || (toolArgs?.old_string && toolArgs?.new_string)
);
// Smart default: expand for pending approvals and successful tools with rich UI
// Failed, rejected, and no-output should always be collapsed
const [expanded, setExpanded] = useState(
isPendingApproval || (hasRichUI && !isFailed && !isRejected)
);
const [detailsExpanded, setDetailsExpanded] = useState(false);
const [copied, setCopied] = useState(false);
// Auto-collapse after approval is resolved, but keep open if tool has rich UI and succeeded
useEffect(() => {
if (requireApproval && approvalStatus && approvalStatus !== 'pending') {
// Collapse if rejected or if no rich UI to show
if (isRejected || !hasRichUI) {
setExpanded(false);
}
}
}, [requireApproval, approvalStatus, hasRichUI, isRejected]);
const { displayName, source } = stripToolPrefix(toolName);
const summary = getSummary(displayName, toolArgs);
// For sub-agent progress, format the agent name nicely
const subAgentLabel = hasSubAgentProgress
? subAgentProgress.agentId
.replace(/-agent$/, '')
.charAt(0)
.toUpperCase() + subAgentProgress.agentId.replace(/-agent$/, '').slice(1)
: null;
// Status icon
const StatusIcon = isPendingApproval ? (
<div className="relative">
<AlertCircle className="h-3.5 w-3.5 text-amber-500" />
<span className="absolute inset-0 rounded-full bg-amber-500/30 animate-ping" />
</div>
) : isProcessing ? (
<Loader2 className="h-3.5 w-3.5 text-blue-500 animate-spin" />
) : isFailed || isRejected ? (
<XCircle className="h-3.5 w-3.5 text-red-500" />
) : (
<CheckCircle2 className="h-3.5 w-3.5 text-green-500" />
);
// Header click handler
const toggleExpanded = () => {
if (hasResult || isPendingApproval || hasExpandableContent) {
setExpanded(!expanded);
}
};
const canExpand = hasResult || isPendingApproval || hasExpandableContent;
return (
<div
className={cn(
'my-0.5 rounded-md transition-colors inline-block max-w-full',
isPendingApproval &&
'bg-amber-50/50 dark:bg-amber-950/20 border border-amber-200/50 dark:border-amber-800/30'
)}
>
{/* Collapsed Header - Always Visible */}
<button
onClick={toggleExpanded}
disabled={!canExpand}
className={cn(
'inline-flex items-center gap-2 p-1.5 text-left rounded-md',
canExpand && 'hover:bg-muted/40 cursor-pointer',
!canExpand && 'cursor-default'
)}
>
{/* Status icon */}
<div className="flex-shrink-0">{StatusIcon}</div>
{/* Summary text */}
<span
className={cn(
'text-xs flex-1 truncate',
isPendingApproval && 'text-amber-700 dark:text-amber-300 font-medium',
isProcessing && 'text-blue-600 dark:text-blue-400',
isFailed && 'text-red-600 dark:text-red-400',
isRejected && 'text-red-600 dark:text-red-400',
!isPendingApproval &&
!isProcessing &&
!isFailed &&
!isRejected &&
'text-foreground/70'
)}
>
{isPendingApproval ? 'Approval required: ' : ''}
{isFailed ? 'Failed: ' : ''}
{isRejected ? 'Rejected: ' : ''}
{hasSubAgentProgress ? (
<span className="font-mono">
<span className="text-purple-600 dark:text-purple-400 font-medium">
{subAgentLabel}
</span>
<span className="text-muted-foreground/50">(</span>
<span className="text-foreground/80">{subAgentProgress.task}</span>
<span className="text-muted-foreground/50">)</span>
</span>
) : (
<span className="font-mono">
<span className="text-blue-600 dark:text-blue-400">
{summary.name.toLowerCase()}
</span>
<span className="text-muted-foreground/50">(</span>
{summary.detail && (
<span className="text-foreground/80">{summary.detail}</span>
)}
<span className="text-muted-foreground/50">)</span>
</span>
)}
</span>
{/* Badges */}
{source && (
<span className="text-[10px] text-muted-foreground/60 flex-shrink-0">
[{source}]
</span>
)}
{requireApproval && approvalStatus === 'approved' && (
<span className="text-[10px] text-green-600 dark:text-green-500 flex-shrink-0">
approved
</span>
)}
{isProcessing && !hasSubAgentProgress && (
<span className="text-[10px] text-muted-foreground/50 flex-shrink-0">
running...
</span>
)}
{/* Sub-agent progress indicator */}
{hasSubAgentProgress && isProcessing && (
<span className="text-[10px] text-muted-foreground flex-shrink-0">
{subAgentProgress.toolsCalled} tool
{subAgentProgress.toolsCalled !== 1 ? 's' : ''} |{' '}
{subAgentProgress.currentTool}
</span>
)}
{/* Expand chevron */}
{canExpand && (
<ChevronRight
className={cn(
'h-3 w-3 text-muted-foreground/40 flex-shrink-0 transition-transform',
expanded && 'rotate-90'
)}
/>
)}
</button>
{/* Expanded Content */}
{expanded && (
<div className="px-1.5 pb-2 pt-1 space-y-2 animate-fade-in">
{/* Pending Approval Content */}
{isPendingApproval && (
<>
{renderApprovalPreview()}
<div className="flex gap-1.5 flex-wrap pt-1">
<Button
onClick={() => onApprove?.(undefined, false)}
size="sm"
className="bg-green-600 hover:bg-green-700 text-white h-6 text-[11px] px-2.5"
>
Approve
</Button>
<Button
onClick={() => onApprove?.(undefined, true)}
size="sm"
variant="outline"
className="h-6 text-[11px] px-2 text-green-600 border-green-300 hover:bg-green-50 dark:border-green-700 dark:hover:bg-green-950/20"
>
<Shield className="h-3 w-3 mr-1" />
Always
</Button>
<Button
onClick={() => onReject?.()}
variant="outline"
size="sm"
className="h-6 text-[11px] px-2.5 text-red-600 hover:bg-red-50 dark:hover:bg-red-950/20"
>
Reject
</Button>
</div>
</>
)}
{/* Error Content */}
{isFailed && hasResult && renderErrorContent()}
{/* Result Content */}
{hasResult && !isFailed && !isPendingApproval && renderResultContent()}
</div>
)}
</div>
);
// =========================================================================
// Render Functions
// =========================================================================
function renderApprovalPreview() {
const command = toolArgs?.command as string | undefined;
const filePath = (toolArgs?.file_path || toolArgs?.path) as string | undefined;
const content = toolArgs?.content as string | undefined;
const oldString = toolArgs?.old_string as string | undefined;
const newString = toolArgs?.new_string as string | undefined;
// Bash command
if (command) {
return (
<div className="ml-5 bg-zinc-900 rounded overflow-hidden">
<pre className="px-2 py-1.5 text-[11px] text-zinc-300 font-mono whitespace-pre-wrap">
<span className="text-zinc-500">$ </span>
{command}
</pre>
</div>
);
}
// Edit operation - diff view without header (file path is in summary)
if (oldString !== undefined && newString !== undefined) {
return (
<div className="ml-5 bg-muted/30 rounded overflow-hidden border border-border/50 text-[11px] font-mono">
{oldString
.split('\n')
.slice(0, detailsExpanded ? 15 : 3)
.map((line, i) => (
<div
key={`o${i}`}
className="px-2 py-0.5 bg-red-100/50 dark:bg-red-900/20 text-red-800 dark:text-red-300"
>
<span className="text-red-500/50 mr-1">-</span>
{line || ' '}
</div>
))}
{newString
.split('\n')
.slice(0, detailsExpanded ? 15 : 3)
.map((line, i) => (
<div
key={`n${i}`}
className="px-2 py-0.5 bg-green-100/50 dark:bg-green-900/20 text-green-800 dark:text-green-300"
>
<span className="text-green-500/50 mr-1">+</span>
{line || ' '}
</div>
))}
{(oldString.split('\n').length > 3 || newString.split('\n').length > 3) && (
<button
onClick={(e) => {
e.stopPropagation();
setDetailsExpanded(!detailsExpanded);
}}
className="w-full py-0.5 text-[10px] text-blue-500 bg-muted/50 border-t border-border/30"
>
{detailsExpanded ? 'less' : 'more...'}
</button>
)}
</div>
);
}
// Write/Create file
if (content && filePath) {
return (
<div className="ml-5">
<CodePreview
content={content}
filePath={filePath}
maxLines={8}
maxHeight={180}
showHeader={false}
/>
</div>
);
}
return null;
}
function renderErrorContent() {
let errorMessage = 'Unknown error';
if (toolResult && typeof toolResult === 'object') {
const result = toolResult as Record<string, unknown>;
if (result.content && Array.isArray(result.content)) {
const textPart = result.content.find(
(p: unknown) =>
typeof p === 'object' &&
p !== null &&
(p as Record<string, unknown>).type === 'text'
) as { text?: string } | undefined;
if (textPart?.text) errorMessage = textPart.text;
} else if (result.error) {
errorMessage =
typeof result.error === 'string' ? result.error : JSON.stringify(result.error);
}
}
return (
<div className="bg-red-50 dark:bg-red-950/20 border border-red-200 dark:border-red-800/50 rounded px-2 py-1.5">
<pre className="text-[11px] text-red-700 dark:text-red-300 font-mono whitespace-pre-wrap break-all">
{truncate(errorMessage, 500)}
</pre>
</div>
);
}
function renderResultContent() {
// Extract toolArgs for checking rich content availability
const command = toolArgs?.command as string | undefined;
const filePath = (toolArgs?.file_path || toolArgs?.path) as string | undefined;
const content = toolArgs?.content as string | undefined;
const oldString = toolArgs?.old_string as string | undefined;
const newString = toolArgs?.new_string as string | undefined;
// Check if we have rich content that should override simple displayData
const hasRichContent = !!(
content ||
(oldString !== undefined && newString !== undefined) ||
(command && hasResult)
);
// If we have display metadata from tool, use it (unless we have richer content)
if (displayData) {
// Skip simple file metadata display if we have rich content to show
if (displayData.type === 'file' && hasRichContent) {
// Fall through to render rich content below
} else {
switch (displayData.type) {
case 'diff':
return renderDiff(displayData);
case 'shell':
return renderShell(displayData);
case 'search':
return renderSearch(displayData);
case 'file':
return renderFile(displayData);
}
}
}
// Render rich content from toolArgs
// Bash command with result
if (command && hasResult) {
return renderBashResult(command);
}
// Edit operation (old_string -> new_string)
if (oldString !== undefined && newString !== undefined && filePath) {
return renderEditResult(filePath, oldString, newString);
}
// Write/create file
if (content && filePath) {
return renderWriteResult(filePath, content);
}
// Read file - show content from result
if (displayName.toLowerCase().includes('read') && filePath) {
return renderReadResult(filePath);
}
// Fallback to generic
return renderGenericResult();
}
function renderDiff(data: Extract<ToolDisplayData, { type: 'diff' }>) {
const lines = data.unified
.split('\n')
.filter((l) => !l.startsWith('@@') && !l.startsWith('---') && !l.startsWith('+++'));
const displayLines = lines.slice(0, detailsExpanded ? 40 : 8);
return (
<div className="space-y-1">
<div className="flex items-center gap-2 text-[11px]">
<FileEdit className="h-3 w-3 text-muted-foreground" />
<span className="font-mono text-foreground/70">
{getShortPath(data.filename)}
</span>
<span className="text-green-600">+{data.additions}</span>
<span className="text-red-600">-{data.deletions}</span>
</div>
<div className="bg-muted/30 rounded overflow-hidden border border-border/50">
{displayLines.map((line, i) => {
const isAdd = line.startsWith('+');
const isDel = line.startsWith('-');
return (
<div
key={i}
className={cn(
'px-2 py-0.5 text-[11px] font-mono',
isAdd &&
'bg-green-100/50 dark:bg-green-900/20 text-green-800 dark:text-green-300',
isDel &&
'bg-red-100/50 dark:bg-red-900/20 text-red-800 dark:text-red-300',
!isAdd && !isDel && 'text-foreground/50'
)}
>
<span className="mr-1 opacity-50">
{isAdd ? '+' : isDel ? '-' : ' '}
</span>
{line.slice(1) || ' '}
</div>
);
})}
{lines.length > 8 && (
<button
onClick={(e) => {
e.stopPropagation();
setDetailsExpanded(!detailsExpanded);
}}
className="w-full py-0.5 text-[10px] text-blue-500 bg-muted/50 border-t border-border/30"
>
{detailsExpanded ? 'less' : `+${lines.length - 8} more...`}
</button>
)}
</div>
</div>
);
}
function renderShell(data: Extract<ToolDisplayData, { type: 'shell' }>) {
const output = data.stdout || data.stderr || '';
const lines = output.split('\n');
const displayLines = lines.slice(0, detailsExpanded ? 25 : 5);
const isError = data.exitCode !== 0;
return (
<div className="space-y-1">
<div className="flex items-center gap-2 text-[11px]">
<Terminal className="h-3 w-3 text-muted-foreground" />
<code className="font-mono text-foreground/70 truncate flex-1">
{truncate(data.command, 50)}
</code>
<span
className={cn(
'text-[10px] px-1 py-0.5 rounded',
isError
? 'bg-red-100 dark:bg-red-900/30 text-red-600'
: 'bg-green-100 dark:bg-green-900/30 text-green-600'
)}
>
{isError ? `exit ${data.exitCode}` : 'ok'}
</span>
<button
onClick={async (e) => {
e.stopPropagation();
await navigator.clipboard.writeText(data.command);
setCopied(true);
setTimeout(() => setCopied(false), 1500);
}}
className="text-muted-foreground hover:text-foreground"
>
{copied ? (
<Check className="h-3 w-3 text-green-500" />
) : (
<Copy className="h-3 w-3" />
)}
</button>
</div>
{output && (
<div className="bg-zinc-100 dark:bg-zinc-900 rounded overflow-hidden border border-zinc-200 dark:border-zinc-800">
<pre className="p-1.5 text-[11px] text-zinc-800 dark:text-zinc-300 font-mono whitespace-pre-wrap max-h-48 overflow-y-auto">
{displayLines.join('\n')}
</pre>
{lines.length > 5 && (
<button
onClick={(e) => {
e.stopPropagation();
setDetailsExpanded(!detailsExpanded);
}}
className="w-full py-0.5 text-[10px] text-blue-600 dark:text-blue-400 bg-zinc-200 dark:bg-zinc-800 border-t border-zinc-300 dark:border-zinc-700"
>
{detailsExpanded ? 'less' : `+${lines.length - 5} more...`}
</button>
)}
</div>
)}
</div>
);
}
function renderSearch(data: Extract<ToolDisplayData, { type: 'search' }>) {
const matches = data.matches.slice(0, detailsExpanded ? 15 : 5);
return (
<div className="space-y-1">
<div className="flex items-center gap-2 text-[11px]">
<Search className="h-3 w-3 text-muted-foreground" />
<code className="font-mono text-foreground/70">{data.pattern}</code>
<span className="text-muted-foreground">
{data.totalMatches} matches{data.truncated && '+'}
</span>
</div>
<div className="bg-muted/30 rounded overflow-hidden border border-border/50 divide-y divide-border/30">
{matches.map((m, i) => (
<div key={i} className="px-2 py-1 text-[11px]">
<span className="text-blue-600 dark:text-blue-400 font-mono">
{getShortPath(m.file)}
</span>
{m.line > 0 && <span className="text-muted-foreground">:{m.line}</span>}
{m.content && (
<div className="text-foreground/60 font-mono truncate">
{m.content}
</div>
)}
</div>
))}
{data.matches.length > 5 && (
<button
onClick={(e) => {
e.stopPropagation();
setDetailsExpanded(!detailsExpanded);
}}
className="w-full py-0.5 text-[10px] text-blue-500 bg-muted/50"
>
{detailsExpanded ? 'less' : `+${data.matches.length - 5} more...`}
</button>
)}
</div>
</div>
);
}
function renderFile(data: Extract<ToolDisplayData, { type: 'file' }>) {
const OpIcon = { read: FileText, write: FileEdit, create: FilePlus, delete: Trash2 }[
data.operation
];
const opColors = {
read: 'bg-blue-100 dark:bg-blue-900/30 text-blue-600',
write: 'bg-amber-100 dark:bg-amber-900/30 text-amber-600',
create: 'bg-green-100 dark:bg-green-900/30 text-green-600',
delete: 'bg-red-100 dark:bg-red-900/30 text-red-600',
}[data.operation];
return (
<div className="flex items-center gap-2 text-[11px]">
<OpIcon className="h-3 w-3 text-muted-foreground" />
<span className={cn('px-1 py-0.5 rounded text-[10px]', opColors)}>
{data.operation}
</span>
<span className="font-mono text-foreground/70">{getShortPath(data.path)}</span>
{data.lineCount !== undefined && (
<span className="text-muted-foreground">{data.lineCount} lines</span>
)}
</div>
);
}
// =========================================================================
// Render functions for generating preview from toolArgs (no displayData)
// =========================================================================
function renderBashResult(_command: string) {
// Extract and parse bash result from tool result
let stdout = '';
let stderr = '';
let exitCode: number | undefined;
let duration: number | undefined;
if (toolResult && typeof toolResult === 'object') {
const result = toolResult as Record<string, unknown>;
if (result.content && Array.isArray(result.content)) {
const textContent = result.content
.filter(
(p: unknown) =>
typeof p === 'object' &&
p !== null &&
(p as Record<string, unknown>).type === 'text'
)
.map((p: unknown) => (p as { text?: string }).text || '')
.join('\n');
// Try to parse as JSON bash result
try {
const parsed = JSON.parse(textContent);
if (typeof parsed === 'object' && parsed !== null) {
stdout = parsed.stdout || '';
stderr = parsed.stderr || '';
exitCode =
typeof parsed.exit_code === 'number' ? parsed.exit_code : undefined;
duration =
typeof parsed.duration === 'number' ? parsed.duration : undefined;
}
} catch {
// Not JSON, treat as plain output
stdout = textContent;
}
}
}
const output = stdout || stderr;
if (!output && exitCode === undefined) return null;
const lines = output.split('\n').filter((l) => l.trim());
const displayLines = lines.slice(0, detailsExpanded ? 25 : 5);
const isError = exitCode !== undefined && exitCode !== 0;
return (
<div className="ml-5 space-y-1">
{/* Status bar */}
<div className="flex items-center gap-2 text-[10px]">
{exitCode !== undefined && (
<span
className={cn(
'px-1.5 py-0.5 rounded font-medium',
isError
? 'bg-red-100 dark:bg-red-900/30 text-red-600 dark:text-red-400'
: 'bg-green-100 dark:bg-green-900/30 text-green-600 dark:text-green-400'
)}
>
{isError ? `exit ${exitCode}` : 'success'}
</span>
)}
{duration !== undefined && (
<span className="text-muted-foreground">{duration}ms</span>
)}
</div>
{/* Output */}
{output && (
<div className="bg-zinc-100 dark:bg-zinc-900 rounded overflow-hidden border border-zinc-200 dark:border-zinc-800">
<pre className="p-1.5 text-[11px] text-zinc-800 dark:text-zinc-300 font-mono whitespace-pre-wrap max-h-48 overflow-y-auto">
{displayLines.join('\n')}
</pre>
{lines.length > 5 && (
<button
onClick={(e) => {
e.stopPropagation();
setDetailsExpanded(!detailsExpanded);
}}
className="w-full py-0.5 text-[10px] text-blue-600 dark:text-blue-400 bg-zinc-200 dark:bg-zinc-800 border-t border-zinc-300 dark:border-zinc-700"
>
{detailsExpanded ? 'less' : `+${lines.length - 5} more...`}
</button>
)}
</div>
)}
{/* Stderr if present and different from stdout */}
{stderr && stderr !== stdout && (
<div className="bg-red-50 dark:bg-red-950/30 rounded overflow-hidden border border-red-200 dark:border-red-900/30">
<div className="px-2 py-0.5 text-[10px] text-red-700 dark:text-red-400 bg-red-100 dark:bg-red-900/20 border-b border-red-200 dark:border-red-900/30">
stderr
</div>
<pre className="p-1.5 text-[11px] text-red-800 dark:text-red-300 font-mono whitespace-pre-wrap max-h-24 overflow-y-auto">
{stderr}
</pre>
</div>
)}
</div>
);
}
function renderEditResult(_filePath: string, oldString: string, newString: string) {
return (
<div className="ml-5 bg-muted/30 rounded overflow-hidden border border-border/50 text-[11px] font-mono">
{oldString
.split('\n')
.slice(0, detailsExpanded ? 15 : 3)
.map((line, i) => (
<div
key={`o${i}`}
className="px-2 py-0.5 bg-red-100/50 dark:bg-red-900/20 text-red-800 dark:text-red-300"
>
<span className="text-red-500/50 mr-1">-</span>
{line || ' '}
</div>
))}
{newString
.split('\n')
.slice(0, detailsExpanded ? 15 : 3)
.map((line, i) => (
<div
key={`n${i}`}
className="px-2 py-0.5 bg-green-100/50 dark:bg-green-900/20 text-green-800 dark:text-green-300"
>
<span className="text-green-500/50 mr-1">+</span>
{line || ' '}
</div>
))}
{(oldString.split('\n').length > 3 || newString.split('\n').length > 3) && (
<button
onClick={(e) => {
e.stopPropagation();
setDetailsExpanded(!detailsExpanded);
}}
className="w-full py-0.5 text-[10px] text-blue-500 bg-muted/50 border-t border-border/30"
>
{detailsExpanded ? 'less' : 'more...'}
</button>
)}
</div>
);
}
function renderWriteResult(filePath: string, content: string) {
return (
<div className="ml-5">
<CodePreview
content={content}
filePath={filePath}
maxLines={8}
maxHeight={180}
showHeader={false}
/>
</div>
);
}
function renderReadResult(filePath: string) {
// Extract content from tool result
let content = '';
if (toolResult && typeof toolResult === 'object') {
const result = toolResult as Record<string, unknown>;
if (result.content && Array.isArray(result.content)) {
content = result.content
.filter(
(p: unknown) =>
typeof p === 'object' &&
p !== null &&
(p as Record<string, unknown>).type === 'text'
)
.map((p: unknown) => (p as { text?: string }).text || '')
.join('\n');
}
}
if (!content) return null;
return (
<div className="ml-5">
<CodePreview
content={content}
filePath={filePath}
maxLines={8}
maxHeight={180}
showHeader={false}
/>
</div>
);
}
function renderGenericResult() {
// Extract text from result
let resultText = '';
if (toolResult && typeof toolResult === 'object') {
const result = toolResult as Record<string, unknown>;
if (result.content && Array.isArray(result.content)) {
resultText = result.content
.filter(
(p: unknown) =>
typeof p === 'object' &&
p !== null &&
(p as Record<string, unknown>).type === 'text'
)
.map((p: unknown) => (p as { text?: string }).text || '')
.join('\n');
}
}
if (!resultText) return null;
const lines = resultText.split('\n');
const displayLines = lines.slice(0, detailsExpanded ? 20 : 5);
return (
<div className="bg-muted/30 rounded overflow-hidden border border-border/50">
<pre className="p-1.5 text-[11px] text-foreground/70 font-mono whitespace-pre-wrap max-h-40 overflow-y-auto">
{displayLines.join('\n')}
</pre>
{lines.length > 5 && (
<button
onClick={(e) => {
e.stopPropagation();
setDetailsExpanded(!detailsExpanded);
}}
className="w-full py-0.5 text-[10px] text-blue-500 bg-muted/50 border-t border-border/30"
>
{detailsExpanded ? 'less' : `+${lines.length - 5} more...`}
</button>
)}
</div>
);
}
}