refactor: remove legacy chat/agent implementations, keep transcript-debug

Remove FloatingTerminal (#1), AgentBar/FloatBubble (#2), and all related
components, composables, types, handlers, routes, and CSS. Clean up orphaned
references in ToolsDropdown, whisperSocket, and claude-hook comments.
Transcript-debug is now the sole chat/agent system.

Deleted: 15 files (~3500 lines)
Edited: 12 files (-717 lines net)
This commit is contained in:
2026-02-20 13:41:19 -06:00
parent c6197694b5
commit abe6766a85
11 changed files with 35 additions and 716 deletions

View File

@@ -3,12 +3,10 @@ import { ref, onMounted, onUnmounted, watch } from 'vue'
import { RouterView, useRoute, useRouter } from 'vue-router'
import Toolbar from './components/Toolbar.vue'
import TorchButton from './components/TorchButton.vue'
import FloatingTerminal from './components/FloatingTerminal.vue'
import FloatingResponse from './components/FloatingResponse.vue'
import { initWhisperSocket } from './services/whisperSocket'
import FloatingVoice from './components/FloatingVoice.vue'
import FloatingTranscriptDebug from './components/FloatingTranscriptDebug.vue'
import AgentBar from './components/AgentBar.vue'
import PwaInstallBanner from './components/PwaInstallBanner.vue'
import HooksApprovalModal from './components/HooksApprovalModal.vue'
import { useGlobalApproval } from './composables/useGlobalApproval'
@@ -16,14 +14,12 @@ import { initWebMCP, getWebMCP } from './services/webmcp'
import { initTorch, destroyTorch } from './services/torch'
import { endpoints } from './config/endpoints'
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'
import { useProjectCanvasStore } from './stores/projectCanvas'
const route = useRoute()
const router = useRouter()
const showTerminal = ref(false)
const showVoice = ref(false)
const showTranscriptDebug = ref(false)
const showDebugConsole = ref(false)
@@ -68,7 +64,6 @@ function copyDebugLogs() {
function clearDebugLogs() {
debugLogs.value = []
}
const terminalRef = ref<InstanceType<typeof FloatingTerminal> | null>(null)
const responseRef = ref<InstanceType<typeof FloatingResponse> | null>(null)
const voiceRef = ref<InstanceType<typeof FloatingVoice> | null>(null)
const transcriptDebugRef = ref<InstanceType<typeof FloatingTranscriptDebug> | null>(null)
@@ -81,28 +76,8 @@ const voicePTTActive = ref(false)
let voiceTouchStarted = false
let voicePTTTimeout: number | null = null
// Claude status state (for FAB animations)
type ClaudeStatus = 'idle' | 'processing' | 'toolUse' | 'toolDone' | 'reading' | 'writing' | 'sessionStart' | 'subagentStart' | 'subagentStop' | 'notification' | 'permissionRequest' | 'thinking'
const claudeStatus = ref<ClaudeStatus>('idle')
const claudeTool = ref<string | null>(null)
const isProcessing = ref(false) // Main processing state (UserPromptSubmit → Stop)
const isReading = ref(false) // Reading files
const isWriting = ref(false) // Writing files
const hasSubagent = ref(false) // Subagent active
const awaitingPermission = ref(false) // Waiting for user permission (highest priority)
const showSessionStart = ref(false) // Session start animation (3s)
const showNotification = ref(false) // Notification pulse (2s)
const showToolFlash = ref(false) // Tool use flash (500ms)
const keyboardVisible = ref(false) // Virtual keyboard visible
let statusWs: WebSocket | null = null
let statusReconnectTimeout: number | null = null
let processingTimeout: number | null = null
let sessionStartTimeout: number | null = null
let notificationTimeout: number | null = null
let toolFlashTimeout: number | null = null
function hardRefresh() {
location.reload()
}
@@ -161,117 +136,7 @@ function handleVoiceFabTouchEnd(e: TouchEvent) {
setTimeout(() => { voiceTouchStarted = false }, 100)
}
function connectStatusWs() {
if (statusWs?.readyState === WebSocket.OPEN) return
statusWs = new WebSocket(endpoints.claudeStatus)
statusWs.onopen = () => {
console.log('[App] Status WebSocket connected')
}
statusWs.onmessage = (event) => {
try {
const msg = JSON.parse(event.data)
if (msg.type === 'claude-status') {
const status = msg.status as ClaudeStatus
const agent = msg.agent || 'main'
console.log('[App] Claude status:', status, msg.tool, agent)
// Only animate the main FAB for 'main' agent — other agents use AgentBar
if (agent !== 'main') return
claudeStatus.value = status
claudeTool.value = msg.tool || null
// Handle different status types
switch (status) {
case 'processing':
case 'thinking':
isProcessing.value = true
// Auto-reset after 2 minutes (safety)
if (processingTimeout) clearTimeout(processingTimeout)
processingTimeout = window.setTimeout(() => {
isProcessing.value = false
}, 120000)
break
case 'idle':
isProcessing.value = false
isReading.value = false
isWriting.value = false
awaitingPermission.value = false
if (processingTimeout) clearTimeout(processingTimeout)
break
case 'permissionRequest':
awaitingPermission.value = true
break
case 'reading':
isReading.value = true
triggerToolFlash()
break
case 'writing':
isWriting.value = true
triggerToolFlash()
break
case 'toolUse':
triggerToolFlash()
break
case 'toolDone':
isReading.value = false
isWriting.value = false
awaitingPermission.value = false
break
case 'sessionStart':
showSessionStart.value = true
if (sessionStartTimeout) clearTimeout(sessionStartTimeout)
sessionStartTimeout = window.setTimeout(() => {
showSessionStart.value = false
}, 3000)
break
case 'subagentStart':
hasSubagent.value = true
break
case 'subagentStop':
hasSubagent.value = false
break
case 'notification':
showNotification.value = true
if (notificationTimeout) clearTimeout(notificationTimeout)
notificationTimeout = window.setTimeout(() => {
showNotification.value = false
}, 2000)
break
}
}
} catch { /* ignore non-JSON messages */ }
}
statusWs.onclose = () => {
isProcessing.value = false
statusReconnectTimeout = window.setTimeout(connectStatusWs, 2000)
}
}
function triggerToolFlash() {
showToolFlash.value = true
if (toolFlashTimeout) clearTimeout(toolFlashTimeout)
toolFlashTimeout = window.setTimeout(() => {
showToolFlash.value = false
}, 500)
}
type PageName = 'home' | 'canvas' | 'components' | 'themes' | 'project-canvas' | 'database' | 'source' | 'terminal' | 'tools' | 'agents'
type PageName = 'home' | 'canvas' | 'components' | 'themes' | 'project-canvas' | 'database' | 'source' | 'tools' | 'agents'
function syncThemeColor() {
const bg = getComputedStyle(document.documentElement).getPropertyValue('--bg-primary').trim()
@@ -284,9 +149,6 @@ onMounted(async () => {
// Sync Windows titlebar color with CSS variable
syncThemeColor()
// Connect to WebSocket for Claude status updates
connectStatusWs()
// Connect global hooks approval WS
connectApproval()
fetchApprovalPending()
@@ -307,43 +169,6 @@ onMounted(async () => {
const currentPage = (route.name as string) || 'canvas'
initToolsOnRefresh(currentPage as PageName)
// Setup terminal controls for MCP tools
setTerminalControls({
open: (x?: number, y?: number) => {
if (terminalRef.value) {
terminalRef.value.open(x, y)
} else {
showTerminal.value = true
}
},
close: () => {
if (terminalRef.value) {
terminalRef.value.close()
} else {
showTerminal.value = false
}
},
toggle: () => {
if (terminalRef.value) {
terminalRef.value.toggle()
} else {
showTerminal.value = !showTerminal.value
}
},
move: (x: number, y: number) => {
terminalRef.value?.move(x, y)
},
resize: (w: number, h: number) => {
terminalRef.value?.resize(w, h)
},
getState: () => {
if (terminalRef.value) {
return terminalRef.value.getState()
}
return { isOpen: showTerminal.value, position: { x: 0, y: 0 }, size: { w: 580, h: 360 } }
}
})
// Setup response controls for MCP tools
setResponseControls({
addMessage: (message: string, type?: 'info' | 'success' | 'warning' | 'error') => {
@@ -388,12 +213,6 @@ onUnmounted(() => {
document.removeEventListener('keydown', handleGlobalKeydown)
destroyTorch()
disconnectApproval()
if (statusReconnectTimeout) clearTimeout(statusReconnectTimeout)
if (processingTimeout) clearTimeout(processingTimeout)
if (sessionStartTimeout) clearTimeout(sessionStartTimeout)
if (notificationTimeout) clearTimeout(notificationTimeout)
if (toolFlashTimeout) clearTimeout(toolFlashTimeout)
statusWs?.close()
})
// Watch for route changes and update tools
@@ -468,94 +287,10 @@ watch(() => route.name, (newPage) => {
</RouterView>
</main>
<!-- Floating Terminal Toggle Button (Claude Life FAB) -->
<button
class="terminal-fab"
:class="{
active: showTerminal,
processing: isProcessing,
reading: isReading,
writing: isWriting,
subagent: hasSubagent,
permission: awaitingPermission,
'session-start': showSessionStart,
notification: showNotification,
'tool-flash': showToolFlash,
'sheet-open': showTerminal || showVoice || showTranscriptDebug,
'keyboard-visible': keyboardVisible
}"
@click="showTerminal = !showTerminal"
:title="awaitingPermission ? `Permiso requerido: ${claudeTool || 'herramienta'}` : isProcessing ? `Claude: ${claudeTool || 'processing'}` : 'Toggle Terminal'"
>
<!-- Subagent orbital ring -->
<svg v-if="hasSubagent" class="orbital-ring" viewBox="0 0 100 100">
<circle cx="50" cy="50" r="45" fill="none" stroke="currentColor" stroke-width="2" stroke-dasharray="20 10" />
</svg>
<!-- Session start ripples -->
<div v-if="showSessionStart" class="session-ripples">
<span></span>
<span></span>
<span></span>
</div>
<!-- Permission request icon (alert/hand) - highest priority -->
<svg v-if="awaitingPermission" class="permission-icon" xmlns="http://www.w3.org/2000/svg" width="26" height="26" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
<path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/>
<line x1="12" y1="9" x2="12" y2="13"/>
<line x1="12" y1="17" x2="12.01" y2="17"/>
</svg>
<!-- Processing animation (three dots) -->
<div v-else-if="isProcessing && !isReading && !isWriting" class="thinking-dots">
<span></span>
<span></span>
<span></span>
</div>
<!-- Reading icon (eye) -->
<svg v-else-if="isReading" class="status-icon" xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/>
<circle cx="12" cy="12" r="3"/>
</svg>
<!-- Writing icon (pencil) -->
<svg v-else-if="isWriting" class="status-icon" xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M12 19l7-7 3 3-7 7-3-3z"/>
<path d="M18 13l-1.5-7.5L2 2l3.5 14.5L13 18l5-5z"/>
<path d="M2 2l7.586 7.586"/>
<circle cx="11" cy="11" r="2"/>
</svg>
<!-- Normal icons -->
<template v-else>
<!-- Terminal closed: Nucleo atom icon -->
<svg v-if="!showTerminal" class="nucleo-icon" width="28" height="28" viewBox="0 0 24 24" fill="none">
<!-- Core nucleus -->
<circle cx="12" cy="12" r="3.5" fill="white" opacity="0.95"/>
<!-- Orbital rings -->
<ellipse cx="12" cy="12" rx="8" ry="3.5" stroke="white" stroke-width="1.3" fill="none" opacity="0.7" transform="rotate(-30 12 12)"/>
<ellipse cx="12" cy="12" rx="8" ry="3.5" stroke="white" stroke-width="1.3" fill="none" opacity="0.5" transform="rotate(30 12 12)"/>
<ellipse cx="12" cy="12" rx="8" ry="3.5" stroke="white" stroke-width="1.3" fill="none" opacity="0.6" transform="rotate(90 12 12)"/>
<!-- Electrons -->
<circle cx="12" cy="4" r="1.8" fill="white" opacity="0.9"/>
<circle cx="19" cy="14" r="1.8" fill="white" opacity="0.7"/>
<circle cx="5" cy="14" r="1.8" fill="white" opacity="0.8"/>
</svg>
<!-- Terminal open: Minimize chevron with connection indicator -->
<template v-else>
<span class="connection-dot"></span>
<svg class="minimize-icon" xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
<polyline points="6 9 12 15 18 9"></polyline>
</svg>
</template>
</template>
</button>
<!-- Transcript Debug FAB Button (pixel art ocean) -->
<button
class="transcript-fab"
:class="{ active: showTranscriptDebug, 'sheet-open': showTerminal || showVoice || showTranscriptDebug, 'keyboard-visible': keyboardVisible }"
:class="{ active: showTranscriptDebug, 'sheet-open': showVoice || showTranscriptDebug, 'keyboard-visible': keyboardVisible }"
@click="showTranscriptDebug = !showTranscriptDebug"
title="Transcript Debug"
>
@@ -577,7 +312,7 @@ watch(() => route.name, (newPage) => {
<!-- Voice FAB Button -->
<button
class="voice-fab"
:class="{ active: showVoice, 'sheet-open': showTerminal || showVoice || showTranscriptDebug, 'ptt-active': voicePTTActive, 'keyboard-visible': keyboardVisible }"
:class="{ active: showVoice, 'sheet-open': showVoice || showTranscriptDebug, 'ptt-active': voicePTTActive, 'keyboard-visible': keyboardVisible }"
@click="handleVoiceFabClick"
@touchstart="handleVoiceFabTouchStart"
@touchend="handleVoiceFabTouchEnd"
@@ -592,12 +327,6 @@ watch(() => route.name, (newPage) => {
</svg>
</button>
<!-- Agent Bar (bottom pills) -->
<AgentBar />
<!-- Floating Terminal -->
<FloatingTerminal ref="terminalRef" v-model="showTerminal" />
<!-- Floating Response (Agent UI messages) -->
<FloatingResponse ref="responseRef" />
@@ -951,368 +680,6 @@ watch(() => route.name, (newPage) => {
transform: translateX(-20px);
}
/* Terminal FAB - Claude Life */
.terminal-fab {
position: fixed;
bottom: 20px;
right: 20px;
width: 58px;
height: 58px;
border-radius: 18px;
background: linear-gradient(145deg, #7c3aed 0%, #6366f1 50%, #8b5cf6 100%);
color: white;
border: 2px solid rgba(255, 255, 255, 0.15);
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
box-shadow:
0 4px 15px rgba(124, 58, 237, 0.4),
0 8px 30px rgba(99, 102, 241, 0.3),
inset 0 1px 0 rgba(255, 255, 255, 0.2);
transition: all 0.4s cubic-bezier(0.34, 1.56, 0.64, 1);
z-index: 9998;
overflow: visible;
backdrop-filter: blur(10px);
/* Prevent text selection and touch gestures */
-webkit-user-select: none;
user-select: none;
-webkit-touch-callout: none;
touch-action: manipulation;
}
.terminal-fab::before {
content: '';
position: absolute;
inset: -3px;
border-radius: 21px;
background: linear-gradient(145deg, rgba(139, 92, 246, 0.5), rgba(99, 102, 241, 0.2));
z-index: -1;
opacity: 0;
transition: opacity 0.3s ease;
}
.terminal-fab:hover {
transform: translateY(-3px) scale(1.05);
box-shadow:
0 8px 25px rgba(124, 58, 237, 0.5),
0 15px 40px rgba(99, 102, 241, 0.35),
inset 0 1px 0 rgba(255, 255, 255, 0.25);
}
.terminal-fab:hover::before {
opacity: 1;
}
/* Nucleo atom icon animation */
.nucleo-icon {
animation: nucleo-orbit 8s linear infinite;
filter: drop-shadow(0 0 6px rgba(255, 255, 255, 0.6));
}
.nucleo-icon ellipse {
transform-origin: center;
}
/* Terminal active - Connected state */
.terminal-fab.active {
background: linear-gradient(145deg, #6366f1 0%, #4f46e5 50%, #7c3aed 100%);
border-color: rgba(16, 185, 129, 0.5);
box-shadow:
0 4px 15px rgba(99, 102, 241, 0.4),
0 8px 30px rgba(79, 70, 229, 0.3),
0 0 20px rgba(16, 185, 129, 0.2),
inset 0 1px 0 rgba(255, 255, 255, 0.2);
transform: none;
}
.terminal-fab.active:hover {
transform: translateY(-2px);
box-shadow:
0 6px 20px rgba(99, 102, 241, 0.5),
0 12px 35px rgba(79, 70, 229, 0.35),
0 0 25px rgba(16, 185, 129, 0.3),
inset 0 1px 0 rgba(255, 255, 255, 0.25);
}
/* Connection indicator dot */
.connection-dot {
position: absolute;
top: 8px;
right: 8px;
width: 10px;
height: 10px;
background: #10b981;
border-radius: 50%;
border: 2px solid rgba(255, 255, 255, 0.9);
box-shadow: 0 0 8px rgba(16, 185, 129, 0.8);
animation: connection-pulse 2s ease-in-out infinite;
}
/* Minimize icon */
.minimize-icon {
opacity: 0.9;
transition: transform 0.2s ease;
}
.terminal-fab.active:hover .minimize-icon {
transform: translateY(2px);
}
/* Processing state (UserPromptSubmit → Stop) - Orange pulsing */
.terminal-fab.processing {
background: linear-gradient(135deg, #f59e0b 0%, #d97706 100%) !important;
box-shadow: 0 8px 24px rgba(245, 158, 11, 0.4) !important;
animation: processing-pulse 2s ease-in-out infinite !important;
transform: rotate(0deg) !important;
}
.terminal-fab.processing:hover {
box-shadow: 0 12px 32px rgba(245, 158, 11, 0.5) !important;
transform: scale(1.05) !important;
}
/* Reading state - Cyan scanning */
.terminal-fab.reading {
background: linear-gradient(135deg, #06b6d4 0%, #0891b2 100%) !important;
box-shadow: 0 8px 24px rgba(6, 182, 212, 0.4) !important;
animation: reading-scan 1.5s ease-in-out infinite !important;
}
/* Writing state - Green pulse */
.terminal-fab.writing {
background: linear-gradient(135deg, #10b981 0%, #059669 100%) !important;
box-shadow: 0 8px 24px rgba(16, 185, 129, 0.4) !important;
animation: writing-pulse 0.8s ease-in-out infinite !important;
}
/* Subagent active - Purple with orbital ring */
.terminal-fab.subagent {
box-shadow: 0 8px 24px rgba(139, 92, 246, 0.5) !important;
}
/* Session start - Green wake-up effect */
.terminal-fab.session-start {
animation: session-wake 3s ease-out forwards !important;
}
/* Notification - Yellow bounce */
.terminal-fab.notification {
animation: notification-bounce 0.5s ease-in-out 4 !important;
}
/* Permission Request - HIGHEST PRIORITY - Red pulsing alert */
.terminal-fab.permission {
background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%) !important;
box-shadow: 0 0 0 0 rgba(239, 68, 68, 0.7) !important;
animation: permission-pulse 1s ease-in-out infinite !important;
transform: rotate(0deg) scale(1.1) !important;
z-index: 10000 !important;
}
.terminal-fab.permission:hover {
transform: scale(1.15) !important;
}
/* Permission icon animation */
.permission-icon {
animation: permission-shake 0.5s ease-in-out infinite;
filter: drop-shadow(0 0 8px rgba(255, 255, 255, 0.8));
}
/* Tool flash - Quick white flash */
.terminal-fab.tool-flash::after {
content: '';
position: absolute;
inset: -4px;
border-radius: 20px;
background: rgba(255, 255, 255, 0.4);
animation: tool-flash 0.5s ease-out forwards;
pointer-events: none;
}
/* Thinking dots animation */
.thinking-dots {
display: flex;
align-items: center;
justify-content: center;
gap: 4px;
}
.thinking-dots span {
width: 6px;
height: 6px;
background: white;
border-radius: 50%;
animation: thinking-dot 1.4s ease-in-out infinite;
}
.thinking-dots span:nth-child(1) { animation-delay: 0s; }
.thinking-dots span:nth-child(2) { animation-delay: 0.2s; }
.thinking-dots span:nth-child(3) { animation-delay: 0.4s; }
/* Status icons */
.status-icon {
animation: icon-breathe 1s ease-in-out infinite;
}
/* Orbital ring for subagent */
.orbital-ring {
position: absolute;
width: 80px;
height: 80px;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
animation: orbit-spin 3s linear infinite;
color: rgba(139, 92, 246, 0.8);
pointer-events: none;
}
/* Session start ripples */
.session-ripples {
position: absolute;
inset: 0;
pointer-events: none;
}
.session-ripples span {
position: absolute;
inset: -10px;
border: 2px solid rgba(16, 185, 129, 0.6);
border-radius: 20px;
animation: ripple-expand 3s ease-out forwards;
}
.session-ripples span:nth-child(1) { animation-delay: 0s; }
.session-ripples span:nth-child(2) { animation-delay: 0.5s; }
.session-ripples span:nth-child(3) { animation-delay: 1s; }
/* Keyframes */
@keyframes thinking-dot {
0%, 80%, 100% { transform: scale(0.6); opacity: 0.5; }
40% { transform: scale(1); opacity: 1; }
}
@keyframes processing-pulse {
0%, 100% { box-shadow: 0 8px 24px rgba(245, 158, 11, 0.4); }
50% { box-shadow: 0 8px 40px rgba(245, 158, 11, 0.7); }
}
@keyframes reading-scan {
0%, 100% {
box-shadow: 0 8px 24px rgba(6, 182, 212, 0.4);
transform: rotate(0deg);
}
25% { transform: rotate(-2deg); }
75% { transform: rotate(2deg); }
50% {
box-shadow: 0 8px 40px rgba(6, 182, 212, 0.7);
}
}
@keyframes writing-pulse {
0%, 100% {
transform: scale(1);
box-shadow: 0 8px 24px rgba(16, 185, 129, 0.4);
}
50% {
transform: scale(1.05);
box-shadow: 0 8px 32px rgba(16, 185, 129, 0.6);
}
}
@keyframes session-wake {
0% {
transform: scale(0.8);
opacity: 0.5;
box-shadow: 0 0 0 rgba(16, 185, 129, 0);
}
30% {
transform: scale(1.15);
box-shadow: 0 0 60px rgba(16, 185, 129, 0.6);
}
60% { transform: scale(0.95); }
100% {
transform: scale(1);
opacity: 1;
box-shadow: 0 8px 24px rgba(99, 102, 241, 0.4);
}
}
@keyframes notification-bounce {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-8px); }
}
@keyframes tool-flash {
0% { opacity: 1; transform: scale(1); }
100% { opacity: 0; transform: scale(1.3); }
}
@keyframes icon-breathe {
0%, 100% { opacity: 1; }
50% { opacity: 0.7; }
}
@keyframes orbit-spin {
from { transform: translate(-50%, -50%) rotate(0deg); }
to { transform: translate(-50%, -50%) rotate(360deg); }
}
@keyframes ripple-expand {
0% {
transform: scale(1);
opacity: 1;
}
100% {
transform: scale(2);
opacity: 0;
}
}
@keyframes permission-pulse {
0%, 100% {
box-shadow: 0 0 0 0 rgba(239, 68, 68, 0.7), 0 8px 24px rgba(239, 68, 68, 0.5);
transform: scale(1.1);
}
50% {
box-shadow: 0 0 0 15px rgba(239, 68, 68, 0), 0 8px 40px rgba(239, 68, 68, 0.8);
transform: scale(1.15);
}
}
@keyframes permission-shake {
0%, 100% { transform: rotate(0deg); }
25% { transform: rotate(-5deg); }
75% { transform: rotate(5deg); }
}
@keyframes nucleo-orbit {
0% {
transform: rotate(0deg);
filter: drop-shadow(0 0 6px rgba(255, 255, 255, 0.6));
}
50% {
filter: drop-shadow(0 0 10px rgba(255, 255, 255, 0.9));
}
100% {
transform: rotate(360deg);
filter: drop-shadow(0 0 6px rgba(255, 255, 255, 0.6));
}
}
@keyframes connection-pulse {
0%, 100% {
box-shadow: 0 0 8px rgba(16, 185, 129, 0.8);
transform: scale(1);
}
50% {
box-shadow: 0 0 12px rgba(16, 185, 129, 1), 0 0 20px rgba(16, 185, 129, 0.4);
transform: scale(1.1);
}
}
/* Voice FAB */
.voice-fab {
position: fixed;
@@ -1408,30 +775,6 @@ watch(() => route.name, (newPage) => {
}
@media (max-width: 768px) {
.terminal-fab {
bottom: 80px;
right: 16px;
width: 54px;
height: 54px;
border-radius: 16px;
}
.terminal-fab::before {
border-radius: 19px;
}
.connection-dot {
width: 8px;
height: 8px;
top: 6px;
right: 6px;
}
.orbital-ring {
width: 70px;
height: 70px;
}
.voice-fab {
bottom: 80px;
left: 16px;
@@ -1449,26 +792,22 @@ watch(() => route.name, (newPage) => {
/* Mobile: FABs above bottom sheets */
@media (max-width: 1024px) and (pointer: coarse) {
.terminal-fab,
.voice-fab,
.transcript-fab {
z-index: 10001;
transition: bottom 0.25s cubic-bezier(0.4, 0, 0.2, 1), transform 0.3s ease;
}
.terminal-fab.sheet-open,
.voice-fab.sheet-open,
.transcript-fab.sheet-open {
bottom: calc(15vh + 100px);
}
.terminal-fab.keyboard-visible,
.voice-fab.keyboard-visible,
.transcript-fab.keyboard-visible {
bottom: 35vh;
}
.terminal-fab.keyboard-visible.sheet-open,
.voice-fab.keyboard-visible.sheet-open,
.transcript-fab.keyboard-visible.sheet-open {
bottom: 45vh;

View File

@@ -104,13 +104,6 @@ onMounted(() => {
</svg>
</RouterLink>
<RouterLink to="/terminal" class="toolbar-btn" :class="{ active: route.path === '/terminal' }" title="Terminal">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="4 17 10 11 4 5"/>
<line x1="12" y1="19" x2="20" y2="19"/>
</svg>
</RouterLink>
<RouterLink to="/tools" class="toolbar-btn" :class="{ active: route.path === '/tools' }" title="Tools">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z"/>

View File

@@ -21,8 +21,7 @@ const categoryTools: Record<ToolCategory, string[]> = {
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'],
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']
project: ['list_canvases', 'create_canvas', 'get_canvas', 'update_canvas', 'delete_canvas', 'clone_canvas', 'add_component_to_canvas', 'remove_component_from_canvas', 'get_canvas_components']
}
const categories = computed(() => {

View File

@@ -1,5 +1,5 @@
<script setup lang="ts">
import { ref, computed, watch } from 'vue'
import { ref, computed, watch, nextTick } from 'vue'
import VoiceMicButton from './VoiceMicButton.vue'
const props = defineProps<{
@@ -36,6 +36,17 @@ function handleSend() {
}
function handleKeydown(e: KeyboardEvent) {
if (e.key === 'Enter' && e.ctrlKey) {
e.preventDefault()
const ta = e.target as HTMLTextAreaElement
const start = ta.selectionStart
const end = ta.selectionEnd
input.value = input.value.slice(0, start) + '\n' + input.value.slice(end)
nextTick(() => {
ta.selectionStart = ta.selectionEnd = start + 1
})
return
}
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault()
handleSend()
@@ -70,7 +81,6 @@ watch(() => props.voiceTranscript, (newText) => {
class="input-field"
:style="{ maxHeight: maxH }"
:placeholder="notReady ? 'Starting terminal...' : processing ? 'Wait for agent to finish...' : 'Continue this conversation...'"
rows="1"
:disabled="processing || notReady"
@keydown="handleKeydown"
/>
@@ -171,7 +181,7 @@ watch(() => props.voiceTranscript, (newText) => {
field-sizing: content;
min-height: 1lh;
overflow-y: auto;
padding: 0.15rem 0.25rem;
padding: 0 0.25rem;
font-family: inherit;
}

View File

@@ -39,11 +39,6 @@ const router = createRouter({
name: 'source',
component: () => import('../pages/SourceCodePage.vue')
},
{
path: '/terminal',
name: 'terminal',
component: () => import('../pages/TerminalPage.vue')
},
{
path: '/tools',
name: 'tools',

View File

@@ -21,7 +21,6 @@ import {
createDatabaseHandlers,
createProjectCanvasHandlers,
createSourceCodeHandlers,
createTerminalHandlers,
createResponseHandlers,
createGitHandlers,
createTorchHandlers,
@@ -33,7 +32,7 @@ import { setRouter } from './tools/handlers/globalHandlers'
import { setGiteaCredentials, clearGiteaCredentials } from './tools/handlers/sourceCodeHandlers'
import { ALL_TOOL_METAS, getAllToolNames, type ToolCategory } from './tools/toolDefinitions'
export type PageName = 'home' | 'canvas' | 'components' | 'themes' | 'project-canvas' | 'database' | 'source' | 'terminal' | 'tools' | 'git' | 'agents' | 'transcript-debug'
export type PageName = 'home' | 'canvas' | 'components' | 'themes' | 'project-canvas' | 'database' | 'source' | 'tools' | 'git' | 'agents' | 'transcript-debug'
// Internal webmcp functions (not exported for external use)
let webmcpInstance: any = null
@@ -131,7 +130,6 @@ function getToolConfigs(): Map<string, ToolConfig> {
...createDatabaseHandlers(),
...createProjectCanvasHandlers(),
...createSourceCodeHandlers(),
...createTerminalHandlers(),
...createResponseHandlers(),
...createGitHandlers(),
...createTorchHandlers(),
@@ -155,25 +153,23 @@ 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', 'bubbleResponse'],
git: ['get_git_status', 'get_git_diff', 'compare_commits', 'git_log', 'get_git_branches'],
torch: ['list_torch_clients', 'get_torch_status', 'transfer_torch', 'request_torch', 'release_torch']
}
// Page to categories mapping
const pageCategories: Record<PageName, ToolCategory[]> = {
home: ['global', 'torch', 'canvas', 'component', 'project', 'terminal'],
canvas: ['global', 'torch', 'canvas', 'component', 'project', 'terminal'],
'project-canvas': ['global', 'torch', 'canvas', 'component', 'project', 'terminal'],
components: ['global', 'torch', 'component', 'terminal'],
themes: ['global', 'torch', 'theme', 'terminal'],
database: ['global', 'torch', 'database', 'terminal'],
source: ['global', 'torch', 'source', 'terminal'],
terminal: ['global', 'torch', 'terminal'],
tools: ['global', 'torch', 'terminal'],
git: ['global', 'torch', 'git', 'terminal'],
agents: ['global', 'torch', 'terminal'],
'transcript-debug': ['global', 'torch', 'terminal']
home: ['global', 'torch', 'canvas', 'component', 'project'],
canvas: ['global', 'torch', 'canvas', 'component', 'project'],
'project-canvas': ['global', 'torch', 'canvas', 'component', 'project'],
components: ['global', 'torch', 'component'],
themes: ['global', 'torch', 'theme'],
database: ['global', 'torch', 'database'],
source: ['global', 'torch', 'source'],
tools: ['global', 'torch'],
git: ['global', 'torch', 'git'],
agents: ['global', 'torch'],
'transcript-debug': ['global', 'torch']
}
let currentPage: PageName | null = null

View File

@@ -10,8 +10,6 @@ export { createThemeHandlers } from './themeHandlers'
export { createDatabaseHandlers } from './databaseHandlers'
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 { createGitHandlers } from './gitHandlers'
@@ -24,7 +22,7 @@ export type ToolHandler = (args: any) => string | Promise<string>
export interface ToolConfig {
name: string
description: string
category: 'global' | 'canvas' | 'component' | 'theme' | 'database' | 'source' | 'project' | 'terminal' | 'git' | 'torch'
category: 'global' | 'canvas' | 'component' | 'theme' | 'database' | 'source' | 'project' | 'git' | 'torch'
schema: object
handler: ToolHandler
}

View File

@@ -29,7 +29,7 @@ export function createResponseHandlers(): ToolConfig[] {
{
name: 'bubbleResponse',
description: 'Responde al usuario mostrando un mensaje en la UI (terminal flotante) en lugar de en Claude Code',
category: 'terminal',
category: 'global',
schema: {
type: 'object',
properties: {

View File

@@ -1,4 +1,4 @@
export type ToolCategory = 'global' | 'canvas' | 'component' | 'theme' | 'database' | 'source' | 'project' | 'terminal' | 'git' | 'torch'
export type ToolCategory = 'global' | 'canvas' | 'component' | 'theme' | 'database' | 'source' | 'project' | 'git' | 'torch'
export interface ToolMeta {
name: string
@@ -72,16 +72,6 @@ export const ALL_TOOL_METAS: ToolMeta[] = [
{ name: 'remove_component_from_canvas', description: 'Remueve un componente de un canvas', category: 'project' },
{ name: 'get_canvas_components', description: 'Obtiene los componentes de un canvas', category: 'project' },
// Terminal UI tools
{ name: 'terminal_open', description: 'Abre la ventana flotante del terminal', category: 'terminal' },
{ 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' },
// Response UI tools
{ name: 'bubbleResponse', description: 'Muestra un mensaje del agente en la UI', category: 'terminal' },
// Git tools
{ name: 'get_git_status', description: 'Obtiene el estado actual del repositorio Git', category: 'git' },
{ name: 'get_git_diff', description: 'Obtiene el diff de cambios no commiteados', category: 'git' },
@@ -121,7 +111,6 @@ export const CATEGORY_INFO: Record<ToolCategory, { label: string; color: string;
database: { label: 'Database', color: '#3b82f6', icon: 'M12 2C7 2 3 3.5 3 5v14c0 1.5 4 3 9 3s9-1.5 9-3V5c0-1.5-4-3-9-3z' },
source: { label: 'Source', color: '#8b5cf6', icon: 'M16 18l6-6-6-6M8 6l-6 6 6 6' },
project: { label: 'Project', color: '#06b6d4', icon: 'M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z' },
terminal: { label: 'Terminal', color: '#22c55e', icon: 'M4 17l6-6-6-6M12 19h8' },
git: { label: 'Git', color: '#f97316', icon: 'M6 3v12M18 9a3 3 0 1 0 0-6 3 3 0 0 0 0 6zM6 21a3 3 0 1 0 0-6 3 3 0 0 0 0 6zM18 9a9 9 0 0 1-9 9' },
torch: { label: 'Torch', color: '#eab308', icon: 'M8.5 14.5A2.5 2.5 0 0 0 11 12c0-1.38-.5-2-1-3-1.072-2.143-.224-4.054 2-6 .5 2.5 2 4.9 4 6.5 2 1.6 3 3.5 3 5.5a7 7 0 1 1-14 0c0-1.153.433-2.294 1-3a2.5 2.5 0 0 0 2.5 2.5z' }
}

View File

@@ -1,6 +1,6 @@
/**
* Singleton Whisper WebSocket Service
* One shared connection used by all voice components (FloatingVoice, useVoiceCapture, etc.)
* One shared connection used by all voice components (FloatingVoice, useVoiceInput, etc.)
*/
import { ref } from 'vue'

View File

@@ -107,7 +107,7 @@ export async function handleClaudeHook(req: Request): Promise<Response | null> {
console.error('[claude-hook] Failed to forward hook to terminal server:', e)
}
// 2. Forward PermissionRequest to /claude-permission so PromptBar WS listener picks it up
// 2. Forward PermissionRequest to /claude-permission for hooks approval system
if (body.hook_event_name === 'PermissionRequest') {
try {
await fetch(`http://localhost:${PORT_TERMINAL}/claude-permission`, {
@@ -120,7 +120,7 @@ export async function handleClaudeHook(req: Request): Promise<Response | null> {
}
}
// 3. Derive status and broadcast for backward compat (App.vue/AgentBar.vue)
// 3. Derive status and broadcast via WebSocket
const { status, tool } = deriveStatus(body)
try {
await fetch(`http://localhost:${PORT_TERMINAL}/claude-status`, {