From 86b3246fa10f1abedeb92c3ea8b59d3d3ca8e406 Mon Sep 17 00:00:00 2001 From: josedario87 Date: Fri, 13 Feb 2026 19:55:17 -0600 Subject: [PATCH] feat: Add FloatingResponse component with bubbleResponse MCP tool Add a floating response panel that allows the agent to display messages directly in the UI instead of through the terminal. Includes support for info, success, warning, and error message types with auto-dismiss. --- frontend/src/App.vue | 28 ++ frontend/src/components/FloatingResponse.vue | 402 ++++++++++++++++++ frontend/src/components/FloatingTerminal.vue | 58 +++ frontend/src/services/toolRegistry.ts | 6 +- frontend/src/services/tools/handlers/index.ts | 2 + .../tools/handlers/responseHandlers.ts | 59 +++ .../src/services/tools/toolDefinitions.ts | 5 +- 7 files changed, 557 insertions(+), 3 deletions(-) create mode 100644 frontend/src/components/FloatingResponse.vue create mode 100644 frontend/src/services/tools/handlers/responseHandlers.ts 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