waveterm/frontend/wave.ts
Mike Sawka ce978c1205
More Builder Improvements + Tsunami AppInit (#2571)
* Kebab Menu + Switch App Functionality
* Updated Secrets Tab (still more to do, but closer)
* Better Error Handling (Switch to Secrets Tab)
* Add AppInit functionality to Tsunami (fixes initialization issues and
races with embedded assets, and error handling)
* Tsunami - ListStaticFiles
* Tsunami - More Robust Manifest generation (even when there are extra
stdout messages)
2025-11-19 09:41:11 -08:00

295 lines
10 KiB
TypeScript

// Copyright 2025, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0
import { App } from "@/app/app";
import {
globalRefocus,
registerBuilderGlobalKeys,
registerControlShiftStateUpdateHandler,
registerElectronReinjectKeyHandler,
registerGlobalKeys,
} from "@/app/store/keymodel";
import { modalsModel } from "@/app/store/modalmodel";
import { RpcApi } from "@/app/store/wshclientapi";
import { makeBuilderRouteId, makeTabRouteId } from "@/app/store/wshrouter";
import { initWshrpc, TabRpcClient } from "@/app/store/wshrpcutil";
import { loadMonaco } from "@/app/view/codeeditor/codeeditor";
import { BuilderApp } from "@/builder/builder-app";
import { getLayoutModelForStaticTab } from "@/layout/index";
import {
atoms,
countersClear,
countersPrint,
getApi,
globalStore,
initGlobal,
initGlobalWaveEventSubs,
loadConnStatus,
pushFlashError,
pushNotification,
removeNotificationById,
subscribeToConnEvents,
} from "@/store/global";
import * as WOS from "@/store/wos";
import { loadFonts } from "@/util/fontutil";
import { setKeyUtilPlatform } from "@/util/keyutil";
import { createElement } from "react";
import { createRoot } from "react-dom/client";
const platform = getApi().getPlatform();
document.title = `Wave Terminal`;
let savedInitOpts: WaveInitOpts = null;
(window as any).WOS = WOS;
(window as any).globalStore = globalStore;
(window as any).globalAtoms = atoms;
(window as any).RpcApi = RpcApi;
(window as any).isFullScreen = false;
(window as any).countersPrint = countersPrint;
(window as any).countersClear = countersClear;
(window as any).getLayoutModelForStaticTab = getLayoutModelForStaticTab;
(window as any).pushFlashError = pushFlashError;
(window as any).pushNotification = pushNotification;
(window as any).removeNotificationById = removeNotificationById;
(window as any).modalsModel = modalsModel;
function updateZoomFactor(zoomFactor: number) {
console.log("update zoomfactor", zoomFactor);
document.documentElement.style.setProperty("--zoomfactor", String(zoomFactor));
document.documentElement.style.setProperty("--zoomfactor-inv", String(1 / zoomFactor));
}
async function initBare() {
getApi().sendLog("Init Bare");
document.body.style.visibility = "hidden";
document.body.style.opacity = "0";
document.body.classList.add("is-transparent");
getApi().onWaveInit(initWaveWrap);
getApi().onBuilderInit(initBuilderWrap);
setKeyUtilPlatform(platform);
loadFonts();
updateZoomFactor(getApi().getZoomFactor());
getApi().onZoomFactorChange((zoomFactor) => {
updateZoomFactor(zoomFactor);
});
document.fonts.ready.then(() => {
console.log("Init Bare Done");
getApi().setWindowInitStatus("ready");
});
}
document.addEventListener("DOMContentLoaded", initBare);
async function initWaveWrap(initOpts: WaveInitOpts) {
try {
if (savedInitOpts) {
await reinitWave();
return;
}
savedInitOpts = initOpts;
await initWave(initOpts);
} catch (e) {
getApi().sendLog("Error in initWave " + e.message + "\n" + e.stack);
console.error("Error in initWave", e);
} finally {
document.body.style.visibility = null;
document.body.style.opacity = null;
document.body.classList.remove("is-transparent");
}
}
async function reinitWave() {
console.log("Reinit Wave");
getApi().sendLog("Reinit Wave");
// We use this hack to prevent a flicker of the previously-hovered tab when this view was last active.
document.body.classList.add("nohover");
requestAnimationFrame(() =>
setTimeout(() => {
document.body.classList.remove("nohover");
}, 100)
);
await WOS.reloadWaveObject<Client>(WOS.makeORef("client", savedInitOpts.clientId));
const waveWindow = await WOS.reloadWaveObject<WaveWindow>(WOS.makeORef("window", savedInitOpts.windowId));
const ws = await WOS.reloadWaveObject<Workspace>(WOS.makeORef("workspace", waveWindow.workspaceid));
const initialTab = await WOS.reloadWaveObject<Tab>(WOS.makeORef("tab", savedInitOpts.tabId));
await WOS.reloadWaveObject<LayoutState>(WOS.makeORef("layout", initialTab.layoutstate));
reloadAllWorkspaceTabs(ws);
document.title = `Wave Terminal - ${initialTab.name}`; // TODO update with tab name change
getApi().setWindowInitStatus("wave-ready");
globalStore.set(atoms.reinitVersion, globalStore.get(atoms.reinitVersion) + 1);
globalStore.set(atoms.updaterStatusAtom, getApi().getUpdaterStatus());
setTimeout(() => {
globalRefocus();
}, 50);
}
function reloadAllWorkspaceTabs(ws: Workspace) {
if (ws == null || (!ws.tabids?.length && !ws.pinnedtabids?.length)) {
return;
}
ws.tabids?.forEach((tabid) => {
WOS.reloadWaveObject<Tab>(WOS.makeORef("tab", tabid));
});
ws.pinnedtabids?.forEach((tabid) => {
WOS.reloadWaveObject<Tab>(WOS.makeORef("tab", tabid));
});
}
function loadAllWorkspaceTabs(ws: Workspace) {
if (ws == null || (!ws.tabids?.length && !ws.pinnedtabids?.length)) {
return;
}
ws.tabids?.forEach((tabid) => {
WOS.getObjectValue<Tab>(WOS.makeORef("tab", tabid));
});
ws.pinnedtabids?.forEach((tabid) => {
WOS.getObjectValue<Tab>(WOS.makeORef("tab", tabid));
});
}
async function initWave(initOpts: WaveInitOpts) {
getApi().sendLog("Init Wave " + JSON.stringify(initOpts));
console.log(
"Wave Init",
"tabid",
initOpts.tabId,
"clientid",
initOpts.clientId,
"windowid",
initOpts.windowId,
"platform",
platform
);
initGlobal({
tabId: initOpts.tabId,
clientId: initOpts.clientId,
windowId: initOpts.windowId,
platform,
environment: "renderer",
primaryTabStartup: initOpts.primaryTabStartup,
});
(window as any).globalAtoms = atoms;
// Init WPS event handlers
const globalWS = initWshrpc(makeTabRouteId(initOpts.tabId));
(window as any).globalWS = globalWS;
(window as any).TabRpcClient = TabRpcClient;
await loadConnStatus();
initGlobalWaveEventSubs(initOpts);
subscribeToConnEvents();
// ensures client/window/workspace are loaded into the cache before rendering
const [client, waveWindow, initialTab] = await Promise.all([
WOS.loadAndPinWaveObject<Client>(WOS.makeORef("client", initOpts.clientId)),
WOS.loadAndPinWaveObject<WaveWindow>(WOS.makeORef("window", initOpts.windowId)),
WOS.loadAndPinWaveObject<Tab>(WOS.makeORef("tab", initOpts.tabId)),
]);
const [ws, layoutState] = await Promise.all([
WOS.loadAndPinWaveObject<Workspace>(WOS.makeORef("workspace", waveWindow.workspaceid)),
WOS.reloadWaveObject<LayoutState>(WOS.makeORef("layout", initialTab.layoutstate)),
]);
loadAllWorkspaceTabs(ws);
WOS.wpsSubscribeToObject(WOS.makeORef("workspace", waveWindow.workspaceid));
document.title = `Wave Terminal - ${initialTab.name}`; // TODO update with tab name change
registerGlobalKeys();
registerElectronReinjectKeyHandler();
registerControlShiftStateUpdateHandler();
await loadMonaco();
const fullConfig = await RpcApi.GetFullConfigCommand(TabRpcClient);
console.log("fullconfig", fullConfig);
globalStore.set(atoms.fullConfigAtom, fullConfig);
console.log("Wave First Render");
let firstRenderResolveFn: () => void = null;
let firstRenderPromise = new Promise<void>((resolve) => {
firstRenderResolveFn = resolve;
});
const reactElem = createElement(App, { onFirstRender: firstRenderResolveFn }, null);
const elem = document.getElementById("main");
const root = createRoot(elem);
root.render(reactElem);
await firstRenderPromise;
console.log("Wave First Render Done");
getApi().setWindowInitStatus("wave-ready");
}
async function initBuilderWrap(initOpts: BuilderInitOpts) {
try {
await initBuilder(initOpts);
} catch (e) {
getApi().sendLog("Error in initBuilder " + e.message + "\n" + e.stack);
console.error("Error in initBuilder", e);
} finally {
document.body.style.visibility = null;
document.body.style.opacity = null;
document.body.classList.remove("is-transparent");
}
}
async function initBuilder(initOpts: BuilderInitOpts) {
getApi().sendLog("Init Builder " + JSON.stringify(initOpts));
console.log(
"Tsunami Builder Init",
"builderid",
initOpts.builderId,
"clientid",
initOpts.clientId,
"windowid",
initOpts.windowId,
"platform",
platform
);
initGlobal({
clientId: initOpts.clientId,
windowId: initOpts.windowId,
platform,
environment: "renderer",
builderId: initOpts.builderId,
});
(window as any).globalAtoms = atoms;
const globalWS = initWshrpc(makeBuilderRouteId(initOpts.builderId));
(window as any).globalWS = globalWS;
(window as any).TabRpcClient = TabRpcClient;
await loadConnStatus();
let appIdToUse: string = null;
try {
const oref = WOS.makeORef("builder", initOpts.builderId);
const rtInfo = await RpcApi.GetRTInfoCommand(TabRpcClient, { oref });
if (rtInfo && rtInfo["builder:appid"]) {
appIdToUse = rtInfo["builder:appid"];
}
} catch (e) {
console.log("Could not load saved builder appId from rtinfo:", e);
}
document.title = appIdToUse ? `WaveApp Builder (${appIdToUse})` : "WaveApp Builder";
globalStore.set(atoms.builderAppId, appIdToUse);
const client = await WOS.loadAndPinWaveObject<Client>(WOS.makeORef("client", initOpts.clientId));
registerBuilderGlobalKeys();
registerElectronReinjectKeyHandler();
await loadMonaco();
const fullConfig = await RpcApi.GetFullConfigCommand(TabRpcClient);
console.log("fullconfig", fullConfig);
globalStore.set(atoms.fullConfigAtom, fullConfig);
console.log("Tsunami Builder First Render");
let firstRenderResolveFn: () => void = null;
let firstRenderPromise = new Promise<void>((resolve) => {
firstRenderResolveFn = resolve;
});
const reactElem = createElement(BuilderApp, { initOpts, onFirstRender: firstRenderResolveFn }, null);
const elem = document.getElementById("main");
const root = createRoot(elem);
root.render(reactElem);
await firstRenderPromise;
console.log("Tsunami Builder First Render Done");
}