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">
|
<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 { RouterView, useRoute, useRouter } from 'vue-router'
|
||||||
import Toolbar from './components/Toolbar.vue'
|
import Toolbar from './components/Toolbar.vue'
|
||||||
import TorchButton from './components/TorchButton.vue'
|
import TorchButton from './components/TorchButton.vue'
|
||||||
@@ -7,16 +7,19 @@ import FloatingResponse from './components/FloatingResponse.vue'
|
|||||||
import { initWhisperSocket } from './services/whisperSocket'
|
import { initWhisperSocket } from './services/whisperSocket'
|
||||||
import FloatingVoice from './components/FloatingVoice.vue'
|
import FloatingVoice from './components/FloatingVoice.vue'
|
||||||
import FloatingTranscriptDebug from './components/FloatingTranscriptDebug.vue'
|
import FloatingTranscriptDebug from './components/FloatingTranscriptDebug.vue'
|
||||||
|
import TerminalFabStack from './components/transcript-debug/TerminalFabStack.vue'
|
||||||
import PwaInstallBanner from './components/PwaInstallBanner.vue'
|
import PwaInstallBanner from './components/PwaInstallBanner.vue'
|
||||||
import HooksApprovalModal from './components/HooksApprovalModal.vue'
|
import HooksApprovalModal from './components/HooksApprovalModal.vue'
|
||||||
import { useGlobalApproval } from './composables/useGlobalApproval'
|
import { useGlobalApproval } from './composables/useGlobalApproval'
|
||||||
import { initWebMCP, getWebMCP } from './services/webmcp'
|
import { initWebMCP, getWebMCP } from './services/webmcp'
|
||||||
import { initTorch, destroyTorch } from './services/torch'
|
import { initTorch, destroyTorch } from './services/torch'
|
||||||
|
import { initSessionStateWS, destroySessionStateWS } from './services/session-state-ws'
|
||||||
import { endpoints } from './config/endpoints'
|
import { endpoints } from './config/endpoints'
|
||||||
import { initToolRegistry, activatePageTools, initToolsOnRefresh } from './services/toolRegistry'
|
import { initToolRegistry, activatePageTools, initToolsOnRefresh } from './services/toolRegistry'
|
||||||
import { setResponseControls } from './services/tools/handlers/responseHandlers'
|
import { setResponseControls } from './services/tools/handlers/responseHandlers'
|
||||||
import { useCanvasStore } from './stores/canvas'
|
import { useCanvasStore } from './stores/canvas'
|
||||||
import { useProjectCanvasStore } from './stores/projectCanvas'
|
import { useProjectCanvasStore } from './stores/projectCanvas'
|
||||||
|
import { useSessionState } from './stores/session-state'
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
@@ -70,6 +73,7 @@ const transcriptDebugRef = ref<InstanceType<typeof FloatingTranscriptDebug> | nu
|
|||||||
const mousePos = ref({ x: 0, y: 0 })
|
const mousePos = ref({ x: 0, y: 0 })
|
||||||
const canvasStore = useCanvasStore()
|
const canvasStore = useCanvasStore()
|
||||||
const projectCanvasStore = useProjectCanvasStore()
|
const projectCanvasStore = useProjectCanvasStore()
|
||||||
|
const sessionState = useSessionState()
|
||||||
const { totalPending, modalVisible, connect: connectApproval, disconnect: disconnectApproval, fetchPending: fetchApprovalPending } = useGlobalApproval()
|
const { totalPending, modalVisible, connect: connectApproval, disconnect: disconnectApproval, fetchPending: fetchApprovalPending } = useGlobalApproval()
|
||||||
// Voice FAB push-to-talk state
|
// Voice FAB push-to-talk state
|
||||||
const voicePTTActive = ref(false)
|
const voicePTTActive = ref(false)
|
||||||
@@ -78,6 +82,22 @@ let voicePTTTimeout: number | null = null
|
|||||||
|
|
||||||
const keyboardVisible = ref(false) // Virtual keyboard visible
|
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() {
|
function hardRefresh() {
|
||||||
location.reload()
|
location.reload()
|
||||||
}
|
}
|
||||||
@@ -86,6 +106,27 @@ function trackMouse(e: MouseEvent) {
|
|||||||
mousePos.value = { x: e.clientX, y: e.clientY }
|
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) {
|
function handleGlobalKeydown(e: KeyboardEvent) {
|
||||||
if (e.ctrlKey && e.key === 'e') {
|
if (e.ctrlKey && e.key === 'e') {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
@@ -153,6 +194,9 @@ onMounted(async () => {
|
|||||||
connectApproval()
|
connectApproval()
|
||||||
fetchApprovalPending()
|
fetchApprovalPending()
|
||||||
|
|
||||||
|
// Connect centralized session state WS
|
||||||
|
initSessionStateWS()
|
||||||
|
|
||||||
// Initialize Whisper WebSocket connection early
|
// Initialize Whisper WebSocket connection early
|
||||||
initWhisperSocket()
|
initWhisperSocket()
|
||||||
|
|
||||||
@@ -213,6 +257,7 @@ onUnmounted(() => {
|
|||||||
document.removeEventListener('keydown', handleGlobalKeydown)
|
document.removeEventListener('keydown', handleGlobalKeydown)
|
||||||
destroyTorch()
|
destroyTorch()
|
||||||
disconnectApproval()
|
disconnectApproval()
|
||||||
|
destroySessionStateWS()
|
||||||
})
|
})
|
||||||
|
|
||||||
// Watch for route changes and update tools
|
// Watch for route changes and update tools
|
||||||
@@ -289,30 +334,38 @@ watch(() => route.name, (newPage) => {
|
|||||||
|
|
||||||
<!-- 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>
|
<div class="transcript-fab-wrap" :class="{ 'sheet-open': showVoice || showTranscriptDebug, 'keyboard-visible': keyboardVisible }" @contextmenu.prevent>
|
||||||
<span class="fab-bubble b1"></span>
|
<TerminalFabStack
|
||||||
<span class="fab-bubble b2"></span>
|
:terminals="extraTerminals"
|
||||||
<span class="fab-bubble b3"></span>
|
:active-session-id="transcriptDebugRef?.activeTerminalSessionId ?? null"
|
||||||
<button
|
@select="handleFabTerminalSelect"
|
||||||
class="transcript-fab"
|
@create-session="handleFabCreateSession"
|
||||||
:class="{ active: showTranscriptDebug }"
|
/>
|
||||||
@click="showTranscriptDebug = !showTranscriptDebug"
|
<div class="fab-button-area">
|
||||||
@contextmenu.prevent
|
<span class="fab-bubble b1"></span>
|
||||||
title="Transcript Debug"
|
<span class="fab-bubble b2"></span>
|
||||||
>
|
<span class="fab-bubble b3"></span>
|
||||||
<!-- Pixel art chat bubble icon -->
|
<button
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20" shape-rendering="crispEdges">
|
class="transcript-fab"
|
||||||
<rect x="4" y="2" width="12" height="2" fill="currentColor"/>
|
:class="{ active: showTranscriptDebug }"
|
||||||
<rect x="2" y="4" width="2" height="8" fill="currentColor"/>
|
@click="handleMainFabClick"
|
||||||
<rect x="16" y="4" width="2" height="8" fill="currentColor"/>
|
@contextmenu.prevent
|
||||||
<rect x="4" y="12" width="12" height="2" fill="currentColor"/>
|
title="Transcript Debug"
|
||||||
<rect x="4" y="14" width="2" height="2" fill="currentColor"/>
|
>
|
||||||
<rect x="2" y="16" width="2" height="2" fill="currentColor"/>
|
<!-- Pixel art chat bubble icon -->
|
||||||
<!-- Dots inside -->
|
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20" shape-rendering="crispEdges">
|
||||||
<rect x="6" y="7" width="2" height="2" fill="currentColor" opacity="0.5"/>
|
<rect x="4" y="2" width="12" height="2" fill="currentColor"/>
|
||||||
<rect x="9" y="7" width="2" height="2" fill="currentColor" opacity="0.5"/>
|
<rect x="2" y="4" width="2" height="8" fill="currentColor"/>
|
||||||
<rect x="12" y="7" width="2" height="2" fill="currentColor" opacity="0.5"/>
|
<rect x="16" y="4" width="2" height="8" fill="currentColor"/>
|
||||||
</svg>
|
<rect x="4" y="12" width="12" height="2" fill="currentColor"/>
|
||||||
</button>
|
<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>
|
</div>
|
||||||
|
|
||||||
<!-- Voice FAB Button -->
|
<!-- Voice FAB Button -->
|
||||||
@@ -738,18 +791,20 @@ 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 wrapper — holds button + bubbles */
|
/* Transcript Debug FAB wrapper — holds button + terminal stack */
|
||||||
.transcript-fab-wrap {
|
.transcript-fab-wrap {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
bottom: 20px;
|
bottom: 20px;
|
||||||
right: 20px;
|
right: 20px;
|
||||||
width: 44px;
|
z-index: 10000;
|
||||||
height: 44px;
|
|
||||||
z-index: 9998;
|
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
-webkit-user-select: none;
|
-webkit-user-select: none;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
-webkit-touch-callout: none;
|
-webkit-touch-callout: none;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Transcript Debug FAB — pixel art night ocean, elevated */
|
/* Transcript Debug FAB — pixel art night ocean, elevated */
|
||||||
@@ -812,6 +867,12 @@ watch(() => route.name, (newPage) => {
|
|||||||
color: #a5f3fc;
|
color: #a5f3fc;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.fab-button-area {
|
||||||
|
position: relative;
|
||||||
|
width: 44px;
|
||||||
|
height: 44px;
|
||||||
|
}
|
||||||
|
|
||||||
/* Bubble particles — very occasional */
|
/* Bubble particles — very occasional */
|
||||||
.fab-bubble {
|
.fab-bubble {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
@@ -881,6 +942,9 @@ watch(() => route.name, (newPage) => {
|
|||||||
.transcript-fab-wrap {
|
.transcript-fab-wrap {
|
||||||
bottom: 80px;
|
bottom: 80px;
|
||||||
right: 16px;
|
right: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.transcript-fab-wrap .fab-button-area {
|
||||||
width: 40px;
|
width: 40px;
|
||||||
height: 40px;
|
height: 40px;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -435,7 +435,7 @@ function openAtCursor(x: number, y: number) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
defineExpose({ openAtCursor, openTerminals, activeTerminalSessionId, switchToTerminal })
|
defineExpose({ openAtCursor, openTerminals, activeTerminalSessionId, switchToTerminal, handleCreateSession })
|
||||||
|
|
||||||
function handleAgentSwitch(agent: AgentName) {
|
function handleAgentSwitch(agent: AgentName) {
|
||||||
switchAgent(agent)
|
switchAgent(agent)
|
||||||
|
|||||||
178
frontend/src/components/transcript-debug/TerminalFabStack.vue
Normal file
178
frontend/src/components/transcript-debug/TerminalFabStack.vue
Normal file
@@ -0,0 +1,178 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { TerminalSlot } from '@/types/transcript-debug'
|
||||||
|
|
||||||
|
defineProps<{
|
||||||
|
terminals: TerminalSlot[]
|
||||||
|
activeSessionId: string | null
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
select: [sessionId: string]
|
||||||
|
'create-session': []
|
||||||
|
}>()
|
||||||
|
|
||||||
|
function stateColor(t: TerminalSlot): string {
|
||||||
|
if (!t.alive) return '#ef4444'
|
||||||
|
if (t.clients > 0) return '#22c55e'
|
||||||
|
return '#f59e0b'
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pixel art SVG backgrounds for terminals 2-5
|
||||||
|
const artVariants = [
|
||||||
|
// T2: Coral reef — orange/pink corals, warm deep water
|
||||||
|
`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='6' fill='%230c1a2e'/%3E%3Crect y='6' width='44' height='4' fill='%230e2040'/%3E%3Crect y='10' width='44' height='4' fill='%23122848'/%3E%3Crect y='14' width='44' height='4' fill='%23152e50'/%3E%3Crect y='18' width='44' height='4' fill='%23183458'/%3E%3Crect y='22' width='44' height='4' fill='%231a3860'/%3E%3Crect y='26' width='44' height='4' fill='%231c3c68'/%3E%3Crect y='30' width='44' height='4' fill='%231e3e6a'/%3E%3Crect y='34' width='44' height='10' fill='%232a1e10'/%3E%3Crect y='38' width='44' height='6' fill='%23342414'/%3E%3Crect x='6' y='26' width='3' height='8' fill='%23e85d2a' opacity='0.8'/%3E%3Crect x='5' y='24' width='2' height='3' fill='%23f07030' opacity='0.7'/%3E%3Crect x='8' y='23' width='2' height='4' fill='%23f07030' opacity='0.65'/%3E%3Crect x='4' y='22' width='1' height='3' fill='%23ff8844' opacity='0.5'/%3E%3Crect x='10' y='25' width='1' height='2' fill='%23ff8844' opacity='0.45'/%3E%3Crect x='18' y='28' width='4' height='6' fill='%23d4467a' opacity='0.8'/%3E%3Crect x='17' y='26' width='2' height='3' fill='%23e0558a' opacity='0.65'/%3E%3Crect x='21' y='25' width='2' height='4' fill='%23e0558a' opacity='0.6'/%3E%3Crect x='19' y='24' width='1' height='3' fill='%23f06898' opacity='0.45'/%3E%3Crect x='30' y='27' width='3' height='7' fill='%23c83030' opacity='0.75'/%3E%3Crect x='29' y='25' width='2' height='3' fill='%23d84040' opacity='0.6'/%3E%3Crect x='32' y='24' width='2' height='4' fill='%23d84040' opacity='0.55'/%3E%3Crect x='34' y='26' width='1' height='2' fill='%23e85050' opacity='0.45'/%3E%3Crect x='38' y='30' width='2' height='4' fill='%23e8752a' opacity='0.5'/%3E%3Crect x='37' y='28' width='1' height='3' fill='%23f08838' opacity='0.4'/%3E%3Crect x='12' y='16' width='1' height='1' fill='%2388ccff' opacity='0.3'/%3E%3Crect x='25' y='12' width='1' height='1' fill='%2388ccff' opacity='0.25'/%3E%3Crect x='36' y='8' width='1' height='1' fill='%2388ccff' opacity='0.2'/%3E%3Crect x='14' y='14' width='3' height='1' fill='%23ffaa44' opacity='0.4'/%3E%3Crect x='13' y='15' width='1' height='1' fill='%23ffaa44' opacity='0.3'/%3E%3Crect x='8' y='36' width='2' height='1' fill='%234a3820' opacity='0.4'/%3E%3Crect x='24' y='38' width='3' height='1' fill='%234a3820' opacity='0.3'/%3E%3Crect x='15' y='40' width='2' height='1' fill='%23c8a860' opacity='0.15'/%3E%3Crect x='36' y='41' width='2' height='1' fill='%23c8a860' opacity='0.12'/%3E%3C/svg%3E")`,
|
||||||
|
|
||||||
|
// T3: Deep sea — very dark, bioluminescent green/cyan dots
|
||||||
|
`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='44' fill='%23020810'/%3E%3Crect y='4' width='44' height='4' fill='%23030a14'/%3E%3Crect y='8' width='44' height='4' fill='%23040c18'/%3E%3Crect y='12' width='44' height='4' fill='%23050e1c'/%3E%3Crect y='16' width='44' height='4' fill='%23061020'/%3E%3Crect y='20' width='44' height='4' fill='%23051018'/%3E%3Crect y='24' width='44' height='4' fill='%23040e16'/%3E%3Crect y='28' width='44' height='4' fill='%23030c14'/%3E%3Crect y='32' width='44' height='4' fill='%23030a12'/%3E%3Crect y='36' width='44' height='8' fill='%23020810'/%3E%3Crect x='8' y='10' width='2' height='2' fill='%2300ffaa' opacity='0.5'/%3E%3Crect x='7' y='9' width='1' height='1' fill='%2300ffaa' opacity='0.2'/%3E%3Crect x='10' y='11' width='1' height='1' fill='%2300ffaa' opacity='0.15'/%3E%3Crect x='28' y='6' width='2' height='2' fill='%2300e4ff' opacity='0.45'/%3E%3Crect x='27' y='5' width='1' height='1' fill='%2300e4ff' opacity='0.15'/%3E%3Crect x='30' y='7' width='1' height='1' fill='%2300e4ff' opacity='0.12'/%3E%3Crect x='16' y='20' width='2' height='2' fill='%2340ff90' opacity='0.4'/%3E%3Crect x='15' y='19' width='1' height='1' fill='%2340ff90' opacity='0.15'/%3E%3Crect x='36' y='16' width='2' height='2' fill='%2300ffcc' opacity='0.35'/%3E%3Crect x='35' y='15' width='1' height='1' fill='%2300ffcc' opacity='0.12'/%3E%3Crect x='4' y='28' width='2' height='2' fill='%2300e4ff' opacity='0.3'/%3E%3Crect x='22' y='32' width='2' height='2' fill='%2340ff90' opacity='0.35'/%3E%3Crect x='38' y='26' width='2' height='2' fill='%2300ffaa' opacity='0.3'/%3E%3Crect x='12' y='36' width='1' height='1' fill='%2300e4ff' opacity='0.2'/%3E%3Crect x='32' y='38' width='1' height='1' fill='%2340ff90' opacity='0.2'/%3E%3Crect x='20' y='14' width='1' height='1' fill='%2300ffcc' opacity='0.15'/%3E%3Crect x='40' y='34' width='1' height='1' fill='%2300ffaa' opacity='0.15'/%3E%3Crect x='2' y='18' width='1' height='1' fill='%2340ff90' opacity='0.12'/%3E%3Crect x='18' y='38' width='6' height='3' fill='%23081828' opacity='0.5'/%3E%3Crect x='19' y='36' width='3' height='2' fill='%230a1c30' opacity='0.4'/%3E%3Crect x='6' y='40' width='4' height='2' fill='%23081828' opacity='0.4'/%3E%3Crect x='34' y='40' width='5' height='2' fill='%23081828' opacity='0.35'/%3E%3C/svg%3E")`,
|
||||||
|
|
||||||
|
// T4: Tropical lagoon — turquoise water, golden sand
|
||||||
|
`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='6' fill='%23104858'/%3E%3Crect y='6' width='44' height='4' fill='%23148898'/%3E%3Crect y='10' width='44' height='4' fill='%231898a8'/%3E%3Crect y='14' width='44' height='4' fill='%231ca8b8'/%3E%3Crect y='18' width='44' height='4' fill='%2320b8c8'/%3E%3Crect y='22' width='44' height='4' fill='%2318a0b0'/%3E%3Crect y='26' width='44' height='4' fill='%231490a0'/%3E%3Crect y='30' width='44' height='4' fill='%23108090'/%3E%3Crect y='34' width='44' height='4' fill='%23c8a850'/%3E%3Crect y='38' width='44' height='6' fill='%23d4b460'/%3E%3Crect x='2' y='2' width='1' height='1' fill='%23e0f0ff' opacity='0.4'/%3E%3Crect x='12' y='3' width='1' height='1' fill='%23e0f0ff' opacity='0.3'/%3E%3Crect x='30' y='1' width='1' height='1' fill='%23e0f0ff' opacity='0.35'/%3E%3Crect x='4' y='8' width='6' height='2' fill='%2340d8e8' opacity='0.25'/%3E%3Crect x='16' y='9' width='8' height='2' fill='%2340d8e8' opacity='0.2'/%3E%3Crect x='30' y='8' width='6' height='2' fill='%2340d8e8' opacity='0.25'/%3E%3Crect x='8' y='16' width='4' height='1' fill='%23e0f8ff' opacity='0.15'/%3E%3Crect x='24' y='18' width='5' height='1' fill='%23e0f8ff' opacity='0.12'/%3E%3Crect x='6' y='24' width='3' height='1' fill='%23e0f8ff' opacity='0.1'/%3E%3Crect x='14' y='28' width='2' height='2' fill='%2388ddaa' opacity='0.25'/%3E%3Crect x='32' y='26' width='3' height='2' fill='%2388ddaa' opacity='0.2'/%3E%3Crect x='22' y='22' width='2' height='1' fill='%23ffcc44' opacity='0.2'/%3E%3Crect x='10' y='36' width='2' height='1' fill='%23b89840' opacity='0.4'/%3E%3Crect x='26' y='38' width='3' height='1' fill='%23b89840' opacity='0.35'/%3E%3Crect x='18' y='40' width='2' height='1' fill='%23e0c870' opacity='0.3'/%3E%3Crect x='36' y='36' width='2' height='1' fill='%23b89840' opacity='0.3'/%3E%3Crect x='4' y='34' width='3' height='2' fill='%2388ddaa' opacity='0.15'/%3E%3Crect x='38' y='34' width='2' height='2' fill='%2390cc88' opacity='0.12'/%3E%3Crect x='6' y='42' width='1' height='1' fill='%23e8d088' opacity='0.2'/%3E%3Crect x='34' y='42' width='1' height='1' fill='%23e8d088' opacity='0.18'/%3E%3C/svg%3E")`,
|
||||||
|
|
||||||
|
// T5: Arctic — ice blue, white icebergs
|
||||||
|
`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='6' fill='%230a1828'/%3E%3Crect y='6' width='44' height='4' fill='%23102030'/%3E%3Crect y='10' width='44' height='4' fill='%23142838'/%3E%3Crect y='14' width='44' height='4' fill='%23183040'/%3E%3Crect y='18' width='44' height='4' fill='%231c3848'/%3E%3Crect y='22' width='44' height='4' fill='%23203e50'/%3E%3Crect y='26' width='44' height='4' fill='%23244458'/%3E%3Crect y='30' width='44' height='4' fill='%23284a60'/%3E%3Crect y='34' width='44' height='4' fill='%232c5068'/%3E%3Crect y='38' width='44' height='6' fill='%23182838'/%3E%3Crect x='4' y='12' width='8' height='6' fill='%23d0e8f4' opacity='0.8'/%3E%3Crect x='5' y='10' width='6' height='2' fill='%23e0f0ff' opacity='0.7'/%3E%3Crect x='6' y='8' width='4' height='2' fill='%23eef6ff' opacity='0.6'/%3E%3Crect x='4' y='18' width='8' height='2' fill='%23a0c8e0' opacity='0.4'/%3E%3Crect x='28' y='16' width='10' height='6' fill='%23d0e8f4' opacity='0.75'/%3E%3Crect x='29' y='14' width='8' height='2' fill='%23e0f0ff' opacity='0.65'/%3E%3Crect x='30' y='12' width='6' height='2' fill='%23eef6ff' opacity='0.55'/%3E%3Crect x='28' y='22' width='10' height='2' fill='%23a0c8e0' opacity='0.35'/%3E%3Crect x='16' y='24' width='6' height='4' fill='%23c8e0f0' opacity='0.5'/%3E%3Crect x='17' y='22' width='4' height='2' fill='%23d8ecf8' opacity='0.45'/%3E%3Crect x='16' y='28' width='6' height='1' fill='%2390b8d0' opacity='0.3'/%3E%3Crect x='8' y='2' width='1' height='1' fill='%23e0f0ff' opacity='0.5'/%3E%3Crect x='20' y='4' width='1' height='1' fill='%23e0f0ff' opacity='0.4'/%3E%3Crect x='36' y='2' width='1' height='1' fill='%23e0f0ff' opacity='0.45'/%3E%3Crect x='14' y='6' width='1' height='1' fill='%23e0f0ff' opacity='0.3'/%3E%3Crect x='24' y='8' width='1' height='1' fill='%23e0f0ff' opacity='0.25'/%3E%3Crect x='40' y='6' width='1' height='1' fill='%23e0f0ff' opacity='0.35'/%3E%3Crect x='2' y='36' width='1' height='1' fill='%2380a8c0' opacity='0.2'/%3E%3Crect x='22' y='38' width='1' height='1' fill='%2380a8c0' opacity='0.15'/%3E%3Crect x='38' y='36' width='1' height='1' fill='%2380a8c0' opacity='0.18'/%3E%3Crect x='10' y='40' width='1' height='1' fill='%2380a8c0' opacity='0.12'/%3E%3C/svg%3E")`,
|
||||||
|
]
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<TransitionGroup name="fab-stack" tag="div" class="terminal-fab-stack">
|
||||||
|
<button
|
||||||
|
v-for="(t, idx) in terminals"
|
||||||
|
:key="t.sessionId"
|
||||||
|
class="terminal-fab"
|
||||||
|
:class="{ active: t.sessionId === activeSessionId }"
|
||||||
|
:style="{ backgroundImage: artVariants[idx] || artVariants[0], transitionDelay: `${idx * 30}ms` }"
|
||||||
|
:title="t.label || t.sessionId.slice(0, 8)"
|
||||||
|
@click="emit('select', t.sessionId)"
|
||||||
|
>
|
||||||
|
<span class="fab-number">{{ idx + 2 }}</span>
|
||||||
|
<span class="fab-dot" :style="{ background: stateColor(t) }" />
|
||||||
|
</button>
|
||||||
|
<!-- New session "+" button — last in DOM = top in column-reverse -->
|
||||||
|
<button
|
||||||
|
key="__new__"
|
||||||
|
class="terminal-fab new-session-fab"
|
||||||
|
title="New session"
|
||||||
|
@click="emit('create-session')"
|
||||||
|
>
|
||||||
|
<svg class="plus-icon" width="18" height="18" viewBox="0 0 18 18" shape-rendering="crispEdges">
|
||||||
|
<rect x="8" y="2" width="2" height="14" fill="currentColor"/>
|
||||||
|
<rect x="2" y="8" width="14" height="2" fill="currentColor"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</TransitionGroup>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.terminal-fab-stack {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column-reverse;
|
||||||
|
gap: 6px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.terminal-fab {
|
||||||
|
position: relative;
|
||||||
|
width: 44px;
|
||||||
|
height: 44px;
|
||||||
|
border: 1px solid rgba(14, 165, 233, 0.15);
|
||||||
|
border-radius: 0;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-end;
|
||||||
|
justify-content: flex-start;
|
||||||
|
padding: 3px 4px;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
image-rendering: pixelated;
|
||||||
|
pointer-events: auto;
|
||||||
|
background-size: cover;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
}
|
||||||
|
|
||||||
|
.terminal-fab:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
border-color: rgba(14, 165, 233, 0.35);
|
||||||
|
box-shadow:
|
||||||
|
0 4px 8px rgba(0, 0, 0, 0.5),
|
||||||
|
0 10px 24px rgba(0, 0, 0, 0.6),
|
||||||
|
0 0 14px rgba(14, 165, 233, 0.15),
|
||||||
|
inset 0 1px 0 rgba(14, 165, 233, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.terminal-fab:active {
|
||||||
|
transform: scale(0.95);
|
||||||
|
}
|
||||||
|
|
||||||
|
.terminal-fab.active {
|
||||||
|
border-color: rgba(34, 211, 238, 0.4);
|
||||||
|
box-shadow:
|
||||||
|
0 0 6px rgba(34, 211, 238, 0.15),
|
||||||
|
inset 0 0 4px rgba(34, 211, 238, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.fab-number {
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: 700;
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
color: rgba(255, 255, 255, 0.55);
|
||||||
|
line-height: 1;
|
||||||
|
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.8);
|
||||||
|
}
|
||||||
|
|
||||||
|
.terminal-fab:hover .fab-number {
|
||||||
|
color: rgba(255, 255, 255, 0.8);
|
||||||
|
}
|
||||||
|
|
||||||
|
.terminal-fab.active .fab-number {
|
||||||
|
color: #67e8f9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.new-session-fab {
|
||||||
|
background: rgba(14, 165, 233, 0.08) !important;
|
||||||
|
align-items: center !important;
|
||||||
|
justify-content: center !important;
|
||||||
|
padding: 0 !important;
|
||||||
|
color: rgba(103, 232, 249, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.new-session-fab:hover {
|
||||||
|
color: rgba(103, 232, 249, 0.9);
|
||||||
|
background: rgba(14, 165, 233, 0.18) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.plus-icon {
|
||||||
|
filter: drop-shadow(0 0 3px rgba(103, 232, 249, 0.3));
|
||||||
|
}
|
||||||
|
|
||||||
|
.fab-dot {
|
||||||
|
position: absolute;
|
||||||
|
top: 3px;
|
||||||
|
right: 3px;
|
||||||
|
width: 5px;
|
||||||
|
height: 5px;
|
||||||
|
border-radius: 0;
|
||||||
|
box-shadow: 0 0 4px currentColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* TransitionGroup animations */
|
||||||
|
.fab-stack-enter-active {
|
||||||
|
transition: all 0.25s cubic-bezier(0.16, 1, 0.3, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.fab-stack-leave-active {
|
||||||
|
transition: all 0.2s ease-in;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fab-stack-enter-from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(8px) scale(0.8);
|
||||||
|
}
|
||||||
|
|
||||||
|
.fab-stack-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateX(8px) scale(0.8);
|
||||||
|
}
|
||||||
|
|
||||||
|
.fab-stack-move {
|
||||||
|
transition: transform 0.25s ease;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
Reference in New Issue
Block a user