feat: add new session FAB button to terminal stack, always show FABs

- Add "+" FAB in TerminalFabStack with create-session emit
- Expose handleCreateSession from FloatingTranscriptDebug
- Remove !showTranscriptDebug condition so FABs stay visible when panel is open
- Wire handleFabCreateSession in App.vue to open panel + show modal
This commit is contained in:
2026-02-20 21:35:31 -06:00
parent 9945be07b1
commit 653c4e6d23
3 changed files with 272 additions and 30 deletions

View File

@@ -1,5 +1,5 @@
<script setup lang="ts">
import { ref, onMounted, onUnmounted, watch } from 'vue'
import { ref, computed, onMounted, onUnmounted, watch, nextTick } from 'vue'
import { RouterView, useRoute, useRouter } from 'vue-router'
import Toolbar from './components/Toolbar.vue'
import TorchButton from './components/TorchButton.vue'
@@ -7,16 +7,19 @@ import FloatingResponse from './components/FloatingResponse.vue'
import { initWhisperSocket } from './services/whisperSocket'
import FloatingVoice from './components/FloatingVoice.vue'
import FloatingTranscriptDebug from './components/FloatingTranscriptDebug.vue'
import TerminalFabStack from './components/transcript-debug/TerminalFabStack.vue'
import PwaInstallBanner from './components/PwaInstallBanner.vue'
import HooksApprovalModal from './components/HooksApprovalModal.vue'
import { useGlobalApproval } from './composables/useGlobalApproval'
import { initWebMCP, getWebMCP } from './services/webmcp'
import { initTorch, destroyTorch } from './services/torch'
import { initSessionStateWS, destroySessionStateWS } from './services/session-state-ws'
import { endpoints } from './config/endpoints'
import { initToolRegistry, activatePageTools, initToolsOnRefresh } from './services/toolRegistry'
import { setResponseControls } from './services/tools/handlers/responseHandlers'
import { useCanvasStore } from './stores/canvas'
import { useProjectCanvasStore } from './stores/projectCanvas'
import { useSessionState } from './stores/session-state'
const route = useRoute()
const router = useRouter()
@@ -70,6 +73,7 @@ const transcriptDebugRef = ref<InstanceType<typeof FloatingTranscriptDebug> | nu
const mousePos = ref({ x: 0, y: 0 })
const canvasStore = useCanvasStore()
const projectCanvasStore = useProjectCanvasStore()
const sessionState = useSessionState()
const { totalPending, modalVisible, connect: connectApproval, disconnect: disconnectApproval, fetchPending: fetchApprovalPending } = useGlobalApproval()
// Voice FAB push-to-talk state
const voicePTTActive = ref(false)
@@ -78,6 +82,22 @@ let voicePTTTimeout: number | null = null
const keyboardVisible = ref(false) // Virtual keyboard visible
// Extra terminals (T2-T5) from Pinia store — fully reactive, no template ref dependency
const extraTerminals = computed(() => {
const reg = sessionState.terminalRegistry
if (reg.length <= 1) return []
return reg.slice(1).map(entry => ({
sessionId: entry.transcriptSessionId,
ephemeralSessionId: entry.ephemeralSessionId,
agent: entry.agent,
label: entry.label,
command: entry.command,
active: entry.transcriptSessionId === transcriptDebugRef.value?.activeTerminalSessionId,
alive: entry.alive,
clients: entry.clients
}))
})
function hardRefresh() {
location.reload()
}
@@ -86,6 +106,27 @@ function trackMouse(e: MouseEvent) {
mousePos.value = { x: e.clientX, y: e.clientY }
}
function handleFabTerminalSelect(sessionId: string) {
if (transcriptDebugRef.value) {
transcriptDebugRef.value.switchToTerminal(sessionId)
}
showTranscriptDebug.value = true
}
function handleFabCreateSession() {
showTranscriptDebug.value = true
nextTick(() => {
transcriptDebugRef.value?.handleCreateSession()
})
}
function handleMainFabClick() {
if (transcriptDebugRef.value?.openTerminals?.length) {
transcriptDebugRef.value.switchToTerminal(transcriptDebugRef.value.openTerminals[0].sessionId)
}
showTranscriptDebug.value = !showTranscriptDebug.value
}
function handleGlobalKeydown(e: KeyboardEvent) {
if (e.ctrlKey && e.key === 'e') {
e.preventDefault()
@@ -153,6 +194,9 @@ onMounted(async () => {
connectApproval()
fetchApprovalPending()
// Connect centralized session state WS
initSessionStateWS()
// Initialize Whisper WebSocket connection early
initWhisperSocket()
@@ -213,6 +257,7 @@ onUnmounted(() => {
document.removeEventListener('keydown', handleGlobalKeydown)
destroyTorch()
disconnectApproval()
destroySessionStateWS()
})
// Watch for route changes and update tools
@@ -289,30 +334,38 @@ watch(() => route.name, (newPage) => {
<!-- 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 }"
@click="showTranscriptDebug = !showTranscriptDebug"
@contextmenu.prevent
title="Transcript Debug"
>
<!-- Pixel art chat bubble icon -->
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20" shape-rendering="crispEdges">
<rect x="4" y="2" width="12" height="2" fill="currentColor"/>
<rect x="2" y="4" width="2" height="8" fill="currentColor"/>
<rect x="16" y="4" width="2" height="8" fill="currentColor"/>
<rect x="4" y="12" width="12" height="2" fill="currentColor"/>
<rect x="4" y="14" width="2" height="2" fill="currentColor"/>
<rect x="2" y="16" width="2" height="2" fill="currentColor"/>
<!-- Dots inside -->
<rect x="6" y="7" width="2" height="2" fill="currentColor" opacity="0.5"/>
<rect x="9" 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>
</button>
<TerminalFabStack
:terminals="extraTerminals"
:active-session-id="transcriptDebugRef?.activeTerminalSessionId ?? null"
@select="handleFabTerminalSelect"
@create-session="handleFabCreateSession"
/>
<div class="fab-button-area">
<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 }"
@click="handleMainFabClick"
@contextmenu.prevent
title="Transcript Debug"
>
<!-- Pixel art chat bubble icon -->
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20" shape-rendering="crispEdges">
<rect x="4" y="2" width="12" height="2" fill="currentColor"/>
<rect x="2" y="4" width="2" height="8" fill="currentColor"/>
<rect x="16" y="4" width="2" height="8" fill="currentColor"/>
<rect x="4" y="12" width="12" height="2" fill="currentColor"/>
<rect x="4" y="14" width="2" height="2" fill="currentColor"/>
<rect x="2" y="16" width="2" height="2" fill="currentColor"/>
<!-- Dots inside -->
<rect x="6" y="7" width="2" height="2" fill="currentColor" opacity="0.5"/>
<rect x="9" 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>
</button>
</div>
</div>
<!-- Voice FAB Button -->
@@ -738,18 +791,20 @@ watch(() => route.name, (newPage) => {
50% { box-shadow: 0 0 50px rgba(249, 115, 22, 0.9); }
}
/* Transcript Debug FAB wrapper — holds button + bubbles */
/* Transcript Debug FAB wrapper — holds button + terminal stack */
.transcript-fab-wrap {
position: fixed;
bottom: 20px;
right: 20px;
width: 44px;
height: 44px;
z-index: 9998;
z-index: 10000;
pointer-events: none;
-webkit-user-select: none;
user-select: none;
-webkit-touch-callout: none;
display: flex;
flex-direction: column;
align-items: center;
gap: 6px;
}
/* Transcript Debug FAB — pixel art night ocean, elevated */
@@ -812,6 +867,12 @@ watch(() => route.name, (newPage) => {
color: #a5f3fc;
}
.fab-button-area {
position: relative;
width: 44px;
height: 44px;
}
/* Bubble particles — very occasional */
.fab-bubble {
position: absolute;
@@ -881,6 +942,9 @@ watch(() => route.name, (newPage) => {
.transcript-fab-wrap {
bottom: 80px;
right: 16px;
}
.transcript-fab-wrap .fab-button-area {
width: 40px;
height: 40px;
}