Release v1.01 Enhanced: Vi Control, TUI Gen5, Core Stability
This commit is contained in:
269
bin/ui/components/ToolRegistry.mjs
Normal file
269
bin/ui/components/ToolRegistry.mjs
Normal file
@@ -0,0 +1,269 @@
|
||||
/**
|
||||
* 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 };
|
||||
Reference in New Issue
Block a user