Skip to content

Commit 50cc08a

Browse files
authored
add tsunami view in wave (#2350)
checkpoint. good to merge. we have a working tsunami view inside of wave (with lots of caveats). but enough for some dev testing. merge so we dont drift too far from main and while we're at a stable point.
1 parent dc3b2c2 commit 50cc08a

32 files changed

Lines changed: 2822 additions & 1306 deletions

Taskfile.yml

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,7 @@ tasks:
166166
- "cmd/server/*.go"
167167
- "pkg/**/*.go"
168168
- "pkg/**/*.json"
169+
- tsunami/**/*.go
169170
generates:
170171
- dist/bin/wavesrv.*
171172

@@ -194,6 +195,7 @@ tasks:
194195
- "cmd/server/*.go"
195196
- "pkg/**/*.go"
196197
- "pkg/**/*.json"
198+
- "tsunami/**/*.go"
197199
generates:
198200
- dist/bin/wavesrv.*
199201

@@ -485,6 +487,19 @@ tasks:
485487
cmds:
486488
- task: tsunami:scaffold:internal
487489

490+
tsunami:scaffold:packagejson:
491+
desc: Create package.json for tsunami scaffold using npm commands
492+
dir: tsunami/frontend/scaffold
493+
cmds:
494+
- cmd: "{{.RM}} package.json"
495+
ignore_error: true
496+
- npm --no-workspaces init -y --init-license Apache-2.0
497+
- npm pkg set name=tsunami-scaffold
498+
- npm pkg delete author
499+
- npm pkg set author.name="Command Line Inc"
500+
- npm pkg set author.email="info@commandline.dev"
501+
- npm --no-workspaces install tailwindcss@4.1.13 @tailwindcss/cli@4.1.13
502+
488503
tsunami:scaffold:internal:
489504
desc: Internal task to create scaffold directory structure
490505
dir: tsunami/frontend
@@ -493,12 +508,8 @@ tasks:
493508
- cmd: "{{.RMRF}} scaffold"
494509
ignore_error: true
495510
- mkdir scaffold
496-
- cd scaffold && npm --no-workspaces init -y --init-license Apache-2.0
497-
- cd scaffold && npm pkg set name=tsunami-scaffold
498-
- cd scaffold && npm pkg delete author
499-
- cd scaffold && npm pkg set author.name="Command Line Inc"
500-
- cd scaffold && npm pkg set author.email="info@commandline.dev"
501-
- cd scaffold && npm --no-workspaces install tailwindcss @tailwindcss/cli
511+
- cp ../templates/package.json.tmpl scaffold/package.json
512+
- cd scaffold && npm install
502513
- cp -r dist scaffold/
503514
- cp ../templates/app-main.go.tmpl scaffold/app-main.go
504515
- cp ../templates/tailwind.css scaffold/

electron.vite.config.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ export default defineConfig({
7878
server: {
7979
open: false,
8080
watch: {
81-
ignored: ["dist/**", "**/*.go", "**/go.mod", "**/go.sum", "**/*.md", "**/*.json"],
81+
ignored: ["dist/**", "**/*.go", "**/go.mod", "**/go.sum", "**/*.md", "**/*.json", "emain/**"],
8282
},
8383
},
8484
css: {

emain/emain-util.ts

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,11 @@ import * as electron from "electron";
55
import { getWebServerEndpoint } from "../frontend/util/endpoints";
66

77
export const WaveAppPathVarName = "WAVETERM_APP_PATH";
8+
export const WaveAppElectronExecPath = "WAVETERM_ELECTRONEXECPATH";
9+
10+
export function getElectronExecPath(): string {
11+
return process.execPath;
12+
}
813

914
// not necessarily exact, but we use this to help get us unstuck in certain cases
1015
let lastCtrlShiftSate: boolean = false;
@@ -57,8 +62,13 @@ export function handleCtrlShiftState(sender: Electron.WebContents, waveEvent: Wa
5762

5863
export function shNavHandler(event: Electron.Event<Electron.WebContentsWillNavigateEventParams>, url: string) {
5964
const isDev = !electron.app.isPackaged;
60-
if (isDev && (url.startsWith("http://127.0.0.1:5173/index.html") || url.startsWith("http://localhost:5173/index.html") ||
61-
url.startsWith("http://127.0.0.1:5174/index.html") || url.startsWith("http://localhost:5174/index.html"))) {
65+
if (
66+
isDev &&
67+
(url.startsWith("http://127.0.0.1:5173/index.html") ||
68+
url.startsWith("http://localhost:5173/index.html") ||
69+
url.startsWith("http://127.0.0.1:5174/index.html") ||
70+
url.startsWith("http://localhost:5174/index.html"))
71+
) {
6272
// this is a dev-mode hot-reload, ignore it
6373
console.log("allowing hot-reload of index.html");
6474
return;
@@ -97,6 +107,30 @@ export function shFrameNavHandler(event: Electron.Event<Electron.WebContentsWill
97107
// allowed
98108
return;
99109
}
110+
if (event.frame.name != null && event.frame.name.startsWith("tsunami:")) {
111+
// Parse port from frame name: tsunami:[port]:[blockid]
112+
const nameParts = event.frame.name.split(":");
113+
const expectedPort = nameParts.length >= 2 ? nameParts[1] : null;
114+
115+
try {
116+
const tsunamiUrl = new URL(url);
117+
if (
118+
tsunamiUrl.protocol === "http:" &&
119+
tsunamiUrl.hostname === "localhost" &&
120+
expectedPort &&
121+
tsunamiUrl.port === expectedPort
122+
) {
123+
// allowed
124+
return;
125+
}
126+
// If navigation is not to expected port, open externally
127+
event.preventDefault();
128+
electron.shell.openExternal(url);
129+
return;
130+
} catch (e) {
131+
// Invalid URL, fall through to prevent navigation
132+
}
133+
}
100134
event.preventDefault();
101135
console.log("frame navigation canceled");
102136
}

emain/emain-wavesrv.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import * as readline from "readline";
77
import { WebServerEndpointVarName, WSServerEndpointVarName } from "../frontend/util/endpoints";
88
import { AuthKey, WaveAuthKeyEnv } from "./authkey";
99
import { setForceQuit } from "./emain-activity";
10-
import { WaveAppPathVarName } from "./emain-util";
10+
import { WaveAppPathVarName, WaveAppElectronExecPath, getElectronExecPath } from "./emain-util";
1111
import {
1212
getElectronAppUnpackedBasePath,
1313
getWaveConfigDir,
@@ -59,6 +59,7 @@ export function runWaveSrv(handleWSEvent: (evtMsg: WSEventType) => void): Promis
5959
envCopy["XDG_CURRENT_DESKTOP"] = xdgCurrentDesktop;
6060
}
6161
envCopy[WaveAppPathVarName] = getElectronAppUnpackedBasePath();
62+
envCopy[WaveAppElectronExecPath] = getElectronExecPath();
6263
envCopy[WaveAuthKeyEnv] = AuthKey;
6364
envCopy[WaveDataHomeVarName] = getWaveDataDir();
6465
envCopy[WaveConfigHomeVarName] = getWaveConfigDir();

frontend/app/block/block.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
import { LauncherViewModel } from "@/app/view/launcher/launcher";
1313
import { PreviewModel } from "@/app/view/preview/preview-model";
1414
import { SysinfoViewModel } from "@/app/view/sysinfo/sysinfo";
15+
import { TsunamiViewModel } from "@/app/view/tsunami/tsunami";
1516
import { VDomModel } from "@/app/view/vdom/vdom-model";
1617
import { ErrorBoundary } from "@/element/errorboundary";
1718
import { CenteredDiv } from "@/element/quickelems";
@@ -48,6 +49,7 @@ BlockRegistry.set("vdom", VDomModel);
4849
BlockRegistry.set("tips", QuickTipsViewModel);
4950
BlockRegistry.set("help", HelpViewModel);
5051
BlockRegistry.set("launcher", LauncherViewModel);
52+
BlockRegistry.set("tsunami", TsunamiViewModel);
5153

5254
function makeViewModel(blockId: string, blockView: string, nodeModel: BlockNodeModel): ViewModel {
5355
const ctor = BlockRegistry.get(blockView);
Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
// Copyright 2025, Command Line Inc.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
import { BlockNodeModel } from "@/app/block/blocktypes";
5+
import { atoms, globalStore, WOS } from "@/app/store/global";
6+
import { waveEventSubscribe } from "@/app/store/wps";
7+
import { RpcApi } from "@/app/store/wshclientapi";
8+
import { TabRpcClient } from "@/app/store/wshrpcutil";
9+
import * as services from "@/store/services";
10+
import * as jotai from "jotai";
11+
import { memo, useEffect } from "react";
12+
13+
class TsunamiViewModel implements ViewModel {
14+
viewType: string;
15+
blockAtom: jotai.Atom<Block>;
16+
blockId: string;
17+
viewIcon: jotai.Atom<string>;
18+
viewName: jotai.Atom<string>;
19+
shellProcFullStatus: jotai.PrimitiveAtom<BlockControllerRuntimeStatus>;
20+
shellProcStatusUnsubFn: () => void;
21+
isRestarting: jotai.PrimitiveAtom<boolean>;
22+
23+
constructor(blockId: string, nodeModel: BlockNodeModel) {
24+
this.viewType = "tsunami";
25+
this.blockId = blockId;
26+
this.blockAtom = WOS.getWaveObjectAtom<Block>(`block:${blockId}`);
27+
this.viewIcon = jotai.atom("cube");
28+
this.viewName = jotai.atom("Tsunami");
29+
this.isRestarting = jotai.atom(false);
30+
31+
this.shellProcFullStatus = jotai.atom(null) as jotai.PrimitiveAtom<BlockControllerRuntimeStatus>;
32+
const initialShellProcStatus = services.BlockService.GetControllerStatus(blockId);
33+
initialShellProcStatus.then((rts) => {
34+
this.updateShellProcStatus(rts);
35+
});
36+
this.shellProcStatusUnsubFn = waveEventSubscribe({
37+
eventType: "controllerstatus",
38+
scope: WOS.makeORef("block", blockId),
39+
handler: (event) => {
40+
let bcRTS: BlockControllerRuntimeStatus = event.data;
41+
this.updateShellProcStatus(bcRTS);
42+
},
43+
});
44+
}
45+
46+
get viewComponent(): ViewComponent {
47+
return TsunamiView;
48+
}
49+
50+
updateShellProcStatus(fullStatus: BlockControllerRuntimeStatus) {
51+
console.log("tsunami-status", fullStatus);
52+
if (fullStatus == null) {
53+
return;
54+
}
55+
const curStatus = globalStore.get(this.shellProcFullStatus);
56+
if (curStatus == null || curStatus.version < fullStatus.version) {
57+
globalStore.set(this.shellProcFullStatus, fullStatus);
58+
}
59+
}
60+
61+
triggerRestartAtom() {
62+
globalStore.set(this.isRestarting, true);
63+
setTimeout(() => {
64+
globalStore.set(this.isRestarting, false);
65+
}, 300);
66+
}
67+
68+
resyncController() {
69+
const prtn = RpcApi.ControllerResyncCommand(TabRpcClient, {
70+
tabid: globalStore.get(atoms.staticTabId),
71+
blockid: this.blockId,
72+
forcerestart: false,
73+
});
74+
prtn.catch((e) => console.log("error controller resync", e));
75+
}
76+
77+
forceRestartController() {
78+
if (globalStore.get(this.isRestarting)) {
79+
return;
80+
}
81+
this.triggerRestartAtom();
82+
const prtn = RpcApi.ControllerResyncCommand(TabRpcClient, {
83+
tabid: globalStore.get(atoms.staticTabId),
84+
blockid: this.blockId,
85+
forcerestart: true,
86+
});
87+
prtn.catch((e) => console.log("error controller resync (force restart)", e));
88+
}
89+
90+
dispose() {
91+
if (this.shellProcStatusUnsubFn) {
92+
this.shellProcStatusUnsubFn();
93+
}
94+
}
95+
96+
getSettingsMenuItems(): ContextMenuItem[] {
97+
return [];
98+
}
99+
}
100+
101+
type TsunamiViewProps = {
102+
model: TsunamiViewModel;
103+
};
104+
105+
const TsunamiView = memo(({ model }: TsunamiViewProps) => {
106+
const shellProcFullStatus = jotai.useAtomValue(model.shellProcFullStatus);
107+
const blockData = jotai.useAtomValue(model.blockAtom);
108+
const isRestarting = jotai.useAtomValue(model.isRestarting);
109+
110+
useEffect(() => {
111+
model.resyncController();
112+
}, [model]);
113+
114+
const appPath = blockData?.meta?.["tsunami:apppath"];
115+
const controller = blockData?.meta?.controller;
116+
117+
// Check for configuration errors
118+
const errors = [];
119+
if (!appPath) {
120+
errors.push("App path must be set (tsunami:apppath)");
121+
}
122+
if (controller !== "tsunami") {
123+
errors.push("Invalid controller (must be 'tsunami')");
124+
}
125+
126+
// Show errors if any exist
127+
if (errors.length > 0) {
128+
return (
129+
<div className="w-full h-full flex flex-col items-center justify-center gap-4">
130+
<h1 className="text-4xl font-bold text-main-text-color">Tsunami</h1>
131+
<div className="flex flex-col gap-2">
132+
{errors.map((error, index) => (
133+
<div key={index} className="text-sm" style={{ color: "var(--color-error)" }}>
134+
{error}
135+
</div>
136+
))}
137+
</div>
138+
</div>
139+
);
140+
}
141+
142+
// Check if we should show the iframe
143+
const shouldShowIframe =
144+
shellProcFullStatus?.shellprocstatus === "running" &&
145+
shellProcFullStatus?.tsunamiport &&
146+
shellProcFullStatus.tsunamiport !== 0;
147+
148+
if (shouldShowIframe) {
149+
const iframeUrl = `http://localhost:${shellProcFullStatus.tsunamiport}/?clientid=wave:${model.blockId}`;
150+
return <iframe src={iframeUrl} className="w-full h-full border-0" title="Tsunami Application" name={`tsunami:${shellProcFullStatus.tsunamiport}:${model.blockId}`} />;
151+
}
152+
153+
const status = shellProcFullStatus?.shellprocstatus ?? "init";
154+
const isNotRunning = status === "done" || status === "init";
155+
156+
return (
157+
<div className="w-full h-full flex flex-col items-center justify-center gap-4">
158+
<h1 className="text-4xl font-bold text-main-text-color">Tsunami</h1>
159+
{appPath && <div className="text-sm text-main-text-color opacity-70">{appPath}</div>}
160+
{isNotRunning && !isRestarting && (
161+
<button
162+
onClick={() => model.forceRestartController()}
163+
className="px-4 py-2 bg-accent-color text-primary-text-color rounded hover:bg-accent-color/80 transition-colors cursor-pointer"
164+
>
165+
Start
166+
</button>
167+
)}
168+
{isRestarting && <div className="text-sm text-success-color">Starting...</div>}
169+
</div>
170+
);
171+
});
172+
173+
TsunamiView.displayName = "TsunamiView";
174+
175+
export { TsunamiViewModel };

frontend/types/gotypes.d.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ declare global {
6060
shellprocstatus?: string;
6161
shellprocconnname?: string;
6262
shellprocexitcode: number;
63+
tsunamiport?: number;
6364
};
6465

6566
// waveobj.BlockDef
@@ -590,6 +591,10 @@ declare global {
590591
"web:partition"?: string;
591592
"markdown:fontsize"?: number;
592593
"markdown:fixedfontsize"?: number;
594+
"tsunami:*"?: boolean;
595+
"tsunami:sdkreplacepath"?: string;
596+
"tsunami:apppath"?: string;
597+
"tsunami:scaffoldpath"?: string;
593598
"vdom:*"?: boolean;
594599
"vdom:initialized"?: boolean;
595600
"vdom:correlationid"?: string;

go.mod

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ require (
3333
github.com/spf13/cobra v1.10.1
3434
github.com/ubuntu/gowsl v0.0.0-20240906163211-049fd49bd93b
3535
github.com/wavetermdev/htmltoken v0.2.0
36+
github.com/wavetermdev/waveterm/tsunami v0.0.0-00010101000000-000000000000
3637
golang.org/x/crypto v0.42.0
3738
golang.org/x/mod v0.28.0
3839
golang.org/x/sync v0.17.0
@@ -112,3 +113,5 @@ require (
112113
replace github.com/kevinburke/ssh_config => github.com/wavetermdev/ssh_config v0.0.0-20241219203747-6409e4292f34
113114

114115
replace github.com/creack/pty => github.com/photostorm/pty v1.1.19-0.20230903182454-31354506054b
116+
117+
replace github.com/wavetermdev/waveterm/tsunami => ./tsunami

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pkg/blockcontroller/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
*.old

0 commit comments

Comments
 (0)