Skip to content

Commit 18830da

Browse files
committed
Merge remote-tracking branch 'origin/main' into sawka/remove-pinned-tabs
2 parents fa37776 + 90cffe4 commit 18830da

8 files changed

Lines changed: 82 additions & 229 deletions

File tree

cmd/test/test-main.go

Lines changed: 1 addition & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -3,53 +3,6 @@
33

44
package main
55

6-
import (
7-
"context"
8-
"fmt"
9-
"log"
10-
11-
"github.com/wavetermdev/waveterm/pkg/vdom"
12-
"github.com/wavetermdev/waveterm/pkg/wshutil"
13-
)
14-
15-
func Page(ctx context.Context, props map[string]any) any {
16-
clicked, setClicked := vdom.UseState(ctx, false)
17-
var clickedDiv *vdom.VDomElem
18-
if clicked {
19-
clickedDiv = vdom.Bind(`<div>clicked</div>`, nil)
20-
}
21-
clickFn := func() {
22-
log.Printf("run clickFn\n")
23-
setClicked(true)
24-
}
25-
return vdom.Bind(
26-
`
27-
<div>
28-
<h1>hello world</h1>
29-
<Button onClick="#bind:clickFn">hello</Button>
30-
<bind key="clickedDiv"/>
31-
</div>
32-
`,
33-
map[string]any{"clickFn": clickFn, "clickedDiv": clickedDiv},
34-
)
35-
}
36-
37-
func Button(ctx context.Context, props map[string]any) any {
38-
ref := vdom.UseVDomRef(ctx)
39-
clName, setClName := vdom.UseState(ctx, "button")
40-
vdom.UseEffect(ctx, func() func() {
41-
fmt.Printf("Button useEffect\n")
42-
setClName("button mounted")
43-
return nil
44-
}, nil)
45-
return vdom.Bind(`
46-
<div className="#bind:clName" ref="#bind:ref" onClick="#bind:onClick">
47-
<bind key="children"/>
48-
</div>
49-
`, map[string]any{"clName": clName, "ref": ref, "onClick": props["onClick"], "children": props["children"]})
50-
}
51-
526
func main() {
53-
wshutil.SetTermRawModeAndInstallShutdownHandlers(true)
54-
defer wshutil.RestoreTermState()
7+
558
}

cmd/wsh/cmd/wshcmd-root.go

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -84,10 +84,7 @@ func OutputHelpMessage(cmd *cobra.Command) {
8484
func preRunSetupRpcClient(cmd *cobra.Command, args []string) error {
8585
jwtToken := os.Getenv(wshutil.WaveJwtTokenVarName)
8686
if jwtToken == "" {
87-
wshutil.SetTermRawModeAndInstallShutdownHandlers(true)
88-
UsingTermWshMode = true
89-
RpcClient, WrappedStdin = wshutil.SetupTerminalRpcClient(nil, "wshcmd-termclient")
90-
return nil
87+
return fmt.Errorf("wsh must be run inside a Wave-managed SSH session (WAVETERM_JWT not found)")
9188
}
9289
err := setupRpcClient(nil, jwtToken)
9390
if err != nil {

frontend/app/view/term/term.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -286,6 +286,7 @@ const TerminalView = ({ blockId, model }: ViewComponentProps<TermViewModel>) =>
286286
keydownHandler: model.handleTerminalKeydown.bind(model),
287287
useWebGl: !termSettings?.["term:disablewebgl"],
288288
sendDataHandler: model.sendDataToController.bind(model),
289+
nodeModel: model.nodeModel,
289290
}
290291
);
291292
(window as any).term = termWrap;

frontend/app/view/term/termwrap.ts

Lines changed: 68 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,12 @@
11
// Copyright 2025, Command Line Inc.
22
// SPDX-License-Identifier: Apache-2.0
33

4+
import type { BlockNodeModel } from "@/app/block/blocktypes";
45
import { getFileSubject } from "@/app/store/wps";
56
import { sendWSCommand } from "@/app/store/ws";
67
import { RpcApi } from "@/app/store/wshclientapi";
78
import { TabRpcClient } from "@/app/store/wshrpcutil";
8-
import {
9-
WOS,
10-
atoms,
11-
fetchWaveFile,
12-
getApi,
13-
getSettingsKeyAtom,
14-
globalStore,
15-
openLink,
16-
recordTEvent,
17-
} from "@/store/global";
9+
import { WOS, fetchWaveFile, getApi, getSettingsKeyAtom, globalStore, openLink, recordTEvent } from "@/store/global";
1810
import * as services from "@/store/services";
1911
import { PLATFORM, PlatformMacOS } from "@/util/platformutil";
2012
import { base64ToArray, base64ToString, fireAndForget } from "@/util/util";
@@ -35,6 +27,8 @@ const dlog = debug("wave:termwrap");
3527
const TermFileName = "term";
3628
const TermCacheFileName = "cache:term:full";
3729
const MinDataProcessedForCache = 100 * 1024;
30+
const Osc52MaxDecodedSize = 75 * 1024; // max clipboard size for OSC 52 (matches common terminal implementations)
31+
const Osc52MaxRawLength = 128 * 1024; // includes selector + base64 + whitespace (rough check)
3832
export const SupportsImageInput = true;
3933

4034
// detect webgl support
@@ -55,67 +49,83 @@ type TermWrapOptions = {
5549
keydownHandler?: (e: KeyboardEvent) => boolean;
5650
useWebGl?: boolean;
5751
sendDataHandler?: (data: string) => void;
52+
nodeModel?: BlockNodeModel;
5853
};
5954

60-
function handleOscWaveCommand(data: string, blockId: string, loaded: boolean): boolean {
55+
// for xterm OSC handlers, we return true always because we "own" the OSC number.
56+
// even if data is invalid we don't want to propagate to other handlers.
57+
function handleOsc52Command(data: string, blockId: string, loaded: boolean, termWrap: TermWrap): boolean {
6158
if (!loaded) {
6259
return true;
6360
}
61+
const isBlockFocused = termWrap.nodeModel ? globalStore.get(termWrap.nodeModel.isFocused) : false;
62+
if (!document.hasFocus() || !isBlockFocused) {
63+
console.log("OSC 52: rejected, window or block not focused");
64+
return true;
65+
}
6466
if (!data || data.length === 0) {
65-
console.log("Invalid Wave OSC command received (empty)");
67+
console.log("OSC 52: empty data received");
68+
return true;
69+
}
70+
if (data.length > Osc52MaxRawLength) {
71+
console.log("OSC 52: raw data too large", data.length);
6672
return true;
6773
}
6874

69-
// Expected formats:
70-
// "setmeta;{JSONDATA}"
71-
// "setmeta;[wave-id];{JSONDATA}"
72-
const parts = data.split(";");
73-
if (parts[0] !== "setmeta") {
74-
console.log("Invalid Wave OSC command received (bad command)", data);
75+
const semicolonIndex = data.indexOf(";");
76+
if (semicolonIndex === -1) {
77+
console.log("OSC 52: invalid format (no semicolon)", data.substring(0, 50));
7578
return true;
7679
}
77-
let jsonPayload: string;
78-
let waveId: string | undefined;
79-
if (parts.length === 2) {
80-
jsonPayload = parts[1];
81-
} else if (parts.length >= 3) {
82-
waveId = parts[1];
83-
jsonPayload = parts.slice(2).join(";");
84-
} else {
85-
console.log("Invalid Wave OSC command received (1 part)", data);
80+
81+
const clipboardSelection = data.substring(0, semicolonIndex);
82+
const base64Data = data.substring(semicolonIndex + 1);
83+
84+
// clipboard query ("?") is not supported for security (prevents clipboard theft)
85+
if (base64Data === "?") {
86+
console.log("OSC 52: clipboard query not supported");
8687
return true;
8788
}
8889

89-
let meta: any;
90-
try {
91-
meta = JSON.parse(jsonPayload);
92-
} catch (e) {
93-
console.error("Invalid JSON in Wave OSC command:", e);
90+
if (base64Data.length === 0) {
9491
return true;
9592
}
9693

97-
if (waveId) {
98-
// Resolve the wave id to an ORef using our ResolveIdsCommand.
99-
fireAndForget(() => {
100-
return RpcApi.ResolveIdsCommand(TabRpcClient, { blockid: blockId, ids: [waveId] })
101-
.then((response: { resolvedids: { [key: string]: any } }) => {
102-
const oref = response.resolvedids[waveId];
103-
if (!oref) {
104-
console.error("Failed to resolve wave id:", waveId);
105-
return;
106-
}
107-
services.ObjectService.UpdateObjectMeta(oref, meta);
108-
})
109-
.catch((err: any) => {
110-
console.error("Error resolving wave id", waveId, err);
111-
});
112-
});
113-
} else {
114-
// No wave id provided; update using the current block id.
115-
fireAndForget(() => {
116-
return services.ObjectService.UpdateObjectMeta(WOS.makeORef("block", blockId), meta);
94+
if (clipboardSelection.length > 10) {
95+
console.log("OSC 52: clipboard selection too long", clipboardSelection);
96+
return true;
97+
}
98+
99+
const estimatedDecodedSize = Math.ceil(base64Data.length * 0.75);
100+
if (estimatedDecodedSize > Osc52MaxDecodedSize) {
101+
console.log("OSC 52: data too large", estimatedDecodedSize, "bytes");
102+
return true;
103+
}
104+
105+
try {
106+
// strip whitespace from base64 data (some terminals chunk with newlines per RFC 4648)
107+
const cleanBase64Data = base64Data.replace(/\s+/g, "");
108+
const decodedText = base64ToString(cleanBase64Data);
109+
110+
// validate actual decoded size (base64 estimate can be off for multi-byte UTF-8)
111+
const actualByteSize = new TextEncoder().encode(decodedText).length;
112+
if (actualByteSize > Osc52MaxDecodedSize) {
113+
console.log("OSC 52: decoded text too large", actualByteSize, "bytes");
114+
return true;
115+
}
116+
117+
fireAndForget(async () => {
118+
try {
119+
await navigator.clipboard.writeText(decodedText);
120+
dlog("OSC 52: copied", decodedText.length, "characters to clipboard");
121+
} catch (err) {
122+
console.error("OSC 52: clipboard write failed:", err);
123+
}
117124
});
125+
} catch (e) {
126+
console.error("OSC 52: base64 decode error:", e);
118127
}
128+
119129
return true;
120130
}
121131

@@ -386,6 +396,7 @@ export class TermWrap {
386396
promptMarkers: TermTypes.IMarker[] = [];
387397
shellIntegrationStatusAtom: jotai.PrimitiveAtom<"ready" | "running-command" | null>;
388398
lastCommandAtom: jotai.PrimitiveAtom<string | null>;
399+
nodeModel: BlockNodeModel; // this can be null
389400

390401
// IME composition state tracking
391402
// Prevents duplicate input when switching input methods during composition (e.g., using Capslock)
@@ -412,6 +423,7 @@ export class TermWrap {
412423
this.tabId = tabId;
413424
this.blockId = blockId;
414425
this.sendDataHandler = waveOptions.sendDataHandler;
426+
this.nodeModel = waveOptions.nodeModel;
415427
this.ptyOffset = 0;
416428
this.dataBytesProcessed = 0;
417429
this.hasResized = false;
@@ -457,13 +469,13 @@ export class TermWrap {
457469
loggedWebGL = true;
458470
}
459471
}
460-
// Register OSC 9283 handler
461-
this.terminal.parser.registerOscHandler(9283, (data: string) => {
462-
return handleOscWaveCommand(data, this.blockId, this.loaded);
463-
});
472+
// Register OSC handlers
464473
this.terminal.parser.registerOscHandler(7, (data: string) => {
465474
return handleOsc7Command(data, this.blockId, this.loaded);
466475
});
476+
this.terminal.parser.registerOscHandler(52, (data: string) => {
477+
return handleOsc52Command(data, this.blockId, this.loaded, this);
478+
});
467479
this.terminal.parser.registerOscHandler(16162, (data: string) => {
468480
return handleOsc16162Command(data, this.blockId, this.loaded, this);
469481
});

go.mod

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ require (
77
github.com/alexflint/go-filemutex v1.3.0
88
github.com/aws/aws-sdk-go-v2 v1.41.0
99
github.com/aws/aws-sdk-go-v2/config v1.32.6
10-
github.com/aws/aws-sdk-go-v2/service/s3 v1.94.0
10+
github.com/aws/aws-sdk-go-v2/service/s3 v1.95.0
1111
github.com/aws/smithy-go v1.24.0
1212
github.com/creack/pty v1.1.24
1313
github.com/emirpasic/gods v1.18.1
@@ -28,15 +28,15 @@ require (
2828
github.com/mitchellh/mapstructure v1.5.0
2929
github.com/sashabaranov/go-openai v1.41.2
3030
github.com/sawka/txwrap v0.2.0
31-
github.com/shirou/gopsutil/v4 v4.25.11
31+
github.com/shirou/gopsutil/v4 v4.25.12
3232
github.com/skeema/knownhosts v1.3.1
3333
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966
3434
github.com/spf13/cobra v1.10.2
3535
github.com/ubuntu/gowsl v0.0.0-20240906163211-049fd49bd93b
3636
github.com/wavetermdev/htmltoken v0.2.0
3737
github.com/wavetermdev/waveterm/tsunami v0.12.3
3838
golang.org/x/crypto v0.46.0
39-
golang.org/x/mod v0.30.0
39+
golang.org/x/mod v0.31.0
4040
golang.org/x/sync v0.19.0
4141
golang.org/x/sys v0.39.0
4242
golang.org/x/term v0.38.0

go.sum

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -44,8 +44,8 @@ github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.16 h1:oHjJHeUy
4444
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.16/go.mod h1:iRSNGgOYmiYwSCXxXaKb9HfOEj40+oTKn8pTxMlYkRM=
4545
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.16 h1:NSbvS17MlI2lurYgXnCOLvCFX38sBW4eiVER7+kkgsU=
4646
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.16/go.mod h1:SwT8Tmqd4sA6G1qaGdzWCJN99bUmPGHfRwwq3G5Qb+A=
47-
github.com/aws/aws-sdk-go-v2/service/s3 v1.94.0 h1:SWTxh/EcUCDVqi/0s26V6pVUq0BBG7kx0tDTmF/hCgA=
48-
github.com/aws/aws-sdk-go-v2/service/s3 v1.94.0/go.mod h1:79S2BdqCJpScXZA2y+cpZuocWsjGjJINyXnOsf5DTz8=
47+
github.com/aws/aws-sdk-go-v2/service/s3 v1.95.0 h1:MIWra+MSq53CFaXXAywB2qg9YvVZifkk6vEGl/1Qor0=
48+
github.com/aws/aws-sdk-go-v2/service/s3 v1.95.0/go.mod h1:79S2BdqCJpScXZA2y+cpZuocWsjGjJINyXnOsf5DTz8=
4949
github.com/aws/aws-sdk-go-v2/service/signin v1.0.4 h1:HpI7aMmJ+mm1wkSHIA2t5EaFFv5EFYXePW30p1EIrbQ=
5050
github.com/aws/aws-sdk-go-v2/service/signin v1.0.4/go.mod h1:C5RdGMYGlfM0gYq/tifqgn4EbyX99V15P2V3R+VHbQU=
5151
github.com/aws/aws-sdk-go-v2/service/sso v1.30.8 h1:aM/Q24rIlS3bRAhTyFurowU8A0SMyGDtEOY/l/s/1Uw=
@@ -162,8 +162,8 @@ github.com/sashabaranov/go-openai v1.41.2 h1:vfPRBZNMpnqu8ELsclWcAvF19lDNgh1t6TV
162162
github.com/sashabaranov/go-openai v1.41.2/go.mod h1:lj5b/K+zjTSFxVLijLSTDZuP7adOgerWeFyZLUhAKRg=
163163
github.com/sawka/txwrap v0.2.0 h1:V3LfvKVLULxcYSxdMguLwFyQFMEU9nFDJopg0ZkL+94=
164164
github.com/sawka/txwrap v0.2.0/go.mod h1:wwQ2SQiN4U+6DU/iVPhbvr7OzXAtgZlQCIGuvOswEfA=
165-
github.com/shirou/gopsutil/v4 v4.25.11 h1:X53gB7muL9Gnwwo2evPSE+SfOrltMoR6V3xJAXZILTY=
166-
github.com/shirou/gopsutil/v4 v4.25.11/go.mod h1:EivAfP5x2EhLp2ovdpKSozecVXn1TmuG7SMzs/Wh4PU=
165+
github.com/shirou/gopsutil/v4 v4.25.12 h1:e7PvW/0RmJ8p8vPGJH4jvNkOyLmbkXgXW4m6ZPic6CY=
166+
github.com/shirou/gopsutil/v4 v4.25.12/go.mod h1:EivAfP5x2EhLp2ovdpKSozecVXn1TmuG7SMzs/Wh4PU=
167167
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
168168
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
169169
github.com/skeema/knownhosts v1.3.1 h1:X2osQ+RAjK76shCbvhHHHVl3ZlgDm8apHEHFqRjnBY8=
@@ -214,8 +214,8 @@ go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42s
214214
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
215215
golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU=
216216
golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0=
217-
golang.org/x/mod v0.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk=
218-
golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc=
217+
golang.org/x/mod v0.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI=
218+
golang.org/x/mod v0.31.0/go.mod h1:43JraMp9cGx1Rx3AqioxrbrhNsLl2l/iNAvuBkrezpg=
219219
golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
220220
golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
221221
golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw=

pkg/blockcontroller/shellcontroller.go

Lines changed: 1 addition & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -513,14 +513,6 @@ func (bc *ShellController) manageRunningShellProcess(shellProc *shellexec.ShellP
513513
shellInputCh := make(chan *BlockInputUnion, 32)
514514
bc.ShellInputCh = shellInputCh
515515

516-
// make esc sequence wshclient wshProxy
517-
// we don't need to authenticate this wshProxy since it is coming direct
518-
wshProxy := wshutil.MakeRpcProxy(fmt.Sprintf("controller:%s", bc.BlockId))
519-
controllerLinkId, err := wshutil.DefaultRouter.RegisterTrustedLeaf(wshProxy, wshutil.MakeControllerRouteId(bc.BlockId))
520-
if err != nil {
521-
return fmt.Errorf("cannot register controller route: %w", err)
522-
}
523-
ptyBuffer := wshutil.MakePtyBuffer(wshutil.WaveOSCPrefix, shellProc.Cmd, wshProxy.FromRemoteCh)
524516
go func() {
525517
// handles regular output from the pty (goes to the blockfile and xterm)
526518
defer func() {
@@ -546,7 +538,7 @@ func (bc *ShellController) manageRunningShellProcess(shellProc *shellexec.ShellP
546538
}()
547539
buf := make([]byte, 4096)
548540
for {
549-
nr, err := ptyBuffer.Read(buf)
541+
nr, err := shellProc.Cmd.Read(buf)
550542
if nr > 0 {
551543
err := HandleAppendBlockFile(bc.BlockId, wavebase.BlockFile_Term, buf[:nr])
552544
if err != nil {
@@ -577,27 +569,13 @@ func (bc *ShellController) manageRunningShellProcess(shellProc *shellexec.ShellP
577569
}
578570
}
579571
}()
580-
go func() {
581-
defer func() {
582-
panichandler.PanicHandler("blockcontroller:shellproc-output-loop", recover())
583-
}()
584-
// handles outputCh -> shellInputCh
585-
for msg := range wshProxy.ToRemoteCh {
586-
encodedMsg, err := wshutil.EncodeWaveOSCBytes(wshutil.WaveServerOSC, msg)
587-
if err != nil {
588-
log.Printf("error encoding OSC message: %v\n", err)
589-
}
590-
shellInputCh <- &BlockInputUnion{InputData: encodedMsg}
591-
}
592-
}()
593572
go func() {
594573
defer func() {
595574
panichandler.PanicHandler("blockcontroller:shellproc-wait-loop", recover())
596575
}()
597576
// wait for the shell to finish
598577
var exitCode int
599578
defer func() {
600-
wshutil.DefaultRouter.UnregisterLink(controllerLinkId)
601579
bc.UpdateControllerAndSendUpdate(func() bool {
602580
if bc.ProcStatus == Status_Running {
603581
bc.ProcStatus = Status_Done

0 commit comments

Comments
 (0)