Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 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`);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

CRITICAL: Null check breaks preview mode - getWebServerEndpoint() intentionally returns null in preview windows (see frontend/util/endpoints.ts:12), but this null check throws an error. This will crash all backend service calls in preview mode.

The fix should either:

  1. Add an early return for preview windows: if (isPreviewWindow()) return Promise.reject(new Error(...));
  2. Or remove the null check and let it fail with a more descriptive error downstream.

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 thread
coderabbitai[bot] marked this conversation as resolved.

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
Copy Markdown
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