fix: prevent terminal snapshot duplication and sidebar navigation

This commit is contained in:
archipelago
2026-10-09 13:12:38 -04:00
parent 2a40cb7b23
commit ae84f62123
+25 -10
View File
@@ -1,8 +1,8 @@
<template>
<Teleport to="body">
<Transition name="cli-popup">
<div v-if="cliStore.isOpen" class="fixed inset-0 z-[2500] flex items-center justify-end p-4 pointer-events-none">
<section ref="panelRef" class="glass-card terminal-panel relative z-10 w-full overflow-hidden pointer-events-auto md:mr-4" :class="{ minimized }" :style="panelStyle" aria-label="Archipelago terminal">
<div v-if="cliStore.isOpen" class="fixed inset-0 z-[2500] flex items-center justify-start p-4 pointer-events-none">
<section ref="panelRef" class="glass-card terminal-panel relative z-10 w-full overflow-hidden pointer-events-auto md:ml-[300px]" :class="{ minimized }" :style="panelStyle" aria-label="Archipelago terminal">
<header class="terminal-header sticky top-0 z-10 flex items-center gap-3 border-b border-white/10 px-4 py-3 bg-black/60 backdrop-blur-md md:bg-transparent md:backdrop-blur-none" @mousedown="startDrag">
<div class="terminal-drag-handle" title="Drag to move">
<svg class="w-4 h-4 text-white/50" fill="currentColor" viewBox="0 0 24 24"><path d="M8 6h2v2H8V6zm0 5h2v2H8v-2zm0 5h2v2H8v-2zm5-10h2v2h-2V6zm0 5h2v2h-2v-2zm0 5h2v2h-2v-2z" /></svg>
@@ -60,10 +60,18 @@ let fitAddon: FitAddon | null = null
let resizeObserver: ResizeObserver | null = null
let socket: WebSocket | null = null
let renderedSession = ''
let connectionGeneration = 0
const activeSession = computed(() => sessions.value.find(session => session.id === selectedId.value))
const panelStyle = computed(() => ({ transform: `translate(${position.value.x}px, ${position.value.y}px)` }))
const wsUrl = (id: string) => `${location.protocol === 'https:' ? 'wss:' : 'ws:'}//${location.host}/ws/terminal?session=${encodeURIComponent(id)}`
function compactSnapshot(data: string) {
const lines = data.split(/\r?\n/)
const visible = (line: string) => line.replace(/\u001b\[[0-9;?]*[ -/]*[@-~]/g, '').trim().length > 0
while (lines.length && !visible(lines[0] ?? '')) lines.shift()
while (lines.length && !visible(lines[lines.length - 1] ?? '')) lines.pop()
return lines.join('\n')
}
async function loadSessions() {
error.value = ''
@@ -112,15 +120,22 @@ function connectSelected() {
terminal?.reset()
renderedSession = selectedId.value
}
socket = new WebSocket(wsUrl(selectedId.value))
socket.onopen = () => { connected.value = true; resizeTerminal() }
socket.onclose = () => { connected.value = false }
socket.onerror = () => { connected.value = false }
socket.onmessage = event => {
const generation = ++connectionGeneration
const connection = new WebSocket(wsUrl(selectedId.value))
socket = connection
connection.onopen = () => { if (generation !== connectionGeneration) return; connected.value = true; resizeTerminal() }
connection.onclose = () => { if (generation === connectionGeneration) connected.value = false }
connection.onerror = () => { if (generation === connectionGeneration) connected.value = false }
connection.onmessage = event => {
if (generation !== connectionGeneration) return
try {
const message = JSON.parse(event.data)
if (message.type === 'output') {
terminal?.write(message.data, () => terminal?.scrollToBottom())
// The daemon sends a complete visible-pane snapshot, not a byte
// stream. Replace the pane so every prompt does not duplicate the
// previous screen into xterm scrollback.
terminal?.reset()
terminal?.write(compactSnapshot(message.data), () => terminal?.scrollToBottom())
}
} catch { /* ignore malformed frames */ }
}
@@ -188,8 +203,8 @@ onBeforeUnmount(() => {
.terminal-title { flex: 1; font-weight: 500; } .terminal-host { color: rgba(255,255,255,.45); font-size: 12px; font-weight: 400; }
.terminal-drag-handle { display: flex; align-items: center; justify-content: center; width: 32px; height: 32px; border-radius: 6px; flex-shrink: 0; }
.terminal-drag-handle:hover { background: rgba(255,255,255,.1); }
.terminal-body { flex: 1 1 auto; min-height: 0; height: 0; overflow: hidden; }
.terminal-output { box-sizing: border-box; height: 100%; max-height: 100%; margin: 0; overflow: hidden; padding: 14px 16px; color: rgba(255,255,255,.9); outline: none; }
.terminal-body { display: flex; flex: 1 1 0; min-height: 0; max-height: 100%; height: 0; overflow: hidden; }
.terminal-output { box-sizing: border-box; flex: 1 1 auto; min-height: 0; min-width: 0; height: 100%; max-height: 100%; margin: 0; overflow: hidden; padding: 14px 16px; color: rgba(255,255,255,.9); outline: none; }
.terminal-output :deep(.xterm) { height: 100%; width: 100%; }
.terminal-output :deep(.xterm-viewport) { background: transparent !important; }
.terminal-output:not(:focus-within) :deep(.xterm-cursor) { visibility: hidden !important; }