fix: prevent terminal snapshot duplication and sidebar navigation
This commit is contained in:
@@ -1,8 +1,8 @@
|
|||||||
<template>
|
<template>
|
||||||
<Teleport to="body">
|
<Teleport to="body">
|
||||||
<Transition name="cli-popup">
|
<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">
|
<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:mr-4" :class="{ minimized }" :style="panelStyle" aria-label="Archipelago terminal">
|
<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">
|
<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">
|
<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>
|
<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 resizeObserver: ResizeObserver | null = null
|
||||||
let socket: WebSocket | null = null
|
let socket: WebSocket | null = null
|
||||||
let renderedSession = ''
|
let renderedSession = ''
|
||||||
|
let connectionGeneration = 0
|
||||||
|
|
||||||
const activeSession = computed(() => sessions.value.find(session => session.id === selectedId.value))
|
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 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)}`
|
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() {
|
async function loadSessions() {
|
||||||
error.value = ''
|
error.value = ''
|
||||||
@@ -112,15 +120,22 @@ function connectSelected() {
|
|||||||
terminal?.reset()
|
terminal?.reset()
|
||||||
renderedSession = selectedId.value
|
renderedSession = selectedId.value
|
||||||
}
|
}
|
||||||
socket = new WebSocket(wsUrl(selectedId.value))
|
const generation = ++connectionGeneration
|
||||||
socket.onopen = () => { connected.value = true; resizeTerminal() }
|
const connection = new WebSocket(wsUrl(selectedId.value))
|
||||||
socket.onclose = () => { connected.value = false }
|
socket = connection
|
||||||
socket.onerror = () => { connected.value = false }
|
connection.onopen = () => { if (generation !== connectionGeneration) return; connected.value = true; resizeTerminal() }
|
||||||
socket.onmessage = event => {
|
connection.onclose = () => { if (generation === connectionGeneration) connected.value = false }
|
||||||
|
connection.onerror = () => { if (generation === connectionGeneration) connected.value = false }
|
||||||
|
connection.onmessage = event => {
|
||||||
|
if (generation !== connectionGeneration) return
|
||||||
try {
|
try {
|
||||||
const message = JSON.parse(event.data)
|
const message = JSON.parse(event.data)
|
||||||
if (message.type === 'output') {
|
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 */ }
|
} 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-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 { 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-drag-handle:hover { background: rgba(255,255,255,.1); }
|
||||||
.terminal-body { flex: 1 1 auto; min-height: 0; height: 0; overflow: hidden; }
|
.terminal-body { display: flex; flex: 1 1 0; min-height: 0; max-height: 100%; 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-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) { height: 100%; width: 100%; }
|
||||||
.terminal-output :deep(.xterm-viewport) { background: transparent !important; }
|
.terminal-output :deep(.xterm-viewport) { background: transparent !important; }
|
||||||
.terminal-output:not(:focus-within) :deep(.xterm-cursor) { visibility: hidden !important; }
|
.terminal-output:not(:focus-within) :deep(.xterm-cursor) { visibility: hidden !important; }
|
||||||
|
|||||||
Reference in New Issue
Block a user