Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
12 changes: 12 additions & 0 deletions .roo/rules/rules.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,18 @@ To define a new RPC call, add the new definition to `pkg/wshrpc/wshrpctypes.go`

For normal "server" RPCs (where a frontend client is calling the main server) you should implement the RPC call in `pkg/wshrpc/wshserver.go`.

### Electron API

From within the FE to get the electron API (e.g. the preload functions):

```
import { getApi } from "@/store/global";

getApi().getIsDev()
```
Comment on lines +65 to +69
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Add language identifier to fenced code block.

The code block should specify the language (TypeScript) for proper syntax highlighting and better documentation rendering.

Apply this diff:

-```
+```typescript
 import { getApi } from "@/store/global";
 
 getApi().getIsDev()

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 markdownlint-cli2 (0.18.1)</summary>

65-65: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

</details>

</details>

<details>
<summary>🤖 Prompt for AI Agents</summary>

.roo/rules/rules.md around lines 65 to 69: the fenced code block is missing a
language identifier; update the opening fence from totypescript so the
block reads as a TypeScript fenced code block (i.e., replace the existing
opening triple backticks with ```typescript and leave the block content
unchanged).


</details>

<!-- This is an auto-generated comment by CodeRabbit -->


The full API is defined in custom.d.ts as type ElectronApi.

### Code Generation

- **TypeScript Types**: TypeScript types are automatically generated from Go types. After modifying Go types in `pkg/wshrpc/wshrpctypes.go`, run `task generate` to update the TypeScript type definitions in `frontend/types/gotypes.d.ts`.
Expand Down
28 changes: 26 additions & 2 deletions emain/emain-ipc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { RpcApi } from "../frontend/app/store/wshclientapi";
import { getWebServerEndpoint } from "../frontend/util/endpoints";
import * as keyutil from "../frontend/util/keyutil";
import { fireAndForget, parseDataUrl } from "../frontend/util/util";
import { createBuilderWindow, getBuilderWindowByWebContentsId } from "./emain-builder";
import { createBuilderWindow, getAllBuilderWindows, getBuilderWindowByWebContentsId } from "./emain-builder";
import { callWithOriginalXdgCurrentDesktopAsync, unamePlatform } from "./emain-platform";
import { getWaveTabViewByWebContentsId } from "./emain-tabview";
import { handleCtrlShiftState } from "./emain-util";
Expand All @@ -26,6 +26,19 @@ const electronApp = electron.app;
let webviewFocusId: number = null;
let webviewKeys: string[] = [];

export function openBuilderWindow(appId?: string) {
const normalizedAppId = appId || "";
const existingBuilderWindows = getAllBuilderWindows();
const existingWindow = existingBuilderWindows.find(
(win) => win.savedInitOpts?.appId === normalizedAppId
);
if (existingWindow) {
existingWindow.focus();
return;
}
fireAndForget(() => createBuilderWindow(normalizedAppId));
}

type UrlInSessionResult = {
stream: Readable;
mimeType: string;
Expand Down Expand Up @@ -405,7 +418,18 @@ export function initIpcHandlers() {
});

electron.ipcMain.on("open-builder", (event, appId?: string) => {
fireAndForget(() => createBuilderWindow(appId || ""));
openBuilderWindow(appId);
});

electron.ipcMain.on("set-builder-window-appid", (event, appId: string) => {
const bw = getBuilderWindowByWebContentsId(event.sender.id);
if (bw == null) {
return;
}
if (bw.savedInitOpts) {
bw.savedInitOpts.appId = appId;
}
console.log("set-builder-window-appid", bw.builderId, appId);
});

electron.ipcMain.on("open-new-window", () => fireAndForget(createNewWaveWindow));
Expand Down
5 changes: 3 additions & 2 deletions emain/emain-menu.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ import { waveEventSubscribe } from "@/app/store/wps";
import { RpcApi } from "@/app/store/wshclientapi";
import * as electron from "electron";
import { fireAndForget } from "../frontend/util/util";
import { createBuilderWindow, focusedBuilderWindow, getBuilderWindowById } from "./emain-builder";
import { focusedBuilderWindow, getBuilderWindowById } from "./emain-builder";
import { openBuilderWindow } from "./emain-ipc";
import { isDev, unamePlatform } from "./emain-platform";
import { clearTabCache } from "./emain-tabview";
import {
Expand Down Expand Up @@ -128,7 +129,7 @@ function makeFileMenu(numWaveWindows: number, callbacks: AppMenuCallbacks): Elec
fileMenu.splice(1, 0, {
label: "New WaveApp Builder Window",
accelerator: unamePlatform === "darwin" ? "Command+Shift+B" : "Alt+Shift+B",
click: () => fireAndForget(() => createBuilderWindow("")),
click: () => openBuilderWindow(""),
});
}
if (numWaveWindows == 0) {
Expand Down
2 changes: 2 additions & 0 deletions emain/preload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,8 @@ contextBridge.exposeInMainWorld("api", {
closeBuilderWindow: () => ipcRenderer.send("close-builder-window"),
incrementTermCommands: () => ipcRenderer.send("increment-term-commands"),
nativePaste: () => ipcRenderer.send("native-paste"),
openBuilder: (appId?: string) => ipcRenderer.send("open-builder", appId),
setBuilderWindowAppId: (appId: string) => ipcRenderer.send("set-builder-window-appid", appId),
});

// Custom event for "new-window"
Expand Down
16 changes: 13 additions & 3 deletions frontend/app/block/blockframe.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -85,10 +85,19 @@ function handleHeaderContextMenu(
ContextMenuModel.showContextMenu(menu, e);
}

function getViewIconElem(viewIconUnion: string | IconButtonDecl, blockData: Block): React.ReactElement {
function getViewIconElem(
viewIconUnion: string | IconButtonDecl,
blockData: Block,
iconColor?: string
): React.ReactElement {
if (viewIconUnion == null || typeof viewIconUnion === "string") {
const viewIcon = viewIconUnion as string;
return <div className="block-frame-view-icon">{getBlockHeaderIcon(viewIcon, blockData)}</div>;
const style: React.CSSProperties = iconColor ? { color: iconColor, opacity: 1.0 } : {};
return (
<div className="block-frame-view-icon" style={style}>
{getBlockHeaderIcon(viewIcon, blockData)}
</div>
);
} else {
return <IconButton decl={viewIconUnion} className="block-frame-view-icon" />;
}
Expand Down Expand Up @@ -172,6 +181,7 @@ const BlockFrame_Header = ({
let viewName = util.useAtomValueSafe(viewModel?.viewName) ?? blockViewToName(blockData?.meta?.view);
const showBlockIds = jotai.useAtomValue(getSettingsKeyAtom("blockheader:showblockids"));
let viewIconUnion = util.useAtomValueSafe(viewModel?.viewIcon) ?? blockViewToIcon(blockData?.meta?.view);
const viewIconColor = util.useAtomValueSafe(viewModel?.viewIconColor);
const preIconButton = util.useAtomValueSafe(viewModel?.preIconButton);
let headerTextUnion = util.useAtomValueSafe(viewModel?.viewText);
const magnified = jotai.useAtomValue(nodeModel.isMagnified);
Expand Down Expand Up @@ -208,7 +218,7 @@ const BlockFrame_Header = ({
);

const endIconsElem = computeEndIcons(viewModel, nodeModel, onContextMenu);
const viewIconElem = getViewIconElem(viewIconUnion, blockData);
const viewIconElem = getViewIconElem(viewIconUnion, blockData, viewIconColor);
let preIconButtonElem: React.ReactElement = null;
if (preIconButton) {
preIconButtonElem = <IconButton decl={preIconButton} className="block-frame-preicon-button" />;
Expand Down
10 changes: 10 additions & 0 deletions frontend/app/store/wshclientapi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -367,6 +367,11 @@ class RpcApiType {
return client.wshRpcCall("listalleditableapps", null, opts);
}

// command "makedraftfromlocal" [call]
MakeDraftFromLocalCommand(client: WshClient, data: CommandMakeDraftFromLocalData, opts?: RpcOpts): Promise<CommandMakeDraftFromLocalRtnData> {
return client.wshRpcCall("makedraftfromlocal", data, opts);
}

// command "message" [call]
MessageCommand(client: WshClient, data: CommandMessageData, opts?: RpcOpts): Promise<void> {
return client.wshRpcCall("message", data, opts);
Expand Down Expand Up @@ -627,6 +632,11 @@ class RpcApiType {
return client.wshRpcCall("writeappfile", data, opts);
}

// command "writeappgofile" [call]
WriteAppGoFileCommand(client: WshClient, data: CommandWriteAppGoFileData, opts?: RpcOpts): Promise<CommandWriteAppGoFileRtnData> {
return client.wshRpcCall("writeappgofile", data, opts);
}

// command "writeappsecretbindings" [call]
WriteAppSecretBindingsCommand(client: WshClient, data: CommandWriteAppSecretBindingsData, opts?: RpcOpts): Promise<void> {
return client.wshRpcCall("writeappsecretbindings", data, opts);
Expand Down
129 changes: 72 additions & 57 deletions frontend/app/view/tsunami/tsunami.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
// SPDX-License-Identifier: Apache-2.0

import { BlockNodeModel } from "@/app/block/blocktypes";
import { atoms, globalStore, WOS } from "@/app/store/global";
import { atoms, getApi, globalStore, WOS } from "@/app/store/global";
import { waveEventSubscribe } from "@/app/store/wps";
import { RpcApi } from "@/app/store/wshclientapi";
import { TabRpcClient } from "@/app/store/wshrpcutil";
Expand All @@ -11,22 +11,18 @@ import * as services from "@/store/services";
import * as jotai from "jotai";
import { memo, useEffect } from "react";

interface TsunamiAppMeta {
title: string;
shortdesc: string;
}

class TsunamiViewModel extends WebViewModel {
shellProcFullStatus: jotai.PrimitiveAtom<BlockControllerRuntimeStatus>;
shellProcStatusUnsubFn: () => void;
appMeta: jotai.PrimitiveAtom<AppMeta>;
appMetaUnsubFn: () => void;
isRestarting: jotai.PrimitiveAtom<boolean>;
viewName: jotai.PrimitiveAtom<string>;
viewName: jotai.Atom<string>;
viewIconColor: jotai.Atom<string>;

constructor(blockId: string, nodeModel: BlockNodeModel) {
super(blockId, nodeModel);
this.viewType = "tsunami";
this.viewIcon = jotai.atom("cube");
this.viewName = jotai.atom("Tsunami");
this.isRestarting = jotai.atom(false);

// Hide navigation bar (URL bar, back/forward/home buttons)
Expand All @@ -48,6 +44,42 @@ class TsunamiViewModel extends WebViewModel {
this.updateShellProcStatus(bcRTS);
},
});

this.appMeta = jotai.atom(null) as jotai.PrimitiveAtom<AppMeta>;
this.viewIcon = jotai.atom((get) => {
const meta = get(this.appMeta);
return meta?.icon || "cube";
});
this.viewIconColor = jotai.atom((get) => {
const meta = get(this.appMeta);
return meta?.iconcolor;
});
this.viewName = jotai.atom((get) => {
const meta = get(this.appMeta);
return meta?.title || "WaveApp";
});
const initialRTInfo = RpcApi.GetRTInfoCommand(TabRpcClient, {
oref: WOS.makeORef("block", blockId),
});
initialRTInfo.then((rtInfo) => {
if (rtInfo) {
const meta: AppMeta = {
title: rtInfo["tsunami:title"],
shortdesc: rtInfo["tsunami:shortdesc"],
icon: rtInfo["tsunami:icon"],
iconcolor: rtInfo["tsunami:iconcolor"],
};
globalStore.set(this.appMeta, meta);
}
});
this.appMetaUnsubFn = waveEventSubscribe({
eventType: "tsunami:updatemeta",
scope: WOS.makeORef("block", blockId),
handler: (event) => {
const meta: AppMeta = event.data;
globalStore.set(this.appMeta, meta);
},
});
}

get viewComponent(): ViewComponent {
Expand Down Expand Up @@ -126,32 +158,31 @@ class TsunamiViewModel extends WebViewModel {
this.doControllerResync(true, "force restart");
}

setAppMeta(meta: TsunamiAppMeta) {
console.log("tsunami app meta:", meta);
async remixInBuilder() {
const blockData = globalStore.get(this.blockAtom);
const appId = blockData?.meta?.["tsunami:appid"];

const rtInfo: ObjRTInfo = {};
if (meta.title) {
rtInfo["tsunami:title"] = meta.title;
}
if (meta.shortdesc) {
rtInfo["tsunami:shortdesc"] = meta.shortdesc;
if (!appId || !appId.startsWith("local/")) {
return;
}

if (Object.keys(rtInfo).length > 0) {
const oref = WOS.makeORef("block", this.blockId);
const data: CommandSetRTInfoData = {
oref: oref,
data: rtInfo,
};
try {
const result = await RpcApi.MakeDraftFromLocalCommand(TabRpcClient, { localappid: appId });
const draftAppId = result.draftappid;

RpcApi.SetRTInfoCommand(TabRpcClient, data).catch((e) => console.log("error setting RT info", e));
getApi().openBuilder(draftAppId);
} catch (err) {
console.error("Failed to create draft from local app:", err);
}
}

dispose() {
if (this.shellProcStatusUnsubFn) {
this.shellProcStatusUnsubFn();
}
if (this.appMetaUnsubFn) {
this.appMetaUnsubFn();
}
}

getSettingsMenuItems(): ContextMenuItem[] {
Expand All @@ -167,6 +198,11 @@ class TsunamiViewModel extends WebViewModel {
);
});

// Check if we should show the Remix option
const blockData = globalStore.get(this.blockAtom);
const appId = blockData?.meta?.["tsunami:appid"];
const showRemixOption = appId && appId.startsWith("local/");

// Add tsunami-specific menu items at the beginning
const tsunamiItems: ContextMenuItem[] = [
{
Expand All @@ -186,6 +222,18 @@ class TsunamiViewModel extends WebViewModel {
},
];

if (showRemixOption) {
tsunamiItems.push(
{
label: "Remix WaveApp in Builder",
click: () => this.remixInBuilder(),
},
{
type: "separator",
}
);
}

return [...tsunamiItems, ...filteredItems];
}
}
Expand All @@ -201,39 +249,6 @@ const TsunamiView = memo((props: ViewComponentProps<TsunamiViewModel>) => {
model.resyncController();
}, [model]);

useEffect(() => {
if (!domReady || !model.webviewRef?.current) return;

const webviewElement = model.webviewRef.current;

const handleConsoleMessage = (e: any) => {
const message = e.message;
if (typeof message === "string" && message.startsWith("TSUNAMI_META ")) {
try {
const jsonStr = message.substring("TSUNAMI_META ".length);
const meta = JSON.parse(jsonStr);
if (meta.title || meta.shortdesc) {
model.setAppMeta(meta);

if (meta.title) {
const truncatedTitle =
meta.title.length > 77 ? meta.title.substring(0, 77) + "..." : meta.title;
globalStore.set(model.viewName, truncatedTitle);
}
}
} catch (error) {
console.error("Failed to parse TSUNAMI_META message:", error);
}
}
};

webviewElement.addEventListener("console-message", handleConsoleMessage);

return () => {
webviewElement.removeEventListener("console-message", handleConsoleMessage);
};
}, [domReady, model]);

const appPath = blockData?.meta?.["tsunami:apppath"];
const appId = blockData?.meta?.["tsunami:appid"];
const controller = blockData?.meta?.controller;
Expand Down
Loading
Loading