Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
afc886f
add new tsunami view type
sawka Sep 12, 2025
6b4337d
pull a clientid off the URL
sawka Sep 12, 2025
8fe056c
some tsunami meta
sawka Sep 12, 2025
d0c94c8
nodepath option in tsunami. use nodepath to run tailwind instead of npx
sawka Sep 12, 2025
be31657
pass the electron exec path through to wavesrv, use for tsunami
sawka Sep 13, 2025
3d96c1c
switch to apppath (from appdir)
sawka Sep 13, 2025
3b309ff
big refactor of ugly blockcontroller code. MUCH cleaner surface to i…
sawka Sep 13, 2025
8307089
movefilesback flag and TsunamiBuildInternal exposed
sawka Sep 13, 2025
074d671
checkpoint on running tsunami apps
sawka Sep 13, 2025
fabf284
tsunami port in status
sawka Sep 13, 2025
d7b0755
check for abs path
sawka Sep 13, 2025
4203d80
getting there with the tsunami widget
sawka Sep 13, 2025
bc9e8e6
working tsunami app in wave
sawka Sep 14, 2025
cd10f88
tsunami apps, autoload (resync), fix logging, fix sse handling (if mu…
sawka Sep 15, 2025
e2aa49f
just keep package.json for scaffold as a template...
sawka Sep 15, 2025
567110e
consolidate copying from apppath in one func instead of spread to the…
sawka Sep 15, 2025
12fa501
support zip files (.tsapp), run and then a new package command to bui…
sawka Sep 15, 2025
3c8233b
fix dependencies
sawka Sep 15, 2025
725b959
misc changes, tighten up iframe, add more simple tags, etc
sawka Sep 15, 2025
a61f838
more minor cleanups
sawka Sep 15, 2025
81c966d
Merge remote-tracking branch 'origin/main' into sawka/tsunami-view
sawka Sep 15, 2025
92c1a91
remove some code duplication
sawka Sep 15, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 17 additions & 6 deletions Taskfile.yml
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,7 @@ tasks:
- "cmd/server/*.go"
- "pkg/**/*.go"
- "pkg/**/*.json"
- tsunami/**/*.go
generates:
- dist/bin/wavesrv.*

Expand Down Expand Up @@ -194,6 +195,7 @@ tasks:
- "cmd/server/*.go"
- "pkg/**/*.go"
- "pkg/**/*.json"
- "tsunami/**/*.go"
generates:
- dist/bin/wavesrv.*

Expand Down Expand Up @@ -485,6 +487,19 @@ tasks:
cmds:
- task: tsunami:scaffold:internal

tsunami:scaffold:packagejson:
desc: Create package.json for tsunami scaffold using npm commands
dir: tsunami/frontend/scaffold
cmds:
- cmd: "{{.RM}} package.json"
ignore_error: true
- npm --no-workspaces init -y --init-license Apache-2.0
- npm pkg set name=tsunami-scaffold
- npm pkg delete author
- npm pkg set author.name="Command Line Inc"
- npm pkg set author.email="[email protected]"
- npm --no-workspaces install [email protected] @tailwindcss/[email protected]

tsunami:scaffold:internal:
desc: Internal task to create scaffold directory structure
dir: tsunami/frontend
Expand All @@ -493,12 +508,8 @@ tasks:
- cmd: "{{.RMRF}} scaffold"
ignore_error: true
- mkdir scaffold
- cd scaffold && npm --no-workspaces init -y --init-license Apache-2.0
- cd scaffold && npm pkg set name=tsunami-scaffold
- cd scaffold && npm pkg delete author
- cd scaffold && npm pkg set author.name="Command Line Inc"
- cd scaffold && npm pkg set author.email="[email protected]"
- cd scaffold && npm --no-workspaces install tailwindcss @tailwindcss/cli
- cp ../templates/package.json.tmpl scaffold/package.json
- cd scaffold && npm install
- cp -r dist scaffold/
- cp ../templates/app-main.go.tmpl scaffold/app-main.go
- cp ../templates/tailwind.css scaffold/
Expand Down
2 changes: 1 addition & 1 deletion electron.vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ export default defineConfig({
server: {
open: false,
watch: {
ignored: ["dist/**", "**/*.go", "**/go.mod", "**/go.sum", "**/*.md", "**/*.json"],
ignored: ["dist/**", "**/*.go", "**/go.mod", "**/go.sum", "**/*.md", "**/*.json", "emain/**"],
},
},
css: {
Expand Down
38 changes: 36 additions & 2 deletions emain/emain-util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@ import * as electron from "electron";
import { getWebServerEndpoint } from "../frontend/util/endpoints";

export const WaveAppPathVarName = "WAVETERM_APP_PATH";
export const WaveAppElectronExecPath = "WAVETERM_ELECTRONEXECPATH";

export function getElectronExecPath(): string {
return process.execPath;
}

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

export function shNavHandler(event: Electron.Event<Electron.WebContentsWillNavigateEventParams>, url: string) {
const isDev = !electron.app.isPackaged;
if (isDev && (url.startsWith("http://127.0.0.1:5173/index.html") || url.startsWith("http://localhost:5173/index.html") ||
url.startsWith("http://127.0.0.1:5174/index.html") || url.startsWith("http://localhost:5174/index.html"))) {
if (
isDev &&
(url.startsWith("http://127.0.0.1:5173/index.html") ||
url.startsWith("http://localhost:5173/index.html") ||
url.startsWith("http://127.0.0.1:5174/index.html") ||
url.startsWith("http://localhost:5174/index.html"))
) {
// this is a dev-mode hot-reload, ignore it
console.log("allowing hot-reload of index.html");
return;
Expand Down Expand Up @@ -97,6 +107,30 @@ export function shFrameNavHandler(event: Electron.Event<Electron.WebContentsWill
// allowed
return;
}
if (event.frame.name != null && event.frame.name.startsWith("tsunami:")) {
// Parse port from frame name: tsunami:[port]:[blockid]
const nameParts = event.frame.name.split(":");
const expectedPort = nameParts.length >= 2 ? nameParts[1] : null;

try {
const tsunamiUrl = new URL(url);
if (
tsunamiUrl.protocol === "http:" &&
tsunamiUrl.hostname === "localhost" &&
expectedPort &&
tsunamiUrl.port === expectedPort
) {
// allowed
return;
}
// If navigation is not to expected port, open externally
event.preventDefault();
electron.shell.openExternal(url);
return;
} catch (e) {
// Invalid URL, fall through to prevent navigation
}
}
event.preventDefault();
console.log("frame navigation canceled");
}
Expand Down
3 changes: 2 additions & 1 deletion emain/emain-wavesrv.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import * as readline from "readline";
import { WebServerEndpointVarName, WSServerEndpointVarName } from "../frontend/util/endpoints";
import { AuthKey, WaveAuthKeyEnv } from "./authkey";
import { setForceQuit } from "./emain-activity";
import { WaveAppPathVarName } from "./emain-util";
import { WaveAppPathVarName, WaveAppElectronExecPath, getElectronExecPath } from "./emain-util";
import {
getElectronAppUnpackedBasePath,
getWaveConfigDir,
Expand Down Expand Up @@ -59,6 +59,7 @@ export function runWaveSrv(handleWSEvent: (evtMsg: WSEventType) => void): Promis
envCopy["XDG_CURRENT_DESKTOP"] = xdgCurrentDesktop;
}
envCopy[WaveAppPathVarName] = getElectronAppUnpackedBasePath();
envCopy[WaveAppElectronExecPath] = getElectronExecPath();
envCopy[WaveAuthKeyEnv] = AuthKey;
envCopy[WaveDataHomeVarName] = getWaveDataDir();
envCopy[WaveConfigHomeVarName] = getWaveConfigDir();
Expand Down
2 changes: 2 additions & 0 deletions frontend/app/block/block.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
import { LauncherViewModel } from "@/app/view/launcher/launcher";
import { PreviewModel } from "@/app/view/preview/preview-model";
import { SysinfoViewModel } from "@/app/view/sysinfo/sysinfo";
import { TsunamiViewModel } from "@/app/view/tsunami/tsunami";
import { VDomModel } from "@/app/view/vdom/vdom-model";
import { ErrorBoundary } from "@/element/errorboundary";
import { CenteredDiv } from "@/element/quickelems";
Expand Down Expand Up @@ -48,6 +49,7 @@ BlockRegistry.set("vdom", VDomModel);
BlockRegistry.set("tips", QuickTipsViewModel);
BlockRegistry.set("help", HelpViewModel);
BlockRegistry.set("launcher", LauncherViewModel);
BlockRegistry.set("tsunami", TsunamiViewModel);

function makeViewModel(blockId: string, blockView: string, nodeModel: BlockNodeModel): ViewModel {
const ctor = BlockRegistry.get(blockView);
Expand Down
175 changes: 175 additions & 0 deletions frontend/app/view/tsunami/tsunami.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
// Copyright 2025, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0

import { BlockNodeModel } from "@/app/block/blocktypes";
import { atoms, globalStore, WOS } from "@/app/store/global";
import { waveEventSubscribe } from "@/app/store/wps";
import { RpcApi } from "@/app/store/wshclientapi";
import { TabRpcClient } from "@/app/store/wshrpcutil";
import * as services from "@/store/services";
import * as jotai from "jotai";
import { memo, useEffect } from "react";

class TsunamiViewModel implements ViewModel {
viewType: string;
blockAtom: jotai.Atom<Block>;
blockId: string;
viewIcon: jotai.Atom<string>;
viewName: jotai.Atom<string>;
shellProcFullStatus: jotai.PrimitiveAtom<BlockControllerRuntimeStatus>;
shellProcStatusUnsubFn: () => void;
isRestarting: jotai.PrimitiveAtom<boolean>;

constructor(blockId: string, nodeModel: BlockNodeModel) {
this.viewType = "tsunami";
this.blockId = blockId;
this.blockAtom = WOS.getWaveObjectAtom<Block>(`block:${blockId}`);
this.viewIcon = jotai.atom("cube");
this.viewName = jotai.atom("Tsunami");
this.isRestarting = jotai.atom(false);

this.shellProcFullStatus = jotai.atom(null) as jotai.PrimitiveAtom<BlockControllerRuntimeStatus>;
const initialShellProcStatus = services.BlockService.GetControllerStatus(blockId);
initialShellProcStatus.then((rts) => {
this.updateShellProcStatus(rts);
});
this.shellProcStatusUnsubFn = waveEventSubscribe({
eventType: "controllerstatus",
scope: WOS.makeORef("block", blockId),
handler: (event) => {
let bcRTS: BlockControllerRuntimeStatus = event.data;
this.updateShellProcStatus(bcRTS);
},
});
}

get viewComponent(): ViewComponent {
return TsunamiView;
}

updateShellProcStatus(fullStatus: BlockControllerRuntimeStatus) {
console.log("tsunami-status", fullStatus);
if (fullStatus == null) {
return;
}
const curStatus = globalStore.get(this.shellProcFullStatus);
if (curStatus == null || curStatus.version < fullStatus.version) {
globalStore.set(this.shellProcFullStatus, fullStatus);
}
}

triggerRestartAtom() {
globalStore.set(this.isRestarting, true);
setTimeout(() => {
globalStore.set(this.isRestarting, false);
}, 300);
}

resyncController() {
const prtn = RpcApi.ControllerResyncCommand(TabRpcClient, {
tabid: globalStore.get(atoms.staticTabId),
blockid: this.blockId,
forcerestart: false,
});
prtn.catch((e) => console.log("error controller resync", e));
}

forceRestartController() {
if (globalStore.get(this.isRestarting)) {
return;
}
this.triggerRestartAtom();
const prtn = RpcApi.ControllerResyncCommand(TabRpcClient, {
tabid: globalStore.get(atoms.staticTabId),
blockid: this.blockId,
forcerestart: true,
});
prtn.catch((e) => console.log("error controller resync (force restart)", e));
}

dispose() {
if (this.shellProcStatusUnsubFn) {
this.shellProcStatusUnsubFn();
}
}

getSettingsMenuItems(): ContextMenuItem[] {
return [];
}
}

type TsunamiViewProps = {
model: TsunamiViewModel;
};

const TsunamiView = memo(({ model }: TsunamiViewProps) => {
const shellProcFullStatus = jotai.useAtomValue(model.shellProcFullStatus);
const blockData = jotai.useAtomValue(model.blockAtom);
const isRestarting = jotai.useAtomValue(model.isRestarting);

useEffect(() => {
model.resyncController();
}, [model]);

const appPath = blockData?.meta?.["tsunami:apppath"];
const controller = blockData?.meta?.controller;

// Check for configuration errors
const errors = [];
if (!appPath) {
errors.push("App path must be set (tsunami:apppath)");
}
if (controller !== "tsunami") {
errors.push("Invalid controller (must be 'tsunami')");
}

// Show errors if any exist
if (errors.length > 0) {
return (
<div className="w-full h-full flex flex-col items-center justify-center gap-4">
<h1 className="text-4xl font-bold text-main-text-color">Tsunami</h1>
<div className="flex flex-col gap-2">
{errors.map((error, index) => (
<div key={index} className="text-sm" style={{ color: "var(--color-error)" }}>
{error}
</div>
))}
</div>
</div>
);
}

// Check if we should show the iframe
const shouldShowIframe =
shellProcFullStatus?.shellprocstatus === "running" &&
shellProcFullStatus?.tsunamiport &&
shellProcFullStatus.tsunamiport !== 0;

if (shouldShowIframe) {
const iframeUrl = `http://localhost:${shellProcFullStatus.tsunamiport}/?clientid=wave:${model.blockId}`;
return <iframe src={iframeUrl} className="w-full h-full border-0" title="Tsunami Application" name={`tsunami:${shellProcFullStatus.tsunamiport}:${model.blockId}`} />;
}

const status = shellProcFullStatus?.shellprocstatus ?? "init";
const isNotRunning = status === "done" || status === "init";

return (
<div className="w-full h-full flex flex-col items-center justify-center gap-4">
<h1 className="text-4xl font-bold text-main-text-color">Tsunami</h1>
{appPath && <div className="text-sm text-main-text-color opacity-70">{appPath}</div>}
{isNotRunning && !isRestarting && (
<button
onClick={() => model.forceRestartController()}
className="px-4 py-2 bg-accent-color text-primary-text-color rounded hover:bg-accent-color/80 transition-colors cursor-pointer"
>
Start
</button>
)}
{isRestarting && <div className="text-sm text-success-color">Starting...</div>}
</div>
);
});

TsunamiView.displayName = "TsunamiView";

export { TsunamiViewModel };
5 changes: 5 additions & 0 deletions frontend/types/gotypes.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ declare global {
shellprocstatus?: string;
shellprocconnname?: string;
shellprocexitcode: number;
tsunamiport?: number;
};

// waveobj.BlockDef
Expand Down Expand Up @@ -590,6 +591,10 @@ declare global {
"web:partition"?: string;
"markdown:fontsize"?: number;
"markdown:fixedfontsize"?: number;
"tsunami:*"?: boolean;
"tsunami:sdkreplacepath"?: string;
"tsunami:apppath"?: string;
"tsunami:scaffoldpath"?: string;
"vdom:*"?: boolean;
"vdom:initialized"?: boolean;
"vdom:correlationid"?: string;
Expand Down
3 changes: 3 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ require (
github.com/spf13/cobra v1.10.1
github.com/ubuntu/gowsl v0.0.0-20240906163211-049fd49bd93b
github.com/wavetermdev/htmltoken v0.2.0
github.com/wavetermdev/waveterm/tsunami v0.0.0-00010101000000-000000000000
golang.org/x/crypto v0.42.0
golang.org/x/mod v0.28.0
golang.org/x/sync v0.17.0
Expand Down Expand Up @@ -112,3 +113,5 @@ require (
replace github.com/kevinburke/ssh_config => github.com/wavetermdev/ssh_config v0.0.0-20241219203747-6409e4292f34

replace github.com/creack/pty => github.com/photostorm/pty v1.1.19-0.20230903182454-31354506054b

replace github.com/wavetermdev/waveterm/tsunami => ./tsunami
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions pkg/blockcontroller/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
*.old
Loading
Loading