diff --git a/frontend/src/App.vue b/frontend/src/App.vue index b50fcab..1d8b079 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -7,16 +7,19 @@ import ComponentsDropdown from './components/ComponentsDropdown.vue' import ToolsDropdown from './components/ToolsDropdown.vue' import ConnectionDropdown from './components/ConnectionDropdown.vue' import FloatingTerminal from './components/FloatingTerminal.vue' +import FloatingResponse from './components/FloatingResponse.vue' import PwaInstallBanner from './components/PwaInstallBanner.vue' import { initWebMCP, getWebMCP, startTokenPolling, stopTokenPolling, connectWithToken } from './services/webmcp' import { initToolRegistry, activatePageTools, initToolsOnRefresh } from './services/toolRegistry' import { setTerminalControls } from './services/tools/handlers/terminalHandlers' +import { setResponseControls } from './services/tools/handlers/responseHandlers' import { useCanvasStore } from './stores/canvas' const route = useRoute() const router = useRouter() const showTerminal = ref(false) const terminalRef = ref | null>(null) +const responseRef = ref | null>(null) const canvasStore = useCanvasStore() type PageName = 'home' | 'canvas' | 'components' | 'themes' | 'projects' | 'project-canvas' | 'database' | 'source' | 'terminal' | 'tools' @@ -69,6 +72,28 @@ onMounted(async () => { } }) + // Setup response controls for MCP tools + setResponseControls({ + addMessage: (message: string, type?: 'info' | 'success' | 'warning' | 'error') => { + if (responseRef.value) { + return responseRef.value.addMessage(message, type) + } + return '' + }, + removeMessage: (id: string) => { + responseRef.value?.removeMessage(id) + }, + clearAll: () => { + responseRef.value?.clearAll() + }, + getMessages: () => { + return responseRef.value?.getMessages() || [] + }, + move: (x: number, y: number) => { + responseRef.value?.move(x, y) + } + }) + // Start polling for token if not connected const webmcp = getWebMCP() if (!webmcp?.isConnected) { @@ -134,6 +159,9 @@ watch(() => route.name, (newPage) => { + + + diff --git a/frontend/src/components/FloatingResponse.vue b/frontend/src/components/FloatingResponse.vue new file mode 100644 index 0000000..639d303 --- /dev/null +++ b/frontend/src/components/FloatingResponse.vue @@ -0,0 +1,402 @@ + + + + + diff --git a/frontend/src/components/FloatingTerminal.vue b/frontend/src/components/FloatingTerminal.vue index 91d95f4..b2aecb7 100644 --- a/frontend/src/components/FloatingTerminal.vue +++ b/frontend/src/components/FloatingTerminal.vue @@ -4,6 +4,8 @@ import { Terminal } from '@xterm/xterm' import { FitAddon } from '@xterm/addon-fit' import { WebLinksAddon } from '@xterm/addon-web-links' import '@xterm/xterm/css/xterm.css' +import { connectWithToken, stopTokenPolling } from '../services/webmcp' +import { useCanvasStore } from '../stores/canvas' const props = defineProps<{ modelValue: boolean @@ -18,6 +20,8 @@ const isOpen = computed({ set: (val) => emit('update:modelValue', val) }) +const canvasStore = useCanvasStore() + const terminalContainer = ref(null) const terminalRef = ref(null) const connected = ref(false) @@ -38,6 +42,11 @@ let fitAddon: FitAddon | null = null let socket: WebSocket | null = null let resizeObserver: ResizeObserver | null = null +// Buffer for detecting WebMCP token +let tokenBuffer = '' +let tokenTimeout: number | null = null +const waitingForToken = ref(false) + const WS_URL = `ws://${window.location.hostname}:4103` // Mouse position tracking for Ctrl+E @@ -282,6 +291,38 @@ async function connect() { } else if (msg.type === 'replay') { terminal?.write(msg.data) } else if (msg.type === 'output') { + // Only detect token when waiting for it + if (waitingForToken.value) { + tokenBuffer += msg.data + + // Debounce: process buffer after output stops (300ms) + if (tokenTimeout) clearTimeout(tokenTimeout) + tokenTimeout = window.setTimeout(() => { + if (tokenBuffer.includes('Token copiado')) { + // Clean ANSI codes and whitespace + const clean = tokenBuffer.replace(/\x1b\[[0-9;]*m/g, '').replace(/[\r\n\s]/g, '') + const match = clean.match(/eyJ[A-Za-z0-9_\-+/=]+/) + if (match) { + try { + const decoded = atob(match[0]) + JSON.parse(decoded) + console.log('[Terminal] WebMCP token detected:', match[0]) + waitingForToken.value = false + tokenBuffer = '' + stopTokenPolling() + connectWithToken(match[0]).then(success => { + if (success) { + canvasStore.showNotification('WebMCP connected!', 'success') + } + }).catch(console.error) + } catch { + // Token incomplete, keep waiting + } + } + } + }, 300) + } + terminal?.write(msg.data) } else if (msg.type === 'exit') { terminal?.write(msg.data) @@ -314,6 +355,14 @@ function runClaude() { } } +function requestToken() { + if (socket && socket.readyState === WebSocket.OPEN) { + tokenBuffer = '' + waitingForToken.value = true + socket.send(JSON.stringify({ type: 'input', data: 'genera token usando tu mcp\r' })) + } +} + watch(isOpen, async (open) => { if (open) { await nextTick() @@ -330,6 +379,9 @@ watch(isOpen, async (open) => { terminal?.dispose() terminal = null fitAddon = null + waitingForToken.value = false + tokenBuffer = '' + if (tokenTimeout) clearTimeout(tokenTimeout) } }) @@ -410,6 +462,7 @@ defineExpose({ connect
+
@@ -514,6 +567,11 @@ defineExpose({ border-color: #a22; color: #fff; } +.window-controls button.waiting { + background: rgba(16, 185, 129, 0.3); + border-color: #10b981; + animation: pulse 0.8s infinite; +} .content { flex: 1; diff --git a/frontend/src/services/toolRegistry.ts b/frontend/src/services/toolRegistry.ts index 75122f6..423adca 100644 --- a/frontend/src/services/toolRegistry.ts +++ b/frontend/src/services/toolRegistry.ts @@ -20,6 +20,7 @@ import { createProjectCanvasHandlers, createSourceCodeHandlers, createTerminalHandlers, + createResponseHandlers, type ToolConfig } from './tools/handlers' import { setRouter } from './tools/handlers/globalHandlers' @@ -113,7 +114,8 @@ function getToolConfigs(): Map { ...createDatabaseHandlers(), ...createProjectCanvasHandlers(), ...createSourceCodeHandlers(), - ...createTerminalHandlers() + ...createTerminalHandlers(), + ...createResponseHandlers() ] for (const config of allHandlers) { @@ -132,7 +134,7 @@ const categoryTools: Record = { 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'], project: ['list_canvases', 'create_canvas', 'get_canvas', 'update_canvas', 'delete_canvas', 'clone_canvas', 'add_component_to_canvas', 'remove_component_from_canvas', 'get_canvas_components'], - terminal: ['terminal_open', 'terminal_close', 'terminal_toggle', 'terminal_move', 'terminal_resize'] + terminal: ['terminal_open', 'terminal_close', 'terminal_toggle', 'terminal_move', 'terminal_resize', 'bubbleResponse'] } // Page to categories mapping diff --git a/frontend/src/services/tools/handlers/index.ts b/frontend/src/services/tools/handlers/index.ts index dcb7589..a1ee1ee 100644 --- a/frontend/src/services/tools/handlers/index.ts +++ b/frontend/src/services/tools/handlers/index.ts @@ -12,6 +12,8 @@ export { createProjectCanvasHandlers } from './projectCanvasHandlers' export { createSourceCodeHandlers } from './sourceCodeHandlers' export { createTerminalHandlers, setTerminalControls } from './terminalHandlers' export type { TerminalControls } from './terminalHandlers' +export { createResponseHandlers, setResponseControls } from './responseHandlers' +export type { ResponseControls } from './responseHandlers' export type ToolHandler = (args: any) => string | Promise diff --git a/frontend/src/services/tools/handlers/responseHandlers.ts b/frontend/src/services/tools/handlers/responseHandlers.ts new file mode 100644 index 0000000..86f0a4f --- /dev/null +++ b/frontend/src/services/tools/handlers/responseHandlers.ts @@ -0,0 +1,59 @@ +/** + * Floating Response handlers + * Controls the FloatingResponse component for agent UI responses + */ + +import type { ToolConfig } from './index' + +export interface ResponseControls { + addMessage: (message: string, type?: 'info' | 'success' | 'warning' | 'error') => string + removeMessage: (id: string) => void + clearAll: () => void + getMessages: () => Array<{ id: string; message: string; type: string; timestamp: number }> + move: (x: number, y: number) => void +} + +// Global reference to response controls (set by App.vue) +let responseControls: ResponseControls | null = null + +export function setResponseControls(controls: ResponseControls) { + responseControls = controls + ;(window as any).__responseControls = controls +} + +export function getResponseControls(): ResponseControls | null { + return responseControls +} + +export function createResponseHandlers(): ToolConfig[] { + return [ + { + name: 'bubbleResponse', + description: 'Responde al usuario mostrando un mensaje en la UI (terminal flotante) en lugar de en Claude Code', + category: 'terminal', + schema: { + type: 'object', + properties: { + message: { + type: 'string', + description: 'El mensaje a mostrar al usuario en la UI' + }, + type: { + type: 'string', + enum: ['info', 'success', 'warning', 'error'], + description: 'Tipo de mensaje: info (default), success, warning, error' + } + }, + required: ['message'] + }, + handler: (args: { message: string; type?: 'info' | 'success' | 'warning' | 'error' }) => { + if (!responseControls) return 'Error: Response controls not initialized' + + const type = args.type || 'info' + const id = responseControls.addMessage(args.message, type) + + return `Mensaje mostrado en UI (id: ${id}, tipo: ${type})` + } + } + ] +} diff --git a/frontend/src/services/tools/toolDefinitions.ts b/frontend/src/services/tools/toolDefinitions.ts index d74dd6d..c575646 100644 --- a/frontend/src/services/tools/toolDefinitions.ts +++ b/frontend/src/services/tools/toolDefinitions.ts @@ -64,7 +64,10 @@ export const ALL_TOOL_METAS: ToolMeta[] = [ { name: 'terminal_close', description: 'Cierra la ventana flotante del terminal', category: 'terminal' }, { name: 'terminal_toggle', description: 'Alterna abrir/cerrar el terminal', category: 'terminal' }, { name: 'terminal_move', description: 'Mueve la ventana del terminal a una posicion', category: 'terminal' }, - { name: 'terminal_resize', description: 'Cambia el tamano de la ventana del terminal', category: 'terminal' } + { name: 'terminal_resize', description: 'Cambia el tamano de la ventana del terminal', category: 'terminal' }, + + // Response UI tools + { name: 'bubbleResponse', description: 'Muestra un mensaje del agente en la UI', category: 'terminal' } ] // Get all tool names