diff --git a/.claude-ejecutor/.gitignore b/.claude-ejecutor/.gitignore new file mode 100644 index 0000000..511f827 --- /dev/null +++ b/.claude-ejecutor/.gitignore @@ -0,0 +1,21 @@ +# Credenciales (NUNCA versionar) +.credentials.json + +# Estado interno de Claude (regenerable) +.claude.json +.claude.json.backup* + +# Conversaciones y logs (muy grandes, privados) +projects/**/*.jsonl +history.jsonl +debug/ + +# Cache y temporales +cache/ +tmp/ +session-env/ +shell-snapshots/ +todos/ + +# Repos externos clonados +plugins/marketplaces/*/ diff --git a/.claude-ejecutor/CLAUDE.md b/.claude-ejecutor/CLAUDE.md new file mode 100644 index 0000000..51b2251 --- /dev/null +++ b/.claude-ejecutor/CLAUDE.md @@ -0,0 +1,150 @@ +# Ejecutor - Instrucciones + +## Rol +Eres un agente especializado en manipular la interfaz de Agent UI exclusivamente a través de herramientas MCP. + +## Reglas Estrictas + +1. **SIEMPRE** responde usando `bubbleResponse` - nunca respondas con texto plano +2. **SOLO** puedes usar herramientas MCP de `agent-ui` +3. **NUNCA** intentes usar terminal, bash, curl, o cualquier comando del sistema +4. **NUNCA** intentes leer, escribir o editar archivos +5. Tu único propósito es manipular la interfaz gráfica + +--- + +## Sistema de Canvas + +El canvas es un área donde puedes renderizar componentes Vue. Cada componente se muestra en una **ventana flotante estilo Liquid Glass** con: +- **Drag** - Arrastrar desde el header +- **Resize** - Desde bordes y esquinas +- **Close** - Botón rojo en el header + +### render_vue_component + +Renderiza un componente Vue 3 en una ventana flotante. + +```js +render_vue_component({ + // Requeridos + id: "mi-componente", // ID único + name: "Mi Componente", // Título de la ventana + template: "
HTML con sintaxis Vue
", + + // Opcionales + setup: "const count = ref(0); return { count };", + style: ".mi-clase { color: white; }", + imports: ["ref", "reactive", "computed"], + componentProps: { valor: 123 }, + + // Posición y tamaño (opcionales) + x: 100, // Posición X (default: auto-cascada) + y: 100, // Posición Y (default: auto-cascada) + width: 300, // Ancho (default: 400) + height: 200, // Alto (default: 300) + + // Modo + mode: "append" // "replace" limpia canvas, "append" agrega +}) +``` + +### Ejemplos de Componentes + +**Contador interactivo:** +```js +render_vue_component({ + id: "contador", + name: "Contador", + template: ` +
+

{{ count }}

+ +
+ `, + imports: ["ref"], + setup: "const count = ref(0); return { count };", + x: 100, y: 100, width: 200, height: 150 +}) +``` + +**Lista dinámica:** +```js +render_vue_component({ + id: "lista", + name: "Lista", + template: ` +
+ + +
+ `, + imports: ["ref"], + setup: ` + const items = ref(['Item 1', 'Item 2']); + const nuevo = ref(''); + const agregar = () => { + if (nuevo.value) { + items.value.push(nuevo.value); + nuevo.value = ''; + } + }; + return { items, nuevo, agregar }; + ` +}) +``` + +### Vue Composition API + +Imports disponibles: +- `ref` - Valores reactivos +- `reactive` - Objetos reactivos +- `computed` - Valores computados +- `watch` - Observar cambios +- `onMounted` - Hook de montaje +- `onUnmounted` - Hook de desmontaje + +### Helpers Globales + +En el setup tienes acceso a: +- `$emit(event, ...args)` - Emitir eventos +- `$on(event, callback)` - Escuchar eventos +- `$fetch(url)` - Hacer requests HTTP +- `$theme.getVariable(name)` - Obtener variable CSS +- `$theme.setVariable(name, value)` - Cambiar variable CSS + +--- + +## Otras Herramientas + +| Herramienta | Uso | +|-------------|-----| +| `bubbleResponse` | Responder al usuario (OBLIGATORIO) | +| `render_html` | Renderizar HTML plano | +| `navigate_to` | Cambiar de página | +| `page_refresh` | Refrescar página | +| `get_design_tokens` | Obtener tokens del tema | +| `set_theme_variable` | Cambiar variable del tema | +| `switch_theme` | Cambiar tema activo | +| `list_available_tools` | Ver herramientas disponibles | +| `activate_tool` | Activar una herramienta | +| `pin_tool` | Fijar herramienta | + +--- + +## Formato de Respuesta + +**SIEMPRE** usa bubbleResponse para comunicarte: +``` +bubbleResponse({ message: "Tu mensaje aquí" }) +``` + +Nunca escribas texto directamente - todo debe ir a través de bubbleResponse. + +--- + +## Preferencias del Usuario + +- **Detalles sutiles**: Agregar pequeños toques creativos que mejoren el ambiente SIN estorbar el trabajo normal. No widgets completos ni elementos que ocupen espacio - solo detalles casi imperceptibles que den personalidad (ej: un emoji contextual, un color que cambie según la hora, un micro-detalle temático). +- La clave es que el detalle **no interrumpa** ni **ocupe espacio útil**. diff --git a/.claude-ejecutor/plugins/known_marketplaces.json b/.claude-ejecutor/plugins/known_marketplaces.json new file mode 100644 index 0000000..0c1832f --- /dev/null +++ b/.claude-ejecutor/plugins/known_marketplaces.json @@ -0,0 +1,10 @@ +{ + "claude-plugins-official": { + "source": { + "source": "github", + "repo": "anthropics/claude-plugins-official" + }, + "installLocation": "C:\\Users\\jodar\\agent-ui\\.claude-ejecutor\\plugins\\marketplaces\\claude-plugins-official", + "lastUpdated": "2026-02-15T01:40:47.405Z" + } +} \ No newline at end of file diff --git a/.claude-ejecutor/settings.json b/.claude-ejecutor/settings.json new file mode 100644 index 0000000..c0f4984 --- /dev/null +++ b/.claude-ejecutor/settings.json @@ -0,0 +1,22 @@ +{ + "env": { + "DISABLE_TELEMETRY": "1" + }, + "permissions": { + "allow": [ + "mcp__agent-ui*" + ], + "deny": [ + "Bash", + "Edit", + "Write", + "Read", + "Glob", + "Grep", + "WebFetch", + "WebSearch", + "Task", + "NotebookEdit" + ] + } +} diff --git a/.claude-isolated/.gitignore b/.claude-isolated/.gitignore new file mode 100644 index 0000000..ca50cc4 --- /dev/null +++ b/.claude-isolated/.gitignore @@ -0,0 +1,10 @@ +# No versionar credenciales +.credentials.json +*.backup + +# Estado de sesión (regenerable) +.claude.json +.claude.json.backup + +# Archivos temporales +tmp/ diff --git a/.claude-isolated/settings.json b/.claude-isolated/settings.json new file mode 100644 index 0000000..c9c0a74 --- /dev/null +++ b/.claude-isolated/settings.json @@ -0,0 +1,9 @@ +{ + "permissions": { + "allow": [], + "deny": [] + }, + "env": { + "DISABLE_TELEMETRY": "1" + } +} diff --git a/.claude-nucleo000/.gitignore b/.claude-nucleo000/.gitignore new file mode 100644 index 0000000..511f827 --- /dev/null +++ b/.claude-nucleo000/.gitignore @@ -0,0 +1,21 @@ +# Credenciales (NUNCA versionar) +.credentials.json + +# Estado interno de Claude (regenerable) +.claude.json +.claude.json.backup* + +# Conversaciones y logs (muy grandes, privados) +projects/**/*.jsonl +history.jsonl +debug/ + +# Cache y temporales +cache/ +tmp/ +session-env/ +shell-snapshots/ +todos/ + +# Repos externos clonados +plugins/marketplaces/*/ diff --git a/.claude-nucleo000/settings.json b/.claude-nucleo000/settings.json new file mode 100644 index 0000000..c9c0a74 --- /dev/null +++ b/.claude-nucleo000/settings.json @@ -0,0 +1,9 @@ +{ + "permissions": { + "allow": [], + "deny": [] + }, + "env": { + "DISABLE_TELEMETRY": "1" + } +} diff --git a/frontend/src/components/Canvas.vue b/frontend/src/components/Canvas.vue index 793f7e4..c33c4ad 100644 --- a/frontend/src/components/Canvas.vue +++ b/frontend/src/components/Canvas.vue @@ -63,13 +63,15 @@ onUnmounted(() => { display: flex; flex-direction: column; background: var(--bg-primary); - overflow: auto; + overflow: hidden; + position: relative; } .canvas-content { flex: 1; - padding: 1.5rem; + position: relative; min-height: 100%; + overflow: hidden; } .canvas-placeholder { diff --git a/frontend/src/components/WindowContainer.vue b/frontend/src/components/WindowContainer.vue new file mode 100644 index 0000000..b6b9915 --- /dev/null +++ b/frontend/src/components/WindowContainer.vue @@ -0,0 +1,407 @@ + + + + + diff --git a/frontend/src/services/dynamicComponents.ts b/frontend/src/services/dynamicComponents.ts index 68a3214..fb87a74 100644 --- a/frontend/src/services/dynamicComponents.ts +++ b/frontend/src/services/dynamicComponents.ts @@ -19,6 +19,8 @@ import { import { setActivePinia, type Pinia } from 'pinia' import { useCanvasStore } from '../stores/canvas' import { useThemeStore } from '../stores/theme' +import { useWindowsStore } from '../stores/windows' +import WindowContainer from '../components/WindowContainer.vue' // Uses relative URLs - works with Vite proxy in dev and Traefik in production const API_URL = '' @@ -183,6 +185,12 @@ function getThemeStore() { return useThemeStore() } +function getWindowsStore() { + const globalPinia = (window as any).__pinia as Pinia | undefined + if (globalPinia) setActivePinia(globalPinia) + return useWindowsStore() +} + const dynamicHelpers = { $emit: (event: string, ...args: any[]) => eventBus.emit(event, ...args), $on: (event: string, cb: EventCallback) => eventBus.on(event, cb), @@ -271,25 +279,47 @@ export function buildComponent(definition: VueComponentDefinition): Component { const renderedContainers: Map = new Map() +export interface LayoutOptions { + x?: number + y?: number + width?: number + height?: number +} + export function renderInlineComponent( definition: VueComponentDefinition, target: HTMLElement, props: Record = {}, - append: boolean = false + append: boolean = false, + layout?: LayoutOptions ): { unmount: () => void } { const scopeId = generateScopeId(definition.id) + const windowsStore = getWindowsStore() + + // Registrar ventana en el store + const windowState = windowsStore.register(definition.id, { + title: definition.name, + x: layout?.x, + y: layout?.y, + width: layout?.width ?? 400, + height: layout?.height ?? 300 + }) const container = document.createElement('div') container.id = `inline-${definition.id}` container.className = 'dynamic-component-wrapper' container.setAttribute(`data-${scopeId}`, '') + // Siempre hacer append para ventanas flotantes if (append) { target.appendChild(container) } else { + // En modo replace, limpiar contenedor anterior si existe const oldContainer = renderedContainers.get(definition.id) - if (oldContainer) render(null, oldContainer) - target.innerHTML = '' + if (oldContainer) { + render(null, oldContainer) + oldContainer.remove() + } target.appendChild(container) } @@ -300,25 +330,52 @@ export function renderInlineComponent( const isAsync = definition.setup ? /\bawait\b/.test(definition.setup) : false const component = buildComponent(definition) - const vnode = isAsync + // Función unmount que se llamará al cerrar la ventana + const unmount = () => { + render(null, container) + renderedContainers.delete(definition.id) + windowsStore.remove(definition.id) + container.remove() + document.getElementById(`style-${definition.id}`)?.remove() + } + + // Crear el componente interno + const innerComponent = isAsync ? createVNode(Suspense, null, { default: () => createVNode(component, props), fallback: () => createVNode('div', { class: 'loading' }, 'Loading...') }) : createVNode(component, props) - const mainApp = (window as any).__vueApp as App | undefined - if (mainApp?._context) vnode.appContext = mainApp._context + // Envolver en WindowContainer + const windowVNode = createVNode( + WindowContainer, + { + id: definition.id, + title: definition.name, + x: windowState.x, + y: windowState.y, + width: windowState.width, + height: windowState.height, + onClose: unmount, + onMove: (pos: { x: number; y: number }) => { + windowsStore.updatePosition(definition.id, pos.x, pos.y) + }, + onResize: (size: { width: number; height: number }) => { + windowsStore.updateSize(definition.id, size.width, size.height) + }, + onFocus: () => { + windowsStore.bringToFront(definition.id) + } + }, + { default: () => innerComponent } + ) - render(vnode, container) + const mainApp = (window as any).__vueApp as App | undefined + if (mainApp?._context) windowVNode.appContext = mainApp._context + + render(windowVNode, container) renderedContainers.set(definition.id, container) - return { - unmount: () => { - render(null, container) - renderedContainers.delete(definition.id) - container.remove() - document.getElementById(`style-${definition.id}`)?.remove() - } - } + return { unmount } } diff --git a/frontend/src/services/toolRegistry.ts b/frontend/src/services/toolRegistry.ts index 1f1514c..bb92a78 100644 --- a/frontend/src/services/toolRegistry.ts +++ b/frontend/src/services/toolRegistry.ts @@ -140,7 +140,7 @@ function getToolConfigs(): Map { // Category to tool names mapping const categoryTools: Record = { global: ['get_current_page', 'navigate_to', 'list_available_tools', 'activate_tool', 'deactivate_tool', 'pin_tool', 'page_refresh'], - canvas: ['render_html', 'render_vue_component'], + canvas: ['render_html', 'render_vue_component', 'move_window', 'resize_window', 'close_window', 'list_windows'], component: ['save_vue_component', 'load_vue_component', 'list_vue_components', 'delete_vue_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'], diff --git a/frontend/src/services/tools/handlers/canvasHandlers.ts b/frontend/src/services/tools/handlers/canvasHandlers.ts index 287fc32..448ef6e 100644 --- a/frontend/src/services/tools/handlers/canvasHandlers.ts +++ b/frontend/src/services/tools/handlers/canvasHandlers.ts @@ -1,5 +1,6 @@ import type { ToolConfig } from './index' import { useCanvasStore } from '../../../stores/canvas' +import { useWindowsStore } from '../../../stores/windows' import { renderInlineComponent, type VueComponentDefinition @@ -82,7 +83,7 @@ export function createCanvasHandlers(): ToolConfig[] { }, { name: 'render_vue_component', - description: 'Renderiza un componente Vue 3 completo con ref, reactive, computed, etc.', + description: 'Renderiza un componente Vue 3 en una ventana flotante con drag, resize y close.', category: 'canvas', schema: { type: 'object', @@ -95,7 +96,11 @@ export function createCanvasHandlers(): ToolConfig[] { props: { type: 'array', items: { type: 'string' }, description: 'Lista de props' }, imports: { type: 'array', items: { type: 'string' }, description: 'Funciones de Vue a importar' }, componentProps: { type: 'object', description: 'Valores para las props' }, - mode: { type: 'string', enum: ['replace', 'append'], description: 'Modo de renderizado' } + mode: { type: 'string', enum: ['replace', 'append'], description: 'Modo de renderizado' }, + x: { type: 'number', description: 'Posicion X inicial de la ventana' }, + y: { type: 'number', description: 'Posicion Y inicial de la ventana' }, + width: { type: 'number', description: 'Ancho inicial de la ventana' }, + height: { type: 'number', description: 'Alto inicial de la ventana' } }, required: ['id', 'name', 'template'] }, @@ -109,6 +114,10 @@ export function createCanvasHandlers(): ToolConfig[] { imports?: string[] componentProps?: Record mode?: string + x?: number + y?: number + width?: number + height?: number }) => { const container = getCanvasContainer() if (!container) return 'Error: canvas no encontrado' @@ -126,7 +135,14 @@ export function createCanvasHandlers(): ToolConfig[] { } const isAppend = args.mode === 'append' - const result = renderInlineComponent(definition, container, args.componentProps || {}, isAppend) + const layout = { + x: args.x, + y: args.y, + width: args.width, + height: args.height + } + + const result = renderInlineComponent(definition, container, args.componentProps || {}, isAppend, layout) ;(window as any).__vueComponentUnmount = result.unmount @@ -134,7 +150,128 @@ export function createCanvasHandlers(): ToolConfig[] { const canvasStore = useCanvasStore() canvasStore.addToHistory({ tool: 'render_vue_component', args, timestamp: Date.now() }) - return `Componente Vue "${args.name}" renderizado` + return `Componente Vue "${args.name}" renderizado en ventana flotante` + } + }, + { + name: 'move_window', + description: 'Mueve una ventana a una posicion especifica en el canvas.', + category: 'canvas', + schema: { + type: 'object', + properties: { + id: { type: 'string', description: 'ID de la ventana a mover' }, + x: { type: 'number', description: 'Nueva posicion X' }, + y: { type: 'number', description: 'Nueva posicion Y' } + }, + required: ['id', 'x', 'y'] + }, + handler: (args: { id: string; x: number; y: number }) => { + const windowsStore = useWindowsStore() + + if (!windowsStore.has(args.id)) { + return `Error: Ventana "${args.id}" no encontrada` + } + + windowsStore.updatePosition(args.id, args.x, args.y) + + // Actualizar el DOM directamente + const windowEl = document.querySelector(`[data-window-id="${args.id}"]`) as HTMLElement + if (windowEl) { + windowEl.style.left = `${args.x}px` + windowEl.style.top = `${args.y}px` + } + + return `Ventana "${args.id}" movida a (${args.x}, ${args.y})` + } + }, + { + name: 'resize_window', + description: 'Cambia el tamano de una ventana en el canvas.', + category: 'canvas', + schema: { + type: 'object', + properties: { + id: { type: 'string', description: 'ID de la ventana a redimensionar' }, + width: { type: 'number', description: 'Nuevo ancho' }, + height: { type: 'number', description: 'Nuevo alto' } + }, + required: ['id', 'width', 'height'] + }, + handler: (args: { id: string; width: number; height: number }) => { + const windowsStore = useWindowsStore() + + if (!windowsStore.has(args.id)) { + return `Error: Ventana "${args.id}" no encontrada` + } + + windowsStore.updateSize(args.id, args.width, args.height) + + // Actualizar el DOM directamente + const windowEl = document.querySelector(`[data-window-id="${args.id}"]`) as HTMLElement + if (windowEl) { + windowEl.style.width = `${args.width}px` + windowEl.style.height = `${args.height}px` + } + + return `Ventana "${args.id}" redimensionada a ${args.width}x${args.height}` + } + }, + { + name: 'close_window', + description: 'Cierra una ventana del canvas.', + category: 'canvas', + schema: { + type: 'object', + properties: { + id: { type: 'string', description: 'ID de la ventana a cerrar' } + }, + required: ['id'] + }, + handler: (args: { id: string }) => { + const windowsStore = useWindowsStore() + + if (!windowsStore.has(args.id)) { + return `Error: Ventana "${args.id}" no encontrada` + } + + // Buscar y eliminar el contenedor + const container = document.getElementById(`inline-${args.id}`) + if (container) { + container.remove() + } + + // Eliminar estilos + document.getElementById(`style-${args.id}`)?.remove() + + // Eliminar del store + windowsStore.remove(args.id) + + return `Ventana "${args.id}" cerrada` + } + }, + { + name: 'list_windows', + description: 'Lista todas las ventanas abiertas en el canvas.', + category: 'canvas', + schema: { + type: 'object', + properties: {}, + required: [] + }, + handler: () => { + const windowsStore = useWindowsStore() + const windows = windowsStore.windowsList + + if (windows.length === 0) { + return 'No hay ventanas abiertas' + } + + const list = windows.map(w => + `- ${w.id}: "${w.title}" en (${w.x}, ${w.y}) - ${w.width}x${w.height}` + ).join('\n') + + return `Ventanas abiertas (${windows.length}):\n${list}` } } ] diff --git a/frontend/src/services/tools/toolDefinitions.ts b/frontend/src/services/tools/toolDefinitions.ts index c981310..d0ddab1 100644 --- a/frontend/src/services/tools/toolDefinitions.ts +++ b/frontend/src/services/tools/toolDefinitions.ts @@ -20,6 +20,10 @@ export const ALL_TOOL_METAS: ToolMeta[] = [ // Canvas tools { name: 'render_html', description: 'Renderiza HTML en el canvas', category: 'canvas' }, { name: 'render_vue_component', description: 'Renderiza un componente Vue 3 completo', category: 'canvas' }, + { name: 'move_window', description: 'Mueve una ventana a una posicion especifica', category: 'canvas' }, + { name: 'resize_window', description: 'Cambia el tamano de una ventana', category: 'canvas' }, + { name: 'close_window', description: 'Cierra una ventana del canvas', category: 'canvas' }, + { name: 'list_windows', description: 'Lista todas las ventanas abiertas', category: 'canvas' }, // Component tools { name: 'save_vue_component', description: 'Guarda un componente Vue en la base de datos', category: 'component' }, @@ -110,5 +114,6 @@ export const CATEGORY_INFO: Record { + const windows = ref>(new Map()) + const maxZIndex = ref(100) + const windowOffset = ref(0) + + // Getters + const windowsList = computed(() => Array.from(windows.value.values())) + const windowCount = computed(() => windows.value.size) + + // Obtener siguiente posición para ventana nueva (cascada) + function getNextPosition() { + const offset = windowOffset.value * 30 + windowOffset.value = (windowOffset.value + 1) % 10 + return { x: 50 + offset, y: 50 + offset } + } + + // Registrar nueva ventana + function register(id: string, state: Partial = {}) { + const pos = state.x !== undefined && state.y !== undefined + ? { x: state.x, y: state.y } + : getNextPosition() + + windows.value.set(id, { + id, + title: state.title ?? id, + x: pos.x, + y: pos.y, + width: state.width ?? 400, + height: state.height ?? 300, + zIndex: ++maxZIndex.value + }) + + return windows.value.get(id)! + } + + // Traer ventana al frente + function bringToFront(id: string) { + const win = windows.value.get(id) + if (win) { + win.zIndex = ++maxZIndex.value + } + } + + // Actualizar posición + function updatePosition(id: string, x: number, y: number) { + const win = windows.value.get(id) + if (win) { + win.x = x + win.y = y + } + } + + // Actualizar tamaño + function updateSize(id: string, width: number, height: number) { + const win = windows.value.get(id) + if (win) { + win.width = width + win.height = height + } + } + + // Actualizar título + function updateTitle(id: string, title: string) { + const win = windows.value.get(id) + if (win) { + win.title = title + } + } + + // Eliminar ventana + function remove(id: string) { + windows.value.delete(id) + } + + // Obtener ventana por ID + function get(id: string) { + return windows.value.get(id) + } + + // Verificar si existe + function has(id: string) { + return windows.value.has(id) + } + + // Limpiar todas las ventanas + function clear() { + windows.value.clear() + windowOffset.value = 0 + } + + return { + // State + windows, + maxZIndex, + + // Getters + windowsList, + windowCount, + + // Actions + register, + bringToFront, + updatePosition, + updateSize, + updateTitle, + remove, + get, + has, + clear, + getNextPosition + } +})