feat: Add reusable TerminalNavButtons to AgentTerminal with mobile touch drag support
- Extract nav buttons (MCP, Claude, Continue, Resume, Clear, Refresh, keys, arrows, scroll) into TerminalNavButtons.vue - Add toggle button in AgentTerminal titlebar to show/hide nav bar - Add sendRaw() to useAgentTerminal for raw PTY input (no \r append) - Add touch drag support for AgentTerminal on mobile - Skip auto-focus on small screens to prevent virtual keyboard popup
This commit is contained in:
@@ -2,6 +2,7 @@
|
||||
import { ref, computed, watch, nextTick, onBeforeUnmount } from 'vue'
|
||||
import type { Agent } from '../../types/agent'
|
||||
import type { AgentTerminal as AgentTerminalType } from '../../composables/useAgentTerminal'
|
||||
import TerminalNavButtons from '../TerminalNavButtons.vue'
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: boolean
|
||||
@@ -25,6 +26,8 @@ const agentRunning = props.agentTerminal.agentRunning
|
||||
const startAgent = props.agentTerminal.startAgent
|
||||
const stopAgent = props.agentTerminal.stopAgent
|
||||
|
||||
const showNavButtons = ref(false)
|
||||
|
||||
// Local ref for the xterm container - syncs to composable's containerRef
|
||||
const terminalContainer = ref<HTMLElement | null>(null)
|
||||
|
||||
@@ -81,27 +84,41 @@ const terminalStyle = computed((): Record<string, string> => {
|
||||
|
||||
// ── Drag ──
|
||||
|
||||
function startDrag(e: MouseEvent) {
|
||||
function startDrag(e: MouseEvent | TouchEvent) {
|
||||
if ((e.target as HTMLElement).closest('.window-controls')) return
|
||||
if (e instanceof TouchEvent) e.preventDefault()
|
||||
isDragging.value = true
|
||||
|
||||
const touch = e instanceof TouchEvent ? e.touches[0] : null
|
||||
const clientX = e instanceof MouseEvent ? e.clientX : (touch?.clientX ?? 0)
|
||||
const clientY = e instanceof MouseEvent ? e.clientY : (touch?.clientY ?? 0)
|
||||
|
||||
const rect = windowRef.value?.getBoundingClientRect()
|
||||
if (rect) {
|
||||
if (!hasCustomPosition.value) {
|
||||
position.value = { x: rect.left, y: rect.top }
|
||||
}
|
||||
dragOffset.value = { x: e.clientX - rect.left, y: e.clientY - rect.top }
|
||||
dragOffset.value = { x: clientX - rect.left, y: clientY - rect.top }
|
||||
}
|
||||
document.addEventListener('mousemove', onDrag)
|
||||
document.addEventListener('mouseup', stopDrag)
|
||||
document.addEventListener('touchmove', onDrag, { passive: false })
|
||||
document.addEventListener('touchend', stopDrag)
|
||||
}
|
||||
|
||||
function onDrag(e: MouseEvent) {
|
||||
function onDrag(e: MouseEvent | TouchEvent) {
|
||||
if (!isDragging.value) return
|
||||
if (e instanceof TouchEvent) e.preventDefault()
|
||||
|
||||
const touch = e instanceof TouchEvent ? e.touches[0] : null
|
||||
const clientX = e instanceof MouseEvent ? e.clientX : (touch?.clientX ?? 0)
|
||||
const clientY = e instanceof MouseEvent ? e.clientY : (touch?.clientY ?? 0)
|
||||
|
||||
const w = windowRef.value?.offsetWidth || 560
|
||||
const h = windowRef.value?.offsetHeight || 340
|
||||
position.value = {
|
||||
x: Math.max(-w * 0.75, Math.min(e.clientX - dragOffset.value.x, window.innerWidth - w * 0.25)),
|
||||
y: Math.max(-h * 0.75, Math.min(e.clientY - dragOffset.value.y, window.innerHeight - h * 0.25))
|
||||
x: Math.max(-w * 0.75, Math.min(clientX - dragOffset.value.x, window.innerWidth - w * 0.25)),
|
||||
y: Math.max(-h * 0.75, Math.min(clientY - dragOffset.value.y, window.innerHeight - h * 0.25))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -110,6 +127,8 @@ function stopDrag() {
|
||||
hasCustomPosition.value = true
|
||||
document.removeEventListener('mousemove', onDrag)
|
||||
document.removeEventListener('mouseup', stopDrag)
|
||||
document.removeEventListener('touchmove', onDrag)
|
||||
document.removeEventListener('touchend', stopDrag)
|
||||
}
|
||||
|
||||
// ── Resize ──
|
||||
@@ -152,6 +171,42 @@ function handleRestart() {
|
||||
startAgent(true)
|
||||
}
|
||||
|
||||
function toggleNavButtons() {
|
||||
showNavButtons.value = !showNavButtons.value
|
||||
}
|
||||
|
||||
// Nav button actions - send commands through the agent terminal
|
||||
function navRunClaude() {
|
||||
props.agentTerminal.sendInput('claude')
|
||||
}
|
||||
|
||||
function navRunClaudeContinue() {
|
||||
props.agentTerminal.sendInput('claude --continue')
|
||||
}
|
||||
|
||||
function navRunClaudeResume() {
|
||||
props.agentTerminal.sendInput('claude --resume')
|
||||
}
|
||||
|
||||
function navRefresh() {
|
||||
renderer.fit()
|
||||
}
|
||||
|
||||
function navSendKey(key: string) {
|
||||
const keyMap: Record<string, string> = {
|
||||
'up': '\x1b[A', 'down': '\x1b[B', 'left': '\x1b[D', 'right': '\x1b[C',
|
||||
'alt-m': '\x1bm', 'ctrl-c': '\x03', 'tab': '\t', 'esc': '\x1b'
|
||||
}
|
||||
const data = keyMap[key]
|
||||
if (data) props.agentTerminal.sendRaw(data)
|
||||
}
|
||||
|
||||
function navScroll(direction: 'up' | 'down' | 'end') {
|
||||
if (direction === 'up') renderer.scrollLines(-10)
|
||||
else if (direction === 'down') renderer.scrollLines(10)
|
||||
else renderer.scrollToBottom()
|
||||
}
|
||||
|
||||
// ── Watch open/close to init terminal ──
|
||||
|
||||
watch(isOpen, (open) => {
|
||||
@@ -161,9 +216,13 @@ watch(isOpen, (open) => {
|
||||
if (!renderer.isReady.value) {
|
||||
renderer.init()
|
||||
}
|
||||
// Wait for CSS transition then fit + focus
|
||||
// Wait for CSS transition then fit (+ focus only on desktop)
|
||||
setTimeout(() => {
|
||||
renderer.onBecameVisible()
|
||||
renderer.fit()
|
||||
renderer.terminal.value?.refresh(0, renderer.terminal.value.rows - 1)
|
||||
if (window.innerWidth > 1024) {
|
||||
renderer.focus()
|
||||
}
|
||||
}, 150)
|
||||
})
|
||||
} else {
|
||||
@@ -174,6 +233,8 @@ watch(isOpen, (open) => {
|
||||
onBeforeUnmount(() => {
|
||||
document.removeEventListener('mousemove', onDrag)
|
||||
document.removeEventListener('mouseup', stopDrag)
|
||||
document.removeEventListener('touchmove', onDrag)
|
||||
document.removeEventListener('touchend', stopDrag)
|
||||
document.removeEventListener('mousemove', onResize)
|
||||
document.removeEventListener('mouseup', stopResize)
|
||||
})
|
||||
@@ -191,7 +252,7 @@ onBeforeUnmount(() => {
|
||||
>
|
||||
<div class="at-glass" :style="{ borderColor: agentBorder + '40' }">
|
||||
<!-- Titlebar -->
|
||||
<div class="at-titlebar" @mousedown="startDrag">
|
||||
<div class="at-titlebar" @mousedown="startDrag" @touchstart="startDrag">
|
||||
<div class="at-left">
|
||||
<div
|
||||
class="at-badge"
|
||||
@@ -227,12 +288,33 @@ onBeforeUnmount(() => {
|
||||
>
|
||||
<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 6h18M8 6V4a2 2 0 012-2h4a2 2 0 012 2v2m3 0v14a2 2 0 01-2 2H7a2 2 0 01-2-2V6h14"/></svg>
|
||||
</button>
|
||||
<button
|
||||
class="wc-btn nav-toggle"
|
||||
:class="{ active: showNavButtons }"
|
||||
title="Toggle navigation"
|
||||
@click.stop="toggleNavButtons"
|
||||
>
|
||||
<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M4 6h16M4 12h16M4 18h16"/></svg>
|
||||
</button>
|
||||
<button class="wc-btn x" title="Close" @click.stop="close">
|
||||
<svg width="8" height="8" viewBox="0 0 10 10"><line x1="0" y1="0" x2="10" y2="10" stroke="currentColor" stroke-width="1.5"/><line x1="10" y1="0" x2="0" y2="10" stroke="currentColor" stroke-width="1.5"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Nav buttons bar -->
|
||||
<TerminalNavButtons
|
||||
v-if="showNavButtons"
|
||||
@request-token="agentTerminal.sendInput('genera token usando tu mcp')"
|
||||
@run-claude="navRunClaude"
|
||||
@run-claude-continue="navRunClaudeContinue"
|
||||
@run-claude-resume="navRunClaudeResume"
|
||||
@clear-buffer="clearBuffer"
|
||||
@refresh="navRefresh"
|
||||
@send-key="navSendKey"
|
||||
@scroll="navScroll"
|
||||
/>
|
||||
|
||||
<!-- Terminal content -->
|
||||
<div class="at-content" :style="{ background: agentBg }">
|
||||
<div ref="terminalContainer" class="at-term"></div>
|
||||
@@ -298,6 +380,7 @@ onBeforeUnmount(() => {
|
||||
cursor: grab;
|
||||
user-select: none;
|
||||
}
|
||||
.at-titlebar { touch-action: none; }
|
||||
.agent-terminal.dragging .at-titlebar { cursor: grabbing; }
|
||||
|
||||
.at-left {
|
||||
@@ -362,6 +445,8 @@ onBeforeUnmount(() => {
|
||||
.wc-btn.start { color: #0a0; }
|
||||
.wc-btn.start:hover { background: rgba(16, 185, 129, 0.3); }
|
||||
.wc-btn.restart:hover { background: rgba(245, 158, 11, 0.3); color: #a80; }
|
||||
.wc-btn.nav-toggle.active { background: rgba(99, 102, 241, 0.3); border-color: rgba(99, 102, 241, 0.4); color: #818cf8; }
|
||||
.wc-btn.nav-toggle:hover { background: rgba(99, 102, 241, 0.2); color: #818cf8; }
|
||||
|
||||
.at-content {
|
||||
flex: 1;
|
||||
|
||||
Reference in New Issue
Block a user