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:
2026-02-20 14:28:37 -06:00
parent abe6766a85
commit 18378adb77
8 changed files with 558 additions and 64 deletions

View File

@@ -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 */