feat: Ctrl+1..5 terminal shortcuts and improved AgentBadge indicator

- Ctrl+1 through Ctrl+5 switch to open terminals by index
- Opens floating window automatically if closed
- AgentBadge shows active terminal state dot and index (2/3)
- Dropdown items display shortcut numbers for discoverability
This commit is contained in:
2026-02-20 13:32:42 -06:00
parent 2f26bf999c
commit c6197694b5
2 changed files with 56 additions and 9 deletions

View File

@@ -432,11 +432,25 @@ watch(isOpen, async (open) => {
})
// ============================================================================
// ZOOM KEYBOARD HANDLER
// KEYBOARD SHORTCUTS
// ============================================================================
function handleZoomKey(e: KeyboardEvent) {
if (!isOpen.value || !e.ctrlKey) return
function handleKeydown(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
e.preventDefault()
if (!isOpen.value) isOpen.value = true
switchToTerminal(slot.sessionId)
return
}
// Zoom shortcuts (only when open)
if (!isOpen.value) return
if (e.key === '+' || e.key === '=') {
e.preventDefault()
zoom.value = Math.min(2, +(zoom.value + 0.1).toFixed(1))
@@ -453,7 +467,7 @@ function handleZoomKey(e: KeyboardEvent) {
onMounted(async () => {
checkMobile()
window.addEventListener('resize', checkMobile)
document.addEventListener('keydown', handleZoomKey)
document.addEventListener('keydown', handleKeydown)
oceanLifeTimer = setInterval(tickOceanLife, 20000)
tickOceanLife()
await voice.init()
@@ -463,7 +477,7 @@ onBeforeUnmount(() => {
if (oceanLifeTimer) clearInterval(oceanLifeTimer)
disconnectRealtime()
voice.cleanup()
document.removeEventListener('keydown', handleZoomKey)
document.removeEventListener('keydown', handleKeydown)
document.removeEventListener('mousemove', onDrag)
document.removeEventListener('mouseup', stopDrag)
document.removeEventListener('mousemove', onResize)
@@ -1203,6 +1217,8 @@ 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 */

View File

@@ -1,14 +1,23 @@
<script setup lang="ts">
import { ref, onMounted, onBeforeUnmount } from 'vue'
import { ref, computed, onMounted, onBeforeUnmount } from 'vue'
import type { TerminalSlot } from '@/types/transcript-debug'
defineProps<{
const props = defineProps<{
agent: string
connected: boolean
terminals: TerminalSlot[]
activeSessionId: string | null
}>()
const activeIndex = computed(() => {
if (!props.activeSessionId) return -1
return props.terminals.findIndex(t => t.sessionId === props.activeSessionId)
})
const activeSlot = computed(() =>
activeIndex.value >= 0 ? props.terminals[activeIndex.value] : null
)
const emit = defineEmits<{
'switch-terminal': [sessionId: string]
'close-terminal': [sessionId: string]
@@ -51,8 +60,9 @@ onBeforeUnmount(() => document.removeEventListener('mousedown', onClickOutside))
<template>
<div ref="wrapperRef" class="agent-badge-wrapper" :class="{ connected }" @click="toggle">
<span v-if="activeSlot" class="state-dot badge-dot" :style="{ background: slotColor(activeSlot) }" />
<span class="agent-label">{{ agent }}</span>
<span v-if="terminals.length > 1" class="term-count">{{ terminals.length }}</span>
<span v-if="terminals.length" class="term-count">{{ activeIndex >= 0 ? `${activeIndex + 1}/${terminals.length}` : terminals.length }}</span>
<svg class="caret" :class="{ open: isOpen }" width="6" height="6" viewBox="0 0 6 6" shape-rendering="crispEdges">
<rect x="2" y="4" width="2" height="2" fill="currentColor"/>
<rect x="0" y="2" width="2" height="2" fill="currentColor"/>
@@ -62,12 +72,13 @@ onBeforeUnmount(() => document.removeEventListener('mousedown', onClickOutside))
<div v-if="isOpen" class="dropdown">
<div v-if="terminals.length === 0" class="dropdown-item empty">No terminals</div>
<div
v-for="t in terminals"
v-for="(t, idx) in terminals"
:key="t.sessionId"
class="dropdown-item terminal-item"
:class="{ active: t.sessionId === activeSessionId }"
@click.stop="handleSwitch(t.sessionId)"
>
<span class="shortcut-key">{{ idx + 1 }}</span>
<span class="state-dot" :style="{ background: slotColor(t) }" />
<span class="terminal-label">{{ t.label }}</span>
<button class="close-btn" @click="handleClose($event, t.sessionId)" title="Close terminal">
@@ -125,6 +136,12 @@ onBeforeUnmount(() => document.removeEventListener('mousedown', onClickOutside))
color: #86efac;
}
.badge-dot {
width: 5px;
height: 5px;
flex-shrink: 0;
}
.term-count {
font-size: 8px;
font-weight: 700;
@@ -203,6 +220,20 @@ onBeforeUnmount(() => document.removeEventListener('mousedown', onClickOutside))
color: rgba(255, 255, 255, 0.8);
}
.shortcut-key {
font-size: 8px;
font-weight: 700;
font-family: 'Courier New', monospace;
color: rgba(255, 255, 255, 0.2);
min-width: 10px;
text-align: center;
flex-shrink: 0;
}
.dropdown-item.terminal-item.active .shortcut-key {
color: rgba(165, 180, 252, 0.7);
}
.state-dot {
width: 5px;
height: 5px;