Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 2 additions & 2 deletions eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -76,8 +76,8 @@ export default [
"@typescript-eslint/no-unused-vars": [
"warn",
{
argsIgnorePattern: "^_$",
varsIgnorePattern: "^_$",
argsIgnorePattern: "^_[a-z0-9]*$",
varsIgnorePattern: "^_[a-z0-9]*$",
},
],
"prefer-const": "warn",
Expand Down
12 changes: 9 additions & 3 deletions frontend/app/onboarding/onboarding.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,13 @@ type PageName = "init" | "notelemetrystar" | "features";

const pageNameAtom: PrimitiveAtom<PageName> = atom<PageName>("init");

const InitPage = ({ isCompact }: { isCompact: boolean }) => {
const InitPage = ({
isCompact,
telemetryUpdateFn,
}: {
isCompact: boolean;
telemetryUpdateFn: (value: boolean) => Promise<void>;
}) => {
const telemetrySetting = useSettingsKeyAtom("telemetry:enabled");
const clientData = useAtomValue(ClientModel.getInstance().clientAtom);
const [telemetryEnabled, setTelemetryEnabled] = useState<boolean>(!!telemetrySetting);
Expand Down Expand Up @@ -63,7 +69,7 @@ const InitPage = ({ isCompact }: { isCompact: boolean }) => {

const setTelemetry = (value: boolean) => {
fireAndForget(() =>
services.ClientService.TelemetryUpdate(value).then(() => {
telemetryUpdateFn(value).then(() => {
setTelemetryEnabled(value);
})
);
Expand Down Expand Up @@ -319,7 +325,7 @@ const NewInstallOnboardingModal = () => {
let pageComp: React.JSX.Element = null;
switch (pageName) {
case "init":
pageComp = <InitPage isCompact={isCompact} />;
pageComp = <InitPage isCompact={isCompact} telemetryUpdateFn={services.ClientService.TelemetryUpdate} />;
break;
case "notelemetrystar":
pageComp = <NoTelemetryStarPage isCompact={isCompact} />;
Expand Down
4 changes: 2 additions & 2 deletions frontend/app/store/client-model.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright 2025, Command Line Inc
// Copyright 2026, Command Line Inc
// SPDX-License-Identifier: Apache-2.0

import * as WOS from "@/app/store/wos";
Expand Down Expand Up @@ -33,4 +33,4 @@ class ClientModel {
}
}

export { ClientModel };
export { ClientModel };
2 changes: 1 addition & 1 deletion frontend/app/store/global-atoms.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ function initGlobalAtoms(initOpts: GlobalInitOptions) {
const windowIdAtom = atom(initOpts.windowId) as PrimitiveAtom<string>;
const builderIdAtom = atom(initOpts.builderId) as PrimitiveAtom<string>;
const builderAppIdAtom = atom<string>(null) as PrimitiveAtom<string>;
setWaveWindowType(initOpts.builderId != null ? "builder" : "tab");
setWaveWindowType(initOpts.isPreview ? "preview" : initOpts.builderId != null ? "builder" : "tab");
const uiContextAtom = atom((get) => {
const uiContext: UIContext = {
windowid: initOpts.windowId,
Expand Down
18 changes: 17 additions & 1 deletion frontend/app/store/wos.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
// WaveObjectStore

import { waveEventSubscribeSingle } from "@/app/store/wps";
import { isPreviewWindow } from "@/app/store/windowtype";
import { getWebServerEndpoint } from "@/util/endpoints";
import { fetch } from "@/util/fetchutil";
import { fireAndForget } from "@/util/util";
Expand Down Expand Up @@ -57,7 +58,19 @@ function makeORef(otype: string, oid: string): string {
return `${otype}:${oid}`;
}

const previewMockObjects: Map<string, WaveObj> = new Map();

function mockObjectForPreview<T extends WaveObj>(oref: string, obj: T): void {
if (!isPreviewWindow()) {
throw new Error("mockObjectForPreview can only be called in a preview window");
}
previewMockObjects.set(oref, obj);
}

function GetObject<T>(oref: string): Promise<T> {
if (isPreviewWindow()) {
return Promise.resolve((previewMockObjects.get(oref) as T) ?? null);
}
return callBackendService("object", "GetObject", [oref], true);
}

Expand Down Expand Up @@ -105,7 +118,9 @@ function callBackendService(service: string, method: string, args: any[], noUICo
const usp = new URLSearchParams();
usp.set("service", service);
usp.set("method", method);
const url = getWebServerEndpoint() + "/wave/service?" + usp.toString();
const webEndpoint = getWebServerEndpoint();
if (webEndpoint == null) throw new Error(`cannot call ${methodName}: no web endpoint`);
const url = webEndpoint + "/wave/service?" + usp.toString();
const fetchPromise = fetch(url, {
method: "POST",
body: JSON.stringify(waveCall),
Expand Down Expand Up @@ -315,6 +330,7 @@ export {
getWaveObjectLoadingAtom,
loadAndPinWaveObject,
makeORef,
mockObjectForPreview,
reloadWaveObject,
setObjectValue,
splitORef,
Expand Down
67 changes: 67 additions & 0 deletions frontend/preview/preview-electron-api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
// Copyright 2026, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0

const previewElectronApi: ElectronApi = {
getAuthKey: () => "",
getIsDev: () => false,
getCursorPoint: () => ({ x: 0, y: 0 } as Electron.Point),
getPlatform: () => "darwin",
getEnv: (_varName: string) => "",
getUserName: () => "",
getHostName: () => "",
getDataDir: () => "",
getConfigDir: () => "",
getHomeDir: () => "",
getWebviewPreload: () => "",
getAboutModalDetails: () => ({} as AboutModalDetails),
getZoomFactor: () => 1.0,
showWorkspaceAppMenu: (_workspaceId: string) => {},
showBuilderAppMenu: (_builderId: string) => {},
showContextMenu: (_workspaceId: string, _menu: ElectronContextMenuItem[]) => {},
onContextMenuClick: (_callback: (id: string | null) => void) => {},
onNavigate: (_callback: (url: string) => void) => {},
onIframeNavigate: (_callback: (url: string) => void) => {},
downloadFile: (_path: string) => {},
openExternal: (_url: string) => {},
onFullScreenChange: (_callback: (isFullScreen: boolean) => void) => {},
onZoomFactorChange: (_callback: (zoomFactor: number) => void) => {},
onUpdaterStatusChange: (_callback: (status: UpdaterStatus) => void) => {},
getUpdaterStatus: () => "up-to-date",
getUpdaterChannel: () => "",
installAppUpdate: () => {},
onMenuItemAbout: (_callback: () => void) => {},
updateWindowControlsOverlay: (_rect: Dimensions) => {},
onReinjectKey: (_callback: (waveEvent: WaveKeyboardEvent) => void) => {},
setWebviewFocus: (_focusedId: number) => {},
registerGlobalWebviewKeys: (_keys: string[]) => {},
onControlShiftStateUpdate: (_callback: (state: boolean) => void) => {},
createWorkspace: () => {},
switchWorkspace: (_workspaceId: string) => {},
deleteWorkspace: (_workspaceId: string) => {},
setActiveTab: (_tabId: string) => {},
createTab: () => {},
closeTab: (_workspaceId: string, _tabId: string, _confirmClose: boolean) => Promise.resolve(false),
setWindowInitStatus: (_status: "ready" | "wave-ready") => {},
onWaveInit: (_callback: (initOpts: WaveInitOpts) => void) => {},
onBuilderInit: (_callback: (initOpts: BuilderInitOpts) => void) => {},
sendLog: (_log: string) => {},
onQuicklook: (_filePath: string) => {},
openNativePath: (_filePath: string) => {},
captureScreenshot: (_rect: Electron.Rectangle) => Promise.resolve(""),
setKeyboardChordMode: () => {},
clearWebviewStorage: (_webContentsId: number) => Promise.resolve(),
setWaveAIOpen: (_isOpen: boolean) => {},
closeBuilderWindow: () => {},
incrementTermCommands: (_opts?: { isRemote?: boolean; isWsl?: boolean; isDurable?: boolean }) => {},
nativePaste: () => {},
openBuilder: (_appId?: string) => {},
setBuilderWindowAppId: (_appId: string) => {},
doRefresh: () => {},
saveTextFile: (_fileName: string, _content: string) => Promise.resolve(false),
};
Comment on lines +4 to +61
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
python - <<'PY'
import re, pathlib

type_src = pathlib.Path("frontend/types/custom.d.ts").read_text()
mock_src = pathlib.Path("frontend/preview/preview-electron-api.ts").read_text()

type_block = re.search(r'type ElectronApi = \{(.*?)\n\s*\};', type_src, re.S)
mock_block = re.search(r'const previewElectronApi:\s*ElectronApi\s*=\s*\{(.*?)\n\};', mock_src, re.S)

if not type_block or not mock_block:
    raise SystemExit("Could not locate ElectronApi type or previewElectronApi object.")

type_members = set(re.findall(r'^\s*([A-Za-z_]\w*)\s*[:(]', type_block.group(1), re.M))
mock_members = set(re.findall(r'^\s*([A-Za-z_]\w*)\s*:', mock_block.group(1), re.M))

print("Missing members in previewElectronApi:", sorted(type_members - mock_members))
print("Extra members in previewElectronApi:", sorted(mock_members - type_members))
PY

Repository: wavetermdev/waveterm

Length of output: 158


🏁 Script executed:

cat frontend/types/custom.d.ts | grep -A 500 "type ElectronApi"

Repository: wavetermdev/waveterm

Length of output: 14237


🏁 Script executed:

find . -path ".kilocode/skills/electron-api/SKILL.md" -o -path ".kilocode/skills/electron-api/*" | head -20

Repository: wavetermdev/waveterm

Length of output: 46


Implement missing setIsActive method in previewElectronApi.

The object declared as type ElectronApi at line 4 is missing the setIsActive: () => Promise<void> method required by the type contract in frontend/types/custom.d.ts.

Fix
 const previewElectronApi: ElectronApi = {
     doRefresh: () => {},
     saveTextFile: (_fileName: string, _content: string) => Promise.resolve(false),
+    setIsActive: () => Promise.resolve(),
 };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const previewElectronApi: ElectronApi = {
getAuthKey: () => "",
getIsDev: () => false,
getCursorPoint: () => ({ x: 0, y: 0 } as Electron.Point),
getPlatform: () => "darwin",
getEnv: (_varName: string) => "",
getUserName: () => "",
getHostName: () => "",
getDataDir: () => "",
getConfigDir: () => "",
getHomeDir: () => "",
getWebviewPreload: () => "",
getAboutModalDetails: () => ({} as AboutModalDetails),
getZoomFactor: () => 1.0,
showWorkspaceAppMenu: (_workspaceId: string) => {},
showBuilderAppMenu: (_builderId: string) => {},
showContextMenu: (_workspaceId: string, _menu: ElectronContextMenuItem[]) => {},
onContextMenuClick: (_callback: (id: string | null) => void) => {},
onNavigate: (_callback: (url: string) => void) => {},
onIframeNavigate: (_callback: (url: string) => void) => {},
downloadFile: (_path: string) => {},
openExternal: (_url: string) => {},
onFullScreenChange: (_callback: (isFullScreen: boolean) => void) => {},
onZoomFactorChange: (_callback: (zoomFactor: number) => void) => {},
onUpdaterStatusChange: (_callback: (status: UpdaterStatus) => void) => {},
getUpdaterStatus: () => "up-to-date",
getUpdaterChannel: () => "",
installAppUpdate: () => {},
onMenuItemAbout: (_callback: () => void) => {},
updateWindowControlsOverlay: (_rect: Dimensions) => {},
onReinjectKey: (_callback: (waveEvent: WaveKeyboardEvent) => void) => {},
setWebviewFocus: (_focusedId: number) => {},
registerGlobalWebviewKeys: (_keys: string[]) => {},
onControlShiftStateUpdate: (_callback: (state: boolean) => void) => {},
createWorkspace: () => {},
switchWorkspace: (_workspaceId: string) => {},
deleteWorkspace: (_workspaceId: string) => {},
setActiveTab: (_tabId: string) => {},
createTab: () => {},
closeTab: (_workspaceId: string, _tabId: string, _confirmClose: boolean) => Promise.resolve(false),
setWindowInitStatus: (_status: "ready" | "wave-ready") => {},
onWaveInit: (_callback: (initOpts: WaveInitOpts) => void) => {},
onBuilderInit: (_callback: (initOpts: BuilderInitOpts) => void) => {},
sendLog: (_log: string) => {},
onQuicklook: (_filePath: string) => {},
openNativePath: (_filePath: string) => {},
captureScreenshot: (_rect: Electron.Rectangle) => Promise.resolve(""),
setKeyboardChordMode: () => {},
clearWebviewStorage: (_webContentsId: number) => Promise.resolve(),
setWaveAIOpen: (_isOpen: boolean) => {},
closeBuilderWindow: () => {},
incrementTermCommands: (_opts?: { isRemote?: boolean; isWsl?: boolean; isDurable?: boolean }) => {},
nativePaste: () => {},
openBuilder: (_appId?: string) => {},
setBuilderWindowAppId: (_appId: string) => {},
doRefresh: () => {},
saveTextFile: (_fileName: string, _content: string) => Promise.resolve(false),
};
const previewElectronApi: ElectronApi = {
getAuthKey: () => "",
getIsDev: () => false,
getCursorPoint: () => ({ x: 0, y: 0 } as Electron.Point),
getPlatform: () => "darwin",
getEnv: (_varName: string) => "",
getUserName: () => "",
getHostName: () => "",
getDataDir: () => "",
getConfigDir: () => "",
getHomeDir: () => "",
getWebviewPreload: () => "",
getAboutModalDetails: () => ({} as AboutModalDetails),
getZoomFactor: () => 1.0,
showWorkspaceAppMenu: (_workspaceId: string) => {},
showBuilderAppMenu: (_builderId: string) => {},
showContextMenu: (_workspaceId: string, _menu: ElectronContextMenuItem[]) => {},
onContextMenuClick: (_callback: (id: string | null) => void) => {},
onNavigate: (_callback: (url: string) => void) => {},
onIframeNavigate: (_callback: (url: string) => void) => {},
downloadFile: (_path: string) => {},
openExternal: (_url: string) => {},
onFullScreenChange: (_callback: (isFullScreen: boolean) => void) => {},
onZoomFactorChange: (_callback: (zoomFactor: number) => void) => {},
onUpdaterStatusChange: (_callback: (status: UpdaterStatus) => void) => {},
getUpdaterStatus: () => "up-to-date",
getUpdaterChannel: () => "",
installAppUpdate: () => {},
onMenuItemAbout: (_callback: () => void) => {},
updateWindowControlsOverlay: (_rect: Dimensions) => {},
onReinjectKey: (_callback: (waveEvent: WaveKeyboardEvent) => void) => {},
setWebviewFocus: (_focusedId: number) => {},
registerGlobalWebviewKeys: (_keys: string[]) => {},
onControlShiftStateUpdate: (_callback: (state: boolean) => void) => {},
createWorkspace: () => {},
switchWorkspace: (_workspaceId: string) => {},
deleteWorkspace: (_workspaceId: string) => {},
setActiveTab: (_tabId: string) => {},
createTab: () => {},
closeTab: (_workspaceId: string, _tabId: string, _confirmClose: boolean) => Promise.resolve(false),
setWindowInitStatus: (_status: "ready" | "wave-ready") => {},
onWaveInit: (_callback: (initOpts: WaveInitOpts) => void) => {},
onBuilderInit: (_callback: (initOpts: BuilderInitOpts) => void) => {},
sendLog: (_log: string) => {},
onQuicklook: (_filePath: string) => {},
openNativePath: (_filePath: string) => {},
captureScreenshot: (_rect: Electron.Rectangle) => Promise.resolve(""),
setKeyboardChordMode: () => {},
clearWebviewStorage: (_webContentsId: number) => Promise.resolve(),
setWaveAIOpen: (_isOpen: boolean) => {},
closeBuilderWindow: () => {},
incrementTermCommands: (_opts?: { isRemote?: boolean; isWsl?: boolean; isDurable?: boolean }) => {},
nativePaste: () => {},
openBuilder: (_appId?: string) => {},
setBuilderWindowAppId: (_appId: string) => {},
doRefresh: () => {},
saveTextFile: (_fileName: string, _content: string) => Promise.resolve(false),
setIsActive: () => Promise.resolve(),
};
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/preview/preview-electron-api.ts` around lines 4 - 61, The
previewElectronApi object is missing the required async method setIsActive
defined by the ElectronApi interface; add setIsActive: () => Promise<void> to
previewElectronApi (e.g., implement a stub that returns Promise.resolve() or an
async no-op) so the object satisfies the type contract—update the
previewElectronApi declaration to include this method alongside the other
functions.


function installPreviewElectronApi() {
(window as any).api = previewElectronApi;
}

export { installPreviewElectronApi };
25 changes: 20 additions & 5 deletions frontend/preview/preview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@
// SPDX-License-Identifier: Apache-2.0

import Logo from "@/app/asset/logo.svg";
import { ClientModel } from "@/app/store/client-model";
import { setWaveWindowType } from "@/app/store/windowtype";
import { getAtoms, initGlobalAtoms } from "@/app/store/global-atoms";
import { GlobalModel } from "@/app/store/global-model";
import { globalStore } from "@/app/store/jotaiStore";
import { loadFonts } from "@/util/fontutil";
import React, { lazy, Suspense } from "react";
import { createRoot } from "react-dom/client";
import { installPreviewElectronApi } from "./preview-electron-api";

import "../app/app.scss";

Expand Down Expand Up @@ -118,10 +120,23 @@ function PreviewApp() {
return <PreviewIndex />;
}

const PreviewTabId = crypto.randomUUID();
const PreviewWindowId = crypto.randomUUID();
const PreviewClientId = crypto.randomUUID();

function initPreview() {
setWaveWindowType("preview");
// Preview mode has no connected backend client object, but onboarding previews read clientAtom.
ClientModel.getInstance().initialize(null);
installPreviewElectronApi();
const initOpts = {
tabId: PreviewTabId,
windowId: PreviewWindowId,
clientId: PreviewClientId,
environment: "renderer",
platform: "darwin",
isPreview: true,
} as GlobalInitOptions;
initGlobalAtoms(initOpts);
globalStore.set(getAtoms().fullConfigAtom, {} as FullConfigType);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Initialize fullConfigAtom with a structurally safe default, not {}.

Line 138 sets {} as FullConfigType; preview components that read fullConfig.presets/fullConfig.settings directly can throw at runtime.

🔧 Proposed fix
-    globalStore.set(getAtoms().fullConfigAtom, {} as FullConfigType);
+    globalStore.set(
+        getAtoms().fullConfigAtom,
+        {
+            settings: {},
+            presets: {},
+            connections: {},
+        } as FullConfigType
+    );
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
globalStore.set(getAtoms().fullConfigAtom, {} as FullConfigType);
globalStore.set(
getAtoms().fullConfigAtom,
{
settings: {},
presets: {},
connections: {},
} as FullConfigType
);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/preview/preview.tsx` at line 138, Replace the unsafe cast "{} as
FullConfigType" used in globalStore.set(getAtoms().fullConfigAtom, {} as
FullConfigType) with a structurally complete default that matches FullConfigType
(e.g., include default presets array and default settings object or use an
exported DEFAULT_FULL_CONFIG). Update the initialization so fullConfig.presets
and fullConfig.settings are defined (either create and use a DEFAULT_FULL_CONFIG
constant or import an existing default) before calling globalStore.set to avoid
runtime property access errors.

GlobalModel.getInstance().initialize(initOpts);
loadFonts();
const root = createRoot(document.getElementById("main")!);
root.render(<PreviewApp />);
Expand Down
2 changes: 1 addition & 1 deletion frontend/preview/previews/onboarding.preview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ function OnboardingFeaturesV() {
return (
<div className="flex flex-col w-full gap-8">
<OnboardingModalWrapper width="w-[560px]">
<InitPage isCompact={false} />
<InitPage isCompact={false} telemetryUpdateFn={async () => {}} />
</OnboardingModalWrapper>
<OnboardingModalWrapper width="w-[560px]">
<NoTelemetryStarPage isCompact={false} />
Expand Down
1 change: 1 addition & 0 deletions frontend/types/custom.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ declare global {
environment: "electron" | "renderer";
primaryTabStartup?: boolean;
builderId?: string;
isPreview?: boolean;
};

type WaveInitOpts = {
Expand Down
Loading