feat(ui): add ToolRenderer system for Omega chat

Add abstract tool rendering infrastructure to @tpmjs/ui:
- ToolRenderer component with registry-based renderer lookup
- DefaultJsonRenderer as fallback with collapsible JSON display
- RegistrySearchRenderer for registrySearchTool results
- RegistryExecuteRenderer for registryExecuteTool results
- registerBuiltInRenderers() for idempotent initialization

Update Omega chat page to use new ToolRenderer:
- Replace inline ToolCallCard with ToolRenderer component
- Add helper functions to convert between ToolCall and ToolPart
- Remove unused expandedToolCalls state (managed internally)

Also add video file extensions to .gitignore
This commit is contained in:
Ajax Davis 2026-01-23 22:35:17 +10:00
parent ca6f908ebd
commit 0489646e55
12 changed files with 967 additions and 136 deletions

View file

@ -234,6 +234,34 @@
"./PageHeader/PageHeader": {
"types": "./dist/PageHeader/PageHeader.d.ts",
"default": "./dist/PageHeader/PageHeader.js"
},
"./ToolRenderer/ToolRenderer": {
"types": "./dist/ToolRenderer/ToolRenderer.d.ts",
"default": "./dist/ToolRenderer/ToolRenderer.js"
},
"./ToolRenderer/types": {
"types": "./dist/ToolRenderer/types.d.ts",
"default": "./dist/ToolRenderer/types.js"
},
"./ToolRenderer/registry": {
"types": "./dist/ToolRenderer/registry.d.ts",
"default": "./dist/ToolRenderer/registry.js"
},
"./ToolRenderer/DefaultJsonRenderer": {
"types": "./dist/ToolRenderer/DefaultJsonRenderer.d.ts",
"default": "./dist/ToolRenderer/DefaultJsonRenderer.js"
},
"./ToolRenderer/registerBuiltInRenderers": {
"types": "./dist/ToolRenderer/registerBuiltInRenderers.d.ts",
"default": "./dist/ToolRenderer/registerBuiltInRenderers.js"
},
"./ToolRenderer/renderers/RegistrySearchRenderer": {
"types": "./dist/ToolRenderer/renderers/RegistrySearchRenderer.d.ts",
"default": "./dist/ToolRenderer/renderers/RegistrySearchRenderer.js"
},
"./ToolRenderer/renderers/RegistryExecuteRenderer": {
"types": "./dist/ToolRenderer/renderers/RegistryExecuteRenderer.d.ts",
"default": "./dist/ToolRenderer/renderers/RegistryExecuteRenderer.js"
}
},
"files": [

View file

@ -0,0 +1,165 @@
/**
* Default JSON Renderer
*
* Fallback renderer that displays tool input/output as formatted JSON.
* Used when no specific renderer is registered for a tool.
*/
import { cn } from '@tpmjs/utils/cn';
import { useState } from 'react';
import { Button } from '../Button/Button';
import { Icon } from '../Icon/Icon';
import type { ToolRendererProps } from './types';
/**
* Format data as JSON string for display
*/
function formatJson(data: unknown): string {
try {
return JSON.stringify(data, null, 2);
} catch {
return String(data);
}
}
/**
* DefaultJsonRenderer - Fallback renderer for any tool
*
* Displays:
* - Collapsible header with tool name and status
* - Input parameters as formatted JSON
* - Output results as formatted JSON
* - Loading state while executing
* - Error state styling
*/
export function DefaultJsonRenderer({
toolName,
input,
output,
state,
isStreaming,
error,
}: ToolRendererProps): React.ReactElement {
const [isExpanded, setIsExpanded] = useState(true);
// Determine status based on state
const status = error
? 'error'
: state === 'result'
? 'success'
: isStreaming
? 'running'
: 'pending';
const statusConfig = {
pending: {
icon: 'info' as const,
colorClass: 'bg-warning/10 text-warning border-warning/30',
label: 'Pending',
},
running: {
icon: 'loader' as const,
colorClass: 'bg-info/10 text-info border-info/30',
label: 'Running',
},
success: {
icon: 'check' as const,
colorClass: 'bg-success/10 text-success border-success/30',
label: 'Success',
},
error: {
icon: 'alertCircle' as const,
colorClass: 'bg-error/10 text-error border-error/30',
label: 'Error',
},
};
const { icon, colorClass, label } = statusConfig[status];
return (
<div className="rounded-lg border border-border bg-surface-secondary/50 overflow-hidden font-mono text-xs">
{/* Header */}
<Button
variant="ghost"
onClick={() => setIsExpanded(!isExpanded)}
className="w-full flex items-center gap-3 p-3 h-auto justify-start rounded-none hover:bg-surface-secondary/80"
>
<div className={cn('p-1.5 rounded border', colorClass)}>
<Icon icon={icon} size="xs" className={status === 'running' ? 'animate-spin' : ''} />
</div>
<div className="flex-1 text-left">
<div className="flex items-center gap-2">
<span className="text-foreground font-semibold">{toolName}</span>
<span className="text-foreground-tertiary text-[10px]">{label}</span>
</div>
</div>
<Icon
icon="chevronRight"
size="xs"
className={cn('text-foreground-tertiary transition-transform', isExpanded && 'rotate-90')}
/>
</Button>
{/* Expanded Content */}
{isExpanded && (
<div className="border-t border-border">
{/* Input Section */}
{input !== undefined && input !== null && (
<div className="p-3 border-b border-border/50">
<div className="flex items-center gap-2 mb-2">
<span className="text-[10px] uppercase tracking-wider text-foreground-tertiary">
Input
</span>
<div className="flex-1 h-px bg-border/50" />
</div>
<pre className="text-[11px] text-foreground-secondary overflow-x-auto whitespace-pre-wrap break-all max-h-48 overflow-y-auto">
{formatJson(input)}
</pre>
</div>
)}
{/* Output Section */}
{output !== undefined && output !== null && (
<div className="p-3">
<div className="flex items-center gap-2 mb-2">
<span className="text-[10px] uppercase tracking-wider text-foreground-tertiary">
Output
</span>
<div className="flex-1 h-px bg-border/50" />
</div>
<pre
className={cn(
'text-[11px] overflow-x-auto whitespace-pre-wrap break-all max-h-48 overflow-y-auto',
error ? 'text-error' : 'text-success'
)}
>
{formatJson(output)}
</pre>
</div>
)}
{/* Error Section */}
{error && !output && (
<div className="p-3">
<div className="flex items-center gap-2 mb-2">
<span className="text-[10px] uppercase tracking-wider text-error">Error</span>
<div className="flex-1 h-px bg-error/30" />
</div>
<pre className="text-[11px] text-error overflow-x-auto whitespace-pre-wrap break-all">
{error}
</pre>
</div>
)}
{/* Loading state */}
{status === 'running' && !output && !error && (
<div className="p-3 flex items-center gap-2 text-foreground-tertiary">
<Icon icon="loader" size="xs" className="animate-spin" />
<span>Executing...</span>
</div>
)}
</div>
)}
</div>
);
}

View file

@ -0,0 +1,117 @@
/**
* ToolRenderer Component
*
* Main component for rendering tool calls in chat interfaces.
* Uses the registry to find the appropriate renderer for each tool.
*/
import { DefaultJsonRenderer } from './DefaultJsonRenderer';
import { toolRendererRegistry } from './registry';
import type { ToolPart, ToolRendererProps, ToolState } from './types';
export interface ToolRendererComponentProps {
/** The tool part from AI SDK message.parts */
part: ToolPart;
/** Whether this tool is currently streaming/executing */
isStreaming?: boolean;
}
/**
* Extract input from various tool part formats
*/
function extractInput(part: ToolPart): unknown {
// AI SDK uses 'args' for tool-call and tool-invocation
if ('args' in part) {
return part.args;
}
return undefined;
}
/**
* Extract output from various tool part formats
*/
function extractOutput(part: ToolPart): unknown {
// AI SDK uses 'result' for tool-result
if ('result' in part) {
return part.result;
}
return undefined;
}
/**
* Determine the tool state from the part type
*/
function determineState(part: ToolPart): ToolState {
// If state is explicitly set, use it
if (part.state) {
return part.state;
}
// Otherwise infer from type
if (part.type === 'tool-result') {
return 'result';
}
if (part.type === 'tool-call') {
return 'call';
}
return 'partial-call';
}
/**
* Check if output indicates an error
*/
function extractError(output: unknown): string | undefined {
if (output && typeof output === 'object' && 'error' in output) {
const err = (output as { error: unknown }).error;
return typeof err === 'string' ? err : JSON.stringify(err);
}
return undefined;
}
/**
* ToolRenderer - Renders a tool call using the appropriate registered renderer
*
* @example
* ```tsx
* import { ToolRenderer } from '@tpmjs/ui/ToolRenderer/ToolRenderer';
*
* // In a chat message component
* {message.parts?.map((part, idx) => {
* if (part.type.startsWith('tool-')) {
* return (
* <ToolRenderer
* key={part.toolCallId || idx}
* part={part}
* isStreaming={isStreaming && part.state !== 'result'}
* />
* );
* }
* return null;
* })}
* ```
*/
export function ToolRenderer({
part,
isStreaming = false,
}: ToolRendererComponentProps): React.ReactElement {
const toolName = part.toolName;
const input = extractInput(part);
const output = extractOutput(part);
const state = determineState(part);
const error = extractError(output);
// Find a registered renderer or use default
const RegisteredRenderer = toolRendererRegistry.getRenderer(toolName);
const Renderer = RegisteredRenderer ?? DefaultJsonRenderer;
const props: ToolRendererProps = {
part,
toolName,
input,
output,
state,
isStreaming,
error,
};
return <Renderer {...props} />;
}

View file

@ -0,0 +1,54 @@
/**
* Register Built-in Tool Renderers
*
* Call this function once at app initialization to register
* all built-in tool renderers with the registry.
*/
import { DefaultJsonRenderer } from './DefaultJsonRenderer';
import { toolRendererRegistry } from './registry';
import { RegistryExecuteRenderer } from './renderers/RegistryExecuteRenderer';
import { RegistrySearchRenderer } from './renderers/RegistrySearchRenderer';
let isRegistered = false;
/**
* Register all built-in tool renderers
*
* This function is idempotent - calling it multiple times has no effect.
*
* @example
* ```tsx
* // In your app's root or layout
* import { registerBuiltInRenderers } from '@tpmjs/ui/ToolRenderer/registerBuiltInRenderers';
*
* registerBuiltInRenderers();
* ```
*/
export function registerBuiltInRenderers(): void {
if (isRegistered) {
return;
}
// Register specific renderers first (higher priority)
toolRendererRegistry.register({
match: (name) => name === 'registrySearchTool',
priority: 100,
component: RegistrySearchRenderer,
});
toolRendererRegistry.register({
match: (name) => name === 'registryExecuteTool',
priority: 100,
component: RegistryExecuteRenderer,
});
// Register default fallback renderer (lowest priority)
toolRendererRegistry.register({
match: () => true,
priority: -1000,
component: DefaultJsonRenderer,
});
isRegistered = true;
}

View file

@ -0,0 +1,51 @@
/**
* Tool Renderer Registry
*
* Singleton registry for registering and looking up tool renderers.
* Renderers are matched by priority (higher = more specific).
*/
import type { ToolRendererConfig, ToolRendererProps } from './types';
class ToolRendererRegistry {
private renderers: ToolRendererConfig[] = [];
/**
* Register a tool renderer
* @param config - The renderer configuration
*/
register<TInput = unknown, TOutput = unknown>(config: ToolRendererConfig<TInput, TOutput>): void {
// Add renderer and sort by priority (descending)
this.renderers.push(config as ToolRendererConfig);
this.renderers.sort((a, b) => (b.priority ?? 0) - (a.priority ?? 0));
}
/**
* Get the appropriate renderer for a tool
* @param toolName - The name of the tool
* @returns The matching renderer component, or null if none found
*/
getRenderer(toolName: string): React.ComponentType<ToolRendererProps> | null {
const config = this.renderers.find((r) => r.match(toolName));
return config?.component ?? null;
}
/**
* Get all registered renderers (for debugging)
*/
getRegisteredRenderers(): ToolRendererConfig[] {
return [...this.renderers];
}
/**
* Clear all renderers (useful for testing)
*/
clear(): void {
this.renderers = [];
}
}
/**
* Global tool renderer registry
*/
export const toolRendererRegistry = new ToolRendererRegistry();

View file

@ -0,0 +1,219 @@
/**
* Registry Execute Tool Renderer
*
* Specialized renderer for the registryExecuteTool that displays
* the tool being executed with its input and output.
*/
import { cn } from '@tpmjs/utils/cn';
import { useState } from 'react';
import { Badge } from '../../Badge/Badge';
import { Button } from '../../Button/Button';
import { Icon } from '../../Icon/Icon';
import type { ToolRendererProps } from '../types';
/**
* Input for registryExecuteTool
*/
interface ExecuteInput {
toolId?: string;
params?: Record<string, unknown>;
}
/**
* Output from registryExecuteTool
*/
interface ExecuteOutput {
result?: unknown;
error?: string;
executionTimeMs?: number;
}
/**
* Parse toolId into package and tool name
*/
function parseToolId(toolId: string): { packageName: string; toolName: string } {
const parts = toolId.split('::');
if (parts.length >= 2) {
return {
packageName: parts[0] || 'unknown',
toolName: parts.slice(1).join('::') || 'unknown',
};
}
return {
packageName: toolId,
toolName: 'unknown',
};
}
/**
* Format data as JSON string for display
*/
function formatJson(data: unknown): string {
try {
return JSON.stringify(data, null, 2);
} catch {
return String(data);
}
}
/**
* RegistryExecuteRenderer - Displays tool execution with input/output
*/
// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: Complex renderer with multiple UI states
export function RegistryExecuteRenderer({
input,
output,
state,
isStreaming,
error,
}: ToolRendererProps<ExecuteInput, ExecuteOutput>): React.ReactElement {
const [isExpanded, setIsExpanded] = useState(true);
const [showInput, setShowInput] = useState(false);
const execInput = input as ExecuteInput | undefined;
const execOutput = output as ExecuteOutput | undefined;
const hasError = error || execOutput?.error;
const { packageName, toolName } = parseToolId(execInput?.toolId ?? '');
// Determine status
const status = hasError
? 'error'
: state === 'result'
? 'success'
: isStreaming
? 'running'
: 'pending';
const statusConfig = {
pending: {
icon: 'info' as const,
colorClass: 'bg-warning/10 text-warning border-warning/30',
label: 'Pending',
},
running: {
icon: 'loader' as const,
colorClass: 'bg-info/10 text-info border-info/30',
label: 'Executing',
},
success: {
icon: 'check' as const,
colorClass: 'bg-success/10 text-success border-success/30',
label: 'Success',
},
error: {
icon: 'alertCircle' as const,
colorClass: 'bg-error/10 text-error border-error/30',
label: 'Error',
},
};
const { icon, colorClass, label } = statusConfig[status];
return (
<div className="rounded-lg border border-border bg-surface-secondary/50 overflow-hidden">
{/* Header */}
<Button
variant="ghost"
onClick={() => setIsExpanded(!isExpanded)}
className="w-full flex items-center gap-3 p-3 h-auto justify-start rounded-none hover:bg-surface-secondary/80"
>
<div className={cn('p-1.5 rounded border', colorClass)}>
<Icon icon={icon} size="xs" className={status === 'running' ? 'animate-spin' : ''} />
</div>
<div className="flex-1 text-left">
<div className="flex items-center gap-2 flex-wrap">
<Badge variant="outline" size="sm" className="font-mono">
{packageName}
</Badge>
<span className="text-sm font-medium text-foreground">{toolName}</span>
<span className="text-xs text-foreground-tertiary">{label}</span>
</div>
{execOutput?.executionTimeMs && (
<span className="text-[10px] text-foreground-tertiary">
{execOutput.executionTimeMs}ms
</span>
)}
</div>
<Icon
icon="chevronRight"
size="xs"
className={cn('text-foreground-tertiary transition-transform', isExpanded && 'rotate-90')}
/>
</Button>
{/* Expanded Content */}
{isExpanded && (
<div className="border-t border-border">
{/* Input toggle */}
{execInput?.params && Object.keys(execInput.params).length > 0 && (
<div className="border-b border-border/50">
<Button
variant="ghost"
size="sm"
onClick={() => setShowInput(!showInput)}
className="w-full rounded-none text-xs text-foreground-tertiary flex items-center gap-2 justify-start"
>
<Icon
icon="chevronRight"
size="xs"
className={cn('transition-transform', showInput && 'rotate-90')}
/>
<span>Input Parameters ({Object.keys(execInput.params).length})</span>
</Button>
{showInput && (
<pre className="p-3 text-[11px] font-mono text-foreground-secondary bg-background/50 overflow-x-auto whitespace-pre-wrap break-all max-h-40 overflow-y-auto border-t border-border/30">
{formatJson(execInput.params)}
</pre>
)}
</div>
)}
{/* Loading state */}
{status === 'running' && (
<div className="p-4 flex items-center gap-2 text-foreground-tertiary">
<Icon icon="loader" size="sm" className="animate-spin" />
<span className="text-sm">Executing {toolName}...</span>
</div>
)}
{/* Error state */}
{hasError && (
<div className="p-3">
<div className="flex items-center gap-2 mb-2">
<Icon icon="alertCircle" size="xs" className="text-error" />
<span className="text-[10px] uppercase tracking-wider text-error">Error</span>
</div>
<pre className="text-[11px] font-mono text-error overflow-x-auto whitespace-pre-wrap break-all">
{error || execOutput?.error}
</pre>
</div>
)}
{/* Success output */}
{state === 'result' && !hasError && execOutput?.result !== undefined && (
<div className="p-3">
<div className="flex items-center gap-2 mb-2">
<span className="text-[10px] uppercase tracking-wider text-foreground-tertiary">
Output
</span>
<div className="flex-1 h-px bg-border/50" />
</div>
<pre className="text-[11px] font-mono text-success overflow-x-auto whitespace-pre-wrap break-all max-h-60 overflow-y-auto">
{formatJson(execOutput.result)}
</pre>
</div>
)}
{/* No output state */}
{state === 'result' && !hasError && execOutput?.result === undefined && (
<div className="p-3 text-sm text-foreground-tertiary text-center">
No output returned
</div>
)}
</div>
)}
</div>
);
}

View file

@ -0,0 +1,204 @@
/**
* Registry Search Tool Renderer
*
* Specialized renderer for the registrySearchTool that displays
* search results as a grid of tool cards.
*/
import { cn } from '@tpmjs/utils/cn';
import { useState } from 'react';
import { Badge } from '../../Badge/Badge';
import { Button } from '../../Button/Button';
import { Icon } from '../../Icon/Icon';
import type { ToolRendererProps } from '../types';
/**
* Input for registrySearchTool
*/
interface SearchInput {
query?: string;
limit?: number;
}
/**
* A tool result from the search
*/
interface SearchResultTool {
toolId: string;
package: string;
name: string;
description: string;
}
/**
* Output from registrySearchTool
*/
interface SearchOutput {
tools?: SearchResultTool[];
total?: number;
error?: string;
}
/**
* RegistrySearchRenderer - Displays search results as tool cards
*/
// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: Complex renderer with multiple UI states
export function RegistrySearchRenderer({
input,
output,
state,
isStreaming,
error,
}: ToolRendererProps<SearchInput, SearchOutput>): React.ReactElement {
const [isExpanded, setIsExpanded] = useState(true);
const [showRawJson, setShowRawJson] = useState(false);
const searchInput = input as SearchInput | undefined;
const searchOutput = output as SearchOutput | undefined;
const tools = searchOutput?.tools ?? [];
const hasError = error || searchOutput?.error;
// Determine status
const status = hasError
? 'error'
: state === 'result'
? 'success'
: isStreaming
? 'running'
: 'pending';
const statusConfig = {
pending: { icon: 'info' as const, colorClass: 'text-warning' },
running: { icon: 'loader' as const, colorClass: 'text-info' },
success: { icon: 'check' as const, colorClass: 'text-success' },
error: { icon: 'alertCircle' as const, colorClass: 'text-error' },
};
const { icon, colorClass } = statusConfig[status];
const handleCopyToolId = (toolId: string) => {
navigator.clipboard.writeText(toolId);
};
return (
<div className="rounded-lg border border-border bg-surface-secondary/50 overflow-hidden">
{/* Header */}
<Button
variant="ghost"
onClick={() => setIsExpanded(!isExpanded)}
className="w-full flex items-center gap-3 p-3 h-auto justify-start rounded-none hover:bg-surface-secondary/80"
>
<Icon icon="search" size="sm" className="text-primary" />
<div className="flex-1 text-left">
<div className="flex items-center gap-2">
<span className="text-sm font-medium text-foreground">Registry Search</span>
{searchInput?.query && (
<Badge variant="secondary" size="sm">
"{searchInput.query}"
</Badge>
)}
</div>
{state === 'result' && !hasError && (
<span className="text-xs text-foreground-tertiary">
Found {tools.length} tool{tools.length !== 1 ? 's' : ''}
</span>
)}
</div>
<Icon
icon={icon}
size="xs"
className={cn(colorClass, status === 'running' && 'animate-spin')}
/>
<Icon
icon="chevronRight"
size="xs"
className={cn('text-foreground-tertiary transition-transform', isExpanded && 'rotate-90')}
/>
</Button>
{/* Expanded Content */}
{isExpanded && (
<div className="border-t border-border">
{/* Loading state */}
{status === 'running' && (
<div className="p-4 flex items-center justify-center gap-2 text-foreground-tertiary">
<Icon icon="loader" size="sm" className="animate-spin" />
<span className="text-sm">Searching registry...</span>
</div>
)}
{/* Error state */}
{hasError && <div className="p-4 text-sm text-error">{error || searchOutput?.error}</div>}
{/* Results grid */}
{state === 'result' && !hasError && tools.length > 0 && (
<div className="p-3 space-y-2">
{tools.map((tool) => (
<div
key={tool.toolId}
className="p-3 rounded-md border border-border/50 bg-background/50 hover:bg-background transition-colors group"
>
<div className="flex items-start justify-between gap-2">
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 mb-1">
<span className="text-xs text-primary font-mono">{tool.package}</span>
<span className="text-foreground-tertiary">::</span>
<span className="text-sm font-medium text-foreground">{tool.name}</span>
</div>
<p className="text-xs text-foreground-secondary line-clamp-2">
{tool.description}
</p>
</div>
<Button
variant="ghost"
size="sm"
onClick={(e) => {
e.stopPropagation();
handleCopyToolId(tool.toolId);
}}
className="opacity-0 group-hover:opacity-100 transition-opacity"
title="Copy toolId"
>
<Icon icon="copy" size="xs" />
</Button>
</div>
<div className="mt-2 pt-2 border-t border-border/30">
<code className="text-[10px] text-foreground-tertiary font-mono">
{tool.toolId}
</code>
</div>
</div>
))}
</div>
)}
{/* Empty results */}
{state === 'result' && !hasError && tools.length === 0 && (
<div className="p-4 text-center text-sm text-foreground-tertiary">
No tools found for this search
</div>
)}
{/* Raw JSON toggle */}
{state === 'result' && (
<div className="border-t border-border/50">
<Button
variant="ghost"
size="sm"
onClick={() => setShowRawJson(!showRawJson)}
className="w-full rounded-none text-xs text-foreground-tertiary"
>
{showRawJson ? 'Hide' : 'Show'} Raw JSON
</Button>
{showRawJson && (
<pre className="p-3 text-[10px] font-mono text-foreground-tertiary bg-background/50 overflow-x-auto max-h-40 overflow-y-auto">
{JSON.stringify(output, null, 2)}
</pre>
)}
</div>
)}
</div>
)}
</div>
);
}

View file

@ -0,0 +1,8 @@
/**
* Built-in Tool Renderers
*
* Export all built-in renderers for registration.
*/
export { RegistryExecuteRenderer } from './RegistryExecuteRenderer';
export { RegistrySearchRenderer } from './RegistrySearchRenderer';

View file

@ -0,0 +1,55 @@
/**
* Tool Renderer Types
*
* Interfaces for the abstract tool rendering system.
* Used to render tool calls in AI chat interfaces.
*/
/**
* Tool state during streaming
*/
export type ToolState = 'partial-call' | 'call' | 'result';
/**
* A part representing a tool call or result from AI SDK
*/
export interface ToolPart {
type: 'tool-invocation' | 'tool-call' | 'tool-result';
toolCallId: string;
toolName: string;
args?: unknown;
result?: unknown;
state?: ToolState;
}
/**
* Props passed to every tool renderer
*/
export interface ToolRendererProps<TInput = unknown, TOutput = unknown> {
/** The full tool part from AI SDK */
part: ToolPart;
/** Name of the tool being rendered */
toolName: string;
/** Input parameters for the tool */
input: TInput;
/** Output from the tool (undefined while running) */
output: TOutput | undefined;
/** Current state of the tool call */
state: ToolState;
/** Whether the tool is currently streaming/executing */
isStreaming: boolean;
/** Error message if the tool failed */
error?: string;
}
/**
* Configuration for registering a tool renderer
*/
export interface ToolRendererConfig<TInput = unknown, TOutput = unknown> {
/** Function to determine if this renderer handles a given tool */
match: (toolName: string) => boolean;
/** Priority for matching (higher = more specific, checked first) */
priority?: number;
/** The React component to render the tool */
component: React.ComponentType<ToolRendererProps<TInput, TOutput>>;
}

View file

@ -44,6 +44,13 @@ entries.push('src/system/hooks/useParallax.ts');
entries.push('src/system/hooks/useReducedMotion.ts');
entries.push('src/system/hooks/useDitherAnimation.ts');
// Manually add ToolRenderer files that don't match naming conventions
entries.push('src/ToolRenderer/types.ts');
entries.push('src/ToolRenderer/registry.ts');
entries.push('src/ToolRenderer/registerBuiltInRenderers.ts');
entries.push('src/ToolRenderer/renderers/RegistrySearchRenderer.tsx');
entries.push('src/ToolRenderer/renderers/RegistryExecuteRenderer.tsx');
export default defineConfig({
entry: entries,
format: ['esm'],