mirror of
https://github.com/wavetermdev/waveterm
synced 2026-04-21 14:37:16 +00:00
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>
This commit is contained in:
parent
3f4484a9e2
commit
7ef0bcd87f
10 changed files with 133 additions and 17 deletions
|
|
@ -7,11 +7,12 @@ description: Guide for adding new Electron APIs to Wave Terminal. Use when imple
|
|||
|
||||
Electron APIs allow the frontend to call Electron main process functionality directly via IPC.
|
||||
|
||||
## Three Files to Edit
|
||||
## Four Files to Edit
|
||||
|
||||
1. [`frontend/types/custom.d.ts`](frontend/types/custom.d.ts) - TypeScript [`ElectronApi`](frontend/types/custom.d.ts:82) type
|
||||
2. [`emain/preload.ts`](emain/preload.ts) - Expose method via `contextBridge`
|
||||
3. [`emain/emain-ipc.ts`](emain/emain-ipc.ts) - Implement IPC handler
|
||||
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
|
||||
|
||||
## Three Communication Patterns
|
||||
|
||||
|
|
@ -54,7 +55,15 @@ electron.ipcMain.handle("capture-screenshot", async (event, rect) => {
|
|||
});
|
||||
```
|
||||
|
||||
### 4. Call from Frontend
|
||||
### 4. Add Preview Stub
|
||||
|
||||
In [`frontend/preview/preview-electron-api.ts`](frontend/preview/preview-electron-api.ts):
|
||||
|
||||
```typescript
|
||||
captureScreenshot: (_rect: Electron.Rectangle) => Promise.resolve(""),
|
||||
```
|
||||
|
||||
### 5. Call from Frontend
|
||||
|
||||
```typescript
|
||||
import { getApi } from "@/store/global";
|
||||
|
|
@ -167,6 +176,7 @@ webContents.send("zoom-factor-change", newZoomFactor);
|
|||
- [ ] Include IPC channel name in comment
|
||||
- [ ] Expose in [`preload.ts`](emain/preload.ts)
|
||||
- [ ] Implement in [`emain-ipc.ts`](emain/emain-ipc.ts)
|
||||
- [ ] Add no-op stub to [`preview-electron-api.ts`](frontend/preview/preview-electron-api.ts)
|
||||
- [ ] IPC channel names match exactly
|
||||
- [ ] **For sync**: Set `event.returnValue` (or browser hangs!)
|
||||
- [ ] Test end-to-end
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
@ -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 = <InitPage isCompact={isCompact} />;
|
||||
pageComp = <InitPage isCompact={isCompact} telemetryUpdateFn={services.ClientService.TelemetryUpdate} />;
|
||||
break;
|
||||
case "notelemetrystar":
|
||||
pageComp = <NoTelemetryStarPage isCompact={isCompact} />;
|
||||
|
|
|
|||
|
|
@ -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 };
|
||||
export { ClientModel };
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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<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);
|
||||
}
|
||||
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
68
frontend/preview/preview-electron-api.ts
Normal file
68
frontend/preview/preview-electron-api.ts
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
// 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),
|
||||
setIsActive: async () => {},
|
||||
};
|
||||
|
||||
function installPreviewElectronApi() {
|
||||
(window as any).api = previewElectronApi;
|
||||
}
|
||||
|
||||
export { installPreviewElectronApi };
|
||||
|
|
@ -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 <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);
|
||||
GlobalModel.getInstance().initialize(initOpts);
|
||||
loadFonts();
|
||||
const root = createRoot(document.getElementById("main")!);
|
||||
root.render(<PreviewApp />);
|
||||
|
|
|
|||
|
|
@ -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} />
|
||||
|
|
|
|||
1
frontend/types/custom.d.ts
vendored
1
frontend/types/custom.d.ts
vendored
|
|
@ -59,6 +59,7 @@ declare global {
|
|||
environment: "electron" | "renderer";
|
||||
primaryTabStartup?: boolean;
|
||||
builderId?: string;
|
||||
isPreview?: boolean;
|
||||
};
|
||||
|
||||
type WaveInitOpts = {
|
||||
|
|
|
|||
Loading…
Reference in a new issue