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
4 changes: 2 additions & 2 deletions Taskfile.yml
Original file line number Diff line number Diff line change
Expand Up @@ -539,7 +539,7 @@ tasks:
- mv scaffold/node_modules scaffold/nm
- cp -r dist scaffold/
- mkdir -p scaffold/dist/tw
- cp ../templates/app-main.go.tmpl scaffold/app-main.go
- cp ../templates/*.go.tmpl scaffold/
- cp ../templates/tailwind.css scaffold/
- cp ../templates/gitignore.tmpl scaffold/.gitignore
- cp src/element/*.tsx scaffold/dist/tw/
Expand All @@ -560,7 +560,7 @@ tasks:
- powershell Move-Item -Path scaffold/node_modules -Destination scaffold/nm
- powershell Copy-Item -Recurse -Force -Path dist -Destination scaffold/
- powershell New-Item -ItemType Directory -Force -Path scaffold/dist/tw
- powershell Copy-Item -Path ../templates/app-main.go.tmpl -Destination scaffold/app-main.go
- powershell Copy-Item -Path '../templates/*.go.tmpl' -Destination scaffold/
- powershell Copy-Item -Path ../templates/tailwind.css -Destination scaffold/
- powershell Copy-Item -Path ../templates/gitignore.tmpl -Destination scaffold/.gitignore
- powershell Copy-Item -Path 'src/element/*.tsx' -Destination scaffold/dist/tw/
Expand Down
11 changes: 10 additions & 1 deletion emain/emain-builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { ElectronWshClient } from "./emain-wsh";

export type BuilderWindowType = BrowserWindow & {
builderId: string;
builderAppId?: string;
savedInitOpts: BuilderInitOpts;
};

Expand All @@ -39,6 +40,14 @@ export async function createBuilderWindow(appId: string): Promise<BuilderWindowT
const clientId = clientData?.oid;
const windowId = randomUUID();

if (appId) {
const oref = `builder:${builderId}`;
await RpcApi.SetRTInfoCommand(ElectronWshClient, {
oref,
data: { "builder:appid": appId },
});
}

const winBounds = calculateWindowBounds(undefined, undefined, fullConfig.settings);

const builderWindow = new BrowserWindow({
Expand Down Expand Up @@ -71,11 +80,11 @@ export async function createBuilderWindow(appId: string): Promise<BuilderWindowT
builderId,
clientId,
windowId,
appId,
};

const typedBuilderWindow = builderWindow as BuilderWindowType;
typedBuilderWindow.builderId = builderId;
typedBuilderWindow.builderAppId = appId;
typedBuilderWindow.savedInitOpts = initOpts;

typedBuilderWindow.on("focus", () => {
Expand Down
10 changes: 6 additions & 4 deletions emain/emain-ipc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ export function openBuilderWindow(appId?: string) {
const normalizedAppId = appId || "";
const existingBuilderWindows = getAllBuilderWindows();
const existingWindow = existingBuilderWindows.find(
(win) => win.savedInitOpts?.appId === normalizedAppId
(win) => win.builderAppId === normalizedAppId
);
if (existingWindow) {
existingWindow.focus();
Expand Down Expand Up @@ -426,9 +426,7 @@ export function initIpcHandlers() {
if (bw == null) {
return;
}
if (bw.savedInitOpts) {
bw.savedInitOpts.appId = appId;
}
bw.builderAppId = appId;
console.log("set-builder-window-appid", bw.builderId, appId);
});

Expand All @@ -453,4 +451,8 @@ export function initIpcHandlers() {
}
bw.destroy();
});

electron.ipcMain.on("do-refresh", (event) => {
event.sender.reloadIgnoringCache();
});
}
1 change: 1 addition & 0 deletions emain/preload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ contextBridge.exposeInMainWorld("api", {
nativePaste: () => ipcRenderer.send("native-paste"),
openBuilder: (appId?: string) => ipcRenderer.send("open-builder", appId),
setBuilderWindowAppId: (appId: string) => ipcRenderer.send("set-builder-window-appid", appId),
doRefresh: () => ipcRenderer.send("do-refresh"),
});

// Custom event for "new-window"
Expand Down
72 changes: 39 additions & 33 deletions frontend/app/aipanel/thinkingmode.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ export const ThinkingLevelDropdown = memo(() => {
const dropdownRef = useRef<HTMLDivElement>(null);

const hasPremium = !rateLimitInfo || rateLimitInfo.unknown || rateLimitInfo.preq > 0;
const hideQuick = model.inBuilder && hasPremium;

const handleSelect = (mode: ThinkingMode) => {
const metadata = ThinkingModeData[mode];
Expand All @@ -60,6 +61,9 @@ export const ThinkingLevelDropdown = memo(() => {
if (!hasPremium && currentMetadata.premium) {
currentMode = "quick";
}
if (hideQuick && currentMode === "quick") {
currentMode = "balanced";
}

return (
<div className="relative" ref={dropdownRef}>
Expand All @@ -82,39 +86,41 @@ export const ThinkingLevelDropdown = memo(() => {
<>
<div className="fixed inset-0 z-40" onClick={() => setIsOpen(false)} />
<div className="absolute top-full left-0 mt-1 bg-gray-800 border border-gray-600 rounded shadow-lg z-50 min-w-[280px]">
{(Object.keys(ThinkingModeData) as ThinkingMode[]).map((mode, index) => {
const metadata = ThinkingModeData[mode];
const isFirst = index === 0;
const isLast = index === Object.keys(ThinkingModeData).length - 1;
const isDisabled = !hasPremium && metadata.premium;
const isSelected = currentMode === mode;
return (
<button
key={mode}
onClick={() => handleSelect(mode)}
disabled={isDisabled}
className={`w-full flex flex-col gap-0.5 px-3 ${
isFirst ? "pt-1 pb-0.5" : isLast ? "pt-0.5 pb-1" : "pt-0.5 pb-0.5"
} ${
isDisabled
? "text-gray-500 cursor-not-allowed"
: "text-gray-300 hover:bg-gray-700 cursor-pointer"
} transition-colors text-left`}
>
<div className="flex items-center gap-2 w-full">
<i className={`fa ${metadata.icon}`}></i>
<span className={`text-sm ${isSelected ? "font-bold" : ""}`}>
{metadata.name}
{isDisabled && " (premium)"}
</span>
{isSelected && <i className="fa fa-check ml-auto"></i>}
</div>
<div className="text-xs text-muted pl-5" style={{ whiteSpace: "pre-line" }}>
{metadata.desc}
</div>
</button>
);
})}
{(Object.keys(ThinkingModeData) as ThinkingMode[])
.filter((mode) => !(hideQuick && mode === "quick"))
.map((mode, index, filteredModes) => {
const metadata = ThinkingModeData[mode];
const isFirst = index === 0;
const isLast = index === filteredModes.length - 1;
const isDisabled = !hasPremium && metadata.premium;
const isSelected = currentMode === mode;
return (
<button
key={mode}
onClick={() => handleSelect(mode)}
disabled={isDisabled}
className={`w-full flex flex-col gap-0.5 px-3 ${
isFirst ? "pt-1 pb-0.5" : isLast ? "pt-0.5 pb-1" : "pt-0.5 pb-0.5"
} ${
isDisabled
? "text-gray-500 cursor-not-allowed"
: "text-gray-300 hover:bg-gray-700 cursor-pointer"
} transition-colors text-left`}
>
<div className="flex items-center gap-2 w-full">
<i className={`fa ${metadata.icon}`}></i>
<span className={`text-sm ${isSelected ? "font-bold" : ""}`}>
{metadata.name}
{isDisabled && " (premium)"}
</span>
{isSelected && <i className="fa fa-check ml-auto"></i>}
</div>
<div className="text-xs text-muted pl-5" style={{ whiteSpace: "pre-line" }}>
{metadata.desc}
</div>
</button>
);
})}
</div>
</>
)}
Expand Down
2 changes: 2 additions & 0 deletions frontend/app/modals/modalregistry.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { MessageModal } from "@/app/modals/messagemodal";
import { NewInstallOnboardingModal } from "@/app/onboarding/onboarding";
import { UpgradeOnboardingModal } from "@/app/onboarding/onboarding-upgrade";
import { DeleteFileModal, PublishAppModal, RenameFileModal } from "@/builder/builder-apppanel";
import { SetSecretDialog } from "@/builder/tabs/builder-secrettab";
import { AboutModal } from "./about";
import { UserInputModal } from "./userinputmodal";

Expand All @@ -17,6 +18,7 @@ const modalRegistry: { [key: string]: React.ComponentType<any> } = {
[PublishAppModal.displayName || "PublishAppModal"]: PublishAppModal,
[RenameFileModal.displayName || "RenameFileModal"]: RenameFileModal,
[DeleteFileModal.displayName || "DeleteFileModal"]: DeleteFileModal,
[SetSecretDialog.displayName || "SetSecretDialog"]: SetSecretDialog,
};

export const getModalComponent = (key: string): React.ComponentType<any> | undefined => {
Expand Down
5 changes: 5 additions & 0 deletions frontend/app/store/wshclientapi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -547,6 +547,11 @@ class RpcApiType {
return client.wshRpcCall("startbuilder", data, opts);
}

// command "stopbuilder" [call]
StopBuilderCommand(client: WshClient, data: string, opts?: RpcOpts): Promise<void> {
return client.wshRpcCall("stopbuilder", data, opts);
}

// command "streamcpudata" [responsestream]
StreamCpuDataCommand(client: WshClient, data: CpuDataRequest, opts?: RpcOpts): AsyncGenerator<TimeSeriesData, void, boolean> {
return client.wshRpcStream("streamcpudata", data, opts);
Expand Down
9 changes: 7 additions & 2 deletions frontend/builder/builder-app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import { AppSelectionModal } from "@/builder/app-selection-modal";
import { BuilderWorkspace } from "@/builder/builder-workspace";
import { ModalsRenderer } from "@/app/modals/modalsrenderer";
import { atoms, globalStore } from "@/store/global";
import { atoms, globalStore, isDev } from "@/store/global";
import { appHandleKeyDown } from "@/store/keymodel";
import * as keyutil from "@/util/keyutil";
import { isBlank } from "@/util/util";
Expand Down Expand Up @@ -38,9 +38,14 @@ function BuilderAppInner() {
<div className="w-full h-full flex flex-col bg-main-bg text-main-text">
<BuilderKeyHandlers />
<div
className="h-9 shrink-0 border-b border-b-border flex items-center justify-center"
className="h-9 shrink-0 border-b border-b-border flex items-center justify-center gap-2"
style={{ WebkitAppRegion: "drag" } as React.CSSProperties}
>
{isDev() ? (
<div className="text-accent text-xl" title="Running Wave Dev Build">
<i className="fa fa-brands fa-dev fa-fw" />
</div>
) : null}
<div className="text-sm font-medium">
WaveApp Builder{!isBlank(builderAppId) && ` (${builderAppId})`}
</div>
Expand Down
71 changes: 43 additions & 28 deletions frontend/builder/builder-apppanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,16 @@
// SPDX-License-Identifier: Apache-2.0

import { Modal } from "@/app/modals/modal";
import { ContextMenuModel } from "@/app/store/contextmenu";
import { modalsModel } from "@/app/store/modalmodel";
Comment on lines +5 to 6
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 | 🟠 Major

ContextMenuItem type likely needs an explicit import

handleKebabClick declares const menu: ContextMenuItem[] = [...], but this file only imports ContextMenuModel from "@/app/store/contextmenu". Unless ContextMenuItem is declared globally, this will fail type‑checking; consider importing it as a type from the same module, e.g.:

-import { ContextMenuModel } from "@/app/store/contextmenu";
+import { ContextMenuModel, type ContextMenuItem } from "@/app/store/contextmenu";

Also applies to: 259-277

🤖 Prompt for AI Agents
In frontend/builder/builder-apppanel.tsx around lines 5-6 (and also where menu
is created around 259-277), the code uses ContextMenuItem[] but only imports
ContextMenuModel, causing TypeScript to fail type-checking; add an explicit
import for the ContextMenuItem type from "@/app/store/contextmenu" (e.g., import
type { ContextMenuItem } from "@/app/store/contextmenu") and update any other
usages to reference that imported type so the menu declaration and related code
compile correctly.

import { RpcApi } from "@/app/store/wshclientapi";
import { TabRpcClient } from "@/app/store/wshrpcutil";
import { BuilderAppPanelModel, type TabType } from "@/builder/store/builder-apppanel-model";
import { BuilderFocusManager } from "@/builder/store/builder-focusmanager";
import { BuilderCodeTab } from "@/builder/tabs/builder-codetab";
import { BuilderEnvTab } from "@/builder/tabs/builder-envtab";
import { BuilderFilesTab, DeleteFileModal, RenameFileModal } from "@/builder/tabs/builder-filestab";
import { BuilderPreviewTab } from "@/builder/tabs/builder-previewtab";
import { BuilderEnvTab } from "@/builder/tabs/builder-secrettab";
import { builderAppHasSelection } from "@/builder/utils/builder-focus-utils";
import { ErrorBoundary } from "@/element/errorboundary";
import { atoms } from "@/store/global";
Expand Down Expand Up @@ -194,9 +195,9 @@ const BuilderAppPanel = memo(() => {
const activeTab = useAtomValue(model.activeTab);
const focusType = useAtomValue(BuilderFocusManager.getInstance().focusType);
const isAppFocused = focusType === "app";
const envSaveNeeded = useAtomValue(model.envVarsDirtyAtom);
const builderAppId = useAtomValue(atoms.builderAppId);
const builderId = useAtomValue(atoms.builderId);
const hasSecrets = useAtomValue(model.hasSecretsAtom);

useEffect(() => {
model.initialize();
Expand Down Expand Up @@ -241,12 +242,6 @@ const BuilderAppPanel = memo(() => {
[model]
);

const handleEnvSave = useCallback(() => {
if (builderId) {
model.saveEnvVars(builderId);
}
}, [builderId, model]);

const handleRestart = useCallback(() => {
model.restartBuilder();
}, [model]);
Expand All @@ -257,6 +252,30 @@ const BuilderAppPanel = memo(() => {
modalsModel.pushModal("PublishAppModal", { appName });
}, [builderAppId]);

const handleSwitchAppClick = useCallback(() => {
model.switchBuilderApp();
}, [model]);

const handleKebabClick = useCallback(
(e: React.MouseEvent) => {
const menu: ContextMenuItem[] = [
{
label: "Publish App",
click: handlePublishClick,
},
{
type: "separator",
},
{
label: "Switch App",
click: handleSwitchAppClick,
},
];
ContextMenuModel.showContextMenu(menu, e);
},
[handleSwitchAppClick, handlePublishClick]
);

return (
<div
className="w-full h-full flex flex-col border-b-3 border-border shadow-[0_2px_4px_rgba(0,0,0,0.1)]"
Expand Down Expand Up @@ -298,35 +317,31 @@ const BuilderAppPanel = memo(() => {
isAppFocused={isAppFocused}
onClick={() => handleTabClick("files")}
/>
<TabButton
label="Env"
tabType="env"
isActive={activeTab === "env"}
isAppFocused={isAppFocused}
onClick={() => handleTabClick("env")}
/>
{hasSecrets && (
<TabButton
label="Secrets"
tabType="secrets"
isActive={activeTab === "secrets"}
isAppFocused={isAppFocused}
onClick={() => handleTabClick("secrets")}
/>
)}
</div>
<div className="flex items-center gap-2 mr-4">
<div className="flex items-center gap-2 mr-2">
<button
className="px-3 py-1 text-sm font-medium rounded bg-accent/80 text-primary hover:bg-accent transition-colors cursor-pointer"
onClick={handlePublishClick}
>
Publish App
</button>
</div>
{activeTab === "env" && (
<button
className={cn(
"mr-4 px-3 py-1 text-sm font-medium rounded transition-colors",
envSaveNeeded
? "bg-accent text-white hover:opacity-80 cursor-pointer"
: "bg-gray-600 text-gray-400 cursor-default"
)}
onClick={envSaveNeeded ? handleEnvSave : undefined}
className="px-2 py-1 text-sm font-medium rounded hover:bg-secondary/10 transition-colors cursor-pointer"
onClick={handleKebabClick}
aria-label="More options"
>
Save
<i className="fa fa-ellipsis-vertical" />
</button>
)}
</div>
</div>
</div>
<ErrorStrip />
Expand All @@ -346,7 +361,7 @@ const BuilderAppPanel = memo(() => {
<BuilderFilesTab />
</ErrorBoundary>
</div>
<div className="w-full h-full" style={{ display: activeTab === "env" ? "block" : "none" }}>
<div className="w-full h-full" style={{ display: activeTab === "secrets" ? "block" : "none" }}>
<ErrorBoundary>
<BuilderEnvTab />
</ErrorBoundary>
Expand Down
2 changes: 1 addition & 1 deletion frontend/builder/builder-buildpanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ const BuilderBuildPanel = memo(() => {
const filteredLines = showDebug ? outputLines : outputLines.filter((line) => !line.startsWith("[debug]") && line.trim().length > 0);

return (
<div className="w-full h-full flex flex-col bg-black">
<div className="w-full h-full flex flex-col bg-black rounded-br-2">
<div className="flex-shrink-0 px-3 py-2 border-b border-gray-700 flex items-center justify-between">
<span className="text-sm font-semibold text-gray-300">Build Output</span>
<div className="flex items-center gap-4">
Expand Down
Loading
Loading