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>
|
</main>
|
||||||
|
|
||||||
<!-- Transcript Debug FAB Button (pixel art ocean) -->
|
<!-- 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
|
<button
|
||||||
class="transcript-fab"
|
class="transcript-fab"
|
||||||
:class="{ active: showTranscriptDebug, 'sheet-open': showVoice || showTranscriptDebug, 'keyboard-visible': keyboardVisible }"
|
:class="{ active: showTranscriptDebug }"
|
||||||
@click="showTranscriptDebug = !showTranscriptDebug"
|
@click="showTranscriptDebug = !showTranscriptDebug"
|
||||||
|
@contextmenu.prevent
|
||||||
title="Transcript Debug"
|
title="Transcript Debug"
|
||||||
>
|
>
|
||||||
<!-- Pixel art chat bubble icon -->
|
<!-- 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"/>
|
<rect x="12" y="7" width="2" height="2" fill="currentColor" opacity="0.5"/>
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Voice FAB Button -->
|
<!-- Voice FAB Button -->
|
||||||
<button
|
<button
|
||||||
@@ -732,48 +738,138 @@ watch(() => route.name, (newPage) => {
|
|||||||
50% { box-shadow: 0 0 50px rgba(249, 115, 22, 0.9); }
|
50% { box-shadow: 0 0 50px rgba(249, 115, 22, 0.9); }
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Transcript Debug FAB — pixel art night ocean */
|
/* Transcript Debug FAB wrapper — holds button + bubbles */
|
||||||
.transcript-fab {
|
.transcript-fab-wrap {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
bottom: 20px;
|
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;
|
width: 44px;
|
||||||
height: 44px;
|
height: 44px;
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
background:
|
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");
|
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;
|
color: #0ea5e9;
|
||||||
border: none;
|
border: 1px solid rgba(14, 165, 233, 0.2);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: 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;
|
transition: all 0.2s ease;
|
||||||
z-index: 9998;
|
|
||||||
-webkit-user-select: none;
|
-webkit-user-select: none;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
-webkit-touch-callout: none;
|
-webkit-touch-callout: none;
|
||||||
touch-action: manipulation;
|
touch-action: manipulation;
|
||||||
image-rendering: pixelated;
|
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 {
|
.transcript-fab:hover {
|
||||||
transform: translateY(-2px);
|
transform: translateY(-2px);
|
||||||
|
border-color: rgba(14, 165, 233, 0.35);
|
||||||
box-shadow:
|
box-shadow:
|
||||||
0 6px 20px rgba(0, 0, 0, 0.7),
|
0 4px 8px rgba(0, 0, 0, 0.5),
|
||||||
0 0 10px rgba(14, 165, 233, 0.2);
|
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;
|
color: #38bdf8;
|
||||||
}
|
}
|
||||||
|
|
||||||
.transcript-fab.active {
|
.transcript-fab.active {
|
||||||
color: #67e8f9;
|
color: #67e8f9;
|
||||||
|
border-color: rgba(14, 165, 233, 0.3);
|
||||||
}
|
}
|
||||||
|
|
||||||
.transcript-fab.active:hover {
|
.transcript-fab.active:hover {
|
||||||
color: #a5f3fc;
|
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) {
|
@media (max-width: 768px) {
|
||||||
.voice-fab {
|
.voice-fab {
|
||||||
bottom: 80px;
|
bottom: 80px;
|
||||||
@@ -782,9 +878,14 @@ watch(() => route.name, (newPage) => {
|
|||||||
height: 44px;
|
height: 44px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.transcript-fab {
|
.transcript-fab-wrap {
|
||||||
bottom: 80px;
|
bottom: 80px;
|
||||||
left: 68px;
|
right: 16px;
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.transcript-fab-wrap .transcript-fab {
|
||||||
width: 40px;
|
width: 40px;
|
||||||
height: 40px;
|
height: 40px;
|
||||||
}
|
}
|
||||||
@@ -792,24 +893,24 @@ watch(() => route.name, (newPage) => {
|
|||||||
|
|
||||||
/* Mobile: FABs above bottom sheets */
|
/* Mobile: FABs above bottom sheets */
|
||||||
@media (max-width: 1024px) and (pointer: coarse) {
|
@media (max-width: 1024px) and (pointer: coarse) {
|
||||||
.voice-fab,
|
.voice-fab {
|
||||||
.transcript-fab {
|
|
||||||
z-index: 10001;
|
z-index: 10001;
|
||||||
transition: bottom 0.25s cubic-bezier(0.4, 0, 0.2, 1), transform 0.3s ease;
|
transition: bottom 0.25s cubic-bezier(0.4, 0, 0.2, 1), transform 0.3s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.voice-fab.sheet-open,
|
.transcript-fab-wrap {
|
||||||
.transcript-fab.sheet-open {
|
z-index: 10001;
|
||||||
|
}
|
||||||
|
|
||||||
|
.voice-fab.sheet-open {
|
||||||
bottom: calc(15vh + 100px);
|
bottom: calc(15vh + 100px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.voice-fab.keyboard-visible,
|
.voice-fab.keyboard-visible {
|
||||||
.transcript-fab.keyboard-visible {
|
|
||||||
bottom: 35vh;
|
bottom: 35vh;
|
||||||
}
|
}
|
||||||
|
|
||||||
.voice-fab.keyboard-visible.sheet-open,
|
.voice-fab.keyboard-visible.sheet-open {
|
||||||
.transcript-fab.keyboard-visible.sheet-open {
|
|
||||||
bottom: 45vh;
|
bottom: 45vh;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
import { ref, computed, watch, onMounted, onBeforeUnmount, nextTick } from 'vue'
|
import { ref, computed, watch, onMounted, onBeforeUnmount, nextTick } from 'vue'
|
||||||
import { useTranscriptDebug } from '@/composables/transcript-debug'
|
import { useTranscriptDebug } from '@/composables/transcript-debug'
|
||||||
import { useVoiceInput } from '@/composables/useVoiceInput'
|
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'
|
import type { AgentName } from '@/types/transcript-debug'
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
@@ -65,6 +65,7 @@ const agents: { id: AgentName; label: string }[] = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
const showSelector = ref(false)
|
const showSelector = ref(false)
|
||||||
|
const showNewSessionModal = ref(false)
|
||||||
const chatRef = ref<InstanceType<typeof ChatContainer> | null>(null)
|
const chatRef = ref<InstanceType<typeof ChatContainer> | null>(null)
|
||||||
let initialized = false
|
let initialized = false
|
||||||
|
|
||||||
@@ -416,7 +417,23 @@ function handleSend(message: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function handleCreateSession() {
|
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
|
if (!e.ctrlKey) return
|
||||||
|
|
||||||
// Ctrl+1..5 → switch to terminal by index
|
// Ctrl+1..5 → switch to terminal by index
|
||||||
const num = parseInt(e.key)
|
const num = parseInt(e.key)
|
||||||
if (num >= 1 && num <= 5) {
|
if (num >= 1 && num <= 5) {
|
||||||
const slot = openTerminals.value[num - 1]
|
const terminal = openTerminals.value[num - 1]
|
||||||
if (!slot) return
|
if (!terminal) return
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
if (!isOpen.value) isOpen.value = true
|
if (!isOpen.value) isOpen.value = true
|
||||||
switchToTerminal(slot.sessionId)
|
switchToTerminal(terminal.sessionId)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -467,7 +484,7 @@ function handleKeydown(e: KeyboardEvent) {
|
|||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
checkMobile()
|
checkMobile()
|
||||||
window.addEventListener('resize', checkMobile)
|
window.addEventListener('resize', checkMobile)
|
||||||
document.addEventListener('keydown', handleKeydown)
|
document.addEventListener('keydown', handleGlobalKeydown)
|
||||||
oceanLifeTimer = setInterval(tickOceanLife, 20000)
|
oceanLifeTimer = setInterval(tickOceanLife, 20000)
|
||||||
tickOceanLife()
|
tickOceanLife()
|
||||||
await voice.init()
|
await voice.init()
|
||||||
@@ -477,7 +494,7 @@ onBeforeUnmount(() => {
|
|||||||
if (oceanLifeTimer) clearInterval(oceanLifeTimer)
|
if (oceanLifeTimer) clearInterval(oceanLifeTimer)
|
||||||
disconnectRealtime()
|
disconnectRealtime()
|
||||||
voice.cleanup()
|
voice.cleanup()
|
||||||
document.removeEventListener('keydown', handleKeydown)
|
document.removeEventListener('keydown', handleGlobalKeydown)
|
||||||
document.removeEventListener('mousemove', onDrag)
|
document.removeEventListener('mousemove', onDrag)
|
||||||
document.removeEventListener('mouseup', stopDrag)
|
document.removeEventListener('mouseup', stopDrag)
|
||||||
document.removeEventListener('mousemove', onResize)
|
document.removeEventListener('mousemove', onResize)
|
||||||
@@ -686,6 +703,15 @@ onBeforeUnmount(() => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Transition>
|
</Transition>
|
||||||
|
|
||||||
|
<NewSessionModal
|
||||||
|
:visible="showNewSessionModal"
|
||||||
|
:agents="agents"
|
||||||
|
:current-agent="selectedAgent"
|
||||||
|
@close="showNewSessionModal = false"
|
||||||
|
@create-new="handleModalCreateNew"
|
||||||
|
@resume="handleModalResume"
|
||||||
|
/>
|
||||||
</Teleport>
|
</Teleport>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -1217,8 +1243,6 @@ onBeforeUnmount(() => {
|
|||||||
color: rgba(255,255,255,0.85);
|
color: rgba(255,255,255,0.85);
|
||||||
font-family: 'Courier New', monospace;
|
font-family: 'Courier New', monospace;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
line-height: 1.5;
|
|
||||||
field-sizing: content;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Send button: pixel art daytime ocean, no border */
|
/* Send button: pixel art daytime ocean, no border */
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import UserMessageBubble from './UserMessageBubble.vue'
|
|||||||
import AssistantMessageBubble from './AssistantMessageBubble.vue'
|
import AssistantMessageBubble from './AssistantMessageBubble.vue'
|
||||||
import ProgressEvent from './ProgressEvent.vue'
|
import ProgressEvent from './ProgressEvent.vue'
|
||||||
import SystemMessage from './SystemMessage.vue'
|
import SystemMessage from './SystemMessage.vue'
|
||||||
|
import TurnEndDivider from './TurnEndDivider.vue'
|
||||||
import UserInput from './UserInput.vue'
|
import UserInput from './UserInput.vue'
|
||||||
import ResumeTerminalButton from './ResumeTerminalButton.vue'
|
import ResumeTerminalButton from './ResumeTerminalButton.vue'
|
||||||
|
|
||||||
@@ -445,6 +446,10 @@ function formatDuration(start: string, end: string): string {
|
|||||||
v-else-if="msg.kind === 'progress'"
|
v-else-if="msg.kind === 'progress'"
|
||||||
:group="msg"
|
:group="msg"
|
||||||
/>
|
/>
|
||||||
|
<TurnEndDivider
|
||||||
|
v-else-if="msg.kind === 'system' && msg.subtype === 'turn_end'"
|
||||||
|
:message="msg"
|
||||||
|
/>
|
||||||
<SystemMessage
|
<SystemMessage
|
||||||
v-else-if="msg.kind === 'system'"
|
v-else-if="msg.kind === 'system'"
|
||||||
:message="msg"
|
:message="msg"
|
||||||
@@ -503,6 +508,12 @@ function formatDuration(start: string, end: string): string {
|
|||||||
:session-id="conversation.sessionId"
|
:session-id="conversation.sessionId"
|
||||||
:terminal="terminal ?? null"
|
: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">
|
<span v-if="conversation.metadata.startTime && conversation.metadata.endTime" class="meta-duration">
|
||||||
{{ formatDuration(conversation.metadata.startTime, conversation.metadata.endTime) }}
|
{{ formatDuration(conversation.metadata.startTime, conversation.metadata.endTime) }}
|
||||||
</span>
|
</span>
|
||||||
@@ -601,6 +612,26 @@ function formatDuration(start: string, end: string): string {
|
|||||||
flex-shrink: 0;
|
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 {
|
.status-id {
|
||||||
font-size: 9px;
|
font-size: 9px;
|
||||||
font-weight: 600;
|
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"
|
class="input-field"
|
||||||
:style="{ maxHeight: maxH }"
|
:style="{ maxHeight: maxH }"
|
||||||
:placeholder="notReady ? 'Starting terminal...' : processing ? 'Wait for agent to finish...' : 'Continue this conversation...'"
|
:placeholder="notReady ? 'Starting terminal...' : processing ? 'Wait for agent to finish...' : 'Continue this conversation...'"
|
||||||
|
rows="1"
|
||||||
:disabled="processing || notReady"
|
:disabled="processing || notReady"
|
||||||
@keydown="handleKeydown"
|
@keydown="handleKeydown"
|
||||||
/>
|
/>
|
||||||
@@ -181,7 +182,7 @@ watch(() => props.voiceTranscript, (newText) => {
|
|||||||
field-sizing: content;
|
field-sizing: content;
|
||||||
min-height: 1lh;
|
min-height: 1lh;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
padding: 0 0.25rem;
|
padding: 0.15rem 0.25rem;
|
||||||
font-family: inherit;
|
font-family: inherit;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ export { default as ToolCallBlock } from './ToolCallBlock.vue'
|
|||||||
export { default as ToolResultBlock } from './ToolResultBlock.vue'
|
export { default as ToolResultBlock } from './ToolResultBlock.vue'
|
||||||
export { default as ProgressEvent } from './ProgressEvent.vue'
|
export { default as ProgressEvent } from './ProgressEvent.vue'
|
||||||
export { default as SystemMessage } from './SystemMessage.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 UserInput } from './UserInput.vue'
|
||||||
export { default as PermissionApproval } from './PermissionApproval.vue'
|
export { default as PermissionApproval } from './PermissionApproval.vue'
|
||||||
export { default as PlanApproval } from './PlanApproval.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 AgentBadge } from './AgentBadge.vue'
|
||||||
export { default as ResumeTerminalButton } from './ResumeTerminalButton.vue'
|
export { default as ResumeTerminalButton } from './ResumeTerminalButton.vue'
|
||||||
export { default as VoiceMicButton } from './VoiceMicButton.vue'
|
export { default as VoiceMicButton } from './VoiceMicButton.vue'
|
||||||
|
export { default as NewSessionModal } from './NewSessionModal.vue'
|
||||||
export { AquaticBackground } from './aquaticBackground'
|
export { AquaticBackground } from './aquaticBackground'
|
||||||
|
|||||||
@@ -113,6 +113,7 @@ export function useTranscriptDebug() {
|
|||||||
)
|
)
|
||||||
|
|
||||||
const awaitingNewSession = ref(false)
|
const awaitingNewSession = ref(false)
|
||||||
|
const pendingPrompt = ref<string | null>(null)
|
||||||
|
|
||||||
// ── Server registry HTTP helpers ──
|
// ── Server registry HTTP helpers ──
|
||||||
|
|
||||||
@@ -324,7 +325,7 @@ export function useTranscriptDebug() {
|
|||||||
activeTerminalSessionId.value = null
|
activeTerminalSessionId.value = null
|
||||||
}
|
}
|
||||||
|
|
||||||
async function createNewSession() {
|
async function createNewSession(initialPrompt?: string) {
|
||||||
parkCurrentTerminal()
|
parkCurrentTerminal()
|
||||||
|
|
||||||
selectedSessionId.value = null
|
selectedSessionId.value = null
|
||||||
@@ -333,6 +334,7 @@ export function useTranscriptDebug() {
|
|||||||
error.value = null
|
error.value = null
|
||||||
processing.value = false
|
processing.value = false
|
||||||
optimisticMessage.value = null
|
optimisticMessage.value = null
|
||||||
|
pendingPrompt.value = initialPrompt?.trim() || null
|
||||||
|
|
||||||
awaitingNewSession.value = true
|
awaitingNewSession.value = true
|
||||||
startTerminal() // no sessionId → brand new session
|
startTerminal() // no sessionId → brand new session
|
||||||
@@ -421,6 +423,14 @@ export function useTranscriptDebug() {
|
|||||||
selectedSessionId.value = changedSessionId
|
selectedSessionId.value = changedSessionId
|
||||||
saveState()
|
saveState()
|
||||||
await fetchSessionContent(changedSessionId)
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -900,6 +910,27 @@ export function useTranscriptDebug() {
|
|||||||
|
|
||||||
if (entry.type === 'system') {
|
if (entry.type === 'system') {
|
||||||
const se = entry as any
|
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 content = se.message?.content
|
||||||
const textContent = typeof content === 'string'
|
const textContent = typeof content === 'string'
|
||||||
? content
|
? content
|
||||||
@@ -916,6 +947,7 @@ export function useTranscriptDebug() {
|
|||||||
} as ParsedSystemMessage)
|
} as ParsedSystemMessage)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (currentProgressBatch.length > 0) {
|
if (currentProgressBatch.length > 0) {
|
||||||
messages.push({
|
messages.push({
|
||||||
|
|||||||
@@ -214,6 +214,7 @@ export interface ParsedSystemMessage {
|
|||||||
timestamp: string
|
timestamp: string
|
||||||
content: string
|
content: string
|
||||||
subtype?: string
|
subtype?: string
|
||||||
|
durationMs?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Terminal slot (persistent terminal registry) ──
|
// ── Terminal slot (persistent terminal registry) ──
|
||||||
|
|||||||
Reference in New Issue
Block a user