Workspaces layout, automation page

This commit is contained in:
2026-02-18 17:04:32 +00:00
parent 89f23f9426
commit 485ae4c3d7
89 changed files with 2168 additions and 514 deletions
+138
View File
@@ -0,0 +1,138 @@
import { useState } from 'react';
import { FileText, Terminal, Pencil, Search, Globe, Wrench, ChevronRight } from 'lucide-react';
import type { ChatMessage } from './types';
type ToolMessage = Extract<ChatMessage, { role: 'tool' }>;
type ToolActivityProps = {
message: ToolMessage;
};
const toolIcons: Record<string, typeof FileText> = {
Read: FileText,
Edit: Pencil,
Write: Pencil,
Bash: Terminal,
Grep: Search,
Glob: Search,
WebFetch: Globe,
WebSearch: Globe,
};
function getToolSummary(toolName: string, toolInput: Record<string, unknown>): string {
switch (toolName) {
case 'Read':
case 'Edit':
case 'Write':
return (toolInput.file_path as string) ?? '';
case 'Bash':
return truncate((toolInput.command as string) ?? '', 80);
case 'Grep':
case 'Glob':
return (toolInput.pattern as string) ?? '';
case 'WebFetch':
return (toolInput.url as string) ?? '';
case 'WebSearch':
return (toolInput.query as string) ?? '';
default:
return (
Object.values(toolInput)
.find((v) => typeof v === 'string')
?.toString() ?? ''
);
}
}
function truncate(str: string, max: number): string {
return str.length > max ? str.slice(0, max) + '...' : str;
}
export const ToolActivity = ({ message }: ToolActivityProps) => {
const [open, setOpen] = useState(false);
const Icon = toolIcons[message.toolName] ?? Wrench;
const summary = getToolSummary(message.toolName, message.toolInput);
const pending = message.output === undefined;
const isError = message.isError === true;
return (
<div className="my-1">
<button
onClick={() => setOpen(!open)}
className="flex items-center gap-2 w-full text-left px-3 py-1.5 rounded-md hover:bg-duck-dark/5 transition-colors cursor-pointer text-sm"
>
<ChevronRight className={`h-3 w-3 shrink-0 transition-transform ${open ? 'rotate-90' : ''}`} />
<Icon className="h-4 w-4 shrink-0 text-duck-teal" />
<span className="font-medium text-duck-dark/80">{message.toolName}</span>
<span className="text-duck-dark/50 truncate flex-1 font-mono text-xs">{summary}</span>
<span className="shrink-0">
{pending && <span className="inline-block h-2 w-2 rounded-full bg-amber-400 animate-pulse" />}
{!pending && !isError && <span className="text-green-600 text-xs">done</span>}
{!pending && isError && <span className="text-red-600 text-xs">error</span>}
</span>
</button>
{open && (
<div className="ml-7 mt-1 space-y-2 text-xs">
<div className="rounded-md bg-duck-dark/5 p-2 overflow-x-auto">
<div className="text-duck-dark/50 text-[10px] uppercase tracking-wider mb-1">Input</div>
{message.toolName === 'Bash' ? (
<pre className="bg-gray-900 text-green-400 p-2 rounded font-mono whitespace-pre-wrap break-all">
{(message.toolInput.command as string) ?? JSON.stringify(message.toolInput, null, 2)}
</pre>
) : (
<pre className="font-mono whitespace-pre-wrap break-all text-duck-dark/70">
{Object.entries(message.toolInput)
.map(([k, v]) => `${k}: ${typeof v === 'string' ? v : JSON.stringify(v)}`)
.join('\n')}
</pre>
)}
</div>
{message.output !== undefined && (
<div className="rounded-md bg-duck-dark/5 p-2 overflow-x-auto">
<div className="text-duck-dark/50 text-[10px] uppercase tracking-wider mb-1">Output</div>
<ToolOutput toolName={message.toolName} output={message.output} isError={isError} />
</div>
)}
</div>
)}
</div>
);
};
type ToolOutputProps = {
toolName: string;
output: string;
isError: boolean;
};
const ToolOutput = ({ toolName, output, isError }: ToolOutputProps) => {
const [expanded, setExpanded] = useState(false);
const maxLines = 20;
const lines = output.split('\n');
const needsTruncation = lines.length > maxLines;
const displayText = expanded ? output : lines.slice(0, maxLines).join('\n');
const isBash = toolName === 'Bash';
return (
<>
<pre
className={`font-mono whitespace-pre-wrap break-all p-2 rounded ${
isBash ? 'bg-gray-900 text-green-400' : isError ? 'bg-red-50 text-red-700' : 'text-duck-dark/70'
}`}
>
{displayText}
</pre>
{needsTruncation && (
<button
onClick={() => setExpanded(!expanded)}
className="text-duck-teal hover:underline text-[11px] mt-1 cursor-pointer"
>
{expanded ? 'Show less' : `Show more (${lines.length - maxLines} more lines)`}
</button>
)}
</>
);
};