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.
This commit is contained in:
@@ -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<InstanceType<typeof FloatingTerminal> | null>(null)
|
||||
const responseRef = ref<InstanceType<typeof FloatingResponse> | 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) => {
|
||||
|
||||
<!-- Floating Terminal -->
|
||||
<FloatingTerminal ref="terminalRef" v-model="showTerminal" />
|
||||
|
||||
<!-- Floating Response (Agent UI messages) -->
|
||||
<FloatingResponse ref="responseRef" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
402
frontend/src/components/FloatingResponse.vue
Normal file
402
frontend/src/components/FloatingResponse.vue
Normal file
@@ -0,0 +1,402 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onBeforeUnmount } from 'vue'
|
||||
|
||||
export interface ResponseMessage {
|
||||
id: string
|
||||
message: string
|
||||
type: 'info' | 'success' | 'warning' | 'error'
|
||||
timestamp: number
|
||||
}
|
||||
|
||||
const messages = ref<ResponseMessage[]>([])
|
||||
const isDragging = ref(false)
|
||||
const position = ref({ x: 0, y: 0 })
|
||||
const hasCustomPosition = ref(false)
|
||||
const dragOffset = ref({ x: 0, y: 0 })
|
||||
|
||||
const isVisible = computed(() => messages.value.length > 0)
|
||||
|
||||
// Default position: bottom-left, above the terminal FAB
|
||||
function getDefaultPosition() {
|
||||
return {
|
||||
x: 20,
|
||||
y: window.innerHeight - 120
|
||||
}
|
||||
}
|
||||
|
||||
function addMessage(message: string, type: ResponseMessage['type'] = 'info') {
|
||||
const id = `msg-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`
|
||||
|
||||
// If no custom position, set default position
|
||||
if (!hasCustomPosition.value) {
|
||||
position.value = getDefaultPosition()
|
||||
}
|
||||
|
||||
messages.value.push({
|
||||
id,
|
||||
message,
|
||||
type,
|
||||
timestamp: Date.now()
|
||||
})
|
||||
|
||||
// Auto-dismiss after 10 seconds for non-error messages
|
||||
if (type !== 'error') {
|
||||
setTimeout(() => {
|
||||
removeMessage(id)
|
||||
}, 10000)
|
||||
}
|
||||
|
||||
return id
|
||||
}
|
||||
|
||||
function removeMessage(id: string) {
|
||||
const index = messages.value.findIndex(m => m.id === id)
|
||||
if (index !== -1) {
|
||||
messages.value.splice(index, 1)
|
||||
}
|
||||
}
|
||||
|
||||
function clearAll() {
|
||||
messages.value = []
|
||||
}
|
||||
|
||||
function startDrag(e: MouseEvent) {
|
||||
if ((e.target as HTMLElement).closest('.close-btn')) return
|
||||
|
||||
isDragging.value = true
|
||||
const container = document.querySelector('.floating-response') as HTMLElement
|
||||
if (container) {
|
||||
const rect = container.getBoundingClientRect()
|
||||
if (!hasCustomPosition.value) {
|
||||
position.value = { x: rect.left, y: rect.top }
|
||||
}
|
||||
dragOffset.value = {
|
||||
x: e.clientX - rect.left,
|
||||
y: e.clientY - rect.top
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('mousemove', onDrag)
|
||||
document.addEventListener('mouseup', stopDrag)
|
||||
}
|
||||
|
||||
function onDrag(e: MouseEvent) {
|
||||
if (!isDragging.value) return
|
||||
|
||||
const newX = e.clientX - dragOffset.value.x
|
||||
const newY = e.clientY - dragOffset.value.y
|
||||
|
||||
// Keep within viewport with some tolerance
|
||||
const maxX = window.innerWidth - 100
|
||||
const maxY = window.innerHeight - 50
|
||||
|
||||
position.value = {
|
||||
x: Math.max(0, Math.min(newX, maxX)),
|
||||
y: Math.max(0, Math.min(newY, maxY))
|
||||
}
|
||||
}
|
||||
|
||||
function stopDrag() {
|
||||
isDragging.value = false
|
||||
hasCustomPosition.value = true
|
||||
document.removeEventListener('mousemove', onDrag)
|
||||
document.removeEventListener('mouseup', stopDrag)
|
||||
}
|
||||
|
||||
const containerStyle = computed(() => {
|
||||
if (!hasCustomPosition.value) {
|
||||
return {
|
||||
bottom: '120px',
|
||||
left: '20px'
|
||||
}
|
||||
}
|
||||
return {
|
||||
top: `${position.value.y}px`,
|
||||
left: `${position.value.x}px`,
|
||||
bottom: 'auto',
|
||||
right: 'auto'
|
||||
}
|
||||
})
|
||||
|
||||
function getTypeIcon(type: ResponseMessage['type']) {
|
||||
switch (type) {
|
||||
case 'success': return '✓'
|
||||
case 'warning': return '⚠'
|
||||
case 'error': return '✕'
|
||||
default: return 'ℹ'
|
||||
}
|
||||
}
|
||||
|
||||
function getTypeColor(type: ResponseMessage['type']) {
|
||||
switch (type) {
|
||||
case 'success': return '#10b981'
|
||||
case 'warning': return '#f59e0b'
|
||||
case 'error': return '#ef4444'
|
||||
default: return '#6366f1'
|
||||
}
|
||||
}
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
document.removeEventListener('mousemove', onDrag)
|
||||
document.removeEventListener('mouseup', stopDrag)
|
||||
})
|
||||
|
||||
// Expose controls for MCP tools
|
||||
defineExpose({
|
||||
addMessage,
|
||||
removeMessage,
|
||||
clearAll,
|
||||
getMessages: () => messages.value,
|
||||
move: (x: number, y: number) => {
|
||||
position.value = { x, y }
|
||||
hasCustomPosition.value = true
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Teleport to="body">
|
||||
<Transition name="bubble-slide">
|
||||
<div
|
||||
v-if="isVisible"
|
||||
class="floating-response"
|
||||
:class="{ dragging: isDragging }"
|
||||
:style="containerStyle"
|
||||
>
|
||||
<div class="response-glass">
|
||||
<!-- Header -->
|
||||
<div class="response-header" @mousedown="startDrag">
|
||||
<div class="header-left">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/>
|
||||
</svg>
|
||||
<span>Agent Response</span>
|
||||
</div>
|
||||
<button class="close-btn" @click="clearAll" title="Clear all">
|
||||
<svg width="10" height="10" viewBox="0 0 10 10">
|
||||
<line x1="0" y1="0" x2="10" y2="10" stroke="currentColor" stroke-width="1.5"/>
|
||||
<line x1="10" y1="0" x2="0" y2="10" stroke="currentColor" stroke-width="1.5"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Messages -->
|
||||
<div class="messages-container">
|
||||
<TransitionGroup name="message">
|
||||
<div
|
||||
v-for="msg in messages"
|
||||
:key="msg.id"
|
||||
class="message-item"
|
||||
:style="{ '--type-color': getTypeColor(msg.type) }"
|
||||
>
|
||||
<span class="type-icon" :class="msg.type">{{ getTypeIcon(msg.type) }}</span>
|
||||
<span class="message-text">{{ msg.message }}</span>
|
||||
<button class="dismiss-btn" @click="removeMessage(msg.id)">
|
||||
<svg width="8" height="8" viewBox="0 0 10 10">
|
||||
<line x1="0" y1="0" x2="10" y2="10" stroke="currentColor" stroke-width="1.5"/>
|
||||
<line x1="10" y1="0" x2="0" y2="10" stroke="currentColor" stroke-width="1.5"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</TransitionGroup>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
</Teleport>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.floating-response {
|
||||
position: fixed;
|
||||
min-width: 280px;
|
||||
max-width: 420px;
|
||||
z-index: 9997;
|
||||
}
|
||||
|
||||
.response-glass {
|
||||
background: rgba(200, 215, 235, 0.45);
|
||||
backdrop-filter: blur(24px) saturate(1.6);
|
||||
-webkit-backdrop-filter: blur(24px) saturate(1.6);
|
||||
border-radius: 8px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.6);
|
||||
box-shadow:
|
||||
0 0 0 1px rgba(80, 120, 180, 0.25),
|
||||
0 8px 32px rgba(0, 0, 0, 0.2),
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.6);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.response-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 6px 8px;
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.3);
|
||||
cursor: grab;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.floating-response.dragging .response-header {
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
.header-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
color: #333;
|
||||
font: 500 11px/1 system-ui, sans-serif;
|
||||
}
|
||||
|
||||
.header-left svg {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.close-btn {
|
||||
width: 18px;
|
||||
height: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
border: 1px solid rgba(0, 0, 0, 0.1);
|
||||
border-radius: 3px;
|
||||
color: #555;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
.close-btn:hover {
|
||||
background: linear-gradient(180deg, #e66 0%, #c33 100%);
|
||||
border-color: #a22;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.messages-container {
|
||||
padding: 8px;
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.message-item {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
padding: 10px 12px;
|
||||
background: rgba(255, 255, 255, 0.5);
|
||||
border-radius: 6px;
|
||||
border-left: 3px solid var(--type-color);
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
.type-icon {
|
||||
flex-shrink: 0;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 50%;
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
color: white;
|
||||
background: var(--type-color);
|
||||
}
|
||||
|
||||
.message-text {
|
||||
flex: 1;
|
||||
font-size: 12px;
|
||||
line-height: 1.4;
|
||||
color: #333;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.dismiss-btn {
|
||||
flex-shrink: 0;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-radius: 3px;
|
||||
color: #999;
|
||||
cursor: pointer;
|
||||
opacity: 0;
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
.message-item:hover .dismiss-btn {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.dismiss-btn:hover {
|
||||
background: rgba(0, 0, 0, 0.1);
|
||||
color: #666;
|
||||
}
|
||||
|
||||
/* Scrollbar */
|
||||
.messages-container::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
.messages-container::-webkit-scrollbar-track {
|
||||
background: rgba(0, 0, 0, 0.05);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.messages-container::-webkit-scrollbar-thumb {
|
||||
background: rgba(0, 0, 0, 0.15);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.messages-container::-webkit-scrollbar-thumb:hover {
|
||||
background: rgba(0, 0, 0, 0.25);
|
||||
}
|
||||
|
||||
/* Animations */
|
||||
.bubble-slide-enter-active,
|
||||
.bubble-slide-leave-active {
|
||||
transition: all 0.25s ease;
|
||||
}
|
||||
|
||||
.bubble-slide-enter-from,
|
||||
.bubble-slide-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateY(20px) scale(0.95);
|
||||
}
|
||||
|
||||
.message-enter-active {
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.message-leave-active {
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
.message-enter-from {
|
||||
opacity: 0;
|
||||
transform: translateX(-10px);
|
||||
}
|
||||
|
||||
.message-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateX(10px);
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.floating-response {
|
||||
left: 10px !important;
|
||||
right: 10px !important;
|
||||
bottom: 80px !important;
|
||||
max-width: none;
|
||||
min-width: auto;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -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<HTMLElement | null>(null)
|
||||
const terminalRef = ref<HTMLElement | null>(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({
|
||||
<a v-if="!connected && !connecting" class="link" @click.stop="connect">connect</a>
|
||||
</div>
|
||||
<div class="window-controls">
|
||||
<button @click="requestToken" :class="{ waiting: waitingForToken }" title="Connect MCP"><svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/></svg></button>
|
||||
<button @click="runClaude" title="Claude"><svg width="8" height="8" viewBox="0 0 24 24" fill="currentColor"><path d="M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5"/></svg></button>
|
||||
<button class="x" @click="close" title="Close"><svg width="8" height="8" viewBox="0 0 10 10"><line x1="0" y1="0" x2="10" y2="10" stroke="currentColor" stroke-width="1.5"/><line x1="10" y1="0" x2="0" y2="10" stroke="currentColor" stroke-width="1.5"/></svg></button>
|
||||
</div>
|
||||
@@ -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;
|
||||
|
||||
@@ -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<string, ToolConfig> {
|
||||
...createDatabaseHandlers(),
|
||||
...createProjectCanvasHandlers(),
|
||||
...createSourceCodeHandlers(),
|
||||
...createTerminalHandlers()
|
||||
...createTerminalHandlers(),
|
||||
...createResponseHandlers()
|
||||
]
|
||||
|
||||
for (const config of allHandlers) {
|
||||
@@ -132,7 +134,7 @@ const categoryTools: Record<ToolCategory, string[]> = {
|
||||
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
|
||||
|
||||
@@ -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<string>
|
||||
|
||||
|
||||
59
frontend/src/services/tools/handlers/responseHandlers.ts
Normal file
59
frontend/src/services/tools/handlers/responseHandlers.ts
Normal file
@@ -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})`
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user