feat: Add FloatingTranscriptDebug with pixel art dark theme

Floating chat window reusing ChatContainer with draggable/resizable
window, agent/session selector overlay, and pixel art decorations
(galaxy, minecraft dirt block, LED strip) on black transparent backdrop.
This commit is contained in:
2026-02-19 03:35:53 -06:00
parent 06b48ebda3
commit badde06ef9
2 changed files with 1005 additions and 6 deletions

View File

@@ -6,6 +6,7 @@ import TorchButton from './components/TorchButton.vue'
import FloatingTerminal from './components/FloatingTerminal.vue'
import FloatingResponse from './components/FloatingResponse.vue'
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'
@@ -23,6 +24,7 @@ const route = useRoute()
const router = useRouter()
const showTerminal = ref(false)
const showVoice = ref(false)
const showTranscriptDebug = ref(false)
const showDebugConsole = ref(false)
const toolbarVisible = ref(true)
const forceWco = ref(false)
@@ -450,7 +452,7 @@ watch(() => route.name, (newPage) => {
'session-start': showSessionStart,
notification: showNotification,
'tool-flash': showToolFlash,
'sheet-open': showTerminal || showVoice,
'sheet-open': showTerminal || showVoice || showTranscriptDebug,
'keyboard-visible': keyboardVisible
}"
@click="showTerminal = !showTerminal"
@@ -521,10 +523,22 @@ watch(() => route.name, (newPage) => {
</template>
</button>
<!-- Transcript Debug FAB Button -->
<button
class="transcript-fab"
:class="{ active: showTranscriptDebug, 'sheet-open': showTerminal || showVoice || showTranscriptDebug, 'keyboard-visible': keyboardVisible }"
@click="showTranscriptDebug = !showTranscriptDebug"
title="Transcript Debug"
>
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<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>
</button>
<!-- Voice FAB Button -->
<button
class="voice-fab"
:class="{ active: showVoice, 'sheet-open': showTerminal || showVoice, 'ptt-active': voicePTTActive, 'keyboard-visible': keyboardVisible }"
:class="{ active: showVoice, 'sheet-open': showTerminal || showVoice || showTranscriptDebug, 'ptt-active': voicePTTActive, 'keyboard-visible': keyboardVisible }"
@click="handleVoiceFabClick"
@touchstart="handleVoiceFabTouchStart"
@touchend="handleVoiceFabTouchEnd"
@@ -552,6 +566,9 @@ watch(() => route.name, (newPage) => {
<!-- Floating Voice Input -->
<FloatingVoice ref="voiceRef" v-model="showVoice" />
<!-- Floating Transcript Debug -->
<FloatingTranscriptDebug v-model="showTranscriptDebug" />
<!-- Global Hooks Approval Modal -->
<HooksApprovalModal />
@@ -1309,6 +1326,49 @@ watch(() => route.name, (newPage) => {
50% { box-shadow: 0 0 50px rgba(249, 115, 22, 0.9); }
}
/* Transcript Debug FAB */
.transcript-fab {
position: fixed;
bottom: 20px;
left: 80px;
width: 44px;
height: 44px;
border-radius: 4px;
background: rgba(15, 15, 20, 0.85);
color: #818cf8;
border: 1px solid rgba(99, 102, 241, 0.2);
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.4);
transition: all 0.2s ease;
z-index: 9998;
-webkit-user-select: none;
user-select: none;
-webkit-touch-callout: none;
touch-action: manipulation;
backdrop-filter: blur(12px);
}
.transcript-fab:hover {
transform: translateY(-2px);
border-color: rgba(99, 102, 241, 0.4);
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.5), 0 0 12px rgba(99, 102, 241, 0.15);
color: #a5b4fc;
}
.transcript-fab.active {
background: rgba(99, 102, 241, 0.15);
border-color: rgba(99, 102, 241, 0.4);
color: #c7d2fe;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.4), 0 0 8px rgba(99, 102, 241, 0.2);
}
.transcript-fab.active:hover {
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.5), 0 0 12px rgba(99, 102, 241, 0.3);
}
@media (max-width: 768px) {
.terminal-fab {
bottom: 80px;
@@ -1340,28 +1400,39 @@ watch(() => route.name, (newPage) => {
width: 44px;
height: 44px;
}
.transcript-fab {
bottom: 80px;
left: 68px;
width: 40px;
height: 40px;
}
}
/* Mobile: FABs above bottom sheets */
@media (max-width: 1024px) and (pointer: coarse) {
.terminal-fab,
.voice-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 {
.voice-fab.sheet-open,
.transcript-fab.sheet-open {
bottom: calc(15vh + 100px);
}
.terminal-fab.keyboard-visible,
.voice-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 {
.voice-fab.keyboard-visible.sheet-open,
.transcript-fab.keyboard-visible.sheet-open {
bottom: 45vh;
}
}

View File

@@ -0,0 +1,928 @@
<script setup lang="ts">
import { ref, computed, watch, onMounted, onBeforeUnmount, nextTick } from 'vue'
import { useTranscriptDebug } from '@/composables/transcript-debug'
import { ChatContainer } from '@/components/transcript-debug'
import type { AgentName } from '@/types/transcript-debug'
const props = defineProps<{
modelValue: boolean
}>()
const emit = defineEmits<{
'update:modelValue': [value: boolean]
}>()
const isOpen = computed({
get: () => props.modelValue,
set: (val) => emit('update:modelValue', val)
})
// ============================================================================
// TRANSCRIPT DEBUG STATE (composable)
// ============================================================================
const {
selectedAgent,
sessions,
selectedSessionId,
conversation,
loading,
error,
isRealtime,
processing,
init,
switchAgent,
selectSession,
disconnectRealtime,
sendPrompt
} = useTranscriptDebug()
const agents: { id: AgentName; label: string }[] = [
{ id: 'ejecutor', label: 'Ejecutor' },
{ id: 'nucleo000', label: 'nucleo000' },
{ id: 'claude', label: 'Claude' }
]
const showSelector = ref(false)
let initialized = false
// ============================================================================
// DRAG STATE
// ============================================================================
const windowRef = ref<HTMLElement | null>(null)
const isDragging = ref(false)
const position = ref({ x: 0, y: 0 })
const hasCustomPosition = ref(false)
const dragOffset = ref({ x: 0, y: 0 })
// Resize state
const isResizing = ref(false)
const size = ref({ w: 480, h: 600 })
// Mobile bottom sheet state
const isMobile = ref(false)
const sheetHeight = ref(55)
const isDraggingSheet = ref(false)
const sheetDragStart = ref({ y: 0, height: 0 })
const snapPoints = [20, 55, 85]
// ============================================================================
// MOBILE DETECTION
// ============================================================================
function checkMobile() {
const isTouchDevice = 'ontouchstart' in window || navigator.maxTouchPoints > 0
const isSmallScreen = window.innerWidth <= 1024
isMobile.value = isTouchDevice && isSmallScreen
}
function findNearestSnap(height: number): number {
return snapPoints.reduce((prev, curr) =>
Math.abs(curr - height) < Math.abs(prev - height) ? curr : prev
)
}
// ============================================================================
// MOBILE SHEET DRAG
// ============================================================================
function startSheetDrag(e: TouchEvent) {
if (!isMobile.value) return
const touch = e.touches[0]
if (!touch) return
isDraggingSheet.value = true
sheetDragStart.value = { y: touch.clientY, height: sheetHeight.value }
}
function onSheetDrag(e: TouchEvent) {
if (!isDraggingSheet.value || !isMobile.value) return
const touch = e.touches[0]
if (!touch) return
const deltaY = sheetDragStart.value.y - touch.clientY
const deltaPercent = (deltaY / window.innerHeight) * 100
sheetHeight.value = Math.max(15, Math.min(90, sheetDragStart.value.height + deltaPercent))
}
function stopSheetDrag() {
if (!isDraggingSheet.value) return
isDraggingSheet.value = false
sheetHeight.value = findNearestSnap(sheetHeight.value)
}
// ============================================================================
// WINDOW DRAG
// ============================================================================
function startDrag(e: MouseEvent | TouchEvent) {
if ((e.target as HTMLElement).closest('.window-controls')) return
if ((e.target as HTMLElement).closest('.selector-overlay')) return
if (isMobile.value) {
if (e instanceof TouchEvent) startSheetDrag(e)
return
}
isDragging.value = true
const rect = windowRef.value?.getBoundingClientRect()
const touch = e instanceof TouchEvent ? e.touches[0] : null
const clientX = e instanceof MouseEvent ? e.clientX : (touch?.clientX ?? 0)
const clientY = e instanceof MouseEvent ? e.clientY : (touch?.clientY ?? 0)
if (rect) {
if (!hasCustomPosition.value) {
position.value = { x: rect.left, y: rect.top }
}
dragOffset.value = { x: clientX - rect.left, y: clientY - rect.top }
}
document.addEventListener('mousemove', onDrag)
document.addEventListener('mouseup', stopDrag)
document.addEventListener('touchmove', onDrag, { passive: false })
document.addEventListener('touchend', stopDrag)
}
function onDrag(e: MouseEvent | TouchEvent) {
if (!isDragging.value) return
if (e instanceof TouchEvent) e.preventDefault()
const touch = e instanceof TouchEvent ? e.touches[0] : null
const clientX = e instanceof MouseEvent ? e.clientX : (touch?.clientX ?? 0)
const clientY = e instanceof MouseEvent ? e.clientY : (touch?.clientY ?? 0)
const w = windowRef.value?.offsetWidth || 480
const h = windowRef.value?.offsetHeight || 600
const minX = -w * 0.75
const maxX = window.innerWidth - w * 0.25
const minY = -h * 0.75
const maxY = window.innerHeight - h * 0.25
position.value = {
x: Math.max(minX, Math.min(clientX - dragOffset.value.x, maxX)),
y: Math.max(minY, Math.min(clientY - dragOffset.value.y, maxY))
}
}
function stopDrag() {
isDragging.value = false
hasCustomPosition.value = true
document.removeEventListener('mousemove', onDrag)
document.removeEventListener('mouseup', stopDrag)
document.removeEventListener('touchmove', onDrag)
document.removeEventListener('touchend', stopDrag)
}
// ============================================================================
// WINDOW RESIZE
// ============================================================================
const resizeStart = ref({ x: 0, y: 0, w: 0, h: 0 })
function startResize(e: MouseEvent) {
e.preventDefault()
e.stopPropagation()
isResizing.value = true
resizeStart.value = { x: e.clientX, y: e.clientY, w: size.value.w, h: size.value.h }
document.addEventListener('mousemove', onResize)
document.addEventListener('mouseup', stopResize)
}
function onResize(e: MouseEvent) {
if (!isResizing.value) return
size.value = {
w: Math.max(360, Math.min(resizeStart.value.w + e.clientX - resizeStart.value.x, window.innerWidth - 40)),
h: Math.max(300, Math.min(resizeStart.value.h + e.clientY - resizeStart.value.y, window.innerHeight - 40))
}
}
function stopResize() {
isResizing.value = false
document.removeEventListener('mousemove', onResize)
document.removeEventListener('mouseup', stopResize)
}
// ============================================================================
// COMPUTED STYLE
// ============================================================================
const windowStyle = computed((): Record<string, string> => {
if (isMobile.value) {
return {
position: 'fixed',
left: '0',
right: '0',
bottom: '0',
width: '100%',
height: `${sheetHeight.value}vh`,
maxHeight: '90vh'
}
}
if (!hasCustomPosition.value) {
return {
width: `${size.value.w}px`,
height: `${size.value.h}px`,
bottom: '16px',
left: '90px'
}
}
return {
width: `${size.value.w}px`,
height: `${size.value.h}px`,
top: `${position.value.y}px`,
left: `${position.value.x}px`,
bottom: 'auto',
right: 'auto'
}
})
// ============================================================================
// ACTIONS
// ============================================================================
function close() {
isOpen.value = false
showSelector.value = false
}
function handleAgentSwitch(agent: AgentName) {
switchAgent(agent)
}
function handleSessionSelect(sessionId: string) {
selectSession(sessionId)
showSelector.value = false
}
function handleSend(message: string) {
sendPrompt(message)
}
// ============================================================================
// WATCHERS
// ============================================================================
watch(isOpen, async (open) => {
if (open && !initialized) {
initialized = true
await nextTick()
init()
}
})
// ============================================================================
// LIFECYCLE
// ============================================================================
onMounted(() => {
checkMobile()
window.addEventListener('resize', checkMobile)
})
onBeforeUnmount(() => {
disconnectRealtime()
document.removeEventListener('mousemove', onDrag)
document.removeEventListener('mouseup', stopDrag)
document.removeEventListener('touchmove', onDrag)
document.removeEventListener('touchend', stopDrag)
document.removeEventListener('mousemove', onResize)
document.removeEventListener('mouseup', stopResize)
window.removeEventListener('resize', checkMobile)
})
</script>
<template>
<Teleport to="body">
<Transition name="win-slide">
<div
v-show="isOpen"
ref="windowRef"
class="aero-win"
:class="{
dragging: isDragging,
resizing: isResizing,
mobile: isMobile,
'sheet-dragging': isDraggingSheet
}"
:style="windowStyle"
>
<div class="glass">
<!-- Mobile drag handle -->
<div
v-if="isMobile"
class="sheet-handle"
@touchstart.passive="startSheetDrag"
@touchmove.prevent="onSheetDrag"
@touchend="stopSheetDrag"
>
<div class="handle-bar"></div>
</div>
<!-- Titlebar -->
<div
class="titlebar"
@mousedown="startDrag"
@touchstart.passive="startDrag"
@touchmove="onSheetDrag"
@touchend="stopSheetDrag"
>
<div class="left">
<svg class="title-icon" 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 class="title-label">Transcript</span>
<i class="dot" :class="{ on: isRealtime }"></i>
<span v-if="selectedAgent" class="agent-badge">{{ selectedAgent }}</span>
</div>
<div class="window-controls">
<button @click.stop="showSelector = !showSelector" :class="{ active: showSelector }" title="Agent/Session">
<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="12" cy="12" r="3"/>
<path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"/>
</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>
</div>
<!-- Agent/Session Selector Overlay -->
<Transition name="selector-slide">
<div v-if="showSelector" class="selector-overlay" @click.self="showSelector = false">
<div class="selector-panel">
<div class="selector-section">
<label class="selector-label">Agent</label>
<div class="agent-selector">
<button
v-for="a in agents"
:key="a.id"
:class="['agent-btn', { active: selectedAgent === a.id }]"
@click="handleAgentSwitch(a.id)"
>
{{ a.label }}
</button>
</div>
</div>
<div class="selector-section">
<label class="selector-label">Session</label>
<select
class="session-select"
:value="selectedSessionId || ''"
@change="handleSessionSelect(($event.target as HTMLSelectElement).value)"
:disabled="loading"
>
<option value="" disabled>Select session...</option>
<option v-for="s in sessions" :key="s.id" :value="s.id">
{{ s.firstUserMessage ? (s.firstUserMessage.length > 50 ? s.firstUserMessage.slice(0, 50) + '...' : s.firstUserMessage) : s.id.slice(0, 8) + '...' }}
</option>
</select>
<span v-if="loading" class="spinner-sm"></span>
</div>
</div>
</div>
</Transition>
<!-- Error -->
<div v-if="error" class="error-bar">{{ error }}</div>
<!-- Content -->
<div class="content">
<ChatContainer
v-if="conversation"
:conversation="conversation"
:processing="processing"
@send="handleSend"
/>
<div v-else class="empty-state">
<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
<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>Select a session to begin</span>
<small>{{ sessions.length }} sessions available</small>
</div>
</div>
<!-- Resize handle -->
<div v-if="!isMobile" class="resize-handle" @mousedown="startResize"></div>
</div>
</div>
</Transition>
</Teleport>
</template>
<style scoped>
.aero-win {
position: fixed;
min-width: 360px;
min-height: 300px;
z-index: 9999;
}
.glass {
position: relative;
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
background: rgba(0, 0, 0, 0.78);
backdrop-filter: blur(16px) saturate(1.2);
-webkit-backdrop-filter: blur(16px) saturate(1.2);
border-radius: 10px;
border: 1px solid rgba(255,255,255,0.05);
box-shadow:
0 0 0 1px rgba(0,0,0,0.6),
0 16px 56px rgba(0,0,0,0.6),
0 4px 16px rgba(0,0,0,0.4);
overflow: hidden;
}
/* ── Titlebar ── */
.titlebar {
display: flex;
align-items: center;
justify-content: space-between;
height: 28px;
padding: 0 5px 0 8px;
background:
/* Pixel art: Minecraft-style dirt/grass block */
url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' shape-rendering='crispEdges'%3E%3Crect width='24' height='24' fill='%23866043' opacity='0.35'/%3E%3Crect y='0' width='24' height='4' fill='%2355a630' opacity='0.45'/%3E%3Crect x='0' y='4' width='2' height='2' fill='%2355a630' opacity='0.35'/%3E%3Crect x='6' y='4' width='4' height='2' fill='%2355a630' opacity='0.3'/%3E%3Crect x='16' y='4' width='2' height='2' fill='%2355a630' opacity='0.35'/%3E%3Crect x='20' y='4' width='4' height='2' fill='%2355a630' opacity='0.25'/%3E%3Crect x='2' y='2' width='2' height='2' fill='%2367c23a' opacity='0.3'/%3E%3Crect x='8' y='0' width='2' height='2' fill='%2367c23a' opacity='0.35'/%3E%3Crect x='14' y='2' width='4' height='2' fill='%2367c23a' opacity='0.25'/%3E%3Crect x='4' y='8' width='2' height='2' fill='%23724b32' opacity='0.3'/%3E%3Crect x='10' y='10' width='4' height='2' fill='%23966b4a' opacity='0.25'/%3E%3Crect x='18' y='8' width='2' height='4' fill='%23724b32' opacity='0.25'/%3E%3Crect x='2' y='14' width='2' height='2' fill='%23966b4a' opacity='0.2'/%3E%3Crect x='12' y='16' width='2' height='2' fill='%23724b32' opacity='0.25'/%3E%3Crect x='8' y='20' width='4' height='2' fill='%23966b4a' opacity='0.2'/%3E%3Crect x='20' y='18' width='2' height='2' fill='%23724b32' opacity='0.2'/%3E%3C/svg%3E") no-repeat calc(100% - 6px) center / 22px 22px,
rgba(255,255,255,0.02);
border-bottom: 1px solid rgba(255,255,255,0.04);
cursor: grab;
user-select: none;
flex-shrink: 0;
}
.aero-win.dragging .titlebar { cursor: grabbing; }
.left {
display: flex;
align-items: center;
gap: 6px;
color: rgba(255,255,255,0.6);
font: 500 10px/1 'Courier New', monospace;
}
.title-icon {
opacity: 0.4;
flex-shrink: 0;
color: rgba(255,255,255,0.4);
}
.title-label {
font-weight: 700;
font-size: 10px;
color: #818cf8;
letter-spacing: 1px;
text-transform: uppercase;
font-family: 'Courier New', monospace;
/* No gradient - pixel art aesthetic = flat color */
-webkit-text-fill-color: #818cf8;
}
.dot {
width: 4px;
height: 4px;
border-radius: 0;
background: #444;
}
.dot.on {
background: #22c55e;
box-shadow: 0 0 6px #22c55e;
}
.agent-badge {
font-size: 8px;
font-weight: 700;
font-family: 'Courier New', monospace;
padding: 1px 4px;
border-radius: 0;
border: 1px solid rgba(99, 102, 241, 0.3);
background: rgba(99, 102, 241, 0.12);
color: #a5b4fc;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.window-controls {
display: flex;
gap: 2px;
}
.window-controls button {
width: 20px;
height: 18px;
display: flex;
align-items: center;
justify-content: center;
background: rgba(255,255,255,0.04);
border: 1px solid rgba(255,255,255,0.06);
border-radius: 0;
color: rgba(255,255,255,0.4);
cursor: pointer;
transition: all 0.15s;
}
.window-controls button:hover {
background: rgba(255,255,255,0.1);
color: rgba(255,255,255,0.8);
}
.window-controls button.active {
background: rgba(99, 102, 241, 0.2);
border-color: rgba(99, 102, 241, 0.3);
color: #a5b4fc;
}
.window-controls button.x:hover {
background: rgba(239, 68, 68, 0.3);
border-color: rgba(239, 68, 68, 0.4);
color: #fca5a5;
}
/* ── Selector Overlay ── */
.selector-overlay {
position: absolute;
top: 28px;
left: 0;
right: 0;
z-index: 20;
background: rgba(8, 8, 12, 0.92);
backdrop-filter: blur(12px);
border-bottom: 1px solid rgba(255,255,255,0.06);
padding: 10px 12px;
}
.selector-panel {
display: flex;
flex-direction: column;
gap: 8px;
}
.selector-section {
display: flex;
align-items: center;
gap: 8px;
}
.selector-label {
font-size: 9px;
font-weight: 700;
font-family: 'Courier New', monospace;
color: rgba(255,255,255,0.4);
min-width: 48px;
text-transform: uppercase;
letter-spacing: 1px;
}
.agent-selector {
display: flex;
background: rgba(255,255,255,0.04);
border: 1px solid rgba(255,255,255,0.08);
border-radius: 0;
overflow: hidden;
}
.agent-btn {
padding: 4px 10px;
background: transparent;
border: none;
color: rgba(255,255,255,0.4);
font-size: 10px;
font-weight: 700;
font-family: 'Courier New', monospace;
cursor: pointer;
transition: all 0.15s;
}
.agent-btn:not(:last-child) {
border-right: 1px solid rgba(255,255,255,0.06);
}
.agent-btn:hover {
background: rgba(255,255,255,0.06);
color: rgba(255,255,255,0.7);
}
.agent-btn.active {
background: rgba(99, 102, 241, 0.35);
color: #c7d2fe;
}
.session-select {
flex: 1;
min-width: 0;
padding: 4px 8px;
background: rgba(255,255,255,0.04);
border: 1px solid rgba(255,255,255,0.08);
border-radius: 0;
color: rgba(255,255,255,0.8);
font-size: 10px;
font-family: 'Courier New', monospace;
cursor: pointer;
}
.session-select:focus {
outline: none;
border-color: rgba(99, 102, 241, 0.4);
}
.session-select option {
background: #0a0a10;
color: #ccc;
}
.spinner-sm {
width: 14px;
height: 14px;
border: 2px solid rgba(255,255,255,0.1);
border-top-color: #6366f1;
border-radius: 50%;
animation: spin 0.8s linear infinite;
flex-shrink: 0;
}
/* ── Error Bar ── */
.error-bar {
padding: 4px 10px;
background: rgba(239, 68, 68, 0.1);
border-bottom: 1px solid rgba(239, 68, 68, 0.2);
color: #fca5a5;
font-size: 10px;
font-family: 'Courier New', monospace;
flex-shrink: 0;
}
/* ── Content ── */
.content {
flex: 1;
overflow: hidden;
display: flex;
flex-direction: column;
position: relative;
/* Pixel art galaxy: spiral arms, stars, nebula in bottom-right corner */
background:
url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='120' height='120' viewBox='0 0 120 120' shape-rendering='crispEdges'%3E%3C!-- galaxy core --%3E%3Crect x='56' y='56' width='4' height='4' fill='%23fef3c7' opacity='0.35'/%3E%3Crect x='60' y='56' width='4' height='4' fill='%23fde68a' opacity='0.3'/%3E%3Crect x='56' y='60' width='4' height='4' fill='%23fde68a' opacity='0.28'/%3E%3Crect x='60' y='60' width='4' height='4' fill='%23fef9c3' opacity='0.4'/%3E%3Crect x='52' y='56' width='4' height='4' fill='%23c4b5fd' opacity='0.2'/%3E%3Crect x='64' y='56' width='4' height='4' fill='%23a78bfa' opacity='0.18'/%3E%3Crect x='56' y='52' width='4' height='4' fill='%23818cf8' opacity='0.2'/%3E%3Crect x='60' y='64' width='4' height='4' fill='%23c4b5fd' opacity='0.18'/%3E%3C!-- spiral arm top-right --%3E%3Crect x='68' y='48' width='4' height='4' fill='%236366f1' opacity='0.18'/%3E%3Crect x='72' y='44' width='4' height='4' fill='%23818cf8' opacity='0.14'/%3E%3Crect x='76' y='40' width='4' height='4' fill='%236366f1' opacity='0.12'/%3E%3Crect x='80' y='38' width='4' height='2' fill='%23818cf8' opacity='0.1'/%3E%3Crect x='84' y='36' width='4' height='2' fill='%236366f1' opacity='0.08'/%3E%3Crect x='88' y='36' width='2' height='2' fill='%23a78bfa' opacity='0.06'/%3E%3C!-- spiral arm bottom-left --%3E%3Crect x='48' y='68' width='4' height='4' fill='%23818cf8' opacity='0.16'/%3E%3Crect x='44' y='72' width='4' height='4' fill='%236366f1' opacity='0.14'/%3E%3Crect x='40' y='76' width='4' height='4' fill='%23818cf8' opacity='0.12'/%3E%3Crect x='36' y='78' width='4' height='2' fill='%236366f1' opacity='0.1'/%3E%3Crect x='32' y='80' width='4' height='2' fill='%23a78bfa' opacity='0.07'/%3E%3C!-- spiral arm top-left --%3E%3Crect x='48' y='48' width='4' height='4' fill='%23c084fc' opacity='0.15'/%3E%3Crect x='44' y='44' width='4' height='4' fill='%23a855f7' opacity='0.12'/%3E%3Crect x='40' y='40' width='4' height='4' fill='%23c084fc' opacity='0.1'/%3E%3Crect x='36' y='38' width='4' height='2' fill='%23a855f7' opacity='0.08'/%3E%3C!-- spiral arm bottom-right --%3E%3Crect x='68' y='68' width='4' height='4' fill='%23a855f7' opacity='0.14'/%3E%3Crect x='72' y='72' width='4' height='4' fill='%23c084fc' opacity='0.12'/%3E%3Crect x='76' y='76' width='4' height='4' fill='%23a855f7' opacity='0.1'/%3E%3Crect x='80' y='78' width='4' height='2' fill='%23c084fc' opacity='0.07'/%3E%3C!-- nebula clouds --%3E%3Crect x='50' y='40' width='8' height='2' fill='%23f0abfc' opacity='0.07'/%3E%3Crect x='66' y='62' width='6' height='2' fill='%2367e8f9' opacity='0.06'/%3E%3Crect x='42' y='64' width='6' height='2' fill='%23f0abfc' opacity='0.05'/%3E%3Crect x='64' y='44' width='4' height='2' fill='%2367e8f9' opacity='0.06'/%3E%3C!-- scattered stars --%3E%3Crect x='20' y='18' width='2' height='2' fill='white' opacity='0.2'/%3E%3Crect x='95' y='22' width='2' height='2' fill='white' opacity='0.15'/%3E%3Crect x='14' y='90' width='2' height='2' fill='white' opacity='0.12'/%3E%3Crect x='100' y='88' width='2' height='2' fill='white' opacity='0.18'/%3E%3Crect x='30' y='105' width='2' height='2' fill='%23c4b5fd' opacity='0.1'/%3E%3Crect x='108' y='14' width='2' height='2' fill='%23fde68a' opacity='0.12'/%3E%3Crect x='10' y='50' width='2' height='2' fill='white' opacity='0.1'/%3E%3Crect x='110' y='60' width='2' height='2' fill='%2367e8f9' opacity='0.12'/%3E%3Crect x='70' y='20' width='2' height='2' fill='white' opacity='0.08'/%3E%3Crect x='50' y='100' width='2' height='2' fill='white' opacity='0.1'/%3E%3Crect x='85' y='105' width='2' height='2' fill='%23fde68a' opacity='0.08'/%3E%3Crect x='25' y='30' width='2' height='2' fill='%23c4b5fd' opacity='0.08'/%3E%3C/svg%3E") no-repeat center center / 100% 100%;
}
/* Override ChatContainer backgrounds for black transparency */
.content :deep(.chat-container) {
background: transparent;
border: none;
border-radius: 0;
}
.content :deep(.chat-header) {
background: rgba(255,255,255,0.02);
border-bottom: 1px solid rgba(255,255,255,0.04);
}
.content :deep(.messages-scroll) {
background: transparent;
}
.content :deep(.chat-title-row) {
color: rgba(255,255,255,0.6);
}
.content :deep(.session-id) {
color: rgba(255,255,255,0.4);
font-family: 'Courier New', monospace;
}
.content :deep(.meta-badge) {
background: rgba(255,255,255,0.04);
color: rgba(255,255,255,0.4);
border-radius: 0;
font-family: 'Courier New', monospace;
}
.content :deep(.meta-badge.model) {
background: rgba(99, 102, 241, 0.1);
color: #a5b4fc;
}
.content :deep(.meta-cwd),
.content :deep(.meta-duration),
.content :deep(.meta-count) {
color: rgba(255,255,255,0.25);
}
.content :deep(.copy-id-btn) {
color: rgba(255,255,255,0.25);
}
.content :deep(.copy-id-btn:hover) {
background: rgba(255,255,255,0.06);
color: rgba(255,255,255,0.6);
}
.content :deep(.select-mode-btn) {
border-color: rgba(255,255,255,0.08);
color: rgba(255,255,255,0.35);
border-radius: 0;
}
.content :deep(.select-mode-btn:hover) {
background: rgba(255,255,255,0.06);
color: rgba(255,255,255,0.7);
}
.content :deep(.select-mode-btn.active) {
background: rgba(99, 102, 241, 0.25);
border-color: rgba(99, 102, 241, 0.35);
color: #c7d2fe;
}
/* UserInput dark overrides with LED strip pixel art */
.content :deep(.user-input-area),
.content :deep(.input-wrapper) {
background:
/* Pixel LED strip: row of colored square LEDs */
url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='200' height='6' viewBox='0 0 200 6' shape-rendering='crispEdges'%3E%3Crect x='4' y='1' width='4' height='4' fill='%23ef4444' opacity='0.25'/%3E%3Crect x='12' y='1' width='4' height='4' fill='%23f97316' opacity='0.22'/%3E%3Crect x='20' y='1' width='4' height='4' fill='%23eab308' opacity='0.25'/%3E%3Crect x='28' y='1' width='4' height='4' fill='%2322c55e' opacity='0.25'/%3E%3Crect x='36' y='1' width='4' height='4' fill='%2306b6d4' opacity='0.22'/%3E%3Crect x='44' y='1' width='4' height='4' fill='%236366f1' opacity='0.25'/%3E%3Crect x='52' y='1' width='4' height='4' fill='%23a855f7' opacity='0.22'/%3E%3Crect x='60' y='1' width='4' height='4' fill='%23ec4899' opacity='0.2'/%3E%3Crect x='68' y='1' width='4' height='4' fill='%23ef4444' opacity='0.18'/%3E%3Crect x='76' y='1' width='4' height='4' fill='%23f97316' opacity='0.2'/%3E%3Crect x='84' y='1' width='4' height='4' fill='%23eab308' opacity='0.22'/%3E%3Crect x='92' y='1' width='4' height='4' fill='%2322c55e' opacity='0.2'/%3E%3Crect x='100' y='1' width='4' height='4' fill='%2306b6d4' opacity='0.18'/%3E%3Crect x='108' y='1' width='4' height='4' fill='%236366f1' opacity='0.22'/%3E%3Crect x='116' y='1' width='4' height='4' fill='%23a855f7' opacity='0.2'/%3E%3Crect x='124' y='1' width='4' height='4' fill='%23ec4899' opacity='0.18'/%3E%3Crect x='132' y='1' width='4' height='4' fill='%23ef4444' opacity='0.15'/%3E%3Crect x='140' y='1' width='4' height='4' fill='%23f97316' opacity='0.18'/%3E%3Crect x='148' y='1' width='4' height='4' fill='%23eab308' opacity='0.2'/%3E%3Crect x='156' y='1' width='4' height='4' fill='%2322c55e' opacity='0.18'/%3E%3Crect x='164' y='1' width='4' height='4' fill='%2306b6d4' opacity='0.15'/%3E%3Crect x='172' y='1' width='4' height='4' fill='%236366f1' opacity='0.18'/%3E%3Crect x='180' y='1' width='4' height='4' fill='%23a855f7' opacity='0.15'/%3E%3Crect x='188' y='1' width='4' height='4' fill='%23ec4899' opacity='0.12'/%3E%3C/svg%3E") repeat-x left top;
border-top-color: rgba(255,255,255,0.04);
}
.content :deep(.user-input-area textarea),
.content :deep(.input-wrapper textarea) {
background: rgba(255,255,255,0.03);
border-color: rgba(255,255,255,0.06);
color: rgba(255,255,255,0.85);
border-radius: 0;
font-family: 'Courier New', monospace;
}
.content :deep(.user-input-area textarea:focus),
.content :deep(.input-wrapper textarea:focus) {
border-color: rgba(99, 102, 241, 0.3);
background: rgba(255,255,255,0.05);
}
/* Selection bar */
.content :deep(.selection-bar) {
background: rgba(8, 8, 12, 0.92);
border-color: rgba(255,255,255,0.06);
border-radius: 0;
}
.content :deep(.selection-count) {
color: rgba(255,255,255,0.4);
font-family: 'Courier New', monospace;
}
.content :deep(.selection-btn.toggle-all) {
background: rgba(255,255,255,0.06);
color: rgba(255,255,255,0.5);
border-radius: 0;
}
.content :deep(.message-wrapper.selected) {
background: rgba(99, 102, 241, 0.06);
}
/* ── Empty State ── */
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
flex: 1;
gap: 8px;
color: rgba(255,255,255,0.3);
}
.empty-state svg {
opacity: 0.2;
}
.empty-state span {
font-size: 11px;
font-family: 'Courier New', monospace;
color: rgba(255,255,255,0.35);
letter-spacing: 0.5px;
}
.empty-state small {
font-size: 10px;
font-family: 'Courier New', monospace;
color: rgba(255,255,255,0.2);
}
/* ── Resize Handle ── */
.resize-handle {
position: absolute;
right: 0;
bottom: 0;
width: 16px;
height: 16px;
cursor: nwse-resize;
background:
url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 16 16' shape-rendering='crispEdges'%3E%3Crect x='12' y='12' width='2' height='2' fill='%23818cf8' opacity='0.25'/%3E%3Crect x='8' y='12' width='2' height='2' fill='%23818cf8' opacity='0.15'/%3E%3Crect x='12' y='8' width='2' height='2' fill='%23818cf8' opacity='0.15'/%3E%3Crect x='4' y='12' width='2' height='2' fill='%23818cf8' opacity='0.08'/%3E%3Crect x='12' y='4' width='2' height='2' fill='%23818cf8' opacity='0.08'/%3E%3Crect x='8' y='8' width='2' height='2' fill='%23818cf8' opacity='0.08'/%3E%3C/svg%3E") no-repeat center center;
border-radius: 0 0 10px 0;
}
.resize-handle:hover {
background:
url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 16 16' shape-rendering='crispEdges'%3E%3Crect x='12' y='12' width='2' height='2' fill='%23a5b4fc' opacity='0.4'/%3E%3Crect x='8' y='12' width='2' height='2' fill='%23a5b4fc' opacity='0.25'/%3E%3Crect x='12' y='8' width='2' height='2' fill='%23a5b4fc' opacity='0.25'/%3E%3Crect x='4' y='12' width='2' height='2' fill='%23a5b4fc' opacity='0.15'/%3E%3Crect x='12' y='4' width='2' height='2' fill='%23a5b4fc' opacity='0.15'/%3E%3Crect x='8' y='8' width='2' height='2' fill='%23a5b4fc' opacity='0.15'/%3E%3C/svg%3E") no-repeat center center;
}
.aero-win.resizing {
user-select: none;
}
.aero-win.resizing .content {
pointer-events: none;
}
/* ── Transitions ── */
.win-slide-enter-active,
.win-slide-leave-active {
transition: all .2s ease;
}
.win-slide-enter-from,
.win-slide-leave-to {
opacity: 0;
transform: translateY(16px) scale(0.98);
}
.selector-slide-enter-active,
.selector-slide-leave-active {
transition: all 0.15s ease;
}
.selector-slide-enter-from,
.selector-slide-leave-to {
opacity: 0;
transform: translateY(-8px);
}
@keyframes spin {
to { transform: rotate(360deg); }
}
/* ── Mobile ── */
.aero-win.mobile {
min-width: unset;
min-height: unset;
max-width: 100%;
transition: height 0.25s cubic-bezier(0.4, 0, 0.2, 1);
}
.aero-win.mobile.sheet-dragging {
transition: none;
}
.aero-win.mobile .glass {
border-radius: 16px 16px 0 0;
padding-bottom: env(safe-area-inset-bottom, 0);
}
.sheet-handle {
display: flex;
justify-content: center;
align-items: center;
height: 24px;
cursor: grab;
touch-action: none;
background: rgba(255,255,255,0.02);
}
.sheet-handle:active {
cursor: grabbing;
}
.handle-bar {
width: 36px;
height: 4px;
background: rgba(255,255,255,0.12);
border-radius: 0;
}
.aero-win.mobile .titlebar {
cursor: grab;
touch-action: none;
}
.aero-win.mobile .resize-handle {
display: none;
}
.aero-win.mobile .content {
flex: 1;
min-height: 100px;
touch-action: pan-y;
overflow: hidden;
}
.aero-win.mobile.win-slide-enter-from,
.aero-win.mobile.win-slide-leave-to {
transform: translateY(100%);
opacity: 1;
}
</style>