Files
OpenQode/bin/ui/components/ToolRegistry.mjs

270 lines
7.5 KiB
JavaScript

/**
* Tool Registry - Renders tool-specific UI
*
* Based on sst/opencode ToolRegistry pattern
* Credit: https://github.com/sst/opencode
*
* Each tool has a dedicated renderer for consistent output
*/
import React from 'react';
import { Box, Text } from 'ink';
import Spinner from 'ink-spinner';
import { colors } from '../../tui-theme.mjs';
import { icon } from '../../icons.mjs';
import { getCapabilities } from '../../terminal-profile.mjs';
const h = React.createElement;
// Registry of tool renderers
const toolRenderers = new Map();
/**
* Register a tool renderer
* @param {string} toolName - Tool identifier
* @param {object} renderer - { icon, title, renderSummary, renderDetails }
*/
export function registerTool(toolName, renderer) {
toolRenderers.set(toolName, {
icon: renderer.icon || '⚙',
iconAscii: renderer.iconAscii || '*',
title: renderer.title || toolName,
color: renderer.color || colors.accent,
renderSummary: renderer.renderSummary || defaultRenderSummary,
renderDetails: renderer.renderDetails || defaultRenderDetails
});
}
/**
* Get renderer for a tool
*/
export function getToolRenderer(toolName) {
return toolRenderers.get(toolName) || {
icon: '⚙',
iconAscii: '*',
title: toolName,
color: colors.muted,
renderSummary: defaultRenderSummary,
renderDetails: defaultRenderDetails
};
}
/**
* Default summary renderer
*/
function defaultRenderSummary(args, status) {
const summary = Object.entries(args || {})
.slice(0, 2)
.map(([k, v]) => `${k}=${String(v).slice(0, 20)}`)
.join(', ');
return summary || 'Running...';
}
/**
* Default details renderer
*/
function defaultRenderDetails(args, result) {
if (result?.output) {
return result.output.slice(0, 500);
}
return JSON.stringify(args, null, 2).slice(0, 500);
}
// ============================================
// BUILT-IN TOOL RENDERERS
// ============================================
// File read tool
registerTool('read_file', {
icon: '📄',
iconAscii: '[R]',
title: 'Read File',
color: colors.accent,
renderSummary: (args) => args?.path || 'reading...',
renderDetails: (args, result) => result?.content?.slice(0, 500) || ''
});
// File write tool
registerTool('write_file', {
icon: '✏️',
iconAscii: '[W]',
title: 'Write File',
color: 'green',
renderSummary: (args) => args?.path || 'writing...',
renderDetails: (args) => `${args?.content?.split('\n').length || 0} lines`
});
// Edit file tool
registerTool('edit_file', {
icon: '📝',
iconAscii: '[E]',
title: 'Edit File',
color: 'yellow',
renderSummary: (args) => args?.path || 'editing...',
renderDetails: (args) => args?.description || ''
});
// Delete file tool
registerTool('delete_file', {
icon: '🗑️',
iconAscii: '[D]',
title: 'Delete File',
color: 'red',
renderSummary: (args) => args?.path || 'deleting...',
renderDetails: () => ''
});
// Shell/command tool
registerTool('shell', {
icon: '💻',
iconAscii: '>',
title: 'Shell',
color: 'magenta',
renderSummary: (args) => {
const cmd = args?.command || args?.cmd || '';
return cmd.length > 40 ? cmd.slice(0, 37) + '...' : cmd;
},
renderDetails: (args, result) => result?.output?.slice(0, 1000) || ''
});
registerTool('run_command', {
icon: '💻',
iconAscii: '>',
title: 'Command',
color: 'magenta',
renderSummary: (args) => {
const cmd = args?.command || args?.CommandLine || '';
return cmd.length > 40 ? cmd.slice(0, 37) + '...' : cmd;
},
renderDetails: (args, result) => result?.output?.slice(0, 1000) || ''
});
// Search tool
registerTool('search', {
icon: '🔍',
iconAscii: '?',
title: 'Search',
color: colors.accent,
renderSummary: (args) => args?.query || args?.pattern || 'searching...',
renderDetails: (args, result) => `${result?.matches?.length || 0} matches`
});
registerTool('grep_search', {
icon: '🔍',
iconAscii: '?',
title: 'Grep',
color: colors.accent,
renderSummary: (args) => args?.Query || 'searching...',
renderDetails: (args, result) => `${result?.matches?.length || 0} matches`
});
// List files tool
registerTool('list_files', {
icon: '📁',
iconAscii: '[L]',
title: 'List Files',
color: colors.muted,
renderSummary: (args) => args?.path || args?.directory || '.',
renderDetails: (args, result) => `${result?.files?.length || 0} items`
});
registerTool('list_dir', {
icon: '📁',
iconAscii: '[L]',
title: 'List Dir',
color: colors.muted,
renderSummary: (args) => args?.DirectoryPath || '.',
renderDetails: (args, result) => `${result?.children?.length || 0} items`
});
// TODO/task tool
registerTool('todowrite', {
icon: '✅',
iconAscii: '[T]',
title: 'Tasks',
color: 'green',
renderSummary: (args) => {
const todos = args?.todos || [];
const done = todos.filter(t => t.status === 'done').length;
return `${done}/${todos.length} done`;
},
renderDetails: (args) => {
const todos = args?.todos || [];
return todos.map(t =>
`[${t.status === 'done' ? 'x' : t.status === 'in_progress' ? '/' : ' '}] ${t.text}`
).join('\n');
}
});
// Web search tool
registerTool('web_search', {
icon: '🌐',
iconAscii: '[W]',
title: 'Web Search',
color: colors.accent,
renderSummary: (args) => args?.query || 'searching...',
renderDetails: (args, result) => result?.summary?.slice(0, 300) || ''
});
// Browser tool
registerTool('browser', {
icon: '🌐',
iconAscii: '[B]',
title: 'Browser',
color: colors.accent,
renderSummary: (args) => args?.url || 'browsing...',
renderDetails: () => ''
});
/**
* ToolBlock Component - Renders a tool invocation
*/
export const ToolBlock = ({
toolName,
args = {},
status = 'running', // running | done | failed
result = null,
isExpanded = false,
width = 80
}) => {
const caps = getCapabilities();
const renderer = getToolRenderer(toolName);
const railChar = caps.unicodeOK ? '│' : '|';
const toolIcon = caps.unicodeOK ? renderer.icon : renderer.iconAscii;
const statusConfig = {
running: { color: renderer.color, showSpinner: true },
done: { color: colors.success, showSpinner: false },
failed: { color: colors.error, showSpinner: false }
};
const config = statusConfig[status] || statusConfig.running;
// Summary line
const summary = renderer.renderSummary(args, status);
return h(Box, { flexDirection: 'column' },
// Header line
h(Box, { flexDirection: 'row' },
h(Text, { color: 'magenta' }, railChar + ' '),
config.showSpinner
? h(Spinner, { type: 'dots' })
: h(Text, { color: config.color }, toolIcon),
h(Text, {}, ' '),
h(Text, { color: config.color, bold: true }, renderer.title),
h(Text, { color: colors.muted }, ': '),
h(Text, { color: colors.muted, wrap: 'truncate' },
summary.length > width - 25 ? summary.slice(0, width - 28) + '...' : summary
)
),
// Details (if expanded)
isExpanded && result ? h(Box, { paddingLeft: 4 },
h(Text, { color: colors.muted, dimColor: true, wrap: 'wrap' },
renderer.renderDetails(args, result).slice(0, 500)
)
) : null
);
};
export default { registerTool, getToolRenderer, ToolBlock };