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:
@@ -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'],
|
||||
|
||||
@@ -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`
|
||||
|
||||
113
frontend/src/services/tools/handlers/fsComponentHandlers.ts
Normal file
113
frontend/src/services/tools/handlers/fsComponentHandlers.ts
Normal 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}`
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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' },
|
||||
|
||||
Reference in New Issue
Block a user