feat: Replace DB component tools with filesystem-based user-components/

Components are now .vue files in user-components/<folder>/ parsed at runtime.
Replaces 6 DB MCP tools with 2 (list_fs_components, load_fs_component).
Adds vue-parser, fs-components API, and file watcher for live reload.
This commit is contained in:
2026-02-18 10:24:05 -06:00
parent e9451b2a47
commit d27da30494
13 changed files with 597 additions and 19 deletions

View File

@@ -26,6 +26,7 @@ import {
createGitHandlers,
createTorchHandlers,
createSnapshotHandlers,
createFsComponentHandlers,
type ToolConfig
} from './tools/handlers'
import { setRouter } from './tools/handlers/globalHandlers'
@@ -134,7 +135,8 @@ function getToolConfigs(): Map<string, ToolConfig> {
...createResponseHandlers(),
...createGitHandlers(),
...createTorchHandlers(),
...createSnapshotHandlers()
...createSnapshotHandlers(),
...createFsComponentHandlers()
]
for (const config of allHandlers) {
@@ -148,7 +150,7 @@ function getToolConfigs(): Map<string, ToolConfig> {
const categoryTools: Record<ToolCategory, string[]> = {
global: ['get_current_page', 'navigate_to', 'list_available_tools', 'activate_tool', 'deactivate_tool', 'pin_tool', 'page_refresh'],
canvas: ['render_html', 'render_vue_component', 'move_window', 'resize_window', 'close_window', 'list_windows', 'inspect_window', 'get_canvas', 'edit_canvas', 'canvas_css', 'canvas_js', 'get_canvas_css', 'save_canvas_snapshot', 'load_canvas_snapshot', 'list_canvas_snapshots', 'delete_canvas_snapshot'],
component: ['save_vue_component', 'load_vue_component', 'list_vue_components', 'delete_vue_component', 'read_component', 'edit_component'],
component: ['list_fs_components', 'load_fs_component'],
theme: ['get_design_tokens', 'get_active_theme', 'set_theme_variable', 'save_theme', 'list_themes', 'switch_theme', 'reset_theme'],
database: ['list_tables', 'get_table_schema', 'get_table_data', 'get_database_stats', 'execute_query'],
source: ['get_repo_info', 'list_repo_files', 'read_repo_file', 'search_repo_code'],

View File

@@ -3,7 +3,6 @@ import { useCanvasStore } from '../../../stores/canvas'
import { useWindowsStore } from '../../../stores/windows'
import {
renderInlineComponent,
componentsApi,
type VueComponentDefinition
} from '../../dynamicComponents'
@@ -24,7 +23,7 @@ export function clearScriptLog(): void {
// WINDOW DEFINITIONS — tracks component definition per rendered window
// ============================================
export interface WindowDefinitionEntry {
source: 'inline' | 'db'
source: 'inline' | 'db' | 'fs'
componentId?: string
definition: VueComponentDefinition
componentProps: Record<string, any>
@@ -196,13 +195,6 @@ export function createCanvasHandlers(): ToolConfig[] {
emitComponentRendered(args)
// Auto-save component to database
try {
await componentsApi.save(definition)
} catch (e) {
console.warn('[auto-save] Failed to save component:', e)
}
const canvasStore = useCanvasStore()
canvasStore.addToHistory({ tool: 'render_vue_component', args, timestamp: Date.now() })
return `Componente Vue "${args.name}" renderizado en ventana flotante`

View File

@@ -0,0 +1,113 @@
import type { ToolConfig } from './index'
import { useCanvasStore } from '../../../stores/canvas'
import {
renderInlineComponent,
type VueComponentDefinition
} from '../../dynamicComponents'
import { getWindowDefinitions } from './canvasHandlers'
const API_URL = ''
export const fsComponentsApi = {
async list(): Promise<VueComponentDefinition[]> {
const res = await fetch(`${API_URL}/api/fs-components`)
if (!res.ok) throw new Error(`Failed to list fs-components: ${res.statusText}`)
return res.json()
},
async getByFolder(folder: string): Promise<VueComponentDefinition> {
const res = await fetch(`${API_URL}/api/fs-components/${encodeURIComponent(folder)}`)
if (!res.ok) throw new Error(`Component "${folder}" not found`)
return res.json()
}
}
function getCanvasContainer() {
return document.getElementById('canvas-content')
}
function removePlaceholder(container: HTMLElement) {
const placeholder = container.querySelector('.canvas-placeholder')
if (placeholder) {
placeholder.remove()
window.dispatchEvent(new CustomEvent('canvas-content-rendered'))
}
}
export function createFsComponentHandlers(): ToolConfig[] {
return [
{
name: 'list_fs_components',
description: 'Lista componentes Vue del filesystem (user-components/)',
category: 'component',
schema: {
type: 'object',
properties: {},
required: []
},
handler: async () => {
try {
const components = await fsComponentsApi.list()
if (components.length === 0) {
return 'No hay componentes en user-components/. Crea una carpeta con un archivo .vue para empezar.'
}
const list = components.map(c => {
const tags = c.tags?.length ? ` [${c.tags.join(', ')}]` : ''
return `- ${c.folder}: ${c.name}${tags}`
}).join('\n')
return `Componentes en filesystem (${components.length}):\n${list}`
} catch (e: any) {
return `Error: ${e.message}`
}
}
},
{
name: 'load_fs_component',
description: 'Carga y renderiza un componente desde user-components/',
category: 'component',
schema: {
type: 'object',
properties: {
folder: { type: 'string', description: 'Nombre de la carpeta del componente' },
componentProps: { type: 'object', description: 'Props para el componente' },
mode: { type: 'string', enum: ['replace', 'append'], description: 'Modo de renderizado' },
x: { type: 'number', description: 'Posicion X inicial' },
y: { type: 'number', description: 'Posicion Y inicial' },
width: { type: 'number', description: 'Ancho inicial' },
height: { type: 'number', description: 'Alto inicial' }
},
required: ['folder']
},
handler: async (args: { folder: string; componentProps?: Record<string, any>; mode?: string; x?: number; y?: number; width?: number; height?: number }) => {
try {
const definition = await fsComponentsApi.getByFolder(args.folder)
const container = getCanvasContainer()
if (!container) return 'Error: canvas no encontrado'
removePlaceholder(container)
const isAppend = args.mode === 'append'
const layout = { x: args.x, y: args.y, width: args.width, height: args.height }
const result = renderInlineComponent(definition, container, args.componentProps || {}, isAppend, layout)
// Track definition for snapshot capture
getWindowDefinitions().set(definition.id, {
source: 'fs',
componentId: args.folder,
definition,
componentProps: args.componentProps || {}
})
;(window as any).__vueComponentUnmount = result.unmount
const canvasStore = useCanvasStore()
canvasStore.addToHistory({ tool: 'load_fs_component', args, timestamp: Date.now() })
return `Componente "${definition.name}" cargado desde user-components/${args.folder}/`
} catch (e: any) {
return `Error: ${e.message}`
}
}
}
]
}

View File

@@ -17,6 +17,7 @@ export type { ResponseControls } from './responseHandlers'
export { createGitHandlers } from './gitHandlers'
export { createTorchHandlers } from './torchHandlers'
export { createSnapshotHandlers } from './snapshotHandlers'
export { createFsComponentHandlers } from './fsComponentHandlers'
export type ToolHandler = (args: any) => string | Promise<string>

View File

@@ -35,13 +35,9 @@ export const ALL_TOOL_METAS: ToolMeta[] = [
{ name: 'list_canvas_snapshots', description: 'Lista snapshots del canvas guardados', category: 'canvas' },
{ name: 'delete_canvas_snapshot', description: 'Elimina un snapshot del canvas', category: 'canvas' },
// Component tools
{ name: 'save_vue_component', description: 'Guarda un componente Vue en la base de datos', category: 'component' },
{ name: 'load_vue_component', description: 'Carga y renderiza un componente guardado', category: 'component' },
{ name: 'list_vue_components', description: 'Lista componentes guardados', category: 'component' },
{ name: 'delete_vue_component', description: 'Elimina un componente', category: 'component' },
{ name: 'read_component', description: 'Lee campos especificos de un componente guardado', category: 'component' },
{ name: 'edit_component', description: 'Edita un campo de componente con reemplazo de strings (requiere lectura previa)', category: 'component' },
// Component tools (filesystem-based)
{ name: 'list_fs_components', description: 'Lista componentes Vue del filesystem', category: 'component' },
{ name: 'load_fs_component', description: 'Carga y renderiza un componente desde user-components/', category: 'component' },
// Theme tools
{ name: 'get_design_tokens', description: 'Obtiene los design tokens del tema activo', category: 'theme' },