Skip to content

tsunami / builder updates (jsfuncs, devtools, gpt-5.4, etc)#3226

Merged
sawka merged 6 commits intomainfrom
sawka/tsunami-jsfuncs
Apr 17, 2026
Merged

tsunami / builder updates (jsfuncs, devtools, gpt-5.4, etc)#3226
sawka merged 6 commits intomainfrom
sawka/tsunami-jsfuncs

Conversation

@sawka
Copy link
Copy Markdown
Member

@sawka sawka commented Apr 17, 2026

lots of updates for tsunami and builder window:

  • jsfuncs
  • devtools windows
  • devtools proper cleanup (fixes crashes)
  • scrollbar fixes
  • lock AI models -- gpt-5.4, builder prompts, etc

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 17, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 9ebb41c9-e472-402e-80bc-dc187ef81255

📥 Commits

Reviewing files that changed from the base of the PR and between 9dce6c5 and d63a1d7.

📒 Files selected for processing (1)
  • tsconfig.json
💤 Files with no reviewable changes (1)
  • tsconfig.json

Walkthrough

Main process and BrowserWindow handlers now close DevTools for the builder window and any embedded webview guests on close. IPC handler for "close-builder-window" also closes DevTools before destroying the window. Frontend builder adds a webviewRef, an Open DevTools kebab menu action, openPreviewDevTools(), and a persistent preview webview with lastKnownUrl plus overlay states. New builder-specific AI modes (waveaibuilder@default, waveaibuilder@deep) added in JS and Go, and builder requests force premium behavior. VDOM gains preventbackend/jscode with client-side eval support and new Go helpers JSFunc/CombinedFunc. Key parsing and event adapters were hardened; minor layout/type edits applied.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 22.22% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title comprehensively covers the main changes: tsunami updates, builder improvements, jsfuncs, devtools handling, and AI model locking.
Description check ✅ Passed The description directly relates to the changeset, listing specific improvements including jsfuncs, devtools cleanup, scrollbar fixes, and AI model locking.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch sawka/tsunami-jsfuncs

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@cloudflare-workers-and-pages
Copy link
Copy Markdown

cloudflare-workers-and-pages Bot commented Apr 17, 2026

Deploying waveterm with  Cloudflare Pages  Cloudflare Pages

Latest commit: d63a1d7
Status:⚡️  Build in progress...

View logs

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
emain/emain-window.ts (1)

295-332: ⚠️ Potential issue | 🟡 Minor

DevTools are closed even when the user cancels the close confirmation.

closeAllDevTools() is invoked unconditionally at the top of the close handler, before the window:confirmclose dialog runs inside fireAndForget. If the user clicks "Cancel" in the unsaved-tabs confirmation dialog, the window stays open but all of their DevTools instances have already been dismissed and their inspector state/breakpoints/console history is lost. Consider deferring this.closeAllDevTools() until after the confirmation resolves and this.canClose = true is about to be set (i.e., right before the final this.close()), or to the "closed" handler if the timing still works for avoiding the underlying crash.

Proposed restructuring
         this.on("close", (e) => {
             if (this.canClose) {
                 return;
             }
             if (this.isDestroyed()) {
                 return;
             }
-            this.closeAllDevTools();
             console.log("win 'close' handler fired", this.waveWindowId);
             if (getGlobalIsQuitting() || updater?.status == "installing" || getGlobalIsRelaunching()) {
+                this.closeAllDevTools();
                 return;
             }
             e.preventDefault();
             fireAndForget(async () => {
                 const numWindows = waveWindowMap.size;
                 const fullConfig = await RpcApi.GetFullConfigCommand(ElectronWshClient);
                 if (numWindows > 1 || !fullConfig.settings["window:savelastwindow"]) {
                     if (fullConfig.settings["window:confirmclose"]) {
                         const workspace = await WorkspaceService.GetWorkspace(this.workspaceId);
                         if (isNonEmptyUnsavedWorkspace(workspace)) {
                             const choice = dialog.showMessageBoxSync(this, { ... });
                             if (choice === 0) {
                                 return;
                             }
                         }
                     }
                     this.deleteAllowed = true;
                 }
+                this.closeAllDevTools();
                 this.canClose = true;
                 this.close();
             });
         });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@emain/emain-window.ts` around lines 295 - 332, The handler currently calls
this.closeAllDevTools() unconditionally at the top which dismisses DevTools even
when the user cancels the confirmation; move the this.closeAllDevTools() call
out of the start of the "close" handler and invoke it only after the
confirmation has resolved and just before calling this.close() (i.e., inside the
fireAndForget flow immediately before setting this.canClose = true and
this.close()), or alternatively invoke it from the "closed" handler — adjust
references to this.canClose, this.deleteAllowed, dialog.showMessageBoxSync,
isNonEmptyUnsavedWorkspace and fireAndForget accordingly so DevTools are only
closed when the window is actually going to be closed.
🧹 Nitpick comments (5)
frontend/app/aipanel/waveai-model.tsx (1)

44-63: Frontend BuilderAIModeConfigs duplicates backend metadata — keep in sync.

This hard-coded map mirrors builderModeConfigs defined in pkg/aiusechat/usechat-mode.go (display name/order/icon/description, provider, switchcompat, premium). Any future change to the backend builder mode metadata (e.g., description, adding a third mode, renaming) will need to be mirrored here manually, otherwise builder UI and runtime will drift. Consider either:

  • Sourcing this via the same config broadcast mechanism (atoms.waveaiModeConfigAtom) by having the backend also publish builder modes, or
  • Adding a small comment pointing at pkg/aiusechat/usechat-mode.go so future edits are paired.
🤖 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 44 - 63,
BuilderAIModeConfigs is a hard-coded duplicate of backend builderModeConfigs
which will drift; fix by replacing the static map with a single source of truth
(subscribe to atoms.waveaiModeConfigAtom or equivalent published config from the
backend) or at minimum add a comment referencing the backend builderModeConfigs
location so maintainers update both; update the code that currently exports/uses
BuilderAIModeConfigs to read from atoms.waveaiModeConfigAtom (or call into the
published config provider) and remove/replace the hard-coded entries
("waveaibuilder@default", "waveaibuilder@deep") so UI derives display:name,
display:order, display:icon, display:description, ai:provider, ai:switchcompat,
and waveai:premium from the backend-published config.
frontend/app/view/webview/webview.tsx (1)

954-961: LGTM — but useEffect would suffice.

Cleanup doesn't read layout, so useEffect is semantically equivalent here and avoids blocking paint. Not a correctness issue; useLayoutEffect's cleanup runs at the same point during unmount as useEffect's.

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

In `@frontend/app/view/webview/webview.tsx` around lines 954 - 961, Replace
useLayoutEffect with useEffect for the devtools cleanup: in the component where
useLayoutEffect(() => { return () => { const webview = model.webviewRef.current;
if (webview?.isDevToolsOpened()) { webview.closeDevTools(); } }; }, []); is
used, change the hook to useEffect and keep the same cleanup logic (ensure
React's useEffect is imported/available). This keeps the same unmount cleanup
behavior while avoiding layout-effect usage.
frontend/builder/tabs/builder-previewtab.tsx (2)

182-182: Nit: widen useState type to include null.

useState<string>(null) is inconsistent with the initial value. Minor, but it propagates a misleading non-nullable type.

♻️ Suggested change
-    const [lastKnownUrl, setLastKnownUrl] = useState<string>(null);
+    const [lastKnownUrl, setLastKnownUrl] = useState<string | null>(null);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/builder/tabs/builder-previewtab.tsx` at line 182, The state
declaration uses useState<string>(null) but initializes with null; change the
generic to allow null to match the initial value by updating the hook call to
useState<string | null>(null) so lastKnownUrl and setLastKnownUrl have the
correct nullable type; locate the useState usage for lastKnownUrl in
builder-previewtab.tsx and adjust the generic accordingly.

185-192: isWebViewActive returns a truthy number, not a boolean, and setState-in-render pattern noted.

Two small observations:

  1. status === "running" && builderStatus?.port && builderStatus.port !== 0 evaluates to number | boolean | undefined. It works in boolean contexts, but the downstream visibility: isWebViewActive ? "visible" : "hidden" etc. read cleaner if coerced. Consider Boolean(...) or !!builderStatus?.port with port > 0.
  2. Calling setLastKnownUrl during render is fine since it's guarded by inequality (React allows this), but if builderStatus.port ever flips back to a previous value, the webview will happily reload mid-session. Once the first URL is captured, a stable useRef + useEffect might express the "sticky last URL" intent more clearly. Not blocking.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/builder/tabs/builder-previewtab.tsx` around lines 185 - 192, The
boolean expression assigned to isWebViewActive can be a number/undefined; change
it to an explicit boolean (e.g., !!builderStatus?.port && builderStatus.port >
0) so usages like visibility: isWebViewActive ? "visible" : "hidden" are clear,
and avoid calling setLastKnownUrl during render by making the "sticky last URL"
stable: move the preview URL capture into a useEffect that reads
builderStatus.port and builderId and writes to either a useRef (for truly sticky
behavior) or setLastKnownUrl only when lastKnownUrl is empty, referencing
isWebViewActive, lastKnownUrl, setLastKnownUrl, builderStatus.port, previewUrl,
and useEffect/useRef to locate the code.
frontend/builder/store/builder-apppanel-model.ts (1)

10-10: Prefer a type-only import for WebviewTag.

WebviewTag is used purely as a type here (webviewRef: { current: WebviewTag | null }). Using a value import from "electron" risks the bundler attempting to resolve the electron module in the renderer bundle depending on tsconfig importsNotUsedAsValues / verbatimModuleSyntax settings. Make the import type-only for safety.

♻️ Suggested change
-import { WebviewTag } from "electron";
+import type { WebviewTag } from "electron";
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/builder/store/builder-apppanel-model.ts` at line 10, The import of
WebviewTag should be type-only to avoid bundlers resolving the electron module
at runtime; change the import to a type import (replace the current import of
WebviewTag with a type-only import) and keep all usages like webviewRef: {
current: WebviewTag | null } unchanged so it only affects the import; ensure you
update the import statement that references WebviewTag accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@frontend/app/aipanel/waveai-model.tsx`:
- Around line 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.

In `@pkg/aiusechat/usechat.go`:
- Around line 673-674: The current logic sets premium := shouldUsePremium() ||
builderMode which forces premium for any request with req.BuilderId, bypassing
resolveAIMode and rate-limit fallback; change this to only force premium when
the request is both from a builder (req.BuilderId set) and the requested AIMode
is a builder-only cloud mode (e.g., aiMode startsWith "waveaibuilder@" or use a
helper isBuilderOnlyMode(req.AIMode)), otherwise leave premium based solely on
shouldUsePremium() so non-builder WaveAI cloud modes (like "waveai@balanced")
still go through resolveAIMode/rate-limit fallback; update usages in
resolveAIMode/any downstream logic that relies on premium.

In `@tsunami/frontend/src/vdom.tsx`:
- Around line 200-212: The JS handler call is missing the second `elem`
parameter described by the Go-side `JSFunc`/`CombinedFunc` contract; update the
invocation in this block so the evaluated function receives both event and
element (e.g., pass `elem` alongside `...args`) or centralize the fix in
`vdom.go` as noted in the comment; ensure you adjust the call site that uses
`fnDecl.jscode` (the `fn(...args)` invocation) so the `elem` is provided, and
keep the `model.callVDomFunc(fnDecl, e, compId, propName)` behavior unchanged;
optionally consider replacing `eval(fnDecl.jscode)` with `new
Function("e","elem", body)` if the `jscode` format is changed to a bare function
body.

In `@tsunami/vdom/vdom.go`:
- Around line 102-112: The docstrings claim the JS handler receives (e, elem)
but runtime calls fn(...args) without supplying elem; fix this by wiring the
element/ref into the args before invoking the JS function: modify
convertVDomFunc (in tsunami/frontend/src/vdom.tsx) so when it builds the args
for fn(...args) it appends the DOM element or waveid/ref (the same value
described as "elem" in the comments) for event handlers and non-event handlers
as appropriate, ensuring JSFunc and CombinedFunc users get (e, elem) as
advertised.

---

Outside diff comments:
In `@emain/emain-window.ts`:
- Around line 295-332: The handler currently calls this.closeAllDevTools()
unconditionally at the top which dismisses DevTools even when the user cancels
the confirmation; move the this.closeAllDevTools() call out of the start of the
"close" handler and invoke it only after the confirmation has resolved and just
before calling this.close() (i.e., inside the fireAndForget flow immediately
before setting this.canClose = true and this.close()), or alternatively invoke
it from the "closed" handler — adjust references to this.canClose,
this.deleteAllowed, dialog.showMessageBoxSync, isNonEmptyUnsavedWorkspace and
fireAndForget accordingly so DevTools are only closed when the window is
actually going to be closed.

---

Nitpick comments:
In `@frontend/app/aipanel/waveai-model.tsx`:
- Around line 44-63: BuilderAIModeConfigs is a hard-coded duplicate of backend
builderModeConfigs which will drift; fix by replacing the static map with a
single source of truth (subscribe to atoms.waveaiModeConfigAtom or equivalent
published config from the backend) or at minimum add a comment referencing the
backend builderModeConfigs location so maintainers update both; update the code
that currently exports/uses BuilderAIModeConfigs to read from
atoms.waveaiModeConfigAtom (or call into the published config provider) and
remove/replace the hard-coded entries ("waveaibuilder@default",
"waveaibuilder@deep") so UI derives display:name, display:order, display:icon,
display:description, ai:provider, ai:switchcompat, and waveai:premium from the
backend-published config.

In `@frontend/app/view/webview/webview.tsx`:
- Around line 954-961: Replace useLayoutEffect with useEffect for the devtools
cleanup: in the component where useLayoutEffect(() => { return () => { const
webview = model.webviewRef.current; if (webview?.isDevToolsOpened()) {
webview.closeDevTools(); } }; }, []); is used, change the hook to useEffect and
keep the same cleanup logic (ensure React's useEffect is imported/available).
This keeps the same unmount cleanup behavior while avoiding layout-effect usage.

In `@frontend/builder/store/builder-apppanel-model.ts`:
- Line 10: The import of WebviewTag should be type-only to avoid bundlers
resolving the electron module at runtime; change the import to a type import
(replace the current import of WebviewTag with a type-only import) and keep all
usages like webviewRef: { current: WebviewTag | null } unchanged so it only
affects the import; ensure you update the import statement that references
WebviewTag accordingly.

In `@frontend/builder/tabs/builder-previewtab.tsx`:
- Line 182: The state declaration uses useState<string>(null) but initializes
with null; change the generic to allow null to match the initial value by
updating the hook call to useState<string | null>(null) so lastKnownUrl and
setLastKnownUrl have the correct nullable type; locate the useState usage for
lastKnownUrl in builder-previewtab.tsx and adjust the generic accordingly.
- Around line 185-192: The boolean expression assigned to isWebViewActive can be
a number/undefined; change it to an explicit boolean (e.g.,
!!builderStatus?.port && builderStatus.port > 0) so usages like visibility:
isWebViewActive ? "visible" : "hidden" are clear, and avoid calling
setLastKnownUrl during render by making the "sticky last URL" stable: move the
preview URL capture into a useEffect that reads builderStatus.port and builderId
and writes to either a useRef (for truly sticky behavior) or setLastKnownUrl
only when lastKnownUrl is empty, referencing isWebViewActive, lastKnownUrl,
setLastKnownUrl, builderStatus.port, previewUrl, and useEffect/useRef to locate
the code.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 433f9b32-9f01-485b-b925-9cedaa07b95e

📥 Commits

Reviewing files that changed from the base of the PR and between 80cb181 and c55631d.

📒 Files selected for processing (19)
  • emain/emain-builder.ts
  • emain/emain-ipc.ts
  • emain/emain-window.ts
  • frontend/app/aipanel/waveai-model.tsx
  • frontend/app/view/webview/webview.tsx
  • frontend/builder/builder-apppanel.tsx
  • frontend/builder/store/builder-apppanel-model.ts
  • frontend/builder/tabs/builder-previewtab.tsx
  • pkg/aiusechat/uctypes/uctypes.go
  • pkg/aiusechat/usechat-mode.go
  • pkg/aiusechat/usechat.go
  • tsunami/frontend/src/app.tsx
  • tsunami/frontend/src/tailwind.css
  • tsunami/frontend/src/types/custom.d.ts
  • tsunami/frontend/src/types/vdom.d.ts
  • tsunami/frontend/src/util/keyutil.ts
  • tsunami/frontend/src/vdom.tsx
  • tsunami/vdom/vdom.go
  • tsunami/vdom/vdom_types.go

Comment on lines 143 to 147
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";
}
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.

Comment thread pkg/aiusechat/usechat.go
Comment on lines 673 to +674
builderMode := req.BuilderId != ""
premium := shouldUsePremium() || builderMode
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:

# First, let's examine the file and understand the context around lines 673-674
head -n 700 pkg/aiusechat/usechat.go | tail -n 50

Repository: wavetermdev/waveterm

Length of output: 1898


🏁 Script executed:

# Get more context around the critical lines
sed -n '650,700p' pkg/aiusechat/usechat.go

Repository: wavetermdev/waveterm

Length of output: 1921


🏁 Script executed:

# Search for the resolveAIMode function to understand the fallback logic
rg -n "resolveAIMode" pkg/aiusechat/usechat.go -A 10 -B 2

Repository: wavetermdev/waveterm

Length of output: 565


🏁 Script executed:

# Search for mode constants and builder mode definitions
rg -n "waveaibuilder|WaveAIBuilder" pkg/aiusechat/ -B 2 -A 2

Repository: wavetermdev/waveterm

Length of output: 461


🏁 Script executed:

# Look for any validation of builder modes
rg -n "BuilderId|builderMode" pkg/aiusechat/usechat.go -B 2 -A 2

Repository: wavetermdev/waveterm

Length of output: 2131


🏁 Script executed:

# Find the resolveAIMode function implementation
rg -n "func resolveAIMode" pkg/aiusechat/ -A 50

Repository: wavetermdev/waveterm

Length of output: 3228


🏁 Script executed:

# Search for AIModeQuick to understand the fallback
rg -n "AIModeQuick|WaveAIQuick" pkg/aiusechat/uctypes/

Repository: wavetermdev/waveterm

Length of output: 139


🏁 Script executed:

# Check if there's any validation preventing builders from using non-builder modes
rg -n "req.AIMode" pkg/aiusechat/usechat.go -B 3 -A 3

Repository: wavetermdev/waveterm

Length of output: 528


🏁 Script executed:

# Look for the shouldUsePremium function to understand the premium check
rg -n "func shouldUsePremium" pkg/aiusechat/ -A 15

Repository: wavetermdev/waveterm

Length of output: 827


🏁 Script executed:

# Check if getWaveAISettings does any mode validation for builders
rg -n "func getWaveAISettings" pkg/aiusechat/ -A 30

Repository: wavetermdev/waveterm

Length of output: 2007


🏁 Script executed:

# Look for any validation that restricts builder requests to builder-only modes
rg -n "AIMode.*Builder|builder.*AIMode" pkg/aiusechat/ -B 3 -A 3

Repository: wavetermdev/waveterm

Length of output: 2050


🏁 Script executed:

# Check if there's any request validation for builder mode before calling getWaveAISettings
sed -n '650,690p' pkg/aiusechat/usechat.go

Repository: wavetermdev/waveterm

Length of output: 1345


Confirm intent: builder requests always bypass the premium rate-limit fallback.

premium := shouldUsePremium() || builderMode means that whenever BuilderId is present, premium is unconditionally true, regardless of the user's current premium rate-limit state (PReq/ResetEpoch). This prevents the resolveAIMode fallback from waveai@* cloud modes to AIModeQuick when premium is exhausted. For the new waveaibuilder@* modes this is fine (they use WaveAIPremium: true and are the only builder-visible cloud modes), but note that if a builder context ever submits req.AIMode == "waveai@balanced" (or any other WaveAICloud mode), this code will also skip the rate-limit fallback and count against premium quota without the usual guardrail. There is no validation preventing builders from requesting non-builder modes. If that's not desired, consider gating the override on the mode name (e.g., only when aiMode is a known builder mode like waveaibuilder@*).

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

In `@pkg/aiusechat/usechat.go` around lines 673 - 674, The current logic sets
premium := shouldUsePremium() || builderMode which forces premium for any
request with req.BuilderId, bypassing resolveAIMode and rate-limit fallback;
change this to only force premium when the request is both from a builder
(req.BuilderId set) and the requested AIMode is a builder-only cloud mode (e.g.,
aiMode startsWith "waveaibuilder@" or use a helper
isBuilderOnlyMode(req.AIMode)), otherwise leave premium based solely on
shouldUsePremium() so non-builder WaveAI cloud modes (like "waveai@balanced")
still go through resolveAIMode/rate-limit fallback; update usages in
resolveAIMode/any downstream logic that relies on premium.

Comment on lines +200 to +212
let retVal: any;
if (fnDecl.jscode) {
try {
const fn = eval(fnDecl.jscode);
if (typeof fn === "function") retVal = fn(...args);
} catch (err) {
console.error("vdom jscode error:", err);
}
}
if (!fnDecl.preventbackend) {
model.callVDomFunc(fnDecl, e, compId, propName);
}
return retVal;
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

jscode execution: downstream effect of the (e, elem) contract.

fn(...args) forwards only the underlying handler's arguments, so the second elem parameter documented on the Go side (JSFunc / CombinedFunc in tsunami/vdom/vdom.go) is never supplied. See the root-cause comment on vdom.go; the fix (either injecting elem here or correcting the docs) should be centralized there.

Minor, secondary note: eval was flagged by biome (lint/security/noGlobalEval). Given jscode originates from the trusted backend this is a deliberate choice, but consider using new Function("e", "elem", body) if jscode is migrated to a bare function body — it would be slightly more sandboxable (no access to the enclosing lexical scope) and also lets you pass elem explicitly, killing two birds.

🧰 Tools
🪛 Biome (2.4.11)

[error] 203-203: eval() exposes to security risks and performance issues.

(lint/security/noGlobalEval)

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

In `@tsunami/frontend/src/vdom.tsx` around lines 200 - 212, The JS handler call is
missing the second `elem` parameter described by the Go-side
`JSFunc`/`CombinedFunc` contract; update the invocation in this block so the
evaluated function receives both event and element (e.g., pass `elem` alongside
`...args`) or centralize the fix in `vdom.go` as noted in the comment; ensure
you adjust the call site that uses `fnDecl.jscode` (the `fn(...args)`
invocation) so the `elem` is provided, and keep the `model.callVDomFunc(fnDecl,
e, compId, propName)` behavior unchanged; optionally consider replacing
`eval(fnDecl.jscode)` with `new Function("e","elem", body)` if the `jscode`
format is changed to a bare function body.

Comment thread tsunami/vdom/vdom.go
@sawka sawka merged commit 4969ee1 into main Apr 17, 2026
7 of 8 checks passed
@sawka sawka deleted the sawka/tsunami-jsfuncs branch April 17, 2026 19:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant