feat: TurnEndDivider with prismarine floor, elevated FAB with bubbles
- Add TurnEndDivider component with pixel art ocean reef divider - Parser merges stop_hook_summary + turn_duration into single turn_end - Prismarine-inspired mosaic floor with SVG pattern and crystal highlights - Animated duration badge with underwater glow effect - Move transcript FAB to bottom-right, add elevated multi-layer shadow - Add occasional bubble particles rising from FAB button - Prevent long-touch selection on FAB (contextmenu + touch-callout) - FAB stays fixed on mobile when terminal sheet opens
This commit is contained in:
@@ -288,10 +288,15 @@ watch(() => route.name, (newPage) => {
|
||||
</main>
|
||||
|
||||
<!-- Transcript Debug FAB Button (pixel art ocean) -->
|
||||
<div class="transcript-fab-wrap" :class="{ 'sheet-open': showVoice || showTranscriptDebug, 'keyboard-visible': keyboardVisible }" @contextmenu.prevent>
|
||||
<span class="fab-bubble b1"></span>
|
||||
<span class="fab-bubble b2"></span>
|
||||
<span class="fab-bubble b3"></span>
|
||||
<button
|
||||
class="transcript-fab"
|
||||
:class="{ active: showTranscriptDebug, 'sheet-open': showVoice || showTranscriptDebug, 'keyboard-visible': keyboardVisible }"
|
||||
:class="{ active: showTranscriptDebug }"
|
||||
@click="showTranscriptDebug = !showTranscriptDebug"
|
||||
@contextmenu.prevent
|
||||
title="Transcript Debug"
|
||||
>
|
||||
<!-- Pixel art chat bubble icon -->
|
||||
@@ -308,6 +313,7 @@ watch(() => route.name, (newPage) => {
|
||||
<rect x="12" y="7" width="2" height="2" fill="currentColor" opacity="0.5"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Voice FAB Button -->
|
||||
<button
|
||||
@@ -732,48 +738,138 @@ watch(() => route.name, (newPage) => {
|
||||
50% { box-shadow: 0 0 50px rgba(249, 115, 22, 0.9); }
|
||||
}
|
||||
|
||||
/* Transcript Debug FAB — pixel art night ocean */
|
||||
.transcript-fab {
|
||||
/* Transcript Debug FAB wrapper — holds button + bubbles */
|
||||
.transcript-fab-wrap {
|
||||
position: fixed;
|
||||
bottom: 20px;
|
||||
left: 80px;
|
||||
right: 20px;
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
z-index: 9998;
|
||||
pointer-events: none;
|
||||
-webkit-user-select: none;
|
||||
user-select: none;
|
||||
-webkit-touch-callout: none;
|
||||
}
|
||||
|
||||
/* Transcript Debug FAB — pixel art night ocean, elevated */
|
||||
.transcript-fab {
|
||||
position: relative;
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
border-radius: 0;
|
||||
background:
|
||||
url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='44' height='44' viewBox='0 0 44 44' shape-rendering='crispEdges'%3E%3Crect width='44' height='8' fill='%23050a14'/%3E%3Crect y='8' width='44' height='4' fill='%23061020'/%3E%3Crect y='12' width='44' height='4' fill='%23071828'/%3E%3Crect y='16' width='44' height='4' fill='%23081e30'/%3E%3Crect y='20' width='44' height='4' fill='%23092438'/%3E%3Crect y='24' width='44' height='4' fill='%230a2a3e'/%3E%3Crect y='28' width='44' height='4' fill='%23082030'/%3E%3Crect y='32' width='44' height='4' fill='%23061828'/%3E%3Crect y='36' width='44' height='8' fill='%231a1810'/%3E%3Crect x='34' y='2' width='3' height='3' fill='%23e8e4c8' opacity='0.6'/%3E%3Crect x='35' y='1' width='2' height='1' fill='%23e8e4c8' opacity='0.35'/%3E%3Crect x='35' y='5' width='1' height='1' fill='%23c8c4a8' opacity='0.2'/%3E%3Crect x='6' y='2' width='1' height='1' fill='%23c8d8f0' opacity='0.5'/%3E%3Crect x='14' y='4' width='1' height='1' fill='%23c8d8f0' opacity='0.35'/%3E%3Crect x='24' y='1' width='1' height='1' fill='%23c8d8f0' opacity='0.4'/%3E%3Crect x='40' y='6' width='1' height='1' fill='%23c8d8f0' opacity='0.3'/%3E%3Crect x='18' y='7' width='1' height='1' fill='%23c8d8f0' opacity='0.25'/%3E%3Crect x='2' y='9' width='1' height='1' fill='%23c8d8f0' opacity='0.2'/%3E%3Crect x='30' y='3' width='1' height='1' fill='%23c8d8f0' opacity='0.3'/%3E%3Crect x='10' y='6' width='1' height='1' fill='%23fde68a' opacity='0.2'/%3E%3Crect x='3' y='10' width='6' height='2' fill='%230ea5e9' opacity='0.15'/%3E%3Crect x='9' y='11' width='8' height='2' fill='%230284c7' opacity='0.12'/%3E%3Crect x='17' y='10' width='4' height='2' fill='%230ea5e9' opacity='0.15'/%3E%3Crect x='25' y='11' width='10' height='2' fill='%230284c7' opacity='0.12'/%3E%3Crect x='35' y='10' width='6' height='2' fill='%230ea5e9' opacity='0.15'/%3E%3Crect x='3' y='10' width='2' height='1' fill='%23c8d8f0' opacity='0.1'/%3E%3Crect x='17' y='10' width='2' height='1' fill='%23c8d8f0' opacity='0.08'/%3E%3Crect x='35' y='10' width='2' height='1' fill='%23c8d8f0' opacity='0.1'/%3E%3Crect x='10' y='18' width='3' height='2' fill='%23f97316' opacity='0.35'/%3E%3Crect x='9' y='19' width='1' height='1' fill='%23fdba74' opacity='0.2'/%3E%3Crect x='30' y='24' width='2' height='1' fill='%23818cf8' opacity='0.3'/%3E%3Crect x='32' y='24' width='1' height='1' fill='%23a5b4fc' opacity='0.2'/%3E%3Crect x='20' y='30' width='1' height='1' fill='%23c8d8f0' opacity='0.1'/%3E%3Crect x='36' y='22' width='1' height='1' fill='%23c8d8f0' opacity='0.1'/%3E%3Crect x='6' y='26' width='1' height='1' fill='%23c8d8f0' opacity='0.08'/%3E%3Crect x='4' y='36' width='6' height='5' fill='%23052e1e' opacity='0.7'/%3E%3Crect x='5' y='32' width='2' height='4' fill='%23064e33' opacity='0.5'/%3E%3Crect x='8' y='34' width='2' height='2' fill='%23059669' opacity='0.35'/%3E%3Crect x='30' y='38' width='5' height='4' fill='%23052e1e' opacity='0.6'/%3E%3Crect x='32' y='35' width='2' height='3' fill='%23064e33' opacity='0.45'/%3E%3Crect x='18' y='39' width='4' height='3' fill='%23500e28' opacity='0.45'/%3E%3Crect x='19' y='37' width='2' height='2' fill='%23701838' opacity='0.35'/%3E%3Crect x='14' y='40' width='2' height='2' fill='%23052e1e' opacity='0.5'/%3E%3Crect x='38' y='40' width='3' height='2' fill='%231a1810' opacity='0.6'/%3E%3Crect x='24' y='42' width='2' height='1' fill='%23c8b060' opacity='0.15'/%3E%3Crect x='10' y='42' width='2' height='1' fill='%23c8b060' opacity='0.12'/%3E%3C/svg%3E");
|
||||
color: #0ea5e9;
|
||||
border: none;
|
||||
border: 1px solid rgba(14, 165, 233, 0.2);
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.6);
|
||||
box-shadow:
|
||||
0 2px 4px rgba(0, 0, 0, 0.5),
|
||||
0 6px 16px rgba(0, 0, 0, 0.6),
|
||||
0 12px 28px rgba(0, 0, 0, 0.4),
|
||||
inset 0 1px 0 rgba(14, 165, 233, 0.12);
|
||||
transition: all 0.2s ease;
|
||||
z-index: 9998;
|
||||
-webkit-user-select: none;
|
||||
user-select: none;
|
||||
-webkit-touch-callout: none;
|
||||
touch-action: manipulation;
|
||||
image-rendering: pixelated;
|
||||
pointer-events: auto;
|
||||
-webkit-touch-callout: none;
|
||||
-webkit-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.transcript-fab *,
|
||||
.transcript-fab svg {
|
||||
-webkit-touch-callout: none;
|
||||
-webkit-user-select: none;
|
||||
user-select: none;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.transcript-fab:hover {
|
||||
transform: translateY(-2px);
|
||||
border-color: rgba(14, 165, 233, 0.35);
|
||||
box-shadow:
|
||||
0 6px 20px rgba(0, 0, 0, 0.7),
|
||||
0 0 10px rgba(14, 165, 233, 0.2);
|
||||
0 4px 8px rgba(0, 0, 0, 0.5),
|
||||
0 10px 24px rgba(0, 0, 0, 0.6),
|
||||
0 16px 36px rgba(0, 0, 0, 0.4),
|
||||
0 0 14px rgba(14, 165, 233, 0.15),
|
||||
inset 0 1px 0 rgba(14, 165, 233, 0.2);
|
||||
color: #38bdf8;
|
||||
}
|
||||
|
||||
.transcript-fab.active {
|
||||
color: #67e8f9;
|
||||
border-color: rgba(14, 165, 233, 0.3);
|
||||
}
|
||||
|
||||
.transcript-fab.active:hover {
|
||||
color: #a5f3fc;
|
||||
}
|
||||
|
||||
/* Bubble particles — very occasional */
|
||||
.fab-bubble {
|
||||
position: absolute;
|
||||
width: 4px;
|
||||
height: 4px;
|
||||
background: rgba(14, 165, 233, 0.5);
|
||||
border-radius: 50%;
|
||||
pointer-events: none;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.fab-bubble.b1 {
|
||||
left: 10px;
|
||||
bottom: 36px;
|
||||
animation: fab-bubble-rise 18s ease-in 2s infinite;
|
||||
}
|
||||
|
||||
.fab-bubble.b2 {
|
||||
left: 28px;
|
||||
bottom: 38px;
|
||||
animation: fab-bubble-rise 22s ease-in 9s infinite;
|
||||
width: 3px;
|
||||
height: 3px;
|
||||
background: rgba(34, 211, 238, 0.45);
|
||||
}
|
||||
|
||||
.fab-bubble.b3 {
|
||||
left: 18px;
|
||||
bottom: 34px;
|
||||
animation: fab-bubble-rise 25s ease-in 16s infinite;
|
||||
width: 5px;
|
||||
height: 5px;
|
||||
background: rgba(14, 165, 233, 0.4);
|
||||
}
|
||||
|
||||
@keyframes fab-bubble-rise {
|
||||
0%, 85%, 100% {
|
||||
opacity: 0;
|
||||
transform: translateY(0) translateX(0);
|
||||
}
|
||||
88% {
|
||||
opacity: 0.7;
|
||||
transform: translateY(-10px) translateX(2px);
|
||||
}
|
||||
92% {
|
||||
opacity: 0.5;
|
||||
transform: translateY(-24px) translateX(-1px);
|
||||
}
|
||||
96% {
|
||||
opacity: 0.2;
|
||||
transform: translateY(-40px) translateX(3px);
|
||||
}
|
||||
98% {
|
||||
opacity: 0;
|
||||
transform: translateY(-52px) translateX(1px);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.voice-fab {
|
||||
bottom: 80px;
|
||||
@@ -782,9 +878,14 @@ watch(() => route.name, (newPage) => {
|
||||
height: 44px;
|
||||
}
|
||||
|
||||
.transcript-fab {
|
||||
.transcript-fab-wrap {
|
||||
bottom: 80px;
|
||||
left: 68px;
|
||||
right: 16px;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
.transcript-fab-wrap .transcript-fab {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
}
|
||||
@@ -792,24 +893,24 @@ watch(() => route.name, (newPage) => {
|
||||
|
||||
/* Mobile: FABs above bottom sheets */
|
||||
@media (max-width: 1024px) and (pointer: coarse) {
|
||||
.voice-fab,
|
||||
.transcript-fab {
|
||||
.voice-fab {
|
||||
z-index: 10001;
|
||||
transition: bottom 0.25s cubic-bezier(0.4, 0, 0.2, 1), transform 0.3s ease;
|
||||
}
|
||||
|
||||
.voice-fab.sheet-open,
|
||||
.transcript-fab.sheet-open {
|
||||
.transcript-fab-wrap {
|
||||
z-index: 10001;
|
||||
}
|
||||
|
||||
.voice-fab.sheet-open {
|
||||
bottom: calc(15vh + 100px);
|
||||
}
|
||||
|
||||
.voice-fab.keyboard-visible,
|
||||
.transcript-fab.keyboard-visible {
|
||||
.voice-fab.keyboard-visible {
|
||||
bottom: 35vh;
|
||||
}
|
||||
|
||||
.voice-fab.keyboard-visible.sheet-open,
|
||||
.transcript-fab.keyboard-visible.sheet-open {
|
||||
.voice-fab.keyboard-visible.sheet-open {
|
||||
bottom: 45vh;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { ref, computed, watch, onMounted, onBeforeUnmount, nextTick } from 'vue'
|
||||
import { useTranscriptDebug } from '@/composables/transcript-debug'
|
||||
import { useVoiceInput } from '@/composables/useVoiceInput'
|
||||
import { ChatContainer, AquaticBackground, AgentBadge } from '@/components/transcript-debug'
|
||||
import { ChatContainer, AquaticBackground, AgentBadge, NewSessionModal } from '@/components/transcript-debug'
|
||||
import type { AgentName } from '@/types/transcript-debug'
|
||||
|
||||
const props = defineProps<{
|
||||
@@ -65,6 +65,7 @@ const agents: { id: AgentName; label: string }[] = [
|
||||
]
|
||||
|
||||
const showSelector = ref(false)
|
||||
const showNewSessionModal = ref(false)
|
||||
const chatRef = ref<InstanceType<typeof ChatContainer> | null>(null)
|
||||
let initialized = false
|
||||
|
||||
@@ -416,7 +417,23 @@ function handleSend(message: string) {
|
||||
}
|
||||
|
||||
function handleCreateSession() {
|
||||
createNewSession()
|
||||
showNewSessionModal.value = true
|
||||
}
|
||||
|
||||
async function handleModalCreateNew(agent: AgentName, initialPrompt: string) {
|
||||
showNewSessionModal.value = false
|
||||
if (agent !== selectedAgent.value) {
|
||||
await switchAgent(agent)
|
||||
}
|
||||
createNewSession(initialPrompt || undefined)
|
||||
}
|
||||
|
||||
async function handleModalResume(sessionId: string, agent: AgentName) {
|
||||
showNewSessionModal.value = false
|
||||
if (agent !== selectedAgent.value) {
|
||||
await switchAgent(agent)
|
||||
}
|
||||
selectSession(sessionId)
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
@@ -432,20 +449,20 @@ watch(isOpen, async (open) => {
|
||||
})
|
||||
|
||||
// ============================================================================
|
||||
// KEYBOARD SHORTCUTS
|
||||
// KEYBOARD SHORTCUTS (Ctrl+1..5 terminal switch, Ctrl+/- zoom)
|
||||
// ============================================================================
|
||||
|
||||
function handleKeydown(e: KeyboardEvent) {
|
||||
function handleGlobalKeydown(e: KeyboardEvent) {
|
||||
if (!e.ctrlKey) return
|
||||
|
||||
// Ctrl+1..5 → switch to terminal by index
|
||||
const num = parseInt(e.key)
|
||||
if (num >= 1 && num <= 5) {
|
||||
const slot = openTerminals.value[num - 1]
|
||||
if (!slot) return
|
||||
const terminal = openTerminals.value[num - 1]
|
||||
if (!terminal) return
|
||||
e.preventDefault()
|
||||
if (!isOpen.value) isOpen.value = true
|
||||
switchToTerminal(slot.sessionId)
|
||||
switchToTerminal(terminal.sessionId)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -467,7 +484,7 @@ function handleKeydown(e: KeyboardEvent) {
|
||||
onMounted(async () => {
|
||||
checkMobile()
|
||||
window.addEventListener('resize', checkMobile)
|
||||
document.addEventListener('keydown', handleKeydown)
|
||||
document.addEventListener('keydown', handleGlobalKeydown)
|
||||
oceanLifeTimer = setInterval(tickOceanLife, 20000)
|
||||
tickOceanLife()
|
||||
await voice.init()
|
||||
@@ -477,7 +494,7 @@ onBeforeUnmount(() => {
|
||||
if (oceanLifeTimer) clearInterval(oceanLifeTimer)
|
||||
disconnectRealtime()
|
||||
voice.cleanup()
|
||||
document.removeEventListener('keydown', handleKeydown)
|
||||
document.removeEventListener('keydown', handleGlobalKeydown)
|
||||
document.removeEventListener('mousemove', onDrag)
|
||||
document.removeEventListener('mouseup', stopDrag)
|
||||
document.removeEventListener('mousemove', onResize)
|
||||
@@ -686,6 +703,15 @@ onBeforeUnmount(() => {
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
|
||||
<NewSessionModal
|
||||
:visible="showNewSessionModal"
|
||||
:agents="agents"
|
||||
:current-agent="selectedAgent"
|
||||
@close="showNewSessionModal = false"
|
||||
@create-new="handleModalCreateNew"
|
||||
@resume="handleModalResume"
|
||||
/>
|
||||
</Teleport>
|
||||
</template>
|
||||
|
||||
@@ -1217,8 +1243,6 @@ onBeforeUnmount(() => {
|
||||
color: rgba(255,255,255,0.85);
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
field-sizing: content;
|
||||
}
|
||||
|
||||
/* Send button: pixel art daytime ocean, no border */
|
||||
|
||||
@@ -13,6 +13,7 @@ import UserMessageBubble from './UserMessageBubble.vue'
|
||||
import AssistantMessageBubble from './AssistantMessageBubble.vue'
|
||||
import ProgressEvent from './ProgressEvent.vue'
|
||||
import SystemMessage from './SystemMessage.vue'
|
||||
import TurnEndDivider from './TurnEndDivider.vue'
|
||||
import UserInput from './UserInput.vue'
|
||||
import ResumeTerminalButton from './ResumeTerminalButton.vue'
|
||||
|
||||
@@ -445,6 +446,10 @@ function formatDuration(start: string, end: string): string {
|
||||
v-else-if="msg.kind === 'progress'"
|
||||
:group="msg"
|
||||
/>
|
||||
<TurnEndDivider
|
||||
v-else-if="msg.kind === 'system' && msg.subtype === 'turn_end'"
|
||||
:message="msg"
|
||||
/>
|
||||
<SystemMessage
|
||||
v-else-if="msg.kind === 'system'"
|
||||
:message="msg"
|
||||
@@ -503,6 +508,12 @@ function formatDuration(start: string, end: string): string {
|
||||
:session-id="conversation.sessionId"
|
||||
:terminal="terminal ?? null"
|
||||
/>
|
||||
<button class="new-session-status-btn" @click="emit('createSession')" title="New session">
|
||||
<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
|
||||
<line x1="12" y1="5" x2="12" y2="19"/>
|
||||
<line x1="5" y1="12" x2="19" y2="12"/>
|
||||
</svg>
|
||||
</button>
|
||||
<span v-if="conversation.metadata.startTime && conversation.metadata.endTime" class="meta-duration">
|
||||
{{ formatDuration(conversation.metadata.startTime, conversation.metadata.endTime) }}
|
||||
</span>
|
||||
@@ -601,6 +612,26 @@ function formatDuration(start: string, end: string): string {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.new-session-status-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
border-radius: 3px;
|
||||
cursor: pointer;
|
||||
color: var(--text-muted);
|
||||
flex-shrink: 0;
|
||||
transition: all 0.15s;
|
||||
}
|
||||
|
||||
.new-session-status-btn:hover {
|
||||
background: var(--bg-hover);
|
||||
color: var(--accent, #6366f1);
|
||||
}
|
||||
|
||||
.status-id {
|
||||
font-size: 9px;
|
||||
font-weight: 600;
|
||||
|
||||
302
frontend/src/components/transcript-debug/TurnEndDivider.vue
Normal file
302
frontend/src/components/transcript-debug/TurnEndDivider.vue
Normal file
@@ -0,0 +1,302 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import type { ParsedSystemMessage } from '@/types/transcript-debug'
|
||||
|
||||
const props = defineProps<{
|
||||
message: ParsedSystemMessage
|
||||
}>()
|
||||
|
||||
const duration = computed(() => {
|
||||
const ms = props.message.durationMs
|
||||
if (!ms) return ''
|
||||
const s = Math.floor(ms / 1000)
|
||||
if (s < 60) return `${s}s`
|
||||
const m = Math.floor(s / 60)
|
||||
return `${m}m ${s % 60}s`
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="turn-end">
|
||||
<div class="reef-line">
|
||||
<!-- Left reef: anchored to left edge, extends right -->
|
||||
<svg class="reef reef-left" viewBox="0 0 200 14" preserveAspectRatio="xMinYMid slice" shape-rendering="crispEdges">
|
||||
<!-- Ocean stone floor (prismarine-inspired mosaic) -->
|
||||
<defs>
|
||||
<pattern id="pfl" x="0" y="0" width="12" height="2" patternUnits="userSpaceOnUse">
|
||||
<rect width="12" height="2" fill="#0e3b3b"/>
|
||||
<rect x="0" y="0" width="5" height="1" fill="#1f7270"/>
|
||||
<rect x="6" y="0" width="5" height="1" fill="#2a8a7e"/>
|
||||
<rect x="0" y="1" width="2" height="1" fill="#35a098"/>
|
||||
<rect x="3" y="1" width="5" height="1" fill="#1f7270"/>
|
||||
<rect x="9" y="1" width="2" height="1" fill="#2a8a7e"/>
|
||||
</pattern>
|
||||
</defs>
|
||||
<rect x="0" y="12" width="200" height="2" fill="url(#pfl)" opacity="0.85"/>
|
||||
<!-- Crystal highlights -->
|
||||
<rect x="3" y="12" width="1" height="1" fill="#5ec4b8" opacity="0.5"/>
|
||||
<rect x="15" y="13" width="1" height="1" fill="#7edcd2" opacity="0.4"/>
|
||||
<rect x="28" y="12" width="1" height="1" fill="#4ebdb2" opacity="0.45"/>
|
||||
<rect x="42" y="13" width="1" height="1" fill="#5ec4b8" opacity="0.35"/>
|
||||
<rect x="58" y="12" width="1" height="1" fill="#7edcd2" opacity="0.4"/>
|
||||
<rect x="75" y="13" width="1" height="1" fill="#4ebdb2" opacity="0.45"/>
|
||||
<rect x="91" y="12" width="1" height="1" fill="#5ec4b8" opacity="0.35"/>
|
||||
<rect x="108" y="13" width="1" height="1" fill="#7edcd2" opacity="0.4"/>
|
||||
<rect x="130" y="12" width="1" height="1" fill="#4ebdb2" opacity="0.3"/>
|
||||
<rect x="155" y="13" width="1" height="1" fill="#5ec4b8" opacity="0.25"/>
|
||||
<rect x="175" y="12" width="1" height="1" fill="#7edcd2" opacity="0.2"/>
|
||||
|
||||
<!-- Tall coral cluster (left edge) -->
|
||||
<rect x="2" y="4" width="2" height="10" fill="#f87171" opacity="0.85"/>
|
||||
<rect x="4" y="2" width="2" height="12" fill="#fb923c" opacity="0.8"/>
|
||||
<rect x="6" y="5" width="2" height="9" fill="#f87171" opacity="0.75"/>
|
||||
<rect x="8" y="7" width="2" height="7" fill="#ef4444" opacity="0.65"/>
|
||||
<rect x="3" y="1" width="2" height="2" fill="#fca5a5" opacity="0.55"/>
|
||||
|
||||
<!-- Seaweed grove -->
|
||||
<rect x="14" y="1" width="2" height="13" fill="#22c55e" opacity="0.75"/>
|
||||
<rect x="16" y="3" width="2" height="11" fill="#4ade80" opacity="0.7"/>
|
||||
<rect x="18" y="5" width="2" height="9" fill="#16a34a" opacity="0.65"/>
|
||||
<rect x="13" y="0" width="2" height="2" fill="#86efac" opacity="0.5"/>
|
||||
|
||||
<!-- Orange fish school (3 fish) -->
|
||||
<rect x="26" y="4" width="3" height="2" fill="#f97316" opacity="0.85"/>
|
||||
<rect x="25" y="5" width="1" height="1" fill="#fb923c" opacity="0.7"/>
|
||||
<rect x="30" y="6" width="3" height="2" fill="#f97316" opacity="0.75"/>
|
||||
<rect x="29" y="7" width="1" height="1" fill="#fb923c" opacity="0.65"/>
|
||||
<rect x="33" y="3" width="3" height="2" fill="#ea580c" opacity="0.7"/>
|
||||
<rect x="32" y="4" width="1" height="1" fill="#fdba74" opacity="0.55"/>
|
||||
|
||||
<!-- Bubbles -->
|
||||
<rect x="24" y="1" width="1" height="1" fill="white" opacity="0.4"/>
|
||||
<rect x="22" y="3" width="1" height="1" fill="white" opacity="0.35"/>
|
||||
<rect x="37" y="2" width="1" height="1" fill="white" opacity="0.35"/>
|
||||
|
||||
<!-- Purple brain coral -->
|
||||
<rect x="42" y="7" width="4" height="5" fill="#a855f7" opacity="0.7"/>
|
||||
<rect x="43" y="6" width="2" height="2" fill="#c084fc" opacity="0.65"/>
|
||||
<rect x="46" y="8" width="2" height="4" fill="#7c3aed" opacity="0.55"/>
|
||||
<rect x="44" y="12" width="2" height="2" fill="#6d28d9" opacity="0.4"/>
|
||||
|
||||
<!-- Jellyfish -->
|
||||
<rect x="54" y="2" width="3" height="2" fill="#c084fc" opacity="0.65"/>
|
||||
<rect x="55" y="1" width="1" height="1" fill="#e9d5ff" opacity="0.5"/>
|
||||
<rect x="54" y="4" width="1" height="2" fill="#a855f7" opacity="0.4"/>
|
||||
<rect x="56" y="4" width="1" height="2" fill="#a855f7" opacity="0.4"/>
|
||||
|
||||
<!-- Starfish -->
|
||||
<rect x="62" y="10" width="3" height="2" fill="#fbbf24" opacity="0.65"/>
|
||||
<rect x="63" y="9" width="1" height="1" fill="#fde68a" opacity="0.55"/>
|
||||
<rect x="63" y="12" width="1" height="1" fill="#f59e0b" opacity="0.5"/>
|
||||
|
||||
<!-- Anemone -->
|
||||
<rect x="70" y="6" width="2" height="8" fill="#ec4899" opacity="0.65"/>
|
||||
<rect x="72" y="7" width="2" height="7" fill="#f472b6" opacity="0.55"/>
|
||||
<rect x="69" y="5" width="2" height="2" fill="#f9a8d4" opacity="0.5"/>
|
||||
<rect x="73" y="6" width="2" height="2" fill="#f9a8d4" opacity="0.5"/>
|
||||
|
||||
<!-- Blue fish -->
|
||||
<rect x="80" y="5" width="3" height="2" fill="#3b82f6" opacity="0.75"/>
|
||||
<rect x="79" y="6" width="1" height="1" fill="#93c5fd" opacity="0.65"/>
|
||||
|
||||
<!-- Seaweed tuft -->
|
||||
<rect x="88" y="4" width="2" height="10" fill="#059669" opacity="0.6"/>
|
||||
<rect x="90" y="6" width="2" height="8" fill="#10b981" opacity="0.5"/>
|
||||
|
||||
<!-- Small coral -->
|
||||
<rect x="96" y="8" width="2" height="6" fill="#f87171" opacity="0.6"/>
|
||||
<rect x="98" y="9" width="2" height="5" fill="#fb923c" opacity="0.5"/>
|
||||
|
||||
<!-- Seahorse -->
|
||||
<rect x="106" y="4" width="2" height="2" fill="#fbbf24" opacity="0.65"/>
|
||||
<rect x="106" y="6" width="2" height="3" fill="#f59e0b" opacity="0.55"/>
|
||||
<rect x="107" y="9" width="1" height="2" fill="#d97706" opacity="0.5"/>
|
||||
|
||||
<!-- Bubbles -->
|
||||
<rect x="104" y="1" width="1" height="1" fill="white" opacity="0.35"/>
|
||||
<rect x="112" y="3" width="1" height="1" fill="white" opacity="0.3"/>
|
||||
|
||||
<!-- More coral -->
|
||||
<rect x="118" y="7" width="2" height="7" fill="#0ea5e9" opacity="0.5"/>
|
||||
<rect x="120" y="9" width="2" height="5" fill="#22d3ee" opacity="0.4"/>
|
||||
|
||||
<!-- Tiny fish -->
|
||||
<rect x="130" y="6" width="2" height="1" fill="#f97316" opacity="0.55"/>
|
||||
<rect x="140" y="4" width="2" height="1" fill="#818cf8" opacity="0.5"/>
|
||||
|
||||
<!-- Shell -->
|
||||
<rect x="150" y="10" width="3" height="2" fill="#fde68a" opacity="0.5"/>
|
||||
<rect x="151" y="9" width="1" height="1" fill="#fef3c7" opacity="0.4"/>
|
||||
|
||||
<!-- Fade-out elements -->
|
||||
<rect x="160" y="8" width="2" height="6" fill="#22c55e" opacity="0.35"/>
|
||||
<rect x="170" y="9" width="2" height="5" fill="#a855f7" opacity="0.3"/>
|
||||
<rect x="180" y="7" width="1" height="1" fill="white" opacity="0.2"/>
|
||||
<rect x="190" y="10" width="2" height="4" fill="#f87171" opacity="0.2"/>
|
||||
</svg>
|
||||
|
||||
<!-- Center badge -->
|
||||
<span v-if="duration" class="duration-badge">{{ duration }}</span>
|
||||
<span v-else class="end-badge">~</span>
|
||||
|
||||
<!-- Right reef: anchored to right edge, extends left -->
|
||||
<svg class="reef reef-right" viewBox="0 0 200 14" preserveAspectRatio="xMaxYMid slice" shape-rendering="crispEdges">
|
||||
<!-- Ocean stone floor (prismarine-inspired mosaic) -->
|
||||
<defs>
|
||||
<pattern id="pfr" x="0" y="0" width="12" height="2" patternUnits="userSpaceOnUse">
|
||||
<rect width="12" height="2" fill="#0e3b3b"/>
|
||||
<rect x="0" y="0" width="5" height="1" fill="#2a8a7e"/>
|
||||
<rect x="6" y="0" width="5" height="1" fill="#1f7270"/>
|
||||
<rect x="0" y="1" width="2" height="1" fill="#1f7270"/>
|
||||
<rect x="3" y="1" width="5" height="1" fill="#35a098"/>
|
||||
<rect x="9" y="1" width="2" height="1" fill="#1f7270"/>
|
||||
</pattern>
|
||||
</defs>
|
||||
<rect x="0" y="12" width="200" height="2" fill="url(#pfr)" opacity="0.85"/>
|
||||
<!-- Crystal highlights -->
|
||||
<rect x="190" y="12" width="1" height="1" fill="#5ec4b8" opacity="0.5"/>
|
||||
<rect x="178" y="13" width="1" height="1" fill="#7edcd2" opacity="0.4"/>
|
||||
<rect x="162" y="12" width="1" height="1" fill="#4ebdb2" opacity="0.45"/>
|
||||
<rect x="145" y="13" width="1" height="1" fill="#5ec4b8" opacity="0.35"/>
|
||||
<rect x="128" y="12" width="1" height="1" fill="#7edcd2" opacity="0.4"/>
|
||||
<rect x="110" y="13" width="1" height="1" fill="#4ebdb2" opacity="0.45"/>
|
||||
<rect x="92" y="12" width="1" height="1" fill="#5ec4b8" opacity="0.35"/>
|
||||
<rect x="70" y="13" width="1" height="1" fill="#7edcd2" opacity="0.3"/>
|
||||
<rect x="45" y="12" width="1" height="1" fill="#4ebdb2" opacity="0.25"/>
|
||||
<rect x="22" y="13" width="1" height="1" fill="#5ec4b8" opacity="0.2"/>
|
||||
|
||||
<!-- Tall coral cluster (right edge) -->
|
||||
<rect x="192" y="3" width="2" height="11" fill="#ec4899" opacity="0.85"/>
|
||||
<rect x="194" y="5" width="2" height="9" fill="#f472b6" opacity="0.8"/>
|
||||
<rect x="196" y="4" width="2" height="10" fill="#ec4899" opacity="0.75"/>
|
||||
<rect x="190" y="6" width="2" height="8" fill="#db2777" opacity="0.65"/>
|
||||
<rect x="193" y="1" width="2" height="3" fill="#fbcfe8" opacity="0.55"/>
|
||||
|
||||
<!-- Seaweed grove -->
|
||||
<rect x="182" y="2" width="2" height="12" fill="#10b981" opacity="0.75"/>
|
||||
<rect x="184" y="4" width="2" height="10" fill="#34d399" opacity="0.7"/>
|
||||
<rect x="180" y="0" width="2" height="3" fill="#6ee7b7" opacity="0.5"/>
|
||||
|
||||
<!-- Purple fish school -->
|
||||
<rect x="170" y="5" width="3" height="2" fill="#818cf8" opacity="0.85"/>
|
||||
<rect x="173" y="6" width="1" height="1" fill="#a5b4fc" opacity="0.7"/>
|
||||
<rect x="166" y="3" width="3" height="2" fill="#6366f1" opacity="0.75"/>
|
||||
<rect x="169" y="4" width="1" height="1" fill="#c7d2fe" opacity="0.65"/>
|
||||
<rect x="163" y="7" width="3" height="2" fill="#818cf8" opacity="0.7"/>
|
||||
<rect x="166" y="8" width="1" height="1" fill="#a5b4fc" opacity="0.55"/>
|
||||
|
||||
<!-- Bubbles -->
|
||||
<rect x="175" y="1" width="1" height="1" fill="white" opacity="0.4"/>
|
||||
<rect x="178" y="3" width="1" height="1" fill="white" opacity="0.35"/>
|
||||
<rect x="160" y="2" width="1" height="1" fill="white" opacity="0.35"/>
|
||||
|
||||
<!-- Orange fan coral -->
|
||||
<rect x="152" y="6" width="4" height="6" fill="#fb923c" opacity="0.7"/>
|
||||
<rect x="153" y="5" width="2" height="2" fill="#fdba74" opacity="0.65"/>
|
||||
<rect x="150" y="8" width="2" height="4" fill="#ea580c" opacity="0.55"/>
|
||||
|
||||
<!-- Turtle -->
|
||||
<rect x="140" y="4" width="4" height="2" fill="#22c55e" opacity="0.7"/>
|
||||
<rect x="139" y="5" width="1" height="1" fill="#4ade80" opacity="0.55"/>
|
||||
<rect x="144" y="5" width="1" height="1" fill="#4ade80" opacity="0.55"/>
|
||||
<rect x="141" y="3" width="2" height="1" fill="#86efac" opacity="0.5"/>
|
||||
|
||||
<!-- Shell -->
|
||||
<rect x="132" y="10" width="3" height="2" fill="#fde68a" opacity="0.55"/>
|
||||
<rect x="133" y="9" width="1" height="1" fill="#fef3c7" opacity="0.5"/>
|
||||
|
||||
<!-- Cyan coral -->
|
||||
<rect x="124" y="7" width="2" height="7" fill="#0ea5e9" opacity="0.65"/>
|
||||
<rect x="126" y="8" width="2" height="6" fill="#22d3ee" opacity="0.55"/>
|
||||
<rect x="122" y="9" width="2" height="5" fill="#0284c7" opacity="0.5"/>
|
||||
|
||||
<!-- Red anemone -->
|
||||
<rect x="112" y="6" width="2" height="8" fill="#f87171" opacity="0.6"/>
|
||||
<rect x="114" y="7" width="2" height="7" fill="#fca5a5" opacity="0.5"/>
|
||||
<rect x="111" y="5" width="2" height="2" fill="#fecaca" opacity="0.4"/>
|
||||
|
||||
<!-- Tiny fish -->
|
||||
<rect x="104" y="5" width="2" height="1" fill="#f97316" opacity="0.55"/>
|
||||
|
||||
<!-- Seaweed -->
|
||||
<rect x="96" y="5" width="2" height="9" fill="#059669" opacity="0.5"/>
|
||||
<rect x="94" y="7" width="2" height="7" fill="#10b981" opacity="0.4"/>
|
||||
|
||||
<!-- Starfish -->
|
||||
<rect x="86" y="10" width="3" height="2" fill="#fbbf24" opacity="0.5"/>
|
||||
<rect x="87" y="9" width="1" height="1" fill="#fde68a" opacity="0.4"/>
|
||||
|
||||
<!-- Bubbles -->
|
||||
<rect x="80" y="2" width="1" height="1" fill="white" opacity="0.3"/>
|
||||
<rect x="72" y="4" width="1" height="1" fill="white" opacity="0.25"/>
|
||||
|
||||
<!-- Fade-out elements -->
|
||||
<rect x="60" y="8" width="2" height="6" fill="#ec4899" opacity="0.3"/>
|
||||
<rect x="46" y="9" width="2" height="5" fill="#22c55e" opacity="0.25"/>
|
||||
<rect x="30" y="7" width="1" height="1" fill="white" opacity="0.2"/>
|
||||
<rect x="14" y="10" width="2" height="4" fill="#0ea5e9" opacity="0.2"/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.turn-end {
|
||||
padding: 0.25rem 0;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.reef-line {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0;
|
||||
height: 28px;
|
||||
}
|
||||
|
||||
.reef {
|
||||
flex: 1;
|
||||
height: 100%;
|
||||
image-rendering: pixelated;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.duration-badge,
|
||||
.end-badge {
|
||||
flex-shrink: 0;
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
font-family: 'Courier New', monospace;
|
||||
color: rgba(14, 165, 233, 0.85);
|
||||
padding: 0 6px;
|
||||
letter-spacing: 1px;
|
||||
z-index: 1;
|
||||
text-shadow: 0 0 8px rgba(14, 165, 233, 0.4);
|
||||
animation: badge-glow 3s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.end-badge {
|
||||
font-size: 12px;
|
||||
color: rgba(14, 165, 233, 0.5);
|
||||
animation: badge-drift 4s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes badge-glow {
|
||||
0%, 100% {
|
||||
text-shadow: 0 0 6px rgba(14, 165, 233, 0.3);
|
||||
color: rgba(14, 165, 233, 0.8);
|
||||
}
|
||||
50% {
|
||||
text-shadow: 0 0 12px rgba(14, 165, 233, 0.6), 0 0 4px rgba(34, 211, 238, 0.3);
|
||||
color: rgba(14, 165, 233, 0.95);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes badge-drift {
|
||||
0%, 100% {
|
||||
opacity: 0.5;
|
||||
}
|
||||
50% {
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -81,6 +81,7 @@ 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"
|
||||
/>
|
||||
@@ -181,7 +182,7 @@ watch(() => props.voiceTranscript, (newText) => {
|
||||
field-sizing: content;
|
||||
min-height: 1lh;
|
||||
overflow-y: auto;
|
||||
padding: 0 0.25rem;
|
||||
padding: 0.15rem 0.25rem;
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ export { default as ToolCallBlock } from './ToolCallBlock.vue'
|
||||
export { default as ToolResultBlock } from './ToolResultBlock.vue'
|
||||
export { default as ProgressEvent } from './ProgressEvent.vue'
|
||||
export { default as SystemMessage } from './SystemMessage.vue'
|
||||
export { default as TurnEndDivider } from './TurnEndDivider.vue'
|
||||
export { default as UserInput } from './UserInput.vue'
|
||||
export { default as PermissionApproval } from './PermissionApproval.vue'
|
||||
export { default as PlanApproval } from './PlanApproval.vue'
|
||||
@@ -15,4 +16,5 @@ export { default as CodeBlock } from './CodeBlock.vue'
|
||||
export { default as AgentBadge } from './AgentBadge.vue'
|
||||
export { default as ResumeTerminalButton } from './ResumeTerminalButton.vue'
|
||||
export { default as VoiceMicButton } from './VoiceMicButton.vue'
|
||||
export { default as NewSessionModal } from './NewSessionModal.vue'
|
||||
export { AquaticBackground } from './aquaticBackground'
|
||||
|
||||
@@ -113,6 +113,7 @@ export function useTranscriptDebug() {
|
||||
)
|
||||
|
||||
const awaitingNewSession = ref(false)
|
||||
const pendingPrompt = ref<string | null>(null)
|
||||
|
||||
// ── Server registry HTTP helpers ──
|
||||
|
||||
@@ -324,7 +325,7 @@ export function useTranscriptDebug() {
|
||||
activeTerminalSessionId.value = null
|
||||
}
|
||||
|
||||
async function createNewSession() {
|
||||
async function createNewSession(initialPrompt?: string) {
|
||||
parkCurrentTerminal()
|
||||
|
||||
selectedSessionId.value = null
|
||||
@@ -333,6 +334,7 @@ export function useTranscriptDebug() {
|
||||
error.value = null
|
||||
processing.value = false
|
||||
optimisticMessage.value = null
|
||||
pendingPrompt.value = initialPrompt?.trim() || null
|
||||
|
||||
awaitingNewSession.value = true
|
||||
startTerminal() // no sessionId → brand new session
|
||||
@@ -421,6 +423,14 @@ export function useTranscriptDebug() {
|
||||
selectedSessionId.value = changedSessionId
|
||||
saveState()
|
||||
await fetchSessionContent(changedSessionId)
|
||||
|
||||
// Auto-send queued initial prompt
|
||||
if (pendingPrompt.value) {
|
||||
const prompt = pendingPrompt.value
|
||||
pendingPrompt.value = null
|
||||
// Small delay to let terminal fully settle
|
||||
setTimeout(() => sendPrompt(prompt), 300)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
@@ -900,6 +910,27 @@ export function useTranscriptDebug() {
|
||||
|
||||
if (entry.type === 'system') {
|
||||
const se = entry as any
|
||||
const isTurnEnd = se.subtype === 'stop_hook_summary' || se.subtype === 'turn_duration'
|
||||
|
||||
if (isTurnEnd) {
|
||||
// Merge consecutive turn-end messages into one divider
|
||||
const prev = messages[messages.length - 1]
|
||||
if (prev?.kind === 'system' && (prev as ParsedSystemMessage).subtype === 'turn_end') {
|
||||
// Merge into existing turn_end
|
||||
if (se.subtype === 'turn_duration' && se.durationMs != null) {
|
||||
(prev as ParsedSystemMessage).durationMs = se.durationMs
|
||||
}
|
||||
} else {
|
||||
messages.push({
|
||||
kind: 'system',
|
||||
uuid: se.uuid || crypto.randomUUID(),
|
||||
timestamp: se.timestamp || '',
|
||||
content: '',
|
||||
subtype: 'turn_end',
|
||||
durationMs: se.subtype === 'turn_duration' ? se.durationMs : undefined
|
||||
} as ParsedSystemMessage)
|
||||
}
|
||||
} else {
|
||||
const content = se.message?.content
|
||||
const textContent = typeof content === 'string'
|
||||
? content
|
||||
@@ -916,6 +947,7 @@ export function useTranscriptDebug() {
|
||||
} as ParsedSystemMessage)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (currentProgressBatch.length > 0) {
|
||||
messages.push({
|
||||
|
||||
@@ -214,6 +214,7 @@ export interface ParsedSystemMessage {
|
||||
timestamp: string
|
||||
content: string
|
||||
subtype?: string
|
||||
durationMs?: number
|
||||
}
|
||||
|
||||
// ── Terminal slot (persistent terminal registry) ──
|
||||
|
||||
Reference in New Issue
Block a user