Skip to content

Commit 64f6d4f

Browse files
committed
merge main
2 parents dc2315d + 71f7e98 commit 64f6d4f

27 files changed

Lines changed: 609 additions & 227 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

cmd/generatets/main-generatets.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,14 @@ func generateWshClientApiFile(tsTypeMap map[reflect.Type]string) error {
112112
fmt.Fprintf(&buf, "// SPDX-License-Identifier: Apache-2.0\n\n")
113113
fmt.Fprintf(&buf, "// generated by cmd/generate/main-generatets.go\n\n")
114114
fmt.Fprintf(&buf, "import { WshClient } from \"./wshclient\";\n\n")
115+
fmt.Fprintf(&buf, "export interface MockRpcClient {\n")
116+
fmt.Fprintf(&buf, " mockWshRpcCall(client: WshClient, command: string, data: any, opts?: RpcOpts): Promise<any>;\n")
117+
fmt.Fprintf(&buf, " mockWshRpcStream(client: WshClient, command: string, data: any, opts?: RpcOpts): AsyncGenerator<any, void, boolean>;\n")
118+
fmt.Fprintf(&buf, "}\n\n")
119+
fmt.Fprintf(&buf, "let mockClient: MockRpcClient = null;\n\n")
120+
fmt.Fprintf(&buf, "export function setMockRpcClient(client: MockRpcClient): void {\n")
121+
fmt.Fprintf(&buf, " mockClient = client;\n")
122+
fmt.Fprintf(&buf, "}\n\n")
115123
orderedKeys := utilfn.GetOrderedMapKeys(declMap)
116124
fmt.Fprintf(&buf, "// WshServerCommandToDeclMap\n")
117125
fmt.Fprintf(&buf, "class RpcApiType {\n")

frontend/app/modals/about.tsx

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

44
import Logo from "@/app/asset/logo.svg";
5+
import { OnboardingGradientBg } from "@/app/onboarding/onboarding-common";
56
import { modalsModel } from "@/app/store/modalmodel";
6-
import { Modal } from "./modal";
7-
7+
import { RpcApi } from "@/app/store/wshclientapi";
8+
import { TabRpcClient } from "@/app/store/wshrpcutil";
89
import { isDev } from "@/util/isdev";
9-
import { useState } from "react";
10+
import { fireAndForget } from "@/util/util";
11+
import { useEffect, useState } from "react";
1012
import { getApi } from "../store/global";
13+
import { Modal } from "./modal";
1114

1215
interface AboutModalVProps {
1316
versionString: string;
@@ -19,13 +22,14 @@ const AboutModalV = ({ versionString, updaterChannel, onClose }: AboutModalVProp
1922
const currentDate = new Date();
2023

2124
return (
22-
<Modal className="pt-[34px] pb-[34px]" onClose={onClose}>
23-
<div className="flex flex-col gap-[26px] w-full">
25+
<Modal className="pt-[34px] pb-[34px] overflow-hidden w-[450px]" onClose={onClose}>
26+
<OnboardingGradientBg />
27+
<div className="flex flex-col gap-[26px] w-full relative z-10">
2428
<div className="flex flex-col items-center justify-center gap-4 self-stretch w-full text-center">
2529
<Logo />
2630
<div className="text-[25px]">Wave Terminal</div>
2731
<div className="leading-5">
28-
Open-Source AI-Native Terminal
32+
Open-Source AI-Integrated Terminal
2933
<br />
3034
Built for Seamless Workflows
3135
</div>
@@ -35,30 +39,38 @@ const AboutModalV = ({ versionString, updaterChannel, onClose }: AboutModalVProp
3539
<br />
3640
Update Channel: {updaterChannel}
3741
</div>
38-
<div className="flex items-start gap-[10px] self-stretch w-full text-center">
42+
<div className="grid grid-cols-2 gap-[10px] self-stretch w-full">
3943
<a
4044
href="https://github.com/wavetermdev/waveterm?ref=about"
4145
target="_blank"
4246
rel="noopener"
43-
className="inline-flex items-center px-4 py-2 rounded border border-border hover:bg-hoverbg transition-colors duration-200"
47+
className="inline-flex items-center justify-center px-4 py-2 rounded border border-border hover:bg-hoverbg transition-colors duration-200"
4448
>
45-
<i className="fa-brands fa-github mr-2"></i>Github
49+
<i className="fa-brands fa-github mr-2"></i>GitHub
4650
</a>
4751
<a
4852
href="https://www.waveterm.dev/?ref=about"
4953
target="_blank"
5054
rel="noopener"
51-
className="inline-flex items-center px-4 py-2 rounded border border-border hover:bg-hoverbg transition-colors duration-200"
55+
className="inline-flex items-center justify-center px-4 py-2 rounded border border-border hover:bg-hoverbg transition-colors duration-200"
5256
>
5357
<i className="fa-sharp fa-light fa-globe mr-2"></i>Website
5458
</a>
5559
<a
5660
href="https://github.com/wavetermdev/waveterm/blob/main/ACKNOWLEDGEMENTS.md"
5761
target="_blank"
5862
rel="noopener"
59-
className="inline-flex items-center px-4 py-2 rounded border border-border hover:bg-hoverbg transition-colors duration-200"
63+
className="inline-flex items-center justify-center px-4 py-2 rounded border border-border hover:bg-hoverbg transition-colors duration-200"
6064
>
61-
<i className="fa-sharp fa-light fa-heart mr-2"></i>Acknowledgements
65+
<i className="fa-sharp fa-light fa-book mr-2"></i>Open Source
66+
</a>
67+
<a
68+
href="https://github.com/sponsors/wavetermdev"
69+
target="_blank"
70+
rel="noopener"
71+
className="inline-flex items-center justify-center px-4 py-2 rounded border border-border hover:bg-hoverbg transition-colors duration-200"
72+
>
73+
<i className="fa-sharp fa-light fa-heart mr-2"></i>Sponsor
6274
</a>
6375
</div>
6476
<div className="items-center gap-4 self-stretch w-full text-center">
@@ -76,6 +88,16 @@ const AboutModal = () => {
7688
const [updaterChannel] = useState(() => getApi().getUpdaterChannel());
7789
const versionString = `${details.version} (${isDev() ? "dev-" : ""}${details.buildTime})`;
7890

91+
useEffect(() => {
92+
fireAndForget(async () => {
93+
RpcApi.RecordTEventCommand(
94+
TabRpcClient,
95+
{ event: "action:other", props: { "action:type": "about" } },
96+
{ noresponse: true }
97+
);
98+
});
99+
}, []);
100+
79101
return (
80102
<AboutModalV
81103
versionString={versionString}

frontend/app/modals/modalregistry.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import { MessageModal } from "@/app/modals/messagemodal";
55
import { NewInstallOnboardingModal } from "@/app/onboarding/onboarding";
66
import { UpgradeOnboardingModal } from "@/app/onboarding/onboarding-upgrade";
7+
import { UpgradeOnboardingPatch } from "@/app/onboarding/onboarding-upgrade-patch";
78
import { DeleteFileModal, PublishAppModal, RenameFileModal } from "@/builder/builder-apppanel";
89
import { SetSecretDialog } from "@/builder/tabs/builder-secrettab";
910
import { AboutModal } from "./about";
@@ -12,6 +13,7 @@ import { UserInputModal } from "./userinputmodal";
1213
const modalRegistry: { [key: string]: React.ComponentType<any> } = {
1314
[NewInstallOnboardingModal.displayName || "NewInstallOnboardingModal"]: NewInstallOnboardingModal,
1415
[UpgradeOnboardingModal.displayName || "UpgradeOnboardingModal"]: UpgradeOnboardingModal,
16+
[UpgradeOnboardingPatch.displayName || "UpgradeOnboardingPatch"]: UpgradeOnboardingPatch,
1517
[UserInputModal.displayName || "UserInputModal"]: UserInputModal,
1618
[AboutModal.displayName || "AboutModal"]: AboutModal,
1719
[MessageModal.displayName || "MessageModal"]: MessageModal,

frontend/app/onboarding/onboarding-upgrade-patch.tsx

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,10 @@ interface VersionConfig {
3232
nextText?: string;
3333
}
3434

35+
interface UpgradeOnboardingPatchProps {
36+
isReleaseNotes?: boolean;
37+
}
38+
3539
interface UpgradeOnboardingFooterProps {
3640
hasPrev: boolean;
3741
hasNext: boolean;
@@ -131,7 +135,7 @@ export const UpgradeOnboardingVersions: VersionConfig[] = [
131135
},
132136
];
133137

134-
const UpgradeOnboardingPatch = () => {
138+
const UpgradeOnboardingPatch = ({ isReleaseNotes = false }: UpgradeOnboardingPatchProps) => {
135139
const modalRef = useRef<HTMLDivElement | null>(null);
136140
const [isCompact, setIsCompact] = useState<boolean>(window.innerHeight < 800);
137141
const [currentIndex, setCurrentIndex] = useState<number>(UpgradeOnboardingVersions.length - 1);
@@ -174,13 +178,21 @@ const UpgradeOnboardingPatch = () => {
174178
}, []);
175179

176180
const doClose = () => {
177-
globalStore.set(modalsModel.upgradeOnboardingOpen, false);
181+
if (isReleaseNotes) {
182+
modalsModel.popModal();
183+
} else {
184+
globalStore.set(modalsModel.upgradeOnboardingOpen, false);
185+
}
178186
setTimeout(() => {
179187
globalRefocus();
180188
}, 10);
181189
};
182190

183191
const handleClose = () => {
192+
if (isReleaseNotes) {
193+
doClose();
194+
return;
195+
}
184196
const clientId = ClientModel.getInstance().clientId;
185197
RpcApi.SetMetaCommand(TabRpcClient, {
186198
oref: WOS.makeORef("client", clientId),

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
@@ -15,7 +15,7 @@ function initGlobalAtoms(initOpts: GlobalInitOptions) {
1515
const windowIdAtom = atom(initOpts.windowId) as PrimitiveAtom<string>;
1616
const builderIdAtom = atom(initOpts.builderId) as PrimitiveAtom<string>;
1717
const builderAppIdAtom = atom<string>(null) as PrimitiveAtom<string>;
18-
setWaveWindowType(initOpts.builderId != null ? "builder" : "tab");
18+
setWaveWindowType(initOpts.isPreview ? "preview" : initOpts.builderId != null ? "builder" : "tab");
1919
const uiContextAtom = atom((get) => {
2020
const uiContext: UIContext = {
2121
windowid: initOpts.windowId,

frontend/app/store/services.ts

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -113,12 +113,6 @@ class WindowServiceType {
113113
return WOS.callBackendService("window", "GetWindow", Array.from(arguments))
114114
}
115115

116-
// move block to new window
117-
// @returns object updates
118-
MoveBlockToNewWindow(currentTabId: string, blockId: string): Promise<void> {
119-
return WOS.callBackendService("window", "MoveBlockToNewWindow", Array.from(arguments))
120-
}
121-
122116
// set window position and size
123117
// @returns object updates
124118
SetWindowPosAndSize(windowId: string, pos: Point, size: WinSize): Promise<void> {

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,

0 commit comments

Comments
 (0)