Skip to content

Commit e64d1fe

Browse files
committed
feat(wsh): add attach command for read-only terminal observation
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.
1 parent efd450f commit e64d1fe

14 files changed

Lines changed: 1956 additions & 0 deletions

File tree

cmd/wsh/cmd/wshcmd-attach.go

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
// Copyright 2026, Command Line Inc.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
package cmd
5+
6+
import (
7+
"github.com/spf13/cobra"
8+
"github.com/wavetermdev/waveterm/pkg/waveattach"
9+
)
10+
11+
var attachCmd = &cobra.Command{
12+
Use: "attach [blockid]",
13+
Short: "attach to a Wave Terminal block from an external terminal",
14+
Long: "Attach to a running term block in Wave Terminal. Press Ctrl+A D to detach.",
15+
Args: cobra.MaximumNArgs(1),
16+
RunE: attachRun,
17+
DisableFlagsInUseLine: true,
18+
}
19+
20+
func init() {
21+
rootCmd.AddCommand(attachCmd)
22+
}
23+
24+
func attachRun(cmd *cobra.Command, args []string) error {
25+
rpcClient, _, err := waveattach.Connect()
26+
if err != nil {
27+
return err
28+
}
29+
30+
var blockId string
31+
if len(args) == 1 {
32+
blockId = args[0]
33+
} else {
34+
blockId, err = waveattach.SelectBlock(rpcClient)
35+
if err != nil {
36+
return err
37+
}
38+
}
39+
40+
return waveattach.Attach(rpcClient, blockId)
41+
}

go.mod

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ require (
99
github.com/ebitengine/purego v0.10.0
1010
github.com/emirpasic/gods v1.18.1
1111
github.com/fsnotify/fsnotify v1.9.0
12+
github.com/gitpod-io/xterm-go v0.0.0-20260428130508-743856d9b61c
1213
github.com/golang-jwt/jwt/v5 v5.3.1
1314
github.com/golang-migrate/migrate/v4 v4.19.1
1415
github.com/google/generative-ai-go v0.20.1
@@ -49,6 +50,7 @@ require (
4950
github.com/bahlo/generic-list-go v0.2.0 // indirect
5051
github.com/buger/jsonparser v1.1.2 // indirect
5152
github.com/cespare/xxhash/v2 v2.3.0 // indirect
53+
github.com/dustin/go-humanize v1.0.1 // indirect
5254
github.com/felixge/httpsnoop v1.0.4 // indirect
5355
github.com/go-logr/logr v1.4.3 // indirect
5456
github.com/go-logr/stdr v1.2.2 // indirect
@@ -60,7 +62,9 @@ require (
6062
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect
6163
github.com/mailru/easyjson v0.7.7 // indirect
6264
github.com/mattn/go-isatty v0.0.20 // indirect
65+
github.com/ncruces/go-strftime v1.0.0 // indirect
6366
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
67+
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
6468
github.com/rivo/uniseg v0.4.7 // indirect
6569
github.com/sirupsen/logrus v1.9.3 // indirect
6670
github.com/spf13/pflag v1.0.9 // indirect
@@ -84,6 +88,10 @@ require (
8488
google.golang.org/grpc v1.80.0 // indirect
8589
google.golang.org/protobuf v1.36.11 // indirect
8690
gopkg.in/yaml.v3 v3.0.1 // indirect
91+
modernc.org/libc v1.72.0 // indirect
92+
modernc.org/mathutil v1.7.1 // indirect
93+
modernc.org/memory v1.11.0 // indirect
94+
modernc.org/sqlite v1.50.0 // indirect
8795
)
8896

8997
replace github.com/kevinburke/ssh_config => github.com/wavetermdev/ssh_config v0.0.0-20241219203747-6409e4292f34

go.sum

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
3131
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
3232
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
3333
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
34+
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
35+
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
3436
github.com/ebitengine/purego v0.10.0 h1:QIw4xfpWT6GWTzaW5XEKy3HXoqrJGx1ijYHzTF0/ISU=
3537
github.com/ebitengine/purego v0.10.0/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
3638
github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=
@@ -44,6 +46,8 @@ github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2
4446
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
4547
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
4648
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
49+
github.com/gitpod-io/xterm-go v0.0.0-20260428130508-743856d9b61c h1:85eJHLTqQKUTm3D/SHoVj8MnHv00tnZopydPEH4/OHI=
50+
github.com/gitpod-io/xterm-go v0.0.0-20260428130508-743856d9b61c/go.mod h1:xCLG6CEiFPk1bB7Fa2ahEexQm4h4de04NpQrm8Qkxyw=
4751
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
4852
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
4953
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
@@ -108,6 +112,8 @@ github.com/mattn/go-sqlite3 v1.14.40 h1:f7+saIsbq4EF86mUqe0uiecQOJYMOdfi5uATADmU
108112
github.com/mattn/go-sqlite3 v1.14.40/go.mod h1:pjEuOr8IwzLJP2MfGeTb0A35jauH+C2kbHKBr7yXKVQ=
109113
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
110114
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
115+
github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w=
116+
github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
111117
github.com/photostorm/pty v1.1.19-0.20230903182454-31354506054b h1:cLGKfKb1uk0hxI0Q8L83UAJPpeJ+gSpn3cCU/tjd3eg=
112118
github.com/photostorm/pty v1.1.19-0.20230903182454-31354506054b/go.mod h1:KO+FcPtyLAiRC0hJwreJVvfwc7vnNz77UxBTIGHdPVk=
113119
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo=
@@ -117,6 +123,8 @@ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRI
117123
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
118124
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU=
119125
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
126+
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
127+
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
120128
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
121129
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
122130
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
@@ -220,3 +228,11 @@ gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
220228
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
221229
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
222230
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
231+
modernc.org/libc v1.72.0 h1:IEu559v9a0XWjw0DPoVKtXpO2qt5NVLAnFaBbjq+n8c=
232+
modernc.org/libc v1.72.0/go.mod h1:tTU8DL8A+XLVkEY3x5E/tO7s2Q/q42EtnNWda/L5QhQ=
233+
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
234+
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
235+
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
236+
modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
237+
modernc.org/sqlite v1.50.0 h1:eMowQSWLK0MeiQTdmz3lqoF5dqclujdlIKeJA11+7oM=
238+
modernc.org/sqlite v1.50.0/go.mod h1:m0w8xhwYUVY3H6pSDwc3gkJ/irZT/0YEXwBlhaxQEew=

0 commit comments

Comments
 (0)