refactor: unify dashboard/terminal selector into single strip, default to dashboard

Remove separate Chat/SE tab toggle. Dashboard is now a button in the
terminal strip alongside T1-T5. Chat only renders when a specific
terminal route is active (/transcript-debug/:n).
This commit is contained in:
2026-02-24 18:16:35 -06:00
parent 4cb0760c50
commit cfb58c3a9f

View File

@@ -4,7 +4,7 @@ import { useRoute, useRouter } from 'vue-router'
import { useTranscriptDebug } from '@/composables/transcript-debug' import { useTranscriptDebug } from '@/composables/transcript-debug'
import { useVoiceInput } from '@/composables/useVoiceInput' import { useVoiceInput } from '@/composables/useVoiceInput'
import { useSessionState } from '@/stores/session-state' import { useSessionState } from '@/stores/session-state'
import { ChatContainer, AquaticBackground, AgentBadge, NewSessionModal } from '@/components/transcript-debug' import { ChatContainer, AquaticBackground, AgentBadge, NewSessionModal, SyncEnginePanel } from '@/components/transcript-debug'
import type { AgentName } from '@/types/transcript-debug' import type { AgentName } from '@/types/transcript-debug'
import { isTauri, isMobileTauri, getTauriWindow } from '@/lib/tauri' import { isTauri, isMobileTauri, getTauriWindow } from '@/lib/tauri'
import { usePipWindow } from '@/composables/usePipWindow' import { usePipWindow } from '@/composables/usePipWindow'
@@ -63,6 +63,9 @@ const showSelector = ref(false)
const showNewSessionModal = ref(false) const showNewSessionModal = ref(false)
const isPipWindow = computed(() => route.query.pip === '1') const isPipWindow = computed(() => route.query.pip === '1')
// Dashboard vs terminal view — driven by route
const isDashboard = computed(() => !route.params.terminalIndex)
// Readability overlay // Readability overlay
const savedOverlay = localStorage.getItem('transcript-overlay-opacity') const savedOverlay = localStorage.getItem('transcript-overlay-opacity')
const overlayOpacity = ref(savedOverlay !== null ? parseFloat(savedOverlay) : 0.55) const overlayOpacity = ref(savedOverlay !== null ? parseFloat(savedOverlay) : 0.55)
@@ -136,6 +139,10 @@ async function handleModalCreateNew(agent: AgentName, initialPrompt: string) {
} }
} }
function goToDashboard() {
router.push({ name: 'transcript-debug' })
}
function handleTerminalSwitch(sessionId: string) { function handleTerminalSwitch(sessionId: string) {
const idx = sessionState.terminalRegistry.findIndex( const idx = sessionState.terminalRegistry.findIndex(
e => e.transcriptSessionId === sessionId e => e.transcriptSessionId === sessionId
@@ -219,6 +226,15 @@ onMounted(async () => {
await init() await init()
await voice.init() await voice.init()
syncTerminalFromRoute() syncTerminalFromRoute()
// Signal to the parent window that this PiP page is ready to show
if (isTauri && route.query.pip === '1') {
try {
const { getCurrentWebviewWindow } = await import('@tauri-apps/api/webviewWindow')
const { emitTo } = await import('@tauri-apps/api/event')
await emitTo('main', 'pip:ready', getCurrentWebviewWindow().label)
} catch {}
}
}) })
onBeforeUnmount(() => { onBeforeUnmount(() => {
@@ -252,13 +268,23 @@ onBeforeUnmount(() => {
<!-- Terminal selector strip (hidden in PiP) --> <!-- Terminal selector strip (hidden in PiP) -->
<div v-if="!isPipWindow" class="terminal-strip"> <div v-if="!isPipWindow" class="terminal-strip">
<div class="strip-left"> <div class="strip-left">
<button
:class="['strip-terminal-btn', 'strip-dashboard-btn', { active: isDashboard }]"
@click="goToDashboard"
title="Dashboard"
>
<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
<rect x="3" y="3" width="7" height="7" rx="1"/><rect x="14" y="3" width="7" height="7" rx="1"/>
<rect x="3" y="14" width="7" height="7" rx="1"/><rect x="14" y="14" width="7" height="7" rx="1"/>
</svg>
</button>
<button <button
v-for="(entry, idx) in sessionState.terminalRegistry" v-for="(entry, idx) in sessionState.terminalRegistry"
:key="entry.transcriptSessionId" :key="entry.transcriptSessionId"
:class="[ :class="[
'strip-terminal-btn', 'strip-terminal-btn',
{ {
active: String(idx + 1) === route.params.terminalIndex || (!route.params.terminalIndex && entry.transcriptSessionId === activeTerminalSessionId), active: String(idx + 1) === route.params.terminalIndex,
dead: !entry.alive dead: !entry.alive
} }
]" ]"
@@ -360,56 +386,62 @@ onBeforeUnmount(() => {
</div> </div>
</Transition> </Transition>
<ChatContainer <!-- Terminal chat (only when a specific terminal is selected) -->
ref="chatRef" <template v-if="!isDashboard">
v-if="conversation" <ChatContainer
:conversation="conversation" ref="chatRef"
:processing="processing" v-if="conversation"
:terminal-ready="terminalReady" :conversation="conversation"
:terminal="ephemeral" :processing="processing"
:show-selector="showSelector" :terminal-ready="terminalReady"
:agents="agents" :terminal="ephemeral"
:selected-agent="selectedAgent" :show-selector="showSelector"
:sessions="sessions" :agents="agents"
:selected-session-id="selectedSessionId" :selected-agent="selectedAgent"
:sessions-loading="loading" :sessions="sessions"
:voice-mode="voiceMode" :selected-session-id="selectedSessionId"
:whisper-status="whisperStatus" :sessions-loading="loading"
:audio-devices="audioDevices" :voice-mode="voiceMode"
:selected-device-id="selectedDeviceId" :whisper-status="whisperStatus"
:is-recording="voiceRecording" :audio-devices="audioDevices"
:voice-transcript="voiceTranscript + voiceInterim" :selected-device-id="selectedDeviceId"
:last-audio-url="lastAudioUrl" :is-recording="voiceRecording"
:is-playing-audio="isPlayingAudio" :voice-transcript="voiceTranscript + voiceInterim"
:overlay-opacity="overlayOpacity" :last-audio-url="lastAudioUrl"
:input-max-lines="inputMaxLines" :is-playing-audio="isPlayingAudio"
:scroll-jump-percent="scrollJumpPercent" :overlay-opacity="overlayOpacity"
:scroll-nav-mode="scrollNavMode" :input-max-lines="inputMaxLines"
:hook-permission-mode="hookMeta.permissionMode" :scroll-jump-percent="scrollJumpPercent"
@send="handleSend" :scroll-nav-mode="scrollNavMode"
@switch-agent="handleAgentSwitch" :hook-permission-mode="hookMeta.permissionMode"
@select-session="handleSessionSelect" @send="handleSend"
@create-session="handleCreateSession" @switch-agent="handleAgentSwitch"
@close-session="closeTerminal" @select-session="handleSessionSelect"
@start-recording="voice.startRecording()" @create-session="handleCreateSession"
@stop-recording="voice.stopRecording()" @close-session="closeTerminal"
@set-voice-mode="voice.setMode($event)" @start-recording="voice.startRecording()"
@select-microphone="voice.selectMicrophone($event)" @stop-recording="voice.stopRecording()"
@play-last-audio="voice.playLastAudio()" @set-voice-mode="voice.setMode($event)"
@update:overlay-opacity="setOverlayOpacity" @select-microphone="voice.selectMicrophone($event)"
@update:input-max-lines="setInputMaxLines" @play-last-audio="voice.playLastAudio()"
@update:scroll-jump-percent="setScrollJumpPercent" @update:overlay-opacity="setOverlayOpacity"
@update:scroll-nav-mode="setScrollNavMode" @update:input-max-lines="setInputMaxLines"
/> @update:scroll-jump-percent="setScrollJumpPercent"
@update:scroll-nav-mode="setScrollNavMode"
/>
<div v-else-if="!transitioning" class="empty-state"> <div v-else-if="!transitioning" class="empty-state">
<svg width="40" height="40" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"> <svg width="40" height="40" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
<path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/> <path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/>
</svg> </svg>
<span>No active terminal</span> <span>No active terminal</span>
<small v-if="!sessionState.terminalRegistry.length">No terminals registered</small> <small v-if="!sessionState.terminalRegistry.length">No terminals registered</small>
<small v-else>Select a terminal above to begin</small> <small v-else>Select a terminal above to begin</small>
</div> </div>
</template>
<!-- Dashboard (default view) -->
<SyncEnginePanel v-if="isDashboard" />
</div> </div>
<!-- New session modal --> <!-- New session modal -->
@@ -499,6 +531,16 @@ onBeforeUnmount(() => {
gap: 0.5rem; gap: 0.5rem;
} }
.strip-dashboard-btn {
gap: 0;
}
.strip-dashboard-btn svg {
opacity: 0.7;
}
.strip-dashboard-btn.active svg {
opacity: 1;
}
.realtime-dot { .realtime-dot {
color: var(--text-muted); color: var(--text-muted);
opacity: 0.4; opacity: 0.4;
@@ -641,6 +683,13 @@ onBeforeUnmount(() => {
ChatContainer glass-transparent overrides (mirrored from FloatingTranscriptDebug) ChatContainer glass-transparent overrides (mirrored from FloatingTranscriptDebug)
══════════════════════════════════════════════════════════════════════════════ */ ══════════════════════════════════════════════════════════════════════════════ */
.content-area :deep(.sync-engine-panel) {
position: relative;
z-index: 1;
flex: 1;
overflow-y: auto;
}
.content-area :deep(.chat-container) { .content-area :deep(.chat-container) {
background: transparent !important; background: transparent !important;
border: none !important; border: none !important;