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 }}
+ +1
+
+ `,
+ 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
+ }
+})