Phase 3: Dynamic Workflow Engine - VM sandbox (node:vm) with agent/parallel/pipeline API, Claude Code compatible - Workflow file discovery (.boocode/workflows/*.js + ~/.boocode/workflows/*.js) - Workflow manager with session/chat creation and inference dispatch - Built-in catalog: deep-research, review-code, find-issues - Resumability cache: SHA-256 hash of agent spec, in-memory Map Phase 4: Background Subagents - background-task.ts service: spawn/poll/cancel lifecycle - spawn_subagent, subagent_status, subagent_result tools in ALL_TOOLS Phase 5: Multi-modal + Cache Shape - Multi-modal stub with type defs and hook point in payload.ts - CacheShapeBadge component in trace viewer (colored bar + %)
89 lines
3.2 KiB
TypeScript
89 lines
3.2 KiB
TypeScript
import { useMemo, useState } from 'react';
|
|
import { ChevronDown, ChevronRight, FileCode } from 'lucide-react';
|
|
|
|
interface Props {
|
|
diff: string;
|
|
}
|
|
|
|
const INITIAL_LINES = 10;
|
|
|
|
export function DiffSnippet({ diff }: Props) {
|
|
const [expanded, setExpanded] = useState(false);
|
|
|
|
const lines = useMemo(() => diff.split('\n'), [diff]);
|
|
const totalLines = lines.length;
|
|
|
|
// Find the first and last content lines (skip leading ---/+++ headers)
|
|
const firstContentIdx = lines.findIndex(
|
|
(l) => l.startsWith('+') || l.startsWith('-') || l.startsWith(' '),
|
|
);
|
|
|
|
// Count content lines that are either +, -, or context lines
|
|
const contentLineCount = lines.filter(
|
|
(l) => l.startsWith('+') || l.startsWith('-') || l.startsWith(' '),
|
|
).length;
|
|
|
|
// Show first N content lines, plus header lines
|
|
const displayLines = useMemo(() => {
|
|
const sliceEnd = expanded ? lines.length : Math.min(firstContentIdx + INITIAL_LINES + contentLineCount, lines.length);
|
|
return lines.slice(0, sliceEnd);
|
|
}, [lines, expanded, firstContentIdx, contentLineCount]);
|
|
|
|
const hasMore = totalLines > displayLines.length;
|
|
|
|
if (totalLines === 0) return null;
|
|
|
|
return (
|
|
<div className="mt-1 rounded border border-border/40 bg-muted/20 overflow-hidden">
|
|
<button
|
|
type="button"
|
|
onClick={() => setExpanded((v) => !v)}
|
|
className="flex items-center gap-1.5 w-full px-2 py-1 text-left hover:bg-muted/30 text-[10px] font-mono text-muted-foreground"
|
|
>
|
|
<FileCode className="size-3 shrink-0" />
|
|
<span className="font-medium">diff</span>
|
|
<span className="text-muted-foreground/60">
|
|
— {contentLineCount} line{contentLineCount === 1 ? '' : 's'} changed
|
|
</span>
|
|
<span className="ml-auto shrink-0">
|
|
{expanded ? <ChevronDown className="size-3" /> : <ChevronRight className="size-3" />}
|
|
</span>
|
|
</button>
|
|
<div className="px-0 pb-0.5">
|
|
{displayLines.map((line, i) => {
|
|
// Determine color class based on line prefix
|
|
let colorClass = 'text-muted-foreground/60';
|
|
if (line.startsWith('+')) colorClass = 'text-emerald-600 dark:text-emerald-400';
|
|
else if (line.startsWith('-')) colorClass = 'text-red-500 dark:text-red-400';
|
|
else if (line.startsWith('@@')) colorClass = 'text-muted-foreground';
|
|
else if (line.startsWith('---') || line.startsWith('+++')) colorClass = 'text-muted-foreground/50';
|
|
|
|
return (
|
|
<div
|
|
key={i}
|
|
className={`leading-[1.3] px-2 text-[10px] font-mono whitespace-pre ${colorClass} ${
|
|
line.startsWith('+')
|
|
? 'bg-emerald-500/5'
|
|
: line.startsWith('-')
|
|
? 'bg-red-500/5'
|
|
: ''
|
|
}`}
|
|
>
|
|
{line}
|
|
</div>
|
|
);
|
|
})}
|
|
{hasMore && !expanded && (
|
|
<button
|
|
type="button"
|
|
onClick={() => setExpanded(true)}
|
|
className="w-full text-left px-2 py-0.5 text-[10px] font-mono text-muted-foreground/60 hover:text-muted-foreground hover:bg-muted/30"
|
|
>
|
|
Show {totalLines - displayLines.length} more lines…
|
|
</button>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|