Skip to content

Commit 7ef0bcd

Browse files
sawkaCopilot
andauthored
preview updates (mock electron api, wos checks) (#2986)
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: sawka <2722291+sawka@users.noreply.github.com>
1 parent 3f4484a commit 7ef0bcd

10 files changed

Lines changed: 133 additions & 17 deletions

File tree

.kilocode/skills/electron-api/SKILL.md

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,12 @@ description: Guide for adding new Electron APIs to Wave Terminal. Use when imple
77

88
Electron APIs allow the frontend to call Electron main process functionality directly via IPC.
99

10-
## Three Files to Edit
10+
## Four Files to Edit
1111

1212
1. [`frontend/types/custom.d.ts`](frontend/types/custom.d.ts) - TypeScript [`ElectronApi`](frontend/types/custom.d.ts:82) type
1313
2. [`emain/preload.ts`](emain/preload.ts) - Expose method via `contextBridge`
1414
3. [`emain/emain-ipc.ts`](emain/emain-ipc.ts) - Implement IPC handler
15+
4. [`frontend/preview/preview-electron-api.ts`](frontend/preview/preview-electron-api.ts) - Add a no-op stub to keep the `previewElectronApi` object in sync with the `ElectronApi` type
1516

1617
## Three Communication Patterns
1718

@@ -54,7 +55,15 @@ electron.ipcMain.handle("capture-screenshot", async (event, rect) => {
5455
});
5556
```
5657

57-
### 4. Call from Frontend
58+
### 4. Add Preview Stub
59+
60+
In [`frontend/preview/preview-electron-api.ts`](frontend/preview/preview-electron-api.ts):
61+
62+
```typescript
63+
captureScreenshot: (_rect: Electron.Rectangle) => Promise.resolve(""),
64+
```
65+
66+
### 5. Call from Frontend
5867

5968
```typescript
6069
import { getApi } from "@/store/global";
@@ -167,6 +176,7 @@ webContents.send("zoom-factor-change", newZoomFactor);
167176
- [ ] Include IPC channel name in comment
168177
- [ ] Expose in [`preload.ts`](emain/preload.ts)
169178
- [ ] Implement in [`emain-ipc.ts`](emain/emain-ipc.ts)
179+
- [ ] Add no-op stub to [`preview-electron-api.ts`](frontend/preview/preview-electron-api.ts)
170180
- [ ] IPC channel names match exactly
171181
- [ ] **For sync**: Set `event.returnValue` (or browser hangs!)
172182
- [ ] Test end-to-end

eslint.config.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -76,8 +76,8 @@ export default [
7676
"@typescript-eslint/no-unused-vars": [
7777
"warn",
7878
{
79-
argsIgnorePattern: "^_$",
80-
varsIgnorePattern: "^_$",
79+
argsIgnorePattern: "^_[a-z0-9]*$",
80+
varsIgnorePattern: "^_[a-z0-9]*$",
8181
},
8282
],
8383
"prefer-const": "warn",

frontend/app/onboarding/onboarding.tsx

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,13 @@ type PageName = "init" | "notelemetrystar" | "features";
2929

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

32-
const InitPage = ({ isCompact }: { isCompact: boolean }) => {
32+
const InitPage = ({
33+
isCompact,
34+
telemetryUpdateFn,
35+
}: {
36+
isCompact: boolean;
37+
telemetryUpdateFn: (value: boolean) => Promise<void>;
38+
}) => {
3339
const telemetrySetting = useSettingsKeyAtom("telemetry:enabled");
3440
const clientData = useAtomValue(ClientModel.getInstance().clientAtom);
3541
const [telemetryEnabled, setTelemetryEnabled] = useState<boolean>(!!telemetrySetting);
@@ -63,7 +69,7 @@ const InitPage = ({ isCompact }: { isCompact: boolean }) => {
6369

6470
const setTelemetry = (value: boolean) => {
6571
fireAndForget(() =>
66-
services.ClientService.TelemetryUpdate(value).then(() => {
72+
telemetryUpdateFn(value).then(() => {
6773
setTelemetryEnabled(value);
6874
})
6975
);
@@ -319,7 +325,7 @@ const NewInstallOnboardingModal = () => {
319325
let pageComp: React.JSX.Element = null;
320326
switch (pageName) {
321327
case "init":
322-
pageComp = <InitPage isCompact={isCompact} />;
328+
pageComp = <InitPage isCompact={isCompact} telemetryUpdateFn={services.ClientService.TelemetryUpdate} />;
323329
break;
324330
case "notelemetrystar":
325331
pageComp = <NoTelemetryStarPage isCompact={isCompact} />;

frontend/app/store/client-model.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright 2025, Command Line Inc
1+
// Copyright 2026, Command Line Inc
22
// SPDX-License-Identifier: Apache-2.0
33

44
import * as WOS from "@/app/store/wos";
@@ -33,4 +33,4 @@ class ClientModel {
3333
}
3434
}
3535

36-
export { ClientModel };
36+
export { ClientModel };

frontend/app/store/global-atoms.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ function initGlobalAtoms(initOpts: GlobalInitOptions) {
1616
const windowIdAtom = atom(initOpts.windowId) as PrimitiveAtom<string>;
1717
const builderIdAtom = atom(initOpts.builderId) as PrimitiveAtom<string>;
1818
const builderAppIdAtom = atom<string>(null) as PrimitiveAtom<string>;
19-
setWaveWindowType(initOpts.builderId != null ? "builder" : "tab");
19+
setWaveWindowType(initOpts.isPreview ? "preview" : initOpts.builderId != null ? "builder" : "tab");
2020
const uiContextAtom = atom((get) => {
2121
const uiContext: UIContext = {
2222
windowid: initOpts.windowId,

frontend/app/store/wos.ts

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
// WaveObjectStore
55

66
import { waveEventSubscribeSingle } from "@/app/store/wps";
7+
import { isPreviewWindow } from "@/app/store/windowtype";
78
import { getWebServerEndpoint } from "@/util/endpoints";
89
import { fetch } from "@/util/fetchutil";
910
import { fireAndForget } from "@/util/util";
@@ -57,7 +58,19 @@ function makeORef(otype: string, oid: string): string {
5758
return `${otype}:${oid}`;
5859
}
5960

61+
const previewMockObjects: Map<string, WaveObj> = new Map();
62+
63+
function mockObjectForPreview<T extends WaveObj>(oref: string, obj: T): void {
64+
if (!isPreviewWindow()) {
65+
throw new Error("mockObjectForPreview can only be called in a preview window");
66+
}
67+
previewMockObjects.set(oref, obj);
68+
}
69+
6070
function GetObject<T>(oref: string): Promise<T> {
71+
if (isPreviewWindow()) {
72+
return Promise.resolve((previewMockObjects.get(oref) as T) ?? null);
73+
}
6174
return callBackendService("object", "GetObject", [oref], true);
6275
}
6376

@@ -105,7 +118,9 @@ function callBackendService(service: string, method: string, args: any[], noUICo
105118
const usp = new URLSearchParams();
106119
usp.set("service", service);
107120
usp.set("method", method);
108-
const url = getWebServerEndpoint() + "/wave/service?" + usp.toString();
121+
const webEndpoint = getWebServerEndpoint();
122+
if (webEndpoint == null) throw new Error(`cannot call ${methodName}: no web endpoint`);
123+
const url = webEndpoint + "/wave/service?" + usp.toString();
109124
const fetchPromise = fetch(url, {
110125
method: "POST",
111126
body: JSON.stringify(waveCall),
@@ -315,6 +330,7 @@ export {
315330
getWaveObjectLoadingAtom,
316331
loadAndPinWaveObject,
317332
makeORef,
333+
mockObjectForPreview,
318334
reloadWaveObject,
319335
setObjectValue,
320336
splitORef,
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
// Copyright 2026, Command Line Inc.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
const previewElectronApi: ElectronApi = {
5+
getAuthKey: () => "",
6+
getIsDev: () => false,
7+
getCursorPoint: () => ({ x: 0, y: 0 }) as Electron.Point,
8+
getPlatform: () => "darwin",
9+
getEnv: (_varName: string) => "",
10+
getUserName: () => "",
11+
getHostName: () => "",
12+
getDataDir: () => "",
13+
getConfigDir: () => "",
14+
getHomeDir: () => "",
15+
getWebviewPreload: () => "",
16+
getAboutModalDetails: () => ({}) as AboutModalDetails,
17+
getZoomFactor: () => 1.0,
18+
showWorkspaceAppMenu: (_workspaceId: string) => {},
19+
showBuilderAppMenu: (_builderId: string) => {},
20+
showContextMenu: (_workspaceId: string, _menu: ElectronContextMenuItem[]) => {},
21+
onContextMenuClick: (_callback: (id: string | null) => void) => {},
22+
onNavigate: (_callback: (url: string) => void) => {},
23+
onIframeNavigate: (_callback: (url: string) => void) => {},
24+
downloadFile: (_path: string) => {},
25+
openExternal: (_url: string) => {},
26+
onFullScreenChange: (_callback: (isFullScreen: boolean) => void) => {},
27+
onZoomFactorChange: (_callback: (zoomFactor: number) => void) => {},
28+
onUpdaterStatusChange: (_callback: (status: UpdaterStatus) => void) => {},
29+
getUpdaterStatus: () => "up-to-date",
30+
getUpdaterChannel: () => "",
31+
installAppUpdate: () => {},
32+
onMenuItemAbout: (_callback: () => void) => {},
33+
updateWindowControlsOverlay: (_rect: Dimensions) => {},
34+
onReinjectKey: (_callback: (waveEvent: WaveKeyboardEvent) => void) => {},
35+
setWebviewFocus: (_focusedId: number) => {},
36+
registerGlobalWebviewKeys: (_keys: string[]) => {},
37+
onControlShiftStateUpdate: (_callback: (state: boolean) => void) => {},
38+
createWorkspace: () => {},
39+
switchWorkspace: (_workspaceId: string) => {},
40+
deleteWorkspace: (_workspaceId: string) => {},
41+
setActiveTab: (_tabId: string) => {},
42+
createTab: () => {},
43+
closeTab: (_workspaceId: string, _tabId: string, _confirmClose: boolean) => Promise.resolve(false),
44+
setWindowInitStatus: (_status: "ready" | "wave-ready") => {},
45+
onWaveInit: (_callback: (initOpts: WaveInitOpts) => void) => {},
46+
onBuilderInit: (_callback: (initOpts: BuilderInitOpts) => void) => {},
47+
sendLog: (_log: string) => {},
48+
onQuicklook: (_filePath: string) => {},
49+
openNativePath: (_filePath: string) => {},
50+
captureScreenshot: (_rect: Electron.Rectangle) => Promise.resolve(""),
51+
setKeyboardChordMode: () => {},
52+
clearWebviewStorage: (_webContentsId: number) => Promise.resolve(),
53+
setWaveAIOpen: (_isOpen: boolean) => {},
54+
closeBuilderWindow: () => {},
55+
incrementTermCommands: (_opts?: { isRemote?: boolean; isWsl?: boolean; isDurable?: boolean }) => {},
56+
nativePaste: () => {},
57+
openBuilder: (_appId?: string) => {},
58+
setBuilderWindowAppId: (_appId: string) => {},
59+
doRefresh: () => {},
60+
saveTextFile: (_fileName: string, _content: string) => Promise.resolve(false),
61+
setIsActive: async () => {},
62+
};
63+
64+
function installPreviewElectronApi() {
65+
(window as any).api = previewElectronApi;
66+
}
67+
68+
export { installPreviewElectronApi };

frontend/preview/preview.tsx

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,13 @@
22
// SPDX-License-Identifier: Apache-2.0
33

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

1113
import "../app/app.scss";
1214

@@ -118,10 +120,23 @@ function PreviewApp() {
118120
return <PreviewIndex />;
119121
}
120122

123+
const PreviewTabId = crypto.randomUUID();
124+
const PreviewWindowId = crypto.randomUUID();
125+
const PreviewClientId = crypto.randomUUID();
126+
121127
function initPreview() {
122-
setWaveWindowType("preview");
123-
// Preview mode has no connected backend client object, but onboarding previews read clientAtom.
124-
ClientModel.getInstance().initialize(null);
128+
installPreviewElectronApi();
129+
const initOpts = {
130+
tabId: PreviewTabId,
131+
windowId: PreviewWindowId,
132+
clientId: PreviewClientId,
133+
environment: "renderer",
134+
platform: "darwin",
135+
isPreview: true,
136+
} as GlobalInitOptions;
137+
initGlobalAtoms(initOpts);
138+
globalStore.set(getAtoms().fullConfigAtom, {} as FullConfigType);
139+
GlobalModel.getInstance().initialize(initOpts);
125140
loadFonts();
126141
const root = createRoot(document.getElementById("main")!);
127142
root.render(<PreviewApp />);

frontend/preview/previews/onboarding.preview.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ function OnboardingFeaturesV() {
2424
return (
2525
<div className="flex flex-col w-full gap-8">
2626
<OnboardingModalWrapper width="w-[560px]">
27-
<InitPage isCompact={false} />
27+
<InitPage isCompact={false} telemetryUpdateFn={async () => {}} />
2828
</OnboardingModalWrapper>
2929
<OnboardingModalWrapper width="w-[560px]">
3030
<NoTelemetryStarPage isCompact={false} />

frontend/types/custom.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ declare global {
5959
environment: "electron" | "renderer";
6060
primaryTabStartup?: boolean;
6161
builderId?: string;
62+
isPreview?: boolean;
6263
};
6364

6465
type WaveInitOpts = {

0 commit comments

Comments
 (0)