- Add ComparePane.tsx: side-by-side AI response comparison - Add Memory.tsx: memory management page with CRUD UI - Add McpPermissionDialog.tsx: MCP tool permission approval dialog - Add McpResponseDisplay.tsx: MCP response visualization - Add MessageBoundary.tsx + MessageListErrorBoundary.tsx: error resilience - Add EmptyState.tsx: contextual empty state component - Add KeyboardShortcutsDialog.tsx: keyboard shortcut reference - Add message-parts/: ActionRow, CompactCard, MistakeRecoverySentinel, ReasoningBlock, SendToTerminalMenu, StatsLine, SummaryCard - Add useDraftPersistence.ts: draft message persistence hook - Add useTerminals.ts: terminal session management hook - Add keyboard-shortcuts.ts + tool-utils.ts: shared utilities - Extend components: ChatInput, MessageBubble, MessageList, Workspace, panes - Extend hooks: useTerminalSocket, useSessionStream test suite - Update pages: Home, Project — workspace layout and session flow
74 lines
2.6 KiB
TypeScript
74 lines
2.6 KiB
TypeScript
import { useState } from 'react';
|
|
import { ChevronRight } from 'lucide-react';
|
|
import { ToolCallLine, runStatus, type ToolRun } from './ToolCallLine';
|
|
|
|
interface Props {
|
|
// All runs must share the same tool name. Caller (MessageList grouping
|
|
// pass) enforces that invariant.
|
|
runs: ToolRun[];
|
|
}
|
|
|
|
export function ToolCallGroup({ runs }: Props) {
|
|
const [open, setOpen] = useState(false);
|
|
if (runs.length === 0) return null;
|
|
const toolName = runs[0]!.call.name;
|
|
const count = runs.length;
|
|
|
|
// Group-level status: pending if any are still running, error if any
|
|
// finished with an error, otherwise success. Matches the visual the user
|
|
// gets when scanning a long run of greps / view_files.
|
|
let pending = 0;
|
|
let errored = 0;
|
|
for (const r of runs) {
|
|
const s = runStatus(r);
|
|
if (s === 'pending') pending += 1;
|
|
else if (s === 'error') errored += 1;
|
|
}
|
|
const summaryParts: string[] = [];
|
|
if (pending > 0) summaryParts.push(`${pending} running`);
|
|
if (errored > 0) summaryParts.push(`${errored} failed`);
|
|
const summary = summaryParts.length > 0 ? ` (${summaryParts.join(', ')})` : '';
|
|
|
|
return (
|
|
<div className="rounded border border-border/60 bg-muted/20 text-xs">
|
|
<button
|
|
type="button"
|
|
tabIndex={0}
|
|
onClick={() => setOpen((v) => !v)}
|
|
onKeyDown={(e) => {
|
|
if (e.key === 'Enter' || e.key === ' ') {
|
|
e.preventDefault();
|
|
setOpen((v) => !v);
|
|
} else if (e.key === 'Escape') {
|
|
setOpen(false);
|
|
}
|
|
}}
|
|
className="w-full flex items-center gap-1.5 px-2 py-1 hover:bg-muted/40 text-left focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1"
|
|
>
|
|
<ChevronRight
|
|
className={`size-3 text-muted-foreground/60 shrink-0 motion-reduce:transition-none transition-transform ${open ? 'rotate-90' : ''}`}
|
|
/>
|
|
<span className="text-muted-foreground/60 select-none shrink-0">⊞</span>
|
|
<span className="font-mono text-foreground/90">
|
|
{count} {toolName} call{count === 1 ? '' : 's'}
|
|
</span>
|
|
{summary && (
|
|
<span className="text-muted-foreground truncate">{summary}</span>
|
|
)}
|
|
<span className="ml-auto text-muted-foreground/60 shrink-0">tap</span>
|
|
</button>
|
|
{open && (
|
|
<div className="border-t border-border/40 px-2 py-1 space-y-0.5">
|
|
{runs.map((run, i) => (
|
|
<ToolCallLine
|
|
key={`${run.call.id}-${i}`}
|
|
run={run}
|
|
insideGroup
|
|
/>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|