Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 27 additions & 36 deletions web/src/components/Chat/tools/BashToolBlock.tsx
Original file line number Diff line number Diff line change
@@ -1,53 +1,44 @@
import { useState } from 'react';
import { ChevronRight, ChevronDown, Terminal, Loader2 } from 'lucide-react';
import { Terminal, Loader2 } from 'lucide-react';
import type { ToolCallBlockData } from '../../../types/chat';
import { CollapsibleToolBlock } from './CollapsibleToolBlock';

export function BashToolBlock({ block }: { block: ToolCallBlockData }) {
const [expanded, setExpanded] = useState(false);
const isRunning = block.status === 'running';
const command = String(block.input.command || '');
const truncatedCmd = command.length > 80 ? command.slice(0, 80) + '...' : command;

return (
<div className="my-1.5 border border-[#2a2a2a] rounded-lg bg-[#0a0a0a] overflow-hidden">
<button
onClick={() => setExpanded(!expanded)}
className="flex items-center gap-2 w-full px-3 py-2 text-left cursor-pointer hover:bg-[#111] transition-colors"
>
{isRunning
? <Loader2 size={14} className="text-[#6366f1] animate-spin shrink-0" />
: <Terminal size={14} className={`shrink-0 ${block.isError ? 'text-red-400' : 'text-emerald-400'}`} />
}
<CollapsibleToolBlock
isRunning={isRunning}
isError={block.isError}
icon={Terminal}
iconClassName="text-emerald-400"
label=""
theme="default"
headerExtra={<>
<span className="text-emerald-500 text-[13px] font-mono select-none">$</span>
<span className="text-[13px] font-mono text-[#ccc] truncate">{truncatedCmd}</span>
<div className="ml-auto shrink-0">
{expanded ? <ChevronDown size={14} className="text-[#555]" /> : <ChevronRight size={14} className="text-[#555]" />}
</>}
>
{/* Full command */}
{command.length > 80 && (
<div className="px-3 py-2 border-b border-[#1a1a1a]">
<pre className="text-[12px] font-mono text-[#ccc] whitespace-pre-wrap">{command}</pre>
</div>
</button>

{expanded && (
<div className="border-t border-[#1a1a1a]">
{/* Full command */}
{command.length > 80 && (
<div className="px-3 py-2 border-b border-[#1a1a1a]">
<pre className="text-[12px] font-mono text-[#ccc] whitespace-pre-wrap">{command}</pre>
</div>
)}
)}

{/* Output */}
{block.result !== undefined && (
<pre className={`px-3 py-2 text-[12px] font-mono whitespace-pre-wrap max-h-80 overflow-y-auto ${block.isError ? 'text-red-400' : 'text-[#888]'}`}>
{block.result}
</pre>
)}
{/* Output */}
{block.result !== undefined && (
<pre className={`px-3 py-2 text-[12px] font-mono whitespace-pre-wrap max-h-80 overflow-y-auto ${block.isError ? 'text-red-400' : 'text-[#888]'}`}>
{block.result}
</pre>
)}

{isRunning && block.result === undefined && (
<div className="px-3 py-3 text-[12px] text-[#666] flex items-center gap-2">
<Loader2 size={12} className="animate-spin" /> Running...
</div>
)}
{isRunning && block.result === undefined && (
<div className="px-3 py-3 text-[12px] text-[#666] flex items-center gap-2">
<Loader2 size={12} className="animate-spin" /> Running...
</div>
)}
</div>
</CollapsibleToolBlock>
);
}
98 changes: 98 additions & 0 deletions web/src/components/Chat/tools/CollapsibleToolBlock.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import { useState, type ReactNode } from 'react';
import { ChevronRight, ChevronDown, Loader2 } from 'lucide-react';
import type { LucideIcon } from 'lucide-react';

interface CollapsibleToolBlockProps {
/** Whether the tool is currently running. */
isRunning: boolean;
/** Whether the tool resulted in an error. */
isError?: boolean;
/** Primary icon shown in collapsed state. */
icon: LucideIcon;
/** CSS class for the icon (color). Falls back to text-[#888]. */
iconClassName?: string;
/** Short label next to the icon (e.g. "List Tasks"). */
label: string;
/** CSS class for the label text. Falls back to text-[#ccc]. */
labelClassName?: string;
/** Extra elements rendered between label and chevron. */
headerExtra?: ReactNode;
/** Border + background theme. Defaults to neutral gray. */
theme?: 'default' | 'amber' | 'purple' | 'cyan';
/** Start expanded? Defaults to false. */
defaultExpanded?: boolean;
/** Content shown when expanded. */
children: ReactNode;
}

const THEMES = {
default: {
border: 'border-[#2a2a2a]',
bg: 'bg-[#141414]',
hover: 'hover:bg-[#1a1a1a]',
divider: 'border-[#2a2a2a]',
spinner: 'text-[#6366f1]',
},
amber: {
border: 'border-amber-500/20',
bg: 'bg-[#141411]',
hover: 'hover:bg-[#1a1a18]',
divider: 'border-amber-500/10',
spinner: 'text-amber-400',
},
purple: {
border: 'border-purple-500/20',
bg: 'bg-[#141418]',
hover: 'hover:bg-[#1a1a20]',
divider: 'border-purple-500/10',
spinner: 'text-purple-400',
},
cyan: {
border: 'border-cyan-500/20',
bg: 'bg-[#141416]',
hover: 'hover:bg-[#1a1a1e]',
divider: 'border-cyan-500/10',
spinner: 'text-cyan-400',
},
} as const;

export function CollapsibleToolBlock({
isRunning,
isError,
icon: Icon,
iconClassName = 'text-[#888]',
label,
labelClassName = 'text-[#ccc]',
headerExtra,
theme = 'default',
defaultExpanded = false,
children,
}: CollapsibleToolBlockProps) {
const [expanded, setExpanded] = useState(defaultExpanded);
const t = THEMES[theme];

return (
<div className={`my-1.5 border ${t.border} rounded-lg ${t.bg} overflow-hidden`}>
<button
onClick={() => setExpanded(!expanded)}
className={`flex items-center gap-2 w-full px-3 py-2 text-left cursor-pointer ${t.hover} transition-colors`}
>
{isRunning
? <Loader2 size={14} className={`${t.spinner} animate-spin shrink-0`} />
: <Icon size={14} className={`shrink-0 ${isError ? 'text-red-400' : iconClassName}`} />
}
<span className={`text-[13px] font-medium ${labelClassName}`}>{label}</span>
{headerExtra}
<div className="ml-auto shrink-0">
{expanded ? <ChevronDown size={14} className="text-[#555]" /> : <ChevronRight size={14} className="text-[#555]" />}
</div>
</button>

{expanded && (
<div className={`border-t ${t.divider}`}>
{children}
</div>
)}
</div>
);
}
78 changes: 34 additions & 44 deletions web/src/components/Chat/tools/EditToolBlock.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import { useState } from 'react';
import { ChevronRight, ChevronDown, FileEdit, Loader2 } from 'lucide-react';
import { FileEdit, Loader2 } from 'lucide-react';
import type { ToolCallBlockData } from '../../../types/chat';
import { CollapsibleToolBlock } from './CollapsibleToolBlock';

export function EditToolBlock({ block }: { block: ToolCallBlockData }) {
const [expanded, setExpanded] = useState(false);
const isRunning = block.status === 'running';
const filePath = String(block.input.file_path || '');
const oldString = String(block.input.old_string || '');
Expand All @@ -13,52 +12,43 @@ export function EditToolBlock({ block }: { block: ToolCallBlockData }) {
const newLines = newString.split('\n');

return (
<div className="my-1.5 border border-[#2a2a2a] rounded-lg bg-[#141414] overflow-hidden">
<button
onClick={() => setExpanded(!expanded)}
className="flex items-center gap-2 w-full px-3 py-2 text-left cursor-pointer hover:bg-[#1a1a1a] transition-colors"
>
{isRunning
? <Loader2 size={14} className="text-[#6366f1] animate-spin shrink-0" />
: <FileEdit size={14} className={`shrink-0 ${block.isError ? 'text-red-400' : 'text-amber-400'}`} />
}
<span className="text-[13px] font-mono font-medium text-[#ccc]">Edit</span>
<CollapsibleToolBlock
isRunning={isRunning}
isError={block.isError}
icon={FileEdit}
iconClassName="text-amber-400"
label="Edit"
labelClassName="text-[#ccc] font-mono"
headerExtra={
<span className="text-[12px] text-[#666] truncate font-mono">{filePath}</span>
<div className="ml-auto shrink-0">
{expanded ? <ChevronDown size={14} className="text-[#555]" /> : <ChevronRight size={14} className="text-[#555]" />}
</div>
</button>

{expanded && (
<div className="border-t border-[#2a2a2a]">
{/* Diff view */}
<div className="font-mono text-[12px] overflow-x-auto max-h-80 overflow-y-auto">
{oldLines.map((line, i) => (
<div key={`old-${i}`} className="px-3 py-0.5 bg-red-900/15 text-red-300/80">
<span className="select-none text-red-500/50 mr-2">-</span>{line}
</div>
))}
{newLines.map((line, i) => (
<div key={`new-${i}`} className="px-3 py-0.5 bg-green-900/15 text-green-300/80">
<span className="select-none text-green-500/50 mr-2">+</span>{line}
</div>
))}
}
>
{/* Diff view */}
<div className="font-mono text-[12px] overflow-x-auto max-h-80 overflow-y-auto">
{oldLines.map((line, i) => (
<div key={`old-${i}`} className="px-3 py-0.5 bg-red-900/15 text-red-300/80">
<span className="select-none text-red-500/50 mr-2">-</span>{line}
</div>
))}
{newLines.map((line, i) => (
<div key={`new-${i}`} className="px-3 py-0.5 bg-green-900/15 text-green-300/80">
<span className="select-none text-green-500/50 mr-2">+</span>{line}
</div>
))}
</div>

{/* Error */}
{block.isError && block.result && (
<div className="px-3 py-2 border-t border-[#222]">
<pre className="text-[12px] font-mono text-red-400 whitespace-pre-wrap">{block.result}</pre>
</div>
)}
{/* Error */}
{block.isError && block.result && (
<div className="px-3 py-2 border-t border-[#222]">
<pre className="text-[12px] font-mono text-red-400 whitespace-pre-wrap">{block.result}</pre>
</div>
)}

{isRunning && block.result === undefined && (
<div className="px-3 py-3 text-[12px] text-[#666] flex items-center gap-2 border-t border-[#222]">
<Loader2 size={12} className="animate-spin" /> Applying edit...
</div>
)}
{isRunning && block.result === undefined && (
<div className="px-3 py-3 text-[12px] text-[#666] flex items-center gap-2 border-t border-[#222]">
<Loader2 size={12} className="animate-spin" /> Applying edit...
</div>
)}
</div>
</CollapsibleToolBlock>
);
}
52 changes: 21 additions & 31 deletions web/src/components/Chat/tools/FileToolBlock.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import { useState } from 'react';
import { ChevronRight, ChevronDown, FileText, FilePlus, Loader2 } from 'lucide-react';
import { FileText, FilePlus, Loader2 } from 'lucide-react';
import type { ToolCallBlockData } from '../../../types/chat';
import { CollapsibleToolBlock } from './CollapsibleToolBlock';

export function FileToolBlock({ block }: { block: ToolCallBlockData }) {
const [expanded, setExpanded] = useState(false);
const isRunning = block.status === 'running';
const filePath = String(block.input.file_path || block.input.path || '');
const isWrite = block.tool === 'Write';
Expand All @@ -13,40 +12,31 @@ export function FileToolBlock({ block }: { block: ToolCallBlockData }) {
const lineCount = block.result ? block.result.split('\n').length : null;

return (
<div className="my-1.5 border border-[#2a2a2a] rounded-lg bg-[#141414] overflow-hidden">
<button
onClick={() => setExpanded(!expanded)}
className="flex items-center gap-2 w-full px-3 py-2 text-left cursor-pointer hover:bg-[#1a1a1a] transition-colors"
>
{isRunning
? <Loader2 size={14} className="text-[#6366f1] animate-spin shrink-0" />
: <Icon size={14} className={`shrink-0 ${block.isError ? 'text-red-400' : 'text-blue-400'}`} />
}
<span className="text-[13px] font-mono font-medium text-[#ccc]">{block.tool}</span>
<CollapsibleToolBlock
isRunning={isRunning}
isError={block.isError}
icon={Icon}
iconClassName="text-blue-400"
label={block.tool}
labelClassName="text-[#ccc] font-mono"
headerExtra={<>
<span className="text-[12px] text-[#666] truncate font-mono">{filePath}</span>
{lineCount && !isWrite && (
<span className="text-[10px] text-[#444] shrink-0">{lineCount} lines</span>
)}
<div className="ml-auto shrink-0">
{expanded ? <ChevronDown size={14} className="text-[#555]" /> : <ChevronRight size={14} className="text-[#555]" />}
</div>
</button>

{expanded && (
<div className="border-t border-[#2a2a2a]">
{block.result !== undefined && (
<pre className={`px-3 py-2 text-[12px] font-mono whitespace-pre-wrap max-h-80 overflow-y-auto bg-[#0f0f0f] ${block.isError ? 'text-red-400' : 'text-[#999]'}`}>
{block.result}
</pre>
)}
</>}
>
{block.result !== undefined && (
<pre className={`px-3 py-2 text-[12px] font-mono whitespace-pre-wrap max-h-80 overflow-y-auto bg-[#0f0f0f] ${block.isError ? 'text-red-400' : 'text-[#999]'}`}>
{block.result}
</pre>
)}

{isRunning && block.result === undefined && (
<div className="px-3 py-3 text-[12px] text-[#666] flex items-center gap-2">
<Loader2 size={12} className="animate-spin" /> {isWrite ? 'Writing...' : 'Reading...'}
</div>
)}
{isRunning && block.result === undefined && (
<div className="px-3 py-3 text-[12px] text-[#666] flex items-center gap-2">
<Loader2 size={12} className="animate-spin" /> {isWrite ? 'Writing...' : 'Reading...'}
</div>
)}
</div>
</CollapsibleToolBlock>
);
}
Loading