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:
parent
ca6f908ebd
commit
0489646e55
12 changed files with 967 additions and 136 deletions
6
.gitignore
vendored
6
.gitignore
vendored
|
|
@ -16,6 +16,12 @@ dist
|
|||
# misc
|
||||
.DS_Store
|
||||
|
||||
# video files
|
||||
*.mp4
|
||||
*.webm
|
||||
*.mov
|
||||
*.avi
|
||||
|
||||
# debug
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
|
|
|
|||
|
|
@ -4,6 +4,9 @@ import { Badge } from '@tpmjs/ui/Badge/Badge';
|
|||
import { Button } from '@tpmjs/ui/Button/Button';
|
||||
import { Icon } from '@tpmjs/ui/Icon/Icon';
|
||||
import { Textarea } from '@tpmjs/ui/Textarea/Textarea';
|
||||
import { registerBuiltInRenderers } from '@tpmjs/ui/ToolRenderer/registerBuiltInRenderers';
|
||||
import { ToolRenderer } from '@tpmjs/ui/ToolRenderer/ToolRenderer';
|
||||
import type { ToolPart, ToolState } from '@tpmjs/ui/ToolRenderer/types';
|
||||
import Link from 'next/link';
|
||||
import { useParams, useRouter } from 'next/navigation';
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
|
|
@ -11,6 +14,9 @@ import { Streamdown } from 'streamdown';
|
|||
import { AppHeader } from '~/components/AppHeader';
|
||||
import { EnvVarWarningBanner } from '~/components/omega/EnvVarWarningBanner';
|
||||
|
||||
// Initialize built-in tool renderers (idempotent)
|
||||
registerBuiltInRenderers();
|
||||
|
||||
interface ToolDiscoveryInfo {
|
||||
staticTools?: string[];
|
||||
dynamicToolsLoaded?: string[];
|
||||
|
|
@ -29,7 +35,8 @@ interface Message {
|
|||
toolCalls?: Array<{
|
||||
toolCallId: string;
|
||||
toolName: string;
|
||||
args: unknown;
|
||||
args?: unknown;
|
||||
output?: unknown;
|
||||
}>;
|
||||
inputTokens?: number;
|
||||
outputTokens?: number;
|
||||
|
|
@ -69,115 +76,32 @@ interface Conversation {
|
|||
}
|
||||
|
||||
/**
|
||||
* Tool call debug card component
|
||||
* Convert ToolCall status to ToolState
|
||||
*/
|
||||
function ToolCallCard({
|
||||
toolCall,
|
||||
isExpanded,
|
||||
onToggle,
|
||||
}: {
|
||||
toolCall: ToolCall;
|
||||
isExpanded: boolean;
|
||||
onToggle: () => void;
|
||||
}) {
|
||||
const statusColors = {
|
||||
pending: 'bg-warning/10 text-warning border-warning/30',
|
||||
running: 'bg-info/10 text-info border-info/30',
|
||||
success: 'bg-success/10 text-success border-success/30',
|
||||
error: 'bg-error/10 text-error border-error/30',
|
||||
function statusToToolState(status: ToolCall['status']): ToolState {
|
||||
switch (status) {
|
||||
case 'pending':
|
||||
return 'partial-call';
|
||||
case 'running':
|
||||
return 'call';
|
||||
case 'success':
|
||||
case 'error':
|
||||
return 'result';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert ToolCall to ToolPart for rendering
|
||||
*/
|
||||
function toolCallToToolPart(tc: ToolCall): ToolPart {
|
||||
return {
|
||||
type: tc.status === 'success' || tc.status === 'error' ? 'tool-result' : 'tool-call',
|
||||
toolCallId: tc.toolCallId,
|
||||
toolName: tc.toolName,
|
||||
args: tc.input,
|
||||
result: tc.isError ? { error: tc.output } : tc.output,
|
||||
state: statusToToolState(tc.status),
|
||||
};
|
||||
|
||||
const statusIcons: Record<ToolCall['status'], 'loader' | 'check' | 'alertCircle' | 'info'> = {
|
||||
pending: 'info',
|
||||
running: 'loader',
|
||||
success: 'check',
|
||||
error: 'alertCircle',
|
||||
};
|
||||
|
||||
const formatJson = (data: unknown): React.ReactNode => {
|
||||
try {
|
||||
return JSON.stringify(data, null, 2);
|
||||
} catch {
|
||||
return String(data);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="rounded-lg border border-border bg-surface-secondary/50 overflow-hidden font-mono text-xs">
|
||||
{/* Header */}
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={onToggle}
|
||||
className="w-full flex items-center gap-3 p-3 h-auto justify-start rounded-none hover:bg-surface-secondary/80"
|
||||
>
|
||||
<div className={`p-1.5 rounded ${statusColors[toolCall.status]}`}>
|
||||
<Icon
|
||||
icon={statusIcons[toolCall.status]}
|
||||
size="xs"
|
||||
className={toolCall.status === 'running' ? 'animate-spin' : ''}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex-1 text-left">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-foreground font-semibold">{toolCall.toolName}</span>
|
||||
<span className="text-foreground-tertiary text-[10px]">
|
||||
{toolCall.toolCallId.slice(0, 8)}...
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<Icon
|
||||
icon="chevronRight"
|
||||
size="xs"
|
||||
className={`text-foreground-tertiary transition-transform ${isExpanded ? 'rotate-90' : ''}`}
|
||||
/>
|
||||
</Button>
|
||||
|
||||
{/* Expanded Content */}
|
||||
{isExpanded && (
|
||||
<div className="border-t border-border">
|
||||
{/* Input Section */}
|
||||
{toolCall.input !== undefined && toolCall.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(toolCall.input)}
|
||||
</pre>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{/* Output Section */}
|
||||
{toolCall.output !== undefined && toolCall.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={`text-[11px] overflow-x-auto whitespace-pre-wrap break-all max-h-48 overflow-y-auto ${toolCall.isError ? 'text-error' : 'text-success'}`}
|
||||
>
|
||||
{formatJson(toolCall.output)}
|
||||
</pre>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{/* Status indicator for running */}
|
||||
{toolCall.status === 'running' && !toolCall.output && (
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -197,7 +121,6 @@ export default function OmegaChatPage(): React.ReactElement {
|
|||
const [streamingContent, setStreamingContent] = useState('');
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [toolCalls, setToolCalls] = useState<ToolCall[]>([]);
|
||||
const [expandedToolCalls, setExpandedToolCalls] = useState<Set<string>>(new Set());
|
||||
const [viewMode, setViewMode] = useState<'chat' | 'debug'>('chat');
|
||||
// Map of message IDs to their tool discovery info (persisted in frontend state)
|
||||
const [messageToolDiscovery, setMessageToolDiscovery] = useState<Map<string, ToolDiscoveryInfo>>(
|
||||
|
|
@ -209,18 +132,6 @@ export default function OmegaChatPage(): React.ReactElement {
|
|||
const messagesContainerRef = useRef<HTMLDivElement>(null);
|
||||
const inputRef = useRef<HTMLTextAreaElement>(null);
|
||||
|
||||
const toggleToolCall = (toolCallId: string) => {
|
||||
setExpandedToolCalls((prev) => {
|
||||
const next = new Set(prev);
|
||||
if (next.has(toolCallId)) {
|
||||
next.delete(toolCallId);
|
||||
} else {
|
||||
next.add(toolCallId);
|
||||
}
|
||||
return next;
|
||||
});
|
||||
};
|
||||
|
||||
// Fetch conversation details
|
||||
const fetchConversation = useCallback(async () => {
|
||||
try {
|
||||
|
|
@ -353,8 +264,6 @@ export default function OmegaChatPage(): React.ReactElement {
|
|||
status: 'running',
|
||||
},
|
||||
]);
|
||||
// Auto-expand new tool calls
|
||||
setExpandedToolCalls((prev) => new Set([...prev, data.toolCallId]));
|
||||
break;
|
||||
case 'run.step.tool.completed':
|
||||
// Update tool call with result
|
||||
|
|
@ -580,6 +489,7 @@ export default function OmegaChatPage(): React.ReactElement {
|
|||
</div>
|
||||
|
||||
{/* Per-Message View */}
|
||||
{/* biome-ignore lint/complexity/noExcessiveCognitiveComplexity: Debug view with multiple conditional sections */}
|
||||
{messages.map((message, index) => {
|
||||
const discovery = messageToolDiscovery.get(message.id);
|
||||
return (
|
||||
|
|
@ -789,16 +699,24 @@ export default function OmegaChatPage(): React.ReactElement {
|
|||
</div>
|
||||
)}
|
||||
|
||||
{/* TOOL message - show as collapsed tool result */}
|
||||
{message.role === 'TOOL' && (
|
||||
{/* TOOL message - show tool results from toolCalls field */}
|
||||
{message.role === 'TOOL' && message.toolCalls && (
|
||||
<div className="flex justify-start">
|
||||
<div className="max-w-[80%]">
|
||||
<div className="text-xs text-foreground-tertiary mb-1">
|
||||
Tool Results
|
||||
</div>
|
||||
<pre className="text-xs font-mono bg-surface-secondary border border-border rounded p-2 overflow-x-auto max-h-32 overflow-y-auto">
|
||||
{message.content}
|
||||
</pre>
|
||||
<div className="max-w-[80%] space-y-2">
|
||||
{message.toolCalls.map((tc) => (
|
||||
<ToolRenderer
|
||||
key={tc.toolCallId}
|
||||
part={{
|
||||
type: 'tool-result',
|
||||
toolCallId: tc.toolCallId,
|
||||
toolName: tc.toolName,
|
||||
args: tc.args,
|
||||
result: tc.output,
|
||||
state: 'result',
|
||||
}}
|
||||
isStreaming={false}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -811,10 +729,9 @@ export default function OmegaChatPage(): React.ReactElement {
|
|||
{toolCalls.map((tc) => (
|
||||
<div key={tc.toolCallId} className="flex justify-start">
|
||||
<div className="max-w-[80%]">
|
||||
<ToolCallCard
|
||||
toolCall={tc}
|
||||
isExpanded={expandedToolCalls.has(tc.toolCallId)}
|
||||
onToggle={() => toggleToolCall(tc.toolCallId)}
|
||||
<ToolRenderer
|
||||
part={toolCallToToolPart(tc)}
|
||||
isStreaming={tc.status === 'running'}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -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": [
|
||||
|
|
|
|||
165
packages/ui/src/ToolRenderer/DefaultJsonRenderer.tsx
Normal file
165
packages/ui/src/ToolRenderer/DefaultJsonRenderer.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
117
packages/ui/src/ToolRenderer/ToolRenderer.tsx
Normal file
117
packages/ui/src/ToolRenderer/ToolRenderer.tsx
Normal 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} />;
|
||||
}
|
||||
54
packages/ui/src/ToolRenderer/registerBuiltInRenderers.ts
Normal file
54
packages/ui/src/ToolRenderer/registerBuiltInRenderers.ts
Normal 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;
|
||||
}
|
||||
51
packages/ui/src/ToolRenderer/registry.ts
Normal file
51
packages/ui/src/ToolRenderer/registry.ts
Normal 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();
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
8
packages/ui/src/ToolRenderer/renderers/index.ts
Normal file
8
packages/ui/src/ToolRenderer/renderers/index.ts
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
/**
|
||||
* Built-in Tool Renderers
|
||||
*
|
||||
* Export all built-in renderers for registration.
|
||||
*/
|
||||
|
||||
export { RegistryExecuteRenderer } from './RegistryExecuteRenderer';
|
||||
export { RegistrySearchRenderer } from './RegistrySearchRenderer';
|
||||
55
packages/ui/src/ToolRenderer/types.ts
Normal file
55
packages/ui/src/ToolRenderer/types.ts
Normal 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>>;
|
||||
}
|
||||
|
|
@ -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'],
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue