diff --git a/eslint.config.js b/eslint.config.js index 1c72e5f464..d4844a8b64 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -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", diff --git a/frontend/app/onboarding/onboarding.tsx b/frontend/app/onboarding/onboarding.tsx index 2755db002f..7c95ef27a6 100644 --- a/frontend/app/onboarding/onboarding.tsx +++ b/frontend/app/onboarding/onboarding.tsx @@ -29,7 +29,13 @@ type PageName = "init" | "notelemetrystar" | "features"; const pageNameAtom: PrimitiveAtom = atom("init"); -const InitPage = ({ isCompact }: { isCompact: boolean }) => { +const InitPage = ({ + isCompact, + telemetryUpdateFn, +}: { + isCompact: boolean; + telemetryUpdateFn: (value: boolean) => Promise; +}) => { const telemetrySetting = useSettingsKeyAtom("telemetry:enabled"); const clientData = useAtomValue(ClientModel.getInstance().clientAtom); const [telemetryEnabled, setTelemetryEnabled] = useState(!!telemetrySetting); @@ -63,7 +69,7 @@ const InitPage = ({ isCompact }: { isCompact: boolean }) => { const setTelemetry = (value: boolean) => { fireAndForget(() => - services.ClientService.TelemetryUpdate(value).then(() => { + telemetryUpdateFn(value).then(() => { setTelemetryEnabled(value); }) ); @@ -319,7 +325,7 @@ const NewInstallOnboardingModal = () => { let pageComp: React.JSX.Element = null; switch (pageName) { case "init": - pageComp = ; + pageComp = ; break; case "notelemetrystar": pageComp = ; diff --git a/frontend/app/store/client-model.ts b/frontend/app/store/client-model.ts index 240dc6d03d..4ae250f5bb 100644 --- a/frontend/app/store/client-model.ts +++ b/frontend/app/store/client-model.ts @@ -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"; @@ -33,4 +33,4 @@ class ClientModel { } } -export { ClientModel }; \ No newline at end of file +export { ClientModel }; diff --git a/frontend/app/store/global-atoms.ts b/frontend/app/store/global-atoms.ts index ac36fcec8e..18f072070f 100644 --- a/frontend/app/store/global-atoms.ts +++ b/frontend/app/store/global-atoms.ts @@ -16,7 +16,7 @@ function initGlobalAtoms(initOpts: GlobalInitOptions) { const windowIdAtom = atom(initOpts.windowId) as PrimitiveAtom; const builderIdAtom = atom(initOpts.builderId) as PrimitiveAtom; const builderAppIdAtom = atom(null) as PrimitiveAtom; - 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, diff --git a/frontend/app/store/wos.ts b/frontend/app/store/wos.ts index 4ce339acd1..1d3bdeabdb 100644 --- a/frontend/app/store/wos.ts +++ b/frontend/app/store/wos.ts @@ -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"; @@ -57,7 +58,19 @@ function makeORef(otype: string, oid: string): string { return `${otype}:${oid}`; } +const previewMockObjects: Map = new Map(); + +function mockObjectForPreview(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(oref: string): Promise { + if (isPreviewWindow()) { + return Promise.resolve((previewMockObjects.get(oref) as T) ?? null); + } return callBackendService("object", "GetObject", [oref], true); } @@ -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), @@ -315,6 +330,7 @@ export { getWaveObjectLoadingAtom, loadAndPinWaveObject, makeORef, + mockObjectForPreview, reloadWaveObject, setObjectValue, splitORef, diff --git a/frontend/preview/preview-electron-api.ts b/frontend/preview/preview-electron-api.ts new file mode 100644 index 0000000000..8e278e50c9 --- /dev/null +++ b/frontend/preview/preview-electron-api.ts @@ -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), +}; + +function installPreviewElectronApi() { + (window as any).api = previewElectronApi; +} + +export { installPreviewElectronApi }; diff --git a/frontend/preview/preview.tsx b/frontend/preview/preview.tsx index 3b0e8d7825..daa232a51b 100644 --- a/frontend/preview/preview.tsx +++ b/frontend/preview/preview.tsx @@ -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"; @@ -118,10 +120,23 @@ function PreviewApp() { return ; } +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); + GlobalModel.getInstance().initialize(initOpts); loadFonts(); const root = createRoot(document.getElementById("main")!); root.render(); diff --git a/frontend/preview/previews/onboarding.preview.tsx b/frontend/preview/previews/onboarding.preview.tsx index 18d555dff8..063320bbb9 100644 --- a/frontend/preview/previews/onboarding.preview.tsx +++ b/frontend/preview/previews/onboarding.preview.tsx @@ -24,7 +24,7 @@ function OnboardingFeaturesV() { return (
- + {}} /> diff --git a/frontend/types/custom.d.ts b/frontend/types/custom.d.ts index 25c40eefef..6fbe95a0ea 100644 --- a/frontend/types/custom.d.ts +++ b/frontend/types/custom.d.ts @@ -59,6 +59,7 @@ declare global { environment: "electron" | "renderer"; primaryTabStartup?: boolean; builderId?: string; + isPreview?: boolean; }; type WaveInitOpts = {