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) {
|
function handleKeydown(e: KeyboardEvent) {
|
||||||
if (!isOpen.value || !e.ctrlKey) return
|
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 === '=') {
|
if (e.key === '+' || e.key === '=') {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
zoom.value = Math.min(2, +(zoom.value + 0.1).toFixed(1))
|
zoom.value = Math.min(2, +(zoom.value + 0.1).toFixed(1))
|
||||||
@@ -453,7 +467,7 @@ function handleZoomKey(e: KeyboardEvent) {
|
|||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
checkMobile()
|
checkMobile()
|
||||||
window.addEventListener('resize', checkMobile)
|
window.addEventListener('resize', checkMobile)
|
||||||
document.addEventListener('keydown', handleZoomKey)
|
document.addEventListener('keydown', handleKeydown)
|
||||||
oceanLifeTimer = setInterval(tickOceanLife, 20000)
|
oceanLifeTimer = setInterval(tickOceanLife, 20000)
|
||||||
tickOceanLife()
|
tickOceanLife()
|
||||||
await voice.init()
|
await voice.init()
|
||||||
@@ -463,7 +477,7 @@ onBeforeUnmount(() => {
|
|||||||
if (oceanLifeTimer) clearInterval(oceanLifeTimer)
|
if (oceanLifeTimer) clearInterval(oceanLifeTimer)
|
||||||
disconnectRealtime()
|
disconnectRealtime()
|
||||||
voice.cleanup()
|
voice.cleanup()
|
||||||
document.removeEventListener('keydown', handleZoomKey)
|
document.removeEventListener('keydown', handleKeydown)
|
||||||
document.removeEventListener('mousemove', onDrag)
|
document.removeEventListener('mousemove', onDrag)
|
||||||
document.removeEventListener('mouseup', stopDrag)
|
document.removeEventListener('mouseup', stopDrag)
|
||||||
document.removeEventListener('mousemove', onResize)
|
document.removeEventListener('mousemove', onResize)
|
||||||
@@ -1203,6 +1217,8 @@ onBeforeUnmount(() => {
|
|||||||
color: rgba(255,255,255,0.85);
|
color: rgba(255,255,255,0.85);
|
||||||
font-family: 'Courier New', monospace;
|
font-family: 'Courier New', monospace;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
|
line-height: 1.5;
|
||||||
|
field-sizing: content;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Send button: pixel art daytime ocean, no border */
|
/* Send button: pixel art daytime ocean, no border */
|
||||||
|
|||||||
@@ -1,14 +1,23 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, onMounted, onBeforeUnmount } from 'vue'
|
import { ref, computed, onMounted, onBeforeUnmount } from 'vue'
|
||||||
import type { TerminalSlot } from '@/types/transcript-debug'
|
import type { TerminalSlot } from '@/types/transcript-debug'
|
||||||
|
|
||||||
defineProps<{
|
const props = defineProps<{
|
||||||
agent: string
|
agent: string
|
||||||
connected: boolean
|
connected: boolean
|
||||||
terminals: TerminalSlot[]
|
terminals: TerminalSlot[]
|
||||||
activeSessionId: string | null
|
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<{
|
const emit = defineEmits<{
|
||||||
'switch-terminal': [sessionId: string]
|
'switch-terminal': [sessionId: string]
|
||||||
'close-terminal': [sessionId: string]
|
'close-terminal': [sessionId: string]
|
||||||
@@ -51,8 +60,9 @@ onBeforeUnmount(() => document.removeEventListener('mousedown', onClickOutside))
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div ref="wrapperRef" class="agent-badge-wrapper" :class="{ connected }" @click="toggle">
|
<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 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">
|
<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="2" y="4" width="2" height="2" fill="currentColor"/>
|
||||||
<rect x="0" y="2" 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="isOpen" class="dropdown">
|
||||||
<div v-if="terminals.length === 0" class="dropdown-item empty">No terminals</div>
|
<div v-if="terminals.length === 0" class="dropdown-item empty">No terminals</div>
|
||||||
<div
|
<div
|
||||||
v-for="t in terminals"
|
v-for="(t, idx) in terminals"
|
||||||
:key="t.sessionId"
|
:key="t.sessionId"
|
||||||
class="dropdown-item terminal-item"
|
class="dropdown-item terminal-item"
|
||||||
:class="{ active: t.sessionId === activeSessionId }"
|
:class="{ active: t.sessionId === activeSessionId }"
|
||||||
@click.stop="handleSwitch(t.sessionId)"
|
@click.stop="handleSwitch(t.sessionId)"
|
||||||
>
|
>
|
||||||
|
<span class="shortcut-key">{{ idx + 1 }}</span>
|
||||||
<span class="state-dot" :style="{ background: slotColor(t) }" />
|
<span class="state-dot" :style="{ background: slotColor(t) }" />
|
||||||
<span class="terminal-label">{{ t.label }}</span>
|
<span class="terminal-label">{{ t.label }}</span>
|
||||||
<button class="close-btn" @click="handleClose($event, t.sessionId)" title="Close terminal">
|
<button class="close-btn" @click="handleClose($event, t.sessionId)" title="Close terminal">
|
||||||
@@ -125,6 +136,12 @@ onBeforeUnmount(() => document.removeEventListener('mousedown', onClickOutside))
|
|||||||
color: #86efac;
|
color: #86efac;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.badge-dot {
|
||||||
|
width: 5px;
|
||||||
|
height: 5px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.term-count {
|
.term-count {
|
||||||
font-size: 8px;
|
font-size: 8px;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
@@ -203,6 +220,20 @@ onBeforeUnmount(() => document.removeEventListener('mousedown', onClickOutside))
|
|||||||
color: rgba(255, 255, 255, 0.8);
|
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 {
|
.state-dot {
|
||||||
width: 5px;
|
width: 5px;
|
||||||
height: 5px;
|
height: 5px;
|
||||||
|
|||||||
Reference in New Issue
Block a user