feat: Add DOM inspection and manipulation tools for canvas
- inspect_window: Inspect HTML content of a window with selector filter - get_canvas: Read canvas elements using CSS selectors (like Read tool) - edit_canvas: Edit canvas elements with old/new value replacement (like Edit tool) - canvas_css: Inject/update/remove CSS blocks with ID tracking - canvas_js: Execute JavaScript in canvas context with helper functions - get_canvas_css: List or get specific injected CSS blocks
This commit is contained in:
@@ -140,7 +140,7 @@ function getToolConfigs(): Map<string, ToolConfig> {
|
|||||||
// Category to tool names mapping
|
// Category to tool names mapping
|
||||||
const categoryTools: Record<ToolCategory, string[]> = {
|
const categoryTools: Record<ToolCategory, string[]> = {
|
||||||
global: ['get_current_page', 'navigate_to', 'list_available_tools', 'activate_tool', 'deactivate_tool', 'pin_tool', 'page_refresh'],
|
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'],
|
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'],
|
||||||
component: ['save_vue_component', 'load_vue_component', 'list_vue_components', 'delete_vue_component'],
|
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'],
|
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'],
|
database: ['list_tables', 'get_table_schema', 'get_table_data', 'get_database_stats', 'execute_query'],
|
||||||
|
|||||||
@@ -273,6 +273,314 @@ export function createCanvasHandlers(): ToolConfig[] {
|
|||||||
|
|
||||||
return `Ventanas abiertas (${windows.length}):\n${list}`
|
return `Ventanas abiertas (${windows.length}):\n${list}`
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'inspect_window',
|
||||||
|
description: 'Inspecciona el contenido HTML de una ventana. Usa selector para filtrar elementos internos.',
|
||||||
|
category: 'canvas',
|
||||||
|
schema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
id: { type: 'string', description: 'ID de la ventana a inspeccionar' },
|
||||||
|
selector: { type: 'string', description: 'Selector CSS para filtrar elementos dentro de la ventana (opcional)' },
|
||||||
|
attribute: {
|
||||||
|
type: 'string',
|
||||||
|
enum: ['innerHTML', 'outerHTML', 'textContent', 'attributes'],
|
||||||
|
description: 'Que retornar: innerHTML (default), outerHTML, textContent, attributes'
|
||||||
|
},
|
||||||
|
limit: { type: 'number', description: 'Limitar caracteres de salida (default: 2000)' }
|
||||||
|
},
|
||||||
|
required: ['id']
|
||||||
|
},
|
||||||
|
handler: (args: { id: string; selector?: string; attribute?: string; limit?: number }) => {
|
||||||
|
const windowEl = document.querySelector(`[data-window-id="${args.id}"]`)
|
||||||
|
if (!windowEl) {
|
||||||
|
return `Error: Ventana "${args.id}" no encontrada`
|
||||||
|
}
|
||||||
|
|
||||||
|
const content = windowEl.querySelector('.window-content')
|
||||||
|
if (!content) {
|
||||||
|
return `Error: Contenido de ventana "${args.id}" no encontrado`
|
||||||
|
}
|
||||||
|
|
||||||
|
let target: Element | null = content
|
||||||
|
if (args.selector) {
|
||||||
|
target = content.querySelector(args.selector)
|
||||||
|
if (!target) {
|
||||||
|
return `Error: Selector "${args.selector}" no encontrado en ventana "${args.id}"`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const attr = args.attribute || 'innerHTML'
|
||||||
|
const limit = args.limit || 2000
|
||||||
|
let result: string
|
||||||
|
|
||||||
|
if (attr === 'attributes') {
|
||||||
|
const attrs: Record<string, string> = {}
|
||||||
|
for (const a of Array.from(target.attributes)) {
|
||||||
|
attrs[a.name] = a.value
|
||||||
|
}
|
||||||
|
result = JSON.stringify(attrs, null, 2)
|
||||||
|
} else {
|
||||||
|
result = (target as any)[attr] || ''
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.length > limit) {
|
||||||
|
result = result.slice(0, limit) + `\n... (truncado, ${result.length - limit} chars mas)`
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'get_canvas',
|
||||||
|
description: 'Lee elementos del canvas usando selectores CSS. Similar a Read pero para el DOM.',
|
||||||
|
category: 'canvas',
|
||||||
|
schema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
selector: { type: 'string', description: 'Selector CSS para encontrar elementos' },
|
||||||
|
attribute: {
|
||||||
|
type: 'string',
|
||||||
|
enum: ['innerHTML', 'outerHTML', 'textContent', 'attributes', 'style', 'classList'],
|
||||||
|
description: 'Que retornar (default: innerHTML)'
|
||||||
|
},
|
||||||
|
all: { type: 'boolean', description: 'Si true, retorna todos los matches (default: false, solo el primero)' },
|
||||||
|
limit: { type: 'number', description: 'Limitar caracteres de salida (default: 3000)' }
|
||||||
|
},
|
||||||
|
required: ['selector']
|
||||||
|
},
|
||||||
|
handler: (args: { selector: string; attribute?: string; all?: boolean; limit?: number }) => {
|
||||||
|
const container = getCanvasContainer()
|
||||||
|
if (!container) return 'Error: canvas no encontrado'
|
||||||
|
|
||||||
|
const attr = args.attribute || 'innerHTML'
|
||||||
|
const limit = args.limit || 3000
|
||||||
|
|
||||||
|
const extractValue = (el: Element): string => {
|
||||||
|
if (attr === 'attributes') {
|
||||||
|
const attrs: Record<string, string> = {}
|
||||||
|
for (const a of Array.from(el.attributes)) {
|
||||||
|
attrs[a.name] = a.value
|
||||||
|
}
|
||||||
|
return JSON.stringify(attrs)
|
||||||
|
} else if (attr === 'style') {
|
||||||
|
return (el as HTMLElement).style.cssText
|
||||||
|
} else if (attr === 'classList') {
|
||||||
|
return Array.from(el.classList).join(' ')
|
||||||
|
} else {
|
||||||
|
return (el as any)[attr] || ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let result: string
|
||||||
|
|
||||||
|
if (args.all) {
|
||||||
|
const elements = container.querySelectorAll(args.selector)
|
||||||
|
if (elements.length === 0) {
|
||||||
|
return `No se encontraron elementos con selector "${args.selector}"`
|
||||||
|
}
|
||||||
|
const values = Array.from(elements).map((el, i) => `[${i}] ${extractValue(el)}`)
|
||||||
|
result = `Encontrados ${elements.length} elementos:\n${values.join('\n---\n')}`
|
||||||
|
} else {
|
||||||
|
const el = container.querySelector(args.selector)
|
||||||
|
if (!el) {
|
||||||
|
return `No se encontro elemento con selector "${args.selector}"`
|
||||||
|
}
|
||||||
|
result = extractValue(el)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.length > limit) {
|
||||||
|
result = result.slice(0, limit) + `\n... (truncado, ${result.length - limit} chars mas)`
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'edit_canvas',
|
||||||
|
description: 'Edita elementos del canvas. Similar a Edit pero para el DOM. Reemplaza old_value por new_value.',
|
||||||
|
category: 'canvas',
|
||||||
|
schema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
selector: { type: 'string', description: 'Selector CSS del elemento a editar' },
|
||||||
|
attribute: {
|
||||||
|
type: 'string',
|
||||||
|
enum: ['innerHTML', 'textContent', 'outerHTML', 'style', 'className'],
|
||||||
|
description: 'Que atributo editar (default: innerHTML)'
|
||||||
|
},
|
||||||
|
old_value: { type: 'string', description: 'Valor a buscar y reemplazar (si no se pasa, reemplaza todo)' },
|
||||||
|
new_value: { type: 'string', description: 'Nuevo valor' },
|
||||||
|
all: { type: 'boolean', description: 'Si true, edita todos los matches (default: false)' }
|
||||||
|
},
|
||||||
|
required: ['selector', 'new_value']
|
||||||
|
},
|
||||||
|
handler: (args: { selector: string; attribute?: string; old_value?: string; new_value: string; all?: boolean }) => {
|
||||||
|
const container = getCanvasContainer()
|
||||||
|
if (!container) return 'Error: canvas no encontrado'
|
||||||
|
|
||||||
|
const attr = args.attribute || 'innerHTML'
|
||||||
|
const elements = args.all
|
||||||
|
? Array.from(container.querySelectorAll(args.selector))
|
||||||
|
: [container.querySelector(args.selector)].filter(Boolean) as Element[]
|
||||||
|
|
||||||
|
if (elements.length === 0) {
|
||||||
|
return `Error: No se encontro elemento con selector "${args.selector}"`
|
||||||
|
}
|
||||||
|
|
||||||
|
let editedCount = 0
|
||||||
|
for (const el of elements) {
|
||||||
|
const htmlEl = el as HTMLElement
|
||||||
|
|
||||||
|
if (args.old_value) {
|
||||||
|
// Modo Edit: buscar y reemplazar
|
||||||
|
const current = attr === 'style' ? htmlEl.style.cssText : (htmlEl as any)[attr]
|
||||||
|
if (current && current.includes(args.old_value)) {
|
||||||
|
const newVal = current.replace(args.old_value, args.new_value)
|
||||||
|
if (attr === 'style') {
|
||||||
|
htmlEl.style.cssText = newVal
|
||||||
|
} else {
|
||||||
|
(htmlEl as any)[attr] = newVal
|
||||||
|
}
|
||||||
|
editedCount++
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Modo Write: reemplazar todo
|
||||||
|
if (attr === 'style') {
|
||||||
|
htmlEl.style.cssText = args.new_value
|
||||||
|
} else {
|
||||||
|
(htmlEl as any)[attr] = args.new_value
|
||||||
|
}
|
||||||
|
editedCount++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (editedCount === 0) {
|
||||||
|
return `Error: old_value "${args.old_value}" no encontrado en ningun elemento`
|
||||||
|
}
|
||||||
|
|
||||||
|
return `Editado ${editedCount} elemento(s) con selector "${args.selector}"`
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'canvas_css',
|
||||||
|
description: 'Inyecta CSS en el canvas. Usa id para poder actualizar o remover despues.',
|
||||||
|
category: 'canvas',
|
||||||
|
schema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
css: { type: 'string', description: 'Codigo CSS a inyectar' },
|
||||||
|
id: { type: 'string', description: 'ID unico para el bloque de estilos (default: auto)' },
|
||||||
|
mode: {
|
||||||
|
type: 'string',
|
||||||
|
enum: ['inject', 'replace', 'remove'],
|
||||||
|
description: 'inject (default), replace (reemplaza si existe), remove (elimina por id)'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
required: ['css']
|
||||||
|
},
|
||||||
|
handler: (args: { css: string; id?: string; mode?: string }) => {
|
||||||
|
const mode = args.mode || 'inject'
|
||||||
|
const styleId = args.id ? `canvas-css-${args.id}` : `canvas-css-${Date.now()}`
|
||||||
|
|
||||||
|
if (mode === 'remove') {
|
||||||
|
if (!args.id) return 'Error: Se requiere id para remover'
|
||||||
|
const existing = document.getElementById(`canvas-css-${args.id}`)
|
||||||
|
if (existing) {
|
||||||
|
existing.remove()
|
||||||
|
return `CSS "${args.id}" removido`
|
||||||
|
}
|
||||||
|
return `Error: CSS "${args.id}" no encontrado`
|
||||||
|
}
|
||||||
|
|
||||||
|
let styleEl = args.id ? document.getElementById(`canvas-css-${args.id}`) : null
|
||||||
|
|
||||||
|
if (mode === 'replace' && styleEl) {
|
||||||
|
styleEl.textContent = args.css
|
||||||
|
return `CSS "${args.id}" actualizado`
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!styleEl) {
|
||||||
|
styleEl = document.createElement('style')
|
||||||
|
styleEl.id = styleId
|
||||||
|
document.head.appendChild(styleEl)
|
||||||
|
}
|
||||||
|
|
||||||
|
styleEl.textContent = args.css
|
||||||
|
return `CSS inyectado con id "${styleId.replace('canvas-css-', '')}"`
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'canvas_js',
|
||||||
|
description: 'Ejecuta JavaScript en el contexto del canvas. Tiene acceso a las ventanas y el DOM.',
|
||||||
|
category: 'canvas',
|
||||||
|
schema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
code: { type: 'string', description: 'Codigo JavaScript a ejecutar' },
|
||||||
|
async: { type: 'boolean', description: 'Si true, ejecuta como async y espera resultado' }
|
||||||
|
},
|
||||||
|
required: ['code']
|
||||||
|
},
|
||||||
|
handler: async (args: { code: string; async?: boolean }) => {
|
||||||
|
try {
|
||||||
|
const canvas = getCanvasContainer()
|
||||||
|
const windowsStore = useWindowsStore()
|
||||||
|
|
||||||
|
// Contexto disponible para el codigo
|
||||||
|
const context = {
|
||||||
|
canvas,
|
||||||
|
windows: windowsStore.windowsList,
|
||||||
|
getWindow: (id: string) => document.querySelector(`[data-window-id="${id}"]`),
|
||||||
|
$: (selector: string) => canvas?.querySelector(selector),
|
||||||
|
$$: (selector: string) => canvas?.querySelectorAll(selector)
|
||||||
|
}
|
||||||
|
|
||||||
|
const fn = args.async
|
||||||
|
? new Function('ctx', `return (async () => { with(ctx) { ${args.code} } })()`)
|
||||||
|
: new Function('ctx', `with(ctx) { ${args.code} }`)
|
||||||
|
|
||||||
|
const result = args.async ? await fn(context) : fn(context)
|
||||||
|
|
||||||
|
return result !== undefined ? String(result) : 'Ejecutado'
|
||||||
|
} catch (e: any) {
|
||||||
|
return `Error: ${e.message}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'get_canvas_css',
|
||||||
|
description: 'Obtiene los bloques CSS inyectados en el canvas.',
|
||||||
|
category: 'canvas',
|
||||||
|
schema: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
id: { type: 'string', description: 'ID especifico del bloque CSS (opcional, si no se pasa lista todos)' }
|
||||||
|
},
|
||||||
|
required: []
|
||||||
|
},
|
||||||
|
handler: (args: { id?: string }) => {
|
||||||
|
if (args.id) {
|
||||||
|
const styleEl = document.getElementById(`canvas-css-${args.id}`)
|
||||||
|
if (!styleEl) return `Error: CSS "${args.id}" no encontrado`
|
||||||
|
return styleEl.textContent || '(vacio)'
|
||||||
|
}
|
||||||
|
|
||||||
|
// Listar todos
|
||||||
|
const styles = document.querySelectorAll('style[id^="canvas-css-"]')
|
||||||
|
if (styles.length === 0) {
|
||||||
|
return 'No hay CSS inyectado en el canvas'
|
||||||
|
}
|
||||||
|
|
||||||
|
const list = Array.from(styles).map(s => {
|
||||||
|
const id = s.id.replace('canvas-css-', '')
|
||||||
|
const preview = (s.textContent || '').slice(0, 100)
|
||||||
|
return `- ${id}: ${preview}${(s.textContent?.length || 0) > 100 ? '...' : ''}`
|
||||||
|
}).join('\n')
|
||||||
|
|
||||||
|
return `CSS inyectados (${styles.length}):\n${list}`
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,6 +24,12 @@ export const ALL_TOOL_METAS: ToolMeta[] = [
|
|||||||
{ name: 'resize_window', description: 'Cambia el tamano de una ventana', 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: 'close_window', description: 'Cierra una ventana del canvas', category: 'canvas' },
|
||||||
{ name: 'list_windows', description: 'Lista todas las ventanas abiertas', category: 'canvas' },
|
{ name: 'list_windows', description: 'Lista todas las ventanas abiertas', category: 'canvas' },
|
||||||
|
{ name: 'inspect_window', description: 'Inspecciona el HTML de una ventana', category: 'canvas' },
|
||||||
|
{ name: 'get_canvas', description: 'Lee elementos del canvas con selector CSS', category: 'canvas' },
|
||||||
|
{ name: 'edit_canvas', description: 'Edita elementos del canvas (old_value -> new_value)', category: 'canvas' },
|
||||||
|
{ name: 'canvas_css', description: 'Inyecta CSS en el canvas', category: 'canvas' },
|
||||||
|
{ name: 'canvas_js', description: 'Ejecuta JavaScript en el canvas', category: 'canvas' },
|
||||||
|
{ name: 'get_canvas_css', description: 'Obtiene CSS inyectado en el canvas', category: 'canvas' },
|
||||||
|
|
||||||
// Component tools
|
// Component tools
|
||||||
{ name: 'save_vue_component', description: 'Guarda un componente Vue en la base de datos', category: 'component' },
|
{ name: 'save_vue_component', description: 'Guarda un componente Vue en la base de datos', category: 'component' },
|
||||||
|
|||||||
Reference in New Issue
Block a user