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