feat: phase 3-5 — workflow engine, background subagents, multi-modal, cache shape, inline diff

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 + %)
This commit is contained in:
2026-06-08 03:11:39 +00:00
parent 591d373534
commit f22da55734
23 changed files with 2938 additions and 33 deletions

View File

@@ -1,7 +1,9 @@
import { useState } from 'react';
import { Check, ChevronRight, Loader2, X } from 'lucide-react';
import { Check, ChevronRight, Loader2, ShieldAlert, X } from 'lucide-react';
import type { ToolCall, ToolResult } from '@/api/types';
import { linkifyPaths } from '@/lib/linkify-paths';
import { DiffSnippet } from './DiffSnippet';
import { McpPermissionDialog } from './McpPermissionDialog';
// v1.8.2: cap on the inline arg-summary length. Expanded view shows full
// args + full result, so this is purely a single-line render budget.
@@ -105,14 +107,18 @@ interface Props {
// When rendered inside a ToolCallGroup the line is already nested under a
// shared header, so the leading arrow is dropped to avoid double indent.
insideGroup?: boolean;
chatId?: string;
}
export function ToolCallLine({ run, insideGroup }: Props) {
export function ToolCallLine({ run, insideGroup, chatId }: Props) {
const [open, setOpen] = useState(false);
const [approveOpen, setApproveOpen] = useState(false);
const status = runStatus(run);
const args = run.call.args ?? {};
const summary = formatToolArgs(run.call.name, args);
const needsApproval = run.result?.error?.startsWith('requires approval:') === true;
return (
<div className="text-xs">
<button
@@ -129,7 +135,7 @@ export function ToolCallLine({ run, insideGroup }: Props) {
/>
)}
<ChevronRight
className={`size-3 text-muted-foreground/60 shrink-0 transition-transform ${open ? 'rotate-90' : ''}`}
className={`size-3 text-muted-foreground/60 shrink-0 motion-reduce:transition-none transition-transform ${open ? 'rotate-90' : ''}`}
/>
<span className="font-mono text-foreground/90 shrink-0">{run.call.name}</span>
{summary && (
@@ -158,7 +164,27 @@ export function ToolCallLine({ run, insideGroup }: Props) {
{run.result && (
<pre className="text-[11px] font-mono whitespace-pre-wrap bg-muted/30 rounded px-2 py-1 max-h-72 overflow-y-auto">
{run.result.error ? (
<span className="text-destructive">{run.result.error}</span>
needsApproval ? (
<span className="flex flex-col gap-2">
<span className="text-amber-600 dark:text-amber-400">
This tool requires your approval
</span>
{chatId && (
<span>
<button
type="button"
onClick={() => setApproveOpen(true)}
className="inline-flex items-center gap-1 rounded bg-amber-500/10 px-2 py-1 text-xs font-medium text-amber-600 hover:bg-amber-500/20 dark:text-amber-400"
>
<ShieldAlert className="size-3" />
Approve
</button>
</span>
)}
</span>
) : (
<span className="text-destructive">{run.result.error}</span>
)
) : (
linkifyPaths(
typeof run.result.output === 'string'
@@ -171,6 +197,17 @@ export function ToolCallLine({ run, insideGroup }: Props) {
)}
</pre>
)}
{needsApproval && chatId && (
<McpPermissionDialog
toolCallId={run.call.id}
toolName={run.call.name}
toolArgs={run.call.args ?? {}}
chatId={chatId}
open={approveOpen}
onClose={() => setApproveOpen(false)}
/>
)}
{run.result?.diff && <DiffSnippet diff={run.result.diff} />}
</div>
)}
</div>