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
16 changes: 15 additions & 1 deletion emain/emain-builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import { ClientService } from "@/app/store/services";
import { RpcApi } from "@/app/store/wshclientapi";
import { randomUUID } from "crypto";
import { BrowserWindow } from "electron";
import { BrowserWindow, webContents } from "electron";
import { globalEvents } from "emain/emain-events";
import path from "path";
import { getElectronAppBasePath, isDevVite, unamePlatform } from "./emain-platform";
Expand Down Expand Up @@ -87,6 +87,20 @@ export async function createBuilderWindow(appId: string): Promise<BuilderWindowT
typedBuilderWindow.builderAppId = appId;
typedBuilderWindow.savedInitOpts = initOpts;

typedBuilderWindow.on("close", () => {
const wc = typedBuilderWindow.webContents;
if (wc.isDevToolsOpened()) {
wc.closeDevTools();
}
for (const guest of webContents.getAllWebContents()) {
if (guest.getType() === "webview" && guest.hostWebContents?.id === wc.id) {
if (guest.isDevToolsOpened()) {
guest.closeDevTools();
}
}
}
});

typedBuilderWindow.on("focus", () => {
focusedBuilderWindow = typedBuilderWindow;
console.log("builder window focused", builderId);
Expand Down
11 changes: 11 additions & 0 deletions emain/emain-ipc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -490,6 +490,17 @@ export function initIpcHandlers() {
console.error("Error deleting builder rtinfo:", e);
}
}
const wc = bw.webContents;
if (wc.isDevToolsOpened()) {
wc.closeDevTools();
}
for (const guest of electron.webContents.getAllWebContents()) {
if (guest.getType() === "webview" && guest.hostWebContents?.id === wc.id) {
if (guest.isDevToolsOpened()) {
guest.closeDevTools();
}
}
}
bw.destroy();
});

Expand Down
21 changes: 20 additions & 1 deletion emain/emain-window.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { ClientService, ObjectService, WindowService, WorkspaceService } from "@
import { waveEventSubscribeSingle } from "@/app/store/wps";
import { RpcApi } from "@/app/store/wshclientapi";
import { fireAndForget } from "@/util/util";
import { BaseWindow, BaseWindowConstructorOptions, dialog, globalShortcut, ipcMain, screen } from "electron";
import { BaseWindow, BaseWindowConstructorOptions, dialog, globalShortcut, ipcMain, screen, webContents } from "electron";
import { globalEvents } from "emain/emain-events";
import path from "path";
import { debounce } from "throttle-debounce";
Expand Down Expand Up @@ -299,6 +299,7 @@ export class WaveBrowserWindow extends BaseWindow {
if (this.isDestroyed()) {
return;
}
this.closeAllDevTools();
console.log("win 'close' handler fired", this.waveWindowId);
if (getGlobalIsQuitting() || updater?.status == "installing" || getGlobalIsRelaunching()) {
return;
Expand Down Expand Up @@ -358,6 +359,24 @@ export class WaveBrowserWindow extends BaseWindow {
setTimeout(() => globalEvents.emit("windows-updated"), 50);
}

private closeAllDevTools() {
for (const tabView of this.allLoadedTabViews.values()) {
if (tabView.webContents?.isDevToolsOpened()) {
tabView.webContents.closeDevTools();
}
}
const tabViewIds = new Set(
[...this.allLoadedTabViews.values()].map((tv) => tv.webContents?.id).filter((id) => id != null)
);
for (const wc of webContents.getAllWebContents()) {
if (wc.getType() === "webview" && tabViewIds.has(wc.hostWebContents?.id)) {
if (wc.isDevToolsOpened()) {
wc.closeDevTools();
}
}
}
}

private removeAllChildViews() {
for (const tabView of this.allLoadedTabViews.values()) {
if (!this.isDestroyed()) {
Expand Down
29 changes: 27 additions & 2 deletions frontend/app/aipanel/waveai-model.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,27 @@ export interface DroppedFile {
previewUrl?: string;
}

const BuilderAIModeConfigs: Record<string, AIModeConfigType> = {
"waveaibuilder@default": {
"display:name": "Builder Default",
"display:order": -2,
"display:icon": "sparkles",
"display:description": "Good mix of speed and accuracy\n(gpt-5.4 with minimal thinking)",
"ai:provider": "wave",
"ai:switchcompat": ["wavecloud"],
"waveai:premium": true,
},
"waveaibuilder@deep": {
"display:name": "Builder Deep",
"display:order": -1,
"display:icon": "lightbulb",
"display:description": "Slower but most capable\n(gpt-5.4 with full reasoning)",
"ai:provider": "wave",
"ai:switchcompat": ["wavecloud"],
"waveai:premium": true,
},
};

export class WaveAIModel {
private static instance: WaveAIModel | null = null;
inputRef: React.RefObject<AIPanelInputRef> | null = null;
Expand Down Expand Up @@ -80,7 +101,11 @@ export class WaveAIModel {
this.orefContext = orefContext;
this.inBuilder = inBuilder;
this.chatId = jotai.atom(null) as jotai.PrimitiveAtom<string>;
this.aiModeConfigs = atoms.waveaiModeConfigAtom;
if (inBuilder) {
this.aiModeConfigs = jotai.atom(BuilderAIModeConfigs) as jotai.Atom<Record<string, AIModeConfigType>>;
} else {
this.aiModeConfigs = atoms.waveaiModeConfigAtom;
}

this.hasPremiumAtom = jotai.atom((get) => {
const rateLimitInfo = get(atoms.waveAIRateLimitInfoAtom);
Expand Down Expand Up @@ -118,7 +143,7 @@ export class WaveAIModel {
this.defaultModeAtom = jotai.atom((get) => {
const telemetryEnabled = get(getSettingsKeyAtom("telemetry:enabled")) ?? false;
if (this.inBuilder) {
return telemetryEnabled ? "waveai@balanced" : "invalid";
return telemetryEnabled ? "waveaibuilder@default" : "invalid";
}
Comment on lines 143 to 147
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

git ls-files | head -20

Repository: wavetermdev/waveterm

Length of output: 662


🏁 Script executed:

find frontend/app/aipanel -name "*.tsx" -o -name "*.ts" | head -20

Repository: wavetermdev/waveterm

Length of output: 760


🏁 Script executed:

cat -n frontend/app/aipanel/waveai-model.tsx | head -160 | tail -50

Repository: wavetermdev/waveterm

Length of output: 2515


🏁 Script executed:

wc -l frontend/app/aipanel/waveai-model.tsx

Repository: wavetermdev/waveterm

Length of output: 105


🏁 Script executed:

rg -n "isValidMode|BuilderAIModeConfigs" frontend/app/aipanel/ -A 3

Repository: wavetermdev/waveterm

Length of output: 2113


🏁 Script executed:

rg -n "const.*isValidMode|function.*isValidMode|export.*isValidMode" frontend/ -A 5

Repository: wavetermdev/waveterm

Length of output: 46


🏁 Script executed:

cat -n frontend/app/aipanel/aimode.tsx | head -100

Repository: wavetermdev/waveterm

Length of output: 4536


🏁 Script executed:

cat -n frontend/app/aipanel/waveai-model.tsx | sed -n '44,55p'

Repository: wavetermdev/waveterm

Length of output: 629


🏁 Script executed:

cat -n frontend/app/aipanel/waveai-model.tsx | sed -n '421,435p'

Repository: wavetermdev/waveterm

Length of output: 636


🏁 Script executed:

rg -n "return.*isValidMode|const.*modes" frontend/app/aipanel/waveai-model.tsx -A 2 -B 2

Repository: wavetermdev/waveterm

Length of output: 46


🏁 Script executed:

cat -n frontend/app/aipanel/waveai-model.tsx | sed -n '100,110p'

Repository: wavetermdev/waveterm

Length of output: 633


🏁 Script executed:

rg -n "defaultModeAtom" frontend/app/aipanel/waveai-model.tsx

Repository: wavetermdev/waveterm

Length of output: 382


🏁 Script executed:

rg -n "setAIModeToDefault|currentAIMode" frontend/app/aipanel/waveai-model.tsx -A 3 | head -50

Repository: wavetermdev/waveterm

Length of output: 1353


🏁 Script executed:

cat -n frontend/app/aipanel/waveai-model.tsx | sed -n '165,175p'

Repository: wavetermdev/waveterm

Length of output: 413


🏁 Script executed:

rg -n "telemetryrequired|TelemetryRequired" frontend/app/aipanel/ -i -A 2

Repository: wavetermdev/waveterm

Length of output: 1232


🏁 Script executed:

cat -n frontend/app/aipanel/telemetryrequired.tsx | head -50

Repository: wavetermdev/waveterm

Length of output: 2718


🏁 Script executed:

cat -n frontend/app/aipanel/aipanel.tsx | sed -n '580,610p'

Repository: wavetermdev/waveterm

Length of output: 1736


🏁 Script executed:

cat -n frontend/app/aipanel/aipanel.tsx | sed -n '1,50p'

Repository: wavetermdev/waveterm

Length of output: 2760


🏁 Script executed:

rg -n "telemetry.*builder|builder.*telemetry" frontend/app/aipanel/ -i -B 2 -A 2

Repository: wavetermdev/waveterm

Length of output: 551


🏁 Script executed:

rg -n "allowAccess" frontend/app/aipanel/aipanel.tsx -B 5 -A 2 | head -60

Repository: wavetermdev/waveterm

Length of output: 1886


🏁 Script executed:

cat -n frontend/app/aipanel/waveai-model.tsx | sed -n '460,500p'

Repository: wavetermdev/waveterm

Length of output: 1814


🏁 Script executed:

rg -n "SetRTInfoCommand|req\.AIMode|invalid" frontend/app/aipanel/waveai-model.tsx -B 2 -A 2

Repository: wavetermdev/waveterm

Length of output: 1330


🏁 Script executed:

rg -n "setMessage.*mode|mode.*backend|AIMode" frontend/app/aipanel/ -A 3 | head -80

Repository: wavetermdev/waveterm

Length of output: 5799


🏁 Script executed:

rg -n "currentAIMode|useChat|AIMode" frontend/app/aipanel/aipanel.tsx -B 2 -A 2 | head -100

Repository: wavetermdev/waveterm

Length of output: 2451


🏁 Script executed:

rg -n "getUseChatEndpointUrl|reloadChatFromBackend" frontend/app/aipanel/waveai-model.tsx -A 10 | head -80

Repository: wavetermdev/waveterm

Length of output: 1806


🏁 Script executed:

cat -n frontend/app/aipanel/waveai-model.tsx | sed -n '275,290p'

Repository: wavetermdev/waveterm

Length of output: 690


🏁 Script executed:

rg -n "unknown" frontend/app/aipanel/waveai-model.tsx -B 2 -A 2

Repository: wavetermdev/waveterm

Length of output: 552


🏁 Script executed:

find . -name "*.go" -o -name "*.ts" | xargs grep -l "invalid.*AI.*mode|AIMode.*invalid" 2>/dev/null | head -5

Repository: wavetermdev/waveterm

Length of output: 46


🏁 Script executed:

rg -n "aimode" frontend/app/aipanel/aipanel.tsx -B 3 -A 3

Repository: wavetermdev/waveterm

Length of output: 837


🏁 Script executed:

find . -path ./node_modules -prune -o -name "*.go" -type f -exec grep -l "invalid.*mode\|AIMode" {} \; | head -10

Repository: wavetermdev/waveterm

Length of output: 815


🏁 Script executed:

rg -n "post-chat-message" . --type go -A 20 | head -100

Repository: wavetermdev/waveterm

Length of output: 1658


🏁 Script executed:

rg -n "WaveAIPostMessageHandler" pkg/aiusechat/ -A 50 | head -150

Repository: wavetermdev/waveterm

Length of output: 3101


🏁 Script executed:

rg -n "aimode\|AIMode" pkg/aiusechat/usechat.go -B 3 -A 3 | head -100

Repository: wavetermdev/waveterm

Length of output: 46


🏁 Script executed:

rg -n "getWaveAISettings" pkg/aiusechat/ -A 30 | head -100

Repository: wavetermdev/waveterm

Length of output: 4099


🏁 Script executed:

rg -n "invalid.*mode|AIMode.*invalid" pkg/aiusechat/ -B 3 -A 3

Repository: wavetermdev/waveterm

Length of output: 501


🏁 Script executed:

cat -n pkg/aiusechat/usechat-mode.go | sed -n '280,295p'

Repository: wavetermdev/waveterm

Length of output: 605


🏁 Script executed:

rg -n "resolveAIMode" pkg/aiusechat/usechat-mode.go -B 5 -A 20 | head -80

Repository: wavetermdev/waveterm

Length of output: 788


Return "unknown" instead of hard-coded "invalid" for telemetry-disabled builder mode, and consider an explicit guard in setAIModeToDefault().

When inBuilder && !telemetryEnabled, defaultModeAtom returns "invalid" (line 146). While the frontend UI is blocked via allowAccess = false (line 274) and shows TelemetryRequiredMessage, the hard-coded sentinel creates two issues:

  1. Inconsistency: Non-builder telemetry-disabled mode returns "unknown" (line 152), but builder returns "invalid". If any code path bypasses the UI block or if a race condition occurs, isValidMode("invalid") returns false and the backend rejects with "invalid AI mode: invalid" (pkg/aiusechat/usechat-mode.go:291).

  2. Circular fallback logic: setAIModeToDefault() (line 447) calls globalStore.get(this.defaultModeAtom), which can return "invalid" in the builder case. If called, this creates a scenario where the default itself is invalid.

Align with the non-builder pattern by returning "unknown" instead, and consider adding an explicit guard in setAIModeToDefault() to prevent propagating an invalid default.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/app/aipanel/waveai-model.tsx` around lines 143 - 147, Change the
sentinel returned for the builder+telemetry-disabled case in defaultModeAtom
from "invalid" to "unknown" so it matches the non-builder path (update the
branch inside this.defaultModeAtom where it currently returns "invalid"). Also
add an explicit guard inside setAIModeToDefault() to validate the value from
globalStore.get(this.defaultModeAtom) (use isValidMode) and, if invalid,
fallback to "unknown" (or another safe default) instead of propagating an
invalid mode to downstream code.

const aiModeConfigs = get(this.aiModeConfigs);
if (!telemetryEnabled) {
Expand Down
13 changes: 11 additions & 2 deletions frontend/app/view/webview/webview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,9 @@ import { openLink } from "@/store/global";
import { adaptFromReactOrNativeKeyEvent, checkKeyPressed } from "@/util/keyutil";
import { fireAndForget, useAtomValueSafe } from "@/util/util";
import clsx from "clsx";
import { WebviewTag } from "electron";
import type { WebviewTag } from "electron";
import { Atom, PrimitiveAtom, atom, useAtomValue, useSetAtom } from "jotai";
import { Fragment, createRef, memo, useCallback, useEffect, useRef, useState } from "react";
import { Fragment, createRef, memo, useCallback, useEffect, useLayoutEffect, useRef, useState } from "react";
import "./webview.scss";
import type { WebViewEnv } from "./webviewenv";

Expand Down Expand Up @@ -951,6 +951,15 @@ const WebView = memo(({ model, onFailLoad, blockRef, initialSrc }: WebViewProps)
}, 100);
}

useLayoutEffect(() => {
return () => {
const webview = model.webviewRef.current;
if (webview?.isDevToolsOpened()) {
webview.closeDevTools();
}
};
}, []);

useEffect(() => {
return () => {
globalStore.set(model.domReady, false);
Expand Down
13 changes: 12 additions & 1 deletion frontend/builder/builder-apppanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,10 @@ const BuilderAppPanel = memo(() => {
model.switchBuilderApp();
}, [model]);

const handleOpenDevToolsClick = useCallback(() => {
model.openPreviewDevTools();
}, [model]);

const handleKebabClick = useCallback(
(e: React.MouseEvent) => {
const menu: ContextMenuItem[] = [
Expand All @@ -267,14 +271,21 @@ const BuilderAppPanel = memo(() => {
{
type: "separator",
},
{
label: "Open DevTools",
click: handleOpenDevToolsClick,
},
{
type: "separator",
},
{
label: "Switch App",
click: handleSwitchAppClick,
},
];
ContextMenuModel.getInstance().showContextMenu(menu, e);
},
[handleSwitchAppClick, handlePublishClick]
[handleSwitchAppClick, handlePublishClick, handleOpenDevToolsClick]
);

return (
Expand Down
11 changes: 11 additions & 0 deletions frontend/builder/store/builder-apppanel-model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { RpcApi } from "@/app/store/wshclientapi";
import { TabRpcClient } from "@/app/store/wshrpcutil";
import { atoms, getApi, WOS } from "@/store/global";
import { base64ToString, stringToBase64 } from "@/util/util";
import type { WebviewTag } from "electron";
import { atom, type Atom, type PrimitiveAtom } from "jotai";
import type * as MonacoTypes from "monaco-editor";
import { debounce } from "throttle-debounce";
Expand Down Expand Up @@ -35,6 +36,7 @@ export class BuilderAppPanelModel {
saveNeededAtom!: Atom<boolean>;
focusElemRef: { current: HTMLInputElement | null } = { current: null };
monacoEditorRef: { current: MonacoTypes.editor.IStandaloneCodeEditor | null } = { current: null };
webviewRef: { current: WebviewTag | null } = { current: null };
statusUnsubFn: (() => void) | null = null;
appGoUpdateUnsubFn: (() => void) | null = null;
debouncedRestart: (() => void) & { cancel: () => void };
Expand Down Expand Up @@ -314,6 +316,15 @@ export class BuilderAppPanelModel {
this.monacoEditorRef.current = ref;
}

openPreviewDevTools() {
if (!this.webviewRef.current) return;
if (this.webviewRef.current.isDevToolsOpened()) {
this.webviewRef.current.closeDevTools();
} else {
this.webviewRef.current.openDevTools();
}
}

dispose() {
if (this.statusUnsubFn) {
this.statusUnsubFn();
Expand Down
71 changes: 36 additions & 35 deletions frontend/builder/tabs/builder-previewtab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -70,8 +70,8 @@ const ErrorStateView = memo(({ errorMsg }: { errorMsg: string }) => {
<div className="flex flex-col gap-3">
<h2 className="text-2xl font-semibold text-error">Secrets Required</h2>
<p className="text-base text-secondary leading-relaxed">
This app requires secrets that must be configured. Please use the Secrets tab to set and bind
the required secrets for your app to run.
This app requires secrets that must be configured. Please use the Secrets tab to set and
bind the required secrets for your app to run.
</p>
<div className="text-left bg-panel border border-error/30 rounded-lg p-4 max-h-96 overflow-auto mt-2">
<pre className="text-sm text-secondary whitespace-pre-wrap font-mono">{displayMsg}</pre>
Expand Down Expand Up @@ -178,47 +178,48 @@ const BuilderPreviewTab = memo(() => {
const originalContent = useAtomValue(model.originalContentAtom);
const builderStatus = useAtomValue(model.builderStatusAtom);
const builderId = useAtomValue(atoms.builderId);

const fileExists = originalContent.length > 0;

if (isLoading) {
return null;
}

if (builderStatus?.status === "error") {
return <ErrorStateView errorMsg={builderStatus?.errormsg || ""} />;
}

if (!fileExists) {
return <EmptyStateView />;
}
const [lastKnownUrl, setLastKnownUrl] = useState<string>(null);

const status = builderStatus?.status || "init";
const isWebViewActive = status === "running" && builderStatus?.port && builderStatus.port !== 0;

if (status === "init") {
return null;
}

if (status === "building") {
return <BuildingStateView />;
}

if (status === "stopped") {
return <StoppedStateView onStart={() => model.startBuilder()} />;
if (isWebViewActive) {
const previewUrl = `http://localhost:${builderStatus.port}/?clientid=wave:${builderId}`;
if (previewUrl !== lastKnownUrl) {
setLastKnownUrl(previewUrl);
}
}

const shouldShowWebView = status === "running" && builderStatus?.port && builderStatus.port !== 0;

if (shouldShowWebView) {
const previewUrl = `http://localhost:${builderStatus.port}/?clientid=wave:${builderId}`;
return (
<div className="w-full h-full">
<webview src={previewUrl} className="w-full h-full" />
</div>
);
let overlay = null;
if (!isLoading && !isWebViewActive) {
if (builderStatus?.status === "error") {
overlay = <ErrorStateView errorMsg={builderStatus?.errormsg || ""} />;
} else if (!fileExists || status === "init") {
overlay = <EmptyStateView />;
} else if (status === "building") {
overlay = <BuildingStateView />;
} else if (status === "stopped") {
overlay = <StoppedStateView onStart={() => model.startBuilder()} />;
}
}

return null;
return (
<div className="w-full h-full relative">
{lastKnownUrl && (
<webview
ref={model.webviewRef}
src={lastKnownUrl}
className="w-full h-full"
style={{
visibility: isWebViewActive ? "visible" : "hidden",
pointerEvents: isWebViewActive ? "auto" : "none",
}}
/>
)}
{overlay && <div className="absolute inset-0">{overlay}</div>}
</div>
);
});

BuilderPreviewTab.displayName = "BuilderPreviewTab";
Expand Down
8 changes: 5 additions & 3 deletions pkg/aiusechat/uctypes/uctypes.go
Original file line number Diff line number Diff line change
Expand Up @@ -163,9 +163,11 @@ const (
)

const (
AIModeQuick = "waveai@quick"
AIModeBalanced = "waveai@balanced"
AIModeDeep = "waveai@deep"
AIModeQuick = "waveai@quick"
AIModeBalanced = "waveai@balanced"
AIModeDeep = "waveai@deep"
AIModeBuilderDefault = "waveaibuilder@default"
AIModeBuilderDeep = "waveaibuilder@deep"
)

const (
Expand Down
Loading
Loading