diff --git a/frontend/src/components/AgentBar.vue b/frontend/src/components/AgentBar.vue deleted file mode 100644 index b0f6030..0000000 --- a/frontend/src/components/AgentBar.vue +++ /dev/null @@ -1,504 +0,0 @@ - - - - - diff --git a/frontend/src/components/FloatingTerminal.vue b/frontend/src/components/FloatingTerminal.vue deleted file mode 100644 index f69a848..0000000 --- a/frontend/src/components/FloatingTerminal.vue +++ /dev/null @@ -1,1004 +0,0 @@ - - - - - diff --git a/frontend/src/components/FloatingTranscriptDebug.vue b/frontend/src/components/FloatingTranscriptDebug.vue index db5eb99..7d39d76 100644 --- a/frontend/src/components/FloatingTranscriptDebug.vue +++ b/frontend/src/components/FloatingTranscriptDebug.vue @@ -606,6 +606,19 @@ onBeforeUnmount(() => { + - - - - - -
-
- - -
-
- - - - - Agent offline - Click to connect -
-
-
-
-
- Connecting... -
-
-
- - -
- - - - - - - - - - diff --git a/frontend/src/components/agent/ChatInput.vue b/frontend/src/components/agent/ChatInput.vue deleted file mode 100644 index 0dff8f5..0000000 --- a/frontend/src/components/agent/ChatInput.vue +++ /dev/null @@ -1,225 +0,0 @@ - - - - - diff --git a/frontend/src/components/agent/ConversationHistory.vue b/frontend/src/components/agent/ConversationHistory.vue deleted file mode 100644 index 6a1fced..0000000 --- a/frontend/src/components/agent/ConversationHistory.vue +++ /dev/null @@ -1,404 +0,0 @@ - - - - - diff --git a/frontend/src/components/agent/FloatBubble.vue b/frontend/src/components/agent/FloatBubble.vue deleted file mode 100644 index ea236dd..0000000 --- a/frontend/src/components/agent/FloatBubble.vue +++ /dev/null @@ -1,590 +0,0 @@ - - - - - diff --git a/frontend/src/components/agent/InputSettings.vue b/frontend/src/components/agent/InputSettings.vue deleted file mode 100644 index abb6daa..0000000 --- a/frontend/src/components/agent/InputSettings.vue +++ /dev/null @@ -1,351 +0,0 @@ - - - - - diff --git a/frontend/src/components/agent/PromptBar.vue b/frontend/src/components/agent/PromptBar.vue deleted file mode 100644 index 4bec7b4..0000000 --- a/frontend/src/components/agent/PromptBar.vue +++ /dev/null @@ -1,1770 +0,0 @@ - - - - - diff --git a/frontend/src/components/agent/ResponseCard.vue b/frontend/src/components/agent/ResponseCard.vue deleted file mode 100644 index 56e2a5a..0000000 --- a/frontend/src/components/agent/ResponseCard.vue +++ /dev/null @@ -1,142 +0,0 @@ - - - - - diff --git a/frontend/src/components/agent/TranscriptCard.vue b/frontend/src/components/agent/TranscriptCard.vue deleted file mode 100644 index 35d7eb7..0000000 --- a/frontend/src/components/agent/TranscriptCard.vue +++ /dev/null @@ -1,182 +0,0 @@ - - - - - diff --git a/frontend/src/components/transcript-debug/ChatContainer.vue b/frontend/src/components/transcript-debug/ChatContainer.vue index 6fab41b..49e58c1 100644 --- a/frontend/src/components/transcript-debug/ChatContainer.vue +++ b/frontend/src/components/transcript-debug/ChatContainer.vue @@ -73,8 +73,6 @@ function toggleSelectMode() { if (!selectMode.value) selectedUuids.value = new Set() } -defineExpose({ selectMode, toggleSelectMode }) - function toggleSelect(uuid: string) { if (!selectMode.value) return const s = new Set(selectedUuids.value) @@ -143,12 +141,21 @@ async function copySelected() { // ── Collapse sections ── const collapsedSections = ref(new Set()) +// Special user messages (interrupted, meta) are NOT section leaders — +// they belong to the previous normal user message's section. +function isSpecialUserMessage(msg: ConversationMessage): boolean { + if (msg.kind !== 'user') return false + if (msg.isMeta) return true + if (msg.content?.includes('[Request interrupted by user')) return true + return false +} + // For each message, find which user message "owns" it (section leader) const sectionMap = computed(() => { const map = new Map() // messageUuid → ownerUserUuid let currentUserUuid: string | null = null for (const msg of props.conversation.messages) { - if (msg.kind === 'user') { + if (msg.kind === 'user' && !isSpecialUserMessage(msg)) { currentUserUuid = msg.uuid } else if (currentUserUuid) { map.set(msg.uuid, currentUserUuid) @@ -157,12 +164,12 @@ const sectionMap = computed(() => { return map }) -// Count of non-user messages per section +// Count of non-leader messages per section const sectionCounts = computed(() => { const counts = new Map() let currentUserUuid: string | null = null for (const msg of props.conversation.messages) { - if (msg.kind === 'user') { + if (msg.kind === 'user' && !isSpecialUserMessage(msg)) { currentUserUuid = msg.uuid if (!counts.has(currentUserUuid)) counts.set(currentUserUuid, 0) } else if (currentUserUuid) { @@ -180,11 +187,38 @@ function toggleCollapse(userUuid: string) { } function isCollapsedChild(msg: { uuid: string; kind: string }): boolean { - if (msg.kind === 'user') return false + // Special user messages collapse with their section like any other child + if (msg.kind === 'user' && !isSpecialUserMessage(msg as ConversationMessage)) return false const owner = sectionMap.value.get(msg.uuid) return !!owner && collapsedSections.value.has(owner) } +// Collapse all user sections except the last one (only normal user messages) +const userUuids = computed(() => + props.conversation.messages + .filter(m => m.kind === 'user' && !isSpecialUserMessage(m)) + .map(m => m.uuid) +) + +const allCollapsed = computed(() => { + const uuids = userUuids.value + if (uuids.length <= 1) return false + const toCollapse = uuids.slice(0, -1) + return toCollapse.length > 0 && toCollapse.every(u => collapsedSections.value.has(u)) +}) + +function collapseAllExceptLast() { + const uuids = userUuids.value + if (uuids.length <= 1) return + if (allCollapsed.value) { + collapsedSections.value = new Set() + } else { + collapsedSections.value = new Set(uuids.slice(0, -1)) + } +} + +defineExpose({ selectMode, toggleSelectMode, allCollapsed, collapseAllExceptLast }) + // Track messages that just resolved from optimistic → real // These skip the bounce animation and get a smooth transition instead const resolvedUuids = ref(new Set()) diff --git a/frontend/src/composables/useAgentTerminal.ts b/frontend/src/composables/useAgentTerminal.ts deleted file mode 100644 index 6445668..0000000 --- a/frontend/src/composables/useAgentTerminal.ts +++ /dev/null @@ -1,421 +0,0 @@ -/** - * useAgentTerminal - * - * Composable for managing a per-agent terminal session. - * Wraps useTerminalRenderer with agent-specific WebSocket connection, - * agent lifecycle (start/stop), and prompt sending. - */ - -import { ref, computed, type Ref } from 'vue' -import { useTerminalRenderer, type TerminalRenderer } from './useTerminalRenderer' -import { agentTerminalUrl, terminalApiUrl } from '../config/endpoints' - -export type AgentTerminalState = 'off' | 'connecting' | 'ready' | 'crashed' | 'agent-starting' - -export interface AgentTerminal { - // State - terminalState: Ref - connected: Ref - agentRunning: Ref - sessionId: Ref - - // Container ref - bind this to the xterm DOM element in the component - containerRef: Ref - - // Terminal renderer (for mounting xterm) - renderer: TerminalRenderer - - // Connection - connect: () => void - disconnect: () => void - - // Agent lifecycle - startAgent: (force?: boolean) => Promise - stopAgent: () => Promise - checkStatus: () => Promise - - // Prompt - sendPrompt: (text: string) => void - - // Direct PTY input (char-by-char, no agent auto-start) - sendInput: (text: string) => void - - // Raw PTY input (single write, no \r appended) - sendRaw: (data: string) => void - - // Cleanup - dispose: () => void -} - -export function useAgentTerminal(agentId: string): AgentTerminal { - const containerRef = ref(null) - const connected = ref(false) - const connecting = ref(false) - const agentRunning = ref(false) - const agentStarting = ref(false) - const crashed = ref(false) - const sessionId = ref(null) - - let socket: WebSocket | null = null - let reconnectTimeout: number | null = null - let reconnectAttempts = 0 - let pendingPrompt: string | null = null - const MAX_RECONNECT = 10 - const RECONNECT_DELAY = 2000 - - const terminalState = computed(() => { - if (agentStarting.value) return 'agent-starting' - if (crashed.value) return 'crashed' - if (connected.value && agentRunning.value) return 'ready' - if (connecting.value) return 'connecting' - return 'off' - }) - - // Terminal renderer - const renderer = useTerminalRenderer({ - container: containerRef, - onData: (data) => { - if (socket?.readyState === WebSocket.OPEN) { - socket.send(JSON.stringify({ type: 'input', data })) - } - }, - onResize: (cols, rows) => { - if (socket?.readyState === WebSocket.OPEN) { - socket.send(JSON.stringify({ type: 'resize', cols, rows })) - } - }, - onKeyEvent: (e) => { - // Ctrl+V: Paste - if (e.ctrlKey && e.key === 'v' && e.type === 'keydown') { - e.preventDefault() - navigator.clipboard.readText().then((text) => { - if (text && socket?.readyState === WebSocket.OPEN) { - socket.send(JSON.stringify({ type: 'input', data: text })) - } - }).catch(console.error) - return false - } - // Ctrl+C: Copy selection - if (e.ctrlKey && e.key === 'c' && e.type === 'keydown') { - const selection = renderer.getSelection() - if (selection) { - navigator.clipboard.writeText(selection).catch(console.error) - return false - } - } - return true - } - }) - - // ── WebSocket connection ── - - function connect() { - if (connecting.value || connected.value) return - connecting.value = true - crashed.value = false - - const wsUrl = agentTerminalUrl(agentId) - console.log(`[AgentTerminal:${agentId}] Connecting to ${wsUrl}`) - - const timeout = window.setTimeout(() => { - if (connecting.value && !connected.value) { - connecting.value = false - socket?.close() - socket = null - scheduleReconnect() - } - }, 10000) - - try { - socket = new WebSocket(wsUrl) - socket.addEventListener('open', () => clearTimeout(timeout), { once: true }) - - socket.onopen = () => { - connected.value = true - connecting.value = false - reconnectAttempts = 0 - - // Send initial resize - const term = renderer.terminal.value - if (term) { - socket?.send(JSON.stringify({ type: 'resize', cols: term.cols, rows: term.rows })) - } - } - - socket.onmessage = (event) => { - try { - const msg = JSON.parse(event.data) - handleMessage(msg) - } catch { /* ignore parse errors */ } - } - - socket.onclose = () => { - connected.value = false - connecting.value = false - socket = null - - if (reconnectAttempts < MAX_RECONNECT) { - scheduleReconnect() - } - } - - socket.onerror = () => { - connecting.value = false - } - } catch { - connecting.value = false - scheduleReconnect() - } - } - - function disconnect() { - cancelReconnect() - if (socket) { - socket.onclose = null - socket.close() - socket = null - } - connected.value = false - connecting.value = false - } - - function scheduleReconnect() { - if (reconnectTimeout) clearTimeout(reconnectTimeout) - reconnectAttempts++ - const delay = RECONNECT_DELAY * Math.min(reconnectAttempts, 5) - reconnectTimeout = window.setTimeout(() => { - if (!connected.value && !connecting.value) { - connect() - } - }, delay) - } - - function cancelReconnect() { - if (reconnectTimeout) { - clearTimeout(reconnectTimeout) - reconnectTimeout = null - } - reconnectAttempts = 0 - } - - // ── Message handling ── - - function handleMessage(msg: any) { - switch (msg.type) { - case 'connected': - sessionId.value = msg.sessionId - if (msg.hasHistory) { - // Session has existing output, request replay - setTimeout(() => requestReplay(), 50) - // Check if agent is actually running - checkStatus() - } else if (msg.isNew) { - // Brand new session, auto-start agent - autoStartAgent() - } - break - - case 'replay': - renderer.handleReplay(msg.data || '') - break - - case 'output': - renderer.write(msg.data) - break - - case 'exit': - renderer.write(msg.data) - agentRunning.value = false - crashed.value = true - agentStarting.value = false - sessionId.value = null - break - - case 'error': - renderer.writeln(`\x1b[31mError: ${msg.message}\x1b[0m`) - break - - case 'session-restart': - renderer.reset() - renderer.writeln('\x1b[33m[Session restarting...]\x1b[0m') - break - - case 'claude-status': - if (matchesAgent(msg.agent)) { - if (msg.status === 'sessionStart') { - agentRunning.value = true - agentStarting.value = false - crashed.value = false - } - } - break - - case 'buffer-cleared': - break - } - } - - function matchesAgent(name: string): boolean { - if (!name) return agentId === 'main' - return name === agentId || name === 'main' && agentId === 'main' - } - - function requestReplay(tailOnly = true) { - if (socket?.readyState === WebSocket.OPEN) { - socket.send(JSON.stringify({ type: 'request-replay', tailOnly, chunks: 200 })) - } - } - - // ── Agent lifecycle ── - - async function autoStartAgent() { - agentStarting.value = true - try { - await startAgent() - } catch (e) { - console.error(`[AgentTerminal:${agentId}] Auto-start failed:`, e) - agentStarting.value = false - } - } - - async function startAgent(force = false) { - agentStarting.value = true - crashed.value = false - - try { - const res = await fetch(terminalApiUrl('/start-agent'), { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ agentId, force }) - }) - if (!res.ok) { agentStarting.value = false; return } - const contentType = res.headers.get('content-type') || '' - if (!contentType.includes('application/json')) { agentStarting.value = false; return } - const data = await res.json() - if (data.success) { - agentRunning.value = true - // agentStarting stays true until sessionStart is received - // Flush pending prompt - if (pendingPrompt) { - setTimeout(() => { - flushPendingPrompt() - }, 500) - } - } else { - agentStarting.value = false - } - } catch (e) { - console.error(`[AgentTerminal:${agentId}] Start failed:`, e) - agentStarting.value = false - } - } - - async function stopAgent() { - try { - const res = await fetch(terminalApiUrl('/stop-agent'), { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ agentId }) - }) - if (res.ok) { - agentRunning.value = false - agentStarting.value = false - } - } catch (e) { - console.error(`[AgentTerminal:${agentId}] Stop failed:`, e) - } - } - - async function checkStatus() { - try { - const res = await fetch(terminalApiUrl('/agent-sessions')) - if (!res.ok) return - const contentType = res.headers.get('content-type') || '' - if (!contentType.includes('application/json')) return - const data = await res.json() - const state = data[agentId] - if (state) { - agentRunning.value = state.isAgentRunning - if (!state.isAgentRunning && state.sessionExists) { - // Session exists but agent exited - crashed.value = true - } - } - } catch (e) { - console.error(`[AgentTerminal:${agentId}] Status check failed:`, e) - } - } - - // ── Prompt sending ── - - function typeTextToSocket(text: string) { - const chars = (text + '\r').split('') - let i = 0 - const typeChar = () => { - if (i < chars.length && socket?.readyState === WebSocket.OPEN) { - socket.send(JSON.stringify({ type: 'input', data: chars[i] })) - i++ - setTimeout(typeChar, 15) - } - } - typeChar() - } - - function sendPrompt(text: string) { - if (socket?.readyState === WebSocket.OPEN && (agentRunning.value || agentStarting.value)) { - typeTextToSocket(text) - } else if (!connected.value) { - // Queue prompt and auto-connect + start - pendingPrompt = text - connect() - } else if (connected.value && !agentRunning.value) { - // Connected but agent not running, start it and queue - pendingPrompt = text - startAgent() - } - } - - function sendInput(text: string) { - if (socket?.readyState === WebSocket.OPEN) { - typeTextToSocket(text) - } - } - - function sendRaw(data: string) { - if (socket?.readyState === WebSocket.OPEN) { - socket.send(JSON.stringify({ type: 'input', data })) - } - } - - function flushPendingPrompt() { - if (pendingPrompt && socket?.readyState === WebSocket.OPEN) { - typeTextToSocket(pendingPrompt) - pendingPrompt = null - } - } - - // ── Cleanup ── - - function dispose() { - disconnect() - renderer.dispose() - } - - return { - terminalState, - connected, - agentRunning, - sessionId, - containerRef, - renderer, - connect, - disconnect, - startAgent, - stopAgent, - checkStatus, - sendPrompt, - sendInput, - sendRaw, - dispose - } -} diff --git a/frontend/src/composables/useVoiceCapture.ts b/frontend/src/composables/useVoiceCapture.ts deleted file mode 100644 index a901536..0000000 --- a/frontend/src/composables/useVoiceCapture.ts +++ /dev/null @@ -1,397 +0,0 @@ -import { ref, watch, type Ref } from 'vue' -import { - initWhisperSocket, - sendAudio, - onTranscription, - getWhisperStatus, - isConnected -} from '../services/whisperSocket' -export type WhisperStatus = 'offline' | 'loading' | 'ready' - -export interface VoiceCapture { - // State - isRecording: Ref - transcript: Ref - interimTranscript: Ref - animatedTranscript: Ref - error: Ref - voiceMode: Ref<'whisper'> - whisperStatus: Ref - audioDevices: Ref - selectedDeviceId: Ref - isAndroid: Ref - lastAudioUrl: Ref - isPlayingAudio: Ref - // Actions - startRecording: () => void - stopRecording: () => void - loadAudioDevices: (skipPermission?: boolean) => Promise - selectMicrophone: (deviceId: string) => void - playLastAudio: () => void - init: () => Promise - cleanup: () => void - clearTranscript: () => void -} - -const GPU_TIMEOUT_MS = 30_000 // 30s timeout waiting for GPU - -export function useVoiceCapture(options?: { - onNotification?: (message: string, type: 'info' | 'success' | 'error', duration?: number) => void -}): VoiceCapture { - const notify = options?.onNotification || (() => {}) - - // ====== State ====== - const isRecording = ref(false) - const transcript = ref('') - const interimTranscript = ref('') - const animatedTranscript = ref('') - const error = ref('') - const voiceMode = ref<'whisper'>('whisper') // Always whisper, no web speech - const audioDevices = ref([]) - const selectedDeviceId = ref('') - const isAndroid = ref(false) - const lastAudioUrl = ref('') - const isPlayingAudio = ref(false) - - // ====== Internal ====== - const sharedWhisperStatus = getWhisperStatus() - const whisperStatus = ref(sharedWhisperStatus.value) - let mediaRecorder: MediaRecorder | null = null - let audioChunks: Blob[] = [] - let chunkInterval: number | null = null - const CHUNK_INTERVAL_MS = 3000 - let mediaStream: MediaStream | null = null - let supportedMimeType = 'audio/webm;codecs=opus' - let audioElement: HTMLAudioElement | null = null - let recordingStartTime = 0 - let unsubTranscription: (() => void) | null = null - let gpuTimeout: number | null = null - - // Typing animation - let typingTimeout: number | null = null - let lastAnimatedLength = 0 - - // Keep local status in sync with shared - watch(sharedWhisperStatus, (val) => { - whisperStatus.value = val - }) - - // ====== Mobile / Audio Format ====== - - function checkMobile() { - isAndroid.value = /Android/i.test(navigator.userAgent) - } - - function detectAudioFormat(): string { - const formats = [ - 'audio/webm;codecs=opus', 'audio/webm', - 'audio/mp4', 'audio/mp4;codecs=mp4a.40.2', - 'audio/aac', 'audio/ogg;codecs=opus', 'audio/wav' - ] - for (const f of formats) { - if (MediaRecorder.isTypeSupported(f)) { - console.log(`[VoiceCapture] Audio format: ${f}`) - return f - } - } - return '' - } - - // ====== Whisper transcription handler ====== - - function handleTranscription(msg: any) { - if (!isRecording.value) return - - if (msg.success && msg.text) { - const fullText = msg.text.trim() - transcript.value = fullText + ' ' - interimTranscript.value = '' - if (!msg.partial) { - console.log(`[VoiceCapture] WHISPER (${msg.model}/${msg.device}):`, fullText) - } - } else if (msg.error) { - error.value = msg.error - console.error('[VoiceCapture] Whisper error:', msg.error) - } - } - - // ====== Recording ====== - - function startRecording() { - error.value = '' - - // Start capturing audio immediately, regardless of GPU status - startMediaRecorder() - - // If GPU not ready yet, start timeout - if (!isConnected()) { - console.log('[VoiceCapture] Recording started, waiting for GPU...') - gpuTimeout = window.setTimeout(() => { - if (isRecording.value && !isConnected()) { - error.value = 'Whisper GPU timeout — server not available' - notify('Whisper GPU not available', 'error') - stopRecording() - } - }, GPU_TIMEOUT_MS) - } - } - - async function startMediaRecorder() { - try { - const audioConstraints: MediaTrackConstraints = { - echoCancellation: true, - noiseSuppression: true, - autoGainControl: true, - ...(selectedDeviceId.value ? { deviceId: { exact: selectedDeviceId.value } } : {}) - } - - mediaStream = await navigator.mediaDevices.getUserMedia({ audio: audioConstraints }) - - const recorderOptions: MediaRecorderOptions = {} - if (supportedMimeType) { - recorderOptions.mimeType = supportedMimeType - } - - mediaRecorder = new MediaRecorder(mediaStream, recorderOptions) - console.log(`[VoiceCapture] MediaRecorder using: ${mediaRecorder.mimeType}`) - - audioChunks = [] - - mediaRecorder.ondataavailable = (event) => { - if (event.data.size > 0) { - audioChunks.push(event.data) - } - } - - mediaRecorder.start(100) - isRecording.value = true - recordingStartTime = Date.now() - - // Reload devices with labels now that we have permission - loadAudioDevices(true) - - // Send chunks periodically — only when GPU is connected - chunkInterval = window.setInterval(() => { - if (audioChunks.length > 0 && isConnected()) { - // GPU came online — clear timeout if still pending - if (gpuTimeout) { - clearTimeout(gpuTimeout) - gpuTimeout = null - } - sendAudioChunk(false) - } - }, CHUNK_INTERVAL_MS) - } catch (e: any) { - error.value = `Microphone error: ${e.message}` - console.error('[VoiceCapture] Microphone error:', e) - } - } - - function sendAudioChunk(isFinal: boolean) { - if (audioChunks.length === 0) return - if (!isConnected()) { - console.log('[VoiceCapture] GPU not connected, holding audio') - return - } - - const mimeType = mediaRecorder?.mimeType || supportedMimeType || 'audio/webm' - const audioBlob = new Blob(audioChunks, { type: mimeType }) - - if (audioBlob.size < 5000) { - if (isFinal) audioChunks = [] - return - } - - if (isFinal) { - audioChunks = [] - saveAudioForPlayback(audioBlob) - } - - const reader = new FileReader() - reader.onloadend = () => { - const base64 = (reader.result as string).split(',')[1] - sendAudio(base64, 'es', !isFinal) - } - reader.readAsDataURL(audioBlob) - } - - function stopRecording() { - if (gpuTimeout) { - clearTimeout(gpuTimeout) - gpuTimeout = null - } - if (chunkInterval) { - clearInterval(chunkInterval) - chunkInterval = null - } - - // Send final chunk (only if GPU is connected) - if (audioChunks.length > 0) { - sendAudioChunk(true) - } - - if (mediaRecorder && mediaRecorder.state !== 'inactive') { - mediaRecorder.stop() - } - if (mediaStream) { - mediaStream.getTracks().forEach(track => track.stop()) - mediaStream = null - } - - isRecording.value = false - interimTranscript.value = '' - } - - // ====== Audio Save & Playback ====== - - function currentMicName(): string { - if (!selectedDeviceId.value) return 'Default' - const device = audioDevices.value.find(d => d.deviceId === selectedDeviceId.value) - return device?.label || 'Microphone' - } - - function saveAudioForPlayback(blob: Blob) { - if (lastAudioUrl.value) URL.revokeObjectURL(lastAudioUrl.value) - lastAudioUrl.value = URL.createObjectURL(blob) - saveRecordingToBackend(blob) - } - - async function saveRecordingToBackend(blob: Blob) { - try { - const duration_ms = Date.now() - recordingStartTime - const reader = new FileReader() - reader.onloadend = async () => { - const base64 = (reader.result as string).split(',')[1] - const response = await fetch('/api/recordings', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - audio: base64, - transcription: transcript.value.trim(), - microphone: currentMicName(), - duration_ms - }) - }) - const data = await response.json() - if (data.success) { - console.log(`[VoiceCapture] Recording saved: ${data.filename}`) - } - } - reader.readAsDataURL(blob) - } catch (e) { - console.error('[VoiceCapture] Error saving recording:', e) - } - } - - function playLastAudio() { - if (!lastAudioUrl.value) return - if (isPlayingAudio.value && audioElement) { - audioElement.pause() - audioElement.currentTime = 0 - isPlayingAudio.value = false - return - } - audioElement = new Audio(lastAudioUrl.value) - audioElement.onplay = () => { isPlayingAudio.value = true } - audioElement.onended = () => { isPlayingAudio.value = false } - audioElement.onpause = () => { isPlayingAudio.value = false } - audioElement.play().catch(() => { isPlayingAudio.value = false }) - } - - // ====== Microphone ====== - - async function loadAudioDevices(skipPermissionRequest = false) { - try { - if (!skipPermissionRequest) { - const tempStream = await navigator.mediaDevices.getUserMedia({ audio: true }) - tempStream.getTracks().forEach(track => track.stop()) - } - const devices = await navigator.mediaDevices.enumerateDevices() - audioDevices.value = devices.filter(d => d.kind === 'audioinput') - if (!selectedDeviceId.value && audioDevices.value.length > 0) { - selectedDeviceId.value = audioDevices.value[0]?.deviceId || '' - } - } catch (e) { - console.error('[VoiceCapture] Failed to enumerate devices:', e) - } - } - - function selectMicrophone(deviceId: string) { - selectedDeviceId.value = deviceId - if (isRecording.value) { - stopRecording() - setTimeout(() => startRecording(), 100) - } - } - - // ====== Typing Animation ====== - - function animateTyping(targetText: string) { - if (typingTimeout) { clearTimeout(typingTimeout); typingTimeout = null } - if (targetText.length < animatedTranscript.value.length) { - animatedTranscript.value = targetText - lastAnimatedLength = targetText.length - return - } - const startIndex = lastAnimatedLength - function typeNext(index: number) { - if (index <= targetText.length) { - animatedTranscript.value = targetText.substring(0, index) - lastAnimatedLength = index - if (index < targetText.length) { - typingTimeout = window.setTimeout(() => typeNext(index + 1), 15 + Math.random() * 10) - } - } - } - typeNext(startIndex) - } - - watch(transcript, (v) => animateTyping(v)) - - // ====== Transcript ====== - - function clearTranscript() { - transcript.value = '' - interimTranscript.value = '' - animatedTranscript.value = '' - lastAnimatedLength = 0 - if (typingTimeout) { clearTimeout(typingTimeout); typingTimeout = null } - } - - // ====== Lifecycle ====== - - async function init() { - checkMobile() - supportedMimeType = detectAudioFormat() - await loadAudioDevices(true) - - // Subscribe to shared whisper transcriptions - if (!unsubTranscription) { - unsubTranscription = onTranscription(handleTranscription) - } - - // Initialize shared Whisper socket (singleton, safe to call multiple times) - initWhisperSocket() - console.log('[VoiceCapture] Initialized (Whisper-only, record-first)') - } - - function cleanup() { - stopRecording() - if (unsubTranscription) { unsubTranscription(); unsubTranscription = null } - if (chunkInterval) clearInterval(chunkInterval) - if (typingTimeout) clearTimeout(typingTimeout) - if (gpuTimeout) clearTimeout(gpuTimeout) - if (mediaStream) { mediaStream.getTracks().forEach(t => t.stop()); mediaStream = null } - if (audioElement) { audioElement.pause(); audioElement = null } - if (lastAudioUrl.value) { URL.revokeObjectURL(lastAudioUrl.value); lastAudioUrl.value = '' } - isPlayingAudio.value = false - } - - return { - isRecording, transcript, interimTranscript, animatedTranscript, - error, voiceMode, whisperStatus, audioDevices, selectedDeviceId, - isAndroid, lastAudioUrl, isPlayingAudio, - startRecording, stopRecording, loadAudioDevices, selectMicrophone, - playLastAudio, init, cleanup, clearTranscript - } -} diff --git a/frontend/src/pages/TerminalPage.vue b/frontend/src/pages/TerminalPage.vue deleted file mode 100644 index 7828c08..0000000 --- a/frontend/src/pages/TerminalPage.vue +++ /dev/null @@ -1,384 +0,0 @@ - - - - - diff --git a/frontend/src/services/tools/handlers/terminalHandlers.ts b/frontend/src/services/tools/handlers/terminalHandlers.ts deleted file mode 100644 index 705e2e9..0000000 --- a/frontend/src/services/tools/handlers/terminalHandlers.ts +++ /dev/null @@ -1,119 +0,0 @@ -/** - * Terminal UI control handlers - * Controls the FloatingTerminal window (open, close, move, resize) - */ - -import type { ToolConfig } from './index' - -export interface TerminalControls { - open: (x?: number, y?: number) => void - close: () => void - toggle: () => void - move: (x: number, y: number) => void - resize: (width: number, height: number) => void - getState: () => { isOpen: boolean; position: { x: number; y: number }; size: { w: number; h: number } } -} - -// Global reference to terminal controls (set by App.vue) -let terminalControls: TerminalControls | null = null - -export function setTerminalControls(controls: TerminalControls) { - terminalControls = controls - ;(window as any).__terminalControls = controls -} - -export function getTerminalControls(): TerminalControls | null { - return terminalControls -} - -export function createTerminalHandlers(): ToolConfig[] { - return [ - { - name: 'terminal_open', - description: 'Abre la ventana flotante del terminal. Opcionalmente en una posicion especifica.', - category: 'terminal', - schema: { - type: 'object', - properties: { - x: { type: 'number', description: 'Posicion X en pixels (opcional)' }, - y: { type: 'number', description: 'Posicion Y en pixels (opcional)' } - } - }, - handler: (args: { x?: number; y?: number }) => { - if (!terminalControls) return 'Error: Terminal controls not initialized' - terminalControls.open(args.x, args.y) - const pos = args.x !== undefined && args.y !== undefined - ? ` en posicion (${args.x}, ${args.y})` - : '' - return `Terminal abierta${pos}` - } - }, - { - name: 'terminal_close', - description: 'Cierra la ventana flotante del terminal.', - category: 'terminal', - schema: { - type: 'object', - properties: {} - }, - handler: () => { - if (!terminalControls) return 'Error: Terminal controls not initialized' - terminalControls.close() - return 'Terminal cerrada' - } - }, - { - name: 'terminal_toggle', - description: 'Alterna el estado de la ventana del terminal (abre si esta cerrada, cierra si esta abierta).', - category: 'terminal', - schema: { - type: 'object', - properties: {} - }, - handler: () => { - if (!terminalControls) return 'Error: Terminal controls not initialized' - const wasOpen = terminalControls.getState().isOpen - terminalControls.toggle() - return wasOpen ? 'Terminal cerrada' : 'Terminal abierta' - } - }, - { - name: 'terminal_move', - description: 'Mueve la ventana del terminal a una posicion especifica en pixels.', - category: 'terminal', - schema: { - type: 'object', - properties: { - x: { type: 'number', description: 'Posicion X en pixels' }, - y: { type: 'number', description: 'Posicion Y en pixels' } - }, - required: ['x', 'y'] - }, - handler: (args: { x: number; y: number }) => { - if (!terminalControls) return 'Error: Terminal controls not initialized' - terminalControls.move(args.x, args.y) - return `Terminal movida a (${args.x}, ${args.y})` - } - }, - { - name: 'terminal_resize', - description: 'Cambia el tamano de la ventana del terminal.', - category: 'terminal', - schema: { - type: 'object', - properties: { - width: { type: 'number', description: 'Ancho en pixels (min 400)' }, - height: { type: 'number', description: 'Alto en pixels (min 250)' } - }, - required: ['width', 'height'] - }, - handler: (args: { width: number; height: number }) => { - if (!terminalControls) return 'Error: Terminal controls not initialized' - const w = Math.max(400, args.width) - const h = Math.max(250, args.height) - terminalControls.resize(w, h) - return `Terminal redimensionada a ${w}x${h}` - } - } - ] -} diff --git a/frontend/src/types/agent.ts b/frontend/src/types/agent.ts deleted file mode 100644 index 76217d4..0000000 --- a/frontend/src/types/agent.ts +++ /dev/null @@ -1,67 +0,0 @@ -export type ClaudeStatus = - | 'idle' - | 'processing' - | 'toolUse' - | 'toolDone' - | 'reading' - | 'writing' - | 'sessionStart' - | 'subagentStart' - | 'subagentStop' - | 'notification' - | 'permissionRequest' - | 'thinking' - -export interface AgentStatusState { - isProcessing: boolean - isReading: boolean - isWriting: boolean - awaitingPermission: boolean - showToolFlash: boolean - showNotification: boolean - currentTool: string | null -} - -export interface UiConfig { - label: string - shortLabel: string - color: string - gradient: string - terminalBg: string - terminalBorder: string - enabled: boolean - command?: string -} - -export interface Agent { - id: string - name: string - directory: string - uiConfig: UiConfig | null -} - -export interface ConversationEntry { - id: string - role: 'user' | 'agent' - content: string - timestamp: string - method: 'text' | 'voice' -} - -export interface TranscriptSession { - id: string - startTime: string - messageCount: number - model: string -} - -export interface TranscriptMessage { - uuid: string - role: 'user' | 'assistant' - content: string - timestamp: string - isMeta: boolean - tokens?: { input: number; output: number } - toolCalls?: string[] - hasThinking: boolean -}