mirror of
https://github.com/wavetermdev/waveterm
synced 2026-05-24 09:18:27 +00:00
Adds `wsh attach` — a command that streams the live output of any Wave Terminal block to a local terminal window without affecting the remote session. Useful for monitoring long-running processes, CI jobs, or AI coding agents from a separate window or SSH session. Key capabilities: - Interactive block selector (workspace → tab → block) - Live PTY streaming via snapshot + WPS event subscription - Viewport model: server PTY size is fixed; local terminal is a moveable window into the remote screen (Ctrl+Arrow to pan) - Diff-based renderer that emits only changed cells per frame, with full SGR, wide-character, alt-screen, and cursor-style sync - Debounced render loop (16 ms) coalesces rapid PTY bursts so that full-screen TUI repaints are always consumed before rendering - Resync command (Ctrl-A s) rebuilds xterm-go state from a fresh snapshot when local state drifts from the remote Bug fix included: EventRecv messages are now dispatched synchronously in the WshRpc message loop (same pattern as StreamData/StreamDataAck) so that back-to-back PTY events are always processed in arrival order. Without this fix, concurrent goroutines race to write PTY chunks into the terminal emulator, producing mixed-frame garbling.
105 lines
2.6 KiB
Go
105 lines
2.6 KiB
Go
// Copyright 2026, Command Line Inc.
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
package waveattach
|
|
|
|
import (
|
|
"bytes"
|
|
"testing"
|
|
)
|
|
|
|
func TestPrefixKeyMachine_PlainBytesPassThrough(t *testing.T) {
|
|
m := newPrefixKey()
|
|
var out bytes.Buffer
|
|
act, err := m.feed([]byte("hello"), &out)
|
|
if err != nil || act != actionNone {
|
|
t.Fatalf("unexpected: action=%v err=%v", act, err)
|
|
}
|
|
if out.String() != "hello" {
|
|
t.Errorf("want 'hello', got %q", out.String())
|
|
}
|
|
}
|
|
|
|
func TestPrefixKeyMachine_DetachOnCtrlAD(t *testing.T) {
|
|
m := newPrefixKey()
|
|
var out bytes.Buffer
|
|
act, _ := m.feed([]byte{0x01, 'd'}, &out)
|
|
if act != actionDetach {
|
|
t.Fatalf("expected detach, got %v", act)
|
|
}
|
|
if out.Len() != 0 {
|
|
t.Errorf("expected nothing forwarded, got %q", out.String())
|
|
}
|
|
}
|
|
|
|
func TestPrefixKeyMachine_DetachOnCtrlACapitalD(t *testing.T) {
|
|
m := newPrefixKey()
|
|
var out bytes.Buffer
|
|
act, _ := m.feed([]byte{0x01, 'D'}, &out)
|
|
if act != actionDetach {
|
|
t.Fatalf("expected detach, got %v", act)
|
|
}
|
|
}
|
|
|
|
func TestPrefixKeyMachine_RedrawOnCtrlAR(t *testing.T) {
|
|
m := newPrefixKey()
|
|
var out bytes.Buffer
|
|
act, _ := m.feed([]byte{0x01, 'r'}, &out)
|
|
if act != actionRedraw {
|
|
t.Fatalf("expected redraw, got %v", act)
|
|
}
|
|
if out.Len() != 0 {
|
|
t.Errorf("expected nothing forwarded, got %q", out.String())
|
|
}
|
|
}
|
|
|
|
func TestPrefixKeyMachine_ResyncOnCtrlAS(t *testing.T) {
|
|
m := newPrefixKey()
|
|
var out bytes.Buffer
|
|
act, _ := m.feed([]byte{0x01, 'S'}, &out)
|
|
if act != actionResync {
|
|
t.Fatalf("expected resync, got %v", act)
|
|
}
|
|
if out.Len() != 0 {
|
|
t.Errorf("expected nothing forwarded, got %q", out.String())
|
|
}
|
|
}
|
|
|
|
func TestPrefixKeyMachine_LiteralCtrlAByDoubling(t *testing.T) {
|
|
m := newPrefixKey()
|
|
var out bytes.Buffer
|
|
act, _ := m.feed([]byte{0x01, 0x01}, &out)
|
|
if act != actionNone {
|
|
t.Fatalf("did not expect action, got %v", act)
|
|
}
|
|
if !bytes.Equal(out.Bytes(), []byte{0x01}) {
|
|
t.Errorf("want 0x01, got %v", out.Bytes())
|
|
}
|
|
}
|
|
|
|
func TestPrefixKeyMachine_PrefixThenOtherKey(t *testing.T) {
|
|
m := newPrefixKey()
|
|
var out bytes.Buffer
|
|
act, _ := m.feed([]byte{0x01, 'x'}, &out)
|
|
if act != actionNone {
|
|
t.Fatalf("did not expect action, got %v", act)
|
|
}
|
|
if !bytes.Equal(out.Bytes(), []byte{0x01, 'x'}) {
|
|
t.Errorf("want [0x01 'x'], got %v", out.Bytes())
|
|
}
|
|
}
|
|
|
|
func TestPrefixKeyMachine_PrefixSplitAcrossReads(t *testing.T) {
|
|
m := newPrefixKey()
|
|
var out bytes.Buffer
|
|
if act, _ := m.feed([]byte{0x01}, &out); act != actionNone {
|
|
t.Fatalf("did not expect action yet, got %v", act)
|
|
}
|
|
if out.Len() != 0 {
|
|
t.Errorf("expected buffered, got %q", out.String())
|
|
}
|
|
act, _ := m.feed([]byte{'d'}, &out)
|
|
if act != actionDetach {
|
|
t.Fatalf("expected detach on second feed, got %v", act)
|
|
}
|
|
}
|