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:
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user