Skip to content

Conversation

@sawka
Copy link
Member

@sawka sawka commented Nov 14, 2025

  • build manifest
  • working on secrets injection (secretstore + secret-bindings.json)
  • tool progress indicators
  • build output and errors injected as the result of the edit calls so AI gets instant feedback on edits
  • change edits to not be atomic (allows AI to make better progress)
  • updated binary location for waveapps
  • publish button
  • new partial json parser (for sending incremental tool progress indication)
  • updated tsunami view to use new embedded scaffold + config vars
  • lots of work on cleaning up the output so it is more useful to users + AI agents

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Nov 14, 2025

Walkthrough

This PR implements app publishing and secret-bindings end-to-end, adds tool-progress streaming and partial-file/JSON repair utilities, and updates multiple UI elements. Frontend: PublishAppModal, modal registry entry, new RPC client methods, tool-progress rendering/components, and UI position/style tweaks. Backend: RPC handlers for publish and secret bindings, manifest generation during build, builder controller and builder-run signature changes to load manifests and inject secret env, waveappstore manifest/secret read-write and partial file edits, OpenAI tool-progress emission, partial JSON parser, and numerous type and API surface changes (app manifest, tsunami:appid, command types).

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

  • Areas requiring extra attention:
    • Builder controller public signature changes and consumer impact (pkg/buildercontroller/*)
    • RPC surface and types alignment across server, client, and wshrpc types (pkg/wshrpc/*, frontend/app/store/wshclientapi.ts)
    • Build pipeline and manifest generation integration (tsunami/build/build.go) and moveFilesBack interactions
    • OpenAI streaming changes and partial JSON parsing/repair logic (pkg/aiusechat/openai/openai-backend.go, pkg/util/utilfn/partial.go + tests)
    • Partial-edit APIs and file utilities, and their use in waveappstore (pkg/util/fileutil/, pkg/waveappstore/)
    • Frontend tool-progress types and rendering changes (frontend/app/aipanel/*) and duplicated type declarations in usechat types
    • Surface-area checks for duplicated or inconsistent type declarations added in multiple files (pkg/aiusechat/usechat-types.go, pkg/wshrpc/wshrpctypes.go)
    • Ensure no behavioral regressions from removed/rewritten builder init flows and emain builder IPC removal (frontend and emain/*)

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 27.91% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The PR title "big improvements to waveapp builder" is directly related to the main changes, which focus on enhancing the waveapp builder with manifest support, secrets injection, tool progress indicators, and publishing functionality.
Description check ✅ Passed The PR description is well-related to the changeset, providing specific details about manifest building, secrets injection, tool progress indicators, partial JSON parser, non-atomic edits, and publish button features.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch sawka/tsunami-manifest

📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5e892b3 and ff8274d.

📒 Files selected for processing (2)
  • emain/emain-builder.ts (0 hunks)
  • frontend/wave.ts (2 hunks)
💤 Files with no reviewable changes (1)
  • emain/emain-builder.ts
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-10-21T05:09:26.916Z
Learnt from: sawka
Repo: wavetermdev/waveterm PR: 2465
File: frontend/app/onboarding/onboarding-upgrade.tsx:13-21
Timestamp: 2025-10-21T05:09:26.916Z
Learning: In the waveterm codebase, clientData is loaded and awaited in wave.ts before React runs, ensuring it is always available when components mount. This means atoms.client will have data on first render.

Applied to files:

  • frontend/wave.ts
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: Analyze (go)
  • GitHub Check: Analyze (javascript-typescript)
  • GitHub Check: Build for TestDriver.ai
🔇 Additional comments (1)
frontend/wave.ts (1)

262-275: Builder appId resolution and title now aligned with RTInfo and global state

Using RTInfo to override initOpts.appId where available, then driving both document.title and atoms.builderAppId from the resolved appIdToUse, cleanly fixes the previous builder title/appId drift and ensures the builder React tree sees the correct appId on first render. This looks solid and matches the existing init patterns in this file.


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.

Copy link
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: 5

Caution

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

⚠️ Outside diff range comments (2)
frontend/builder/tabs/builder-previewtab.tsx (1)

129-129: Apply the new accent button styling consistently.

The "Start App" button still uses the old styling pattern (bg-accent hover:bg-accent/80), which is inconsistent with the updated "Ask AI to Fix" button at line 76 and the new guideline in rules.md.

Apply this diff to align with the new guideline:

-                        className="px-6 py-2 bg-accent text-primary font-semibold rounded hover:bg-accent/80 transition-colors cursor-pointer"
+                        className="px-6 py-2 bg-accent/80 text-primary font-semibold rounded hover:bg-accent transition-colors cursor-pointer"

Based on coding guidelines

tsunami/build/build.go (1)

808-835: Add timeout and improve error handling for manifest generation.

The manifest generation command has no timeout and only captures stdout. If the binary hangs or writes errors to stderr, the build will hang or lose diagnostic information.

Apply this diff to add timeout and capture combined output:

 func generateManifest(tempDir, exePath string, opts BuildOpts) error {
 	oc := opts.OutputCapture
+	
+	ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
+	defer cancel()
 
-	manifestCmd := exec.Command(exePath, "--manifest")
+	manifestCmd := exec.CommandContext(ctx, exePath, "--manifest")
 	manifestCmd.Dir = tempDir
 
 	if opts.Verbose {
 		oc.Printf("[debug] Running: %s --manifest", exePath)
 		oc.Printf("Generating manifest...")
 	}
 
-	manifestOutput, err := manifestCmd.Output()
+	manifestOutput, err := manifestCmd.CombinedOutput()
 	if err != nil {
+		if ctx.Err() == context.DeadlineExceeded {
+			return fmt.Errorf("manifest generation timed out after 30s")
+		}
-		return fmt.Errorf("manifest generation failed: %w", err)
+		return fmt.Errorf("manifest generation failed: %w\nOutput: %s", err, string(manifestOutput))
 	}
🧹 Nitpick comments (10)
pkg/buildercontroller/buildercontroller.go (1)

26-26: Status enrichment works, but repeated manifest/secret reads under lock may be heavy

Switching GetStatus to return wshrpc.BuilderStatusData and enriching it with AppManifest, SecretBindings, and SecretBindingsComplete is functionally sound and the wshrpc struct mapping looks correct.

However, this method now:

  • Reads the manifest and secret‑bindings files on every GetStatus call.
  • Calls BuildAppSecretEnv just to compute SecretBindingsComplete, which entails resolving all secrets via secretstore.GetSecret.
  • Performs all of that while holding bc.statusLock.

In a UI that polls builder status frequently, this can mean:

  • Extra filesystem traffic and secret‑store calls on the hot path.
  • Longer holds on statusLock, delaying other status updates.

It might be worth (later) refactoring to compute manifest/secret‑binding info outside the lock and/or cache it (invalidated when bc.appId changes or a relevant write occurs), and use a cheaper check for “bindings complete” that doesn’t require re‑fetching all secret values on every status read.

Also applies to: 498-542

pkg/aiusechat/tools_builder.go (2)

94-115: Dynamic write-app descriptions and progress look good; optional line-count tweak

Using parseBuilderWriteAppFileInput in both ToolCallDesc and ToolProgressDesc keeps behavior consistent and gracefully falls back when parsing fails. The line-count-based messages are also very readable for users/AI. If you ever care about micro-allocations or want to reuse this pattern elsewhere, you could factor out a small helper that counts '\n' without strings.Split, but that’s strictly an optional optimization.


277-291: Static 'listing files' ToolCallDesc is fine; optional progress hook for consistency

Returning a simple "listing files" from ToolCallDesc keeps this tool aligned with the newer, more descriptive patterns. If you later want uniform progress reporting across builder tools, you could add a trivial ToolProgressDesc here that just returns []string{"listing files"}, but it’s not required for correctness.

frontend/builder/app-selection-modal.tsx (1)

122-143: Title updates on app select/create look correct; consider centralizing formatting

Setting document.title to WaveApp Builder (${appId}) on both select and create keeps the builder window title in sync with the active app and matches the wave.ts behavior. You might later want a small helper (e.g., setBuilderTitle(appId?: string)) shared with initBuilder/reinitBuilder to avoid three separate call sites drifting, but the current change is functionally sound.

frontend/builder/builder-buildpanel.tsx (1)

7-8: Restart button and filtered output behavior look good

  • Wiring handleRestart to BuilderAppPanelModel.getInstance().restartBuilder() cleanly reuses the existing restart path used by saves/env-var updates.
  • Updated filteredLines logic to hide [debug] lines and blank lines when showDebug is false makes the non-debug view much easier to read.
  • Header layout with the debug toggle and “Restart App” button is straightforward and matches the rest of the builder UI.

If you want to be explicit about ignoring the async return in the click handler, you could wrap the call as void BuilderAppPanelModel.getInstance().restartBuilder();, but it’s not required.

Also applies to: 81-86, 91-107

pkg/wshrpc/wshserver/wshserver.go (1)

1030-1035: New app RPCs and builder status handling are sound

  • WriteAppSecretBindingsCommand correctly rejects empty AppId and delegates to waveappstore.WriteAppSecretBindings, which already validates the appId and handles nil bindings. This mirrors the patterns used by the app file commands above.
  • GetBuilderStatusCommand now returns &status where status := bc.GetStatus(). Since GetStatus returns a value, this gives callers an isolated snapshot of the status without exposing the controller’s internal struct; Go will safely escape this to the heap.
  • PublishAppCommand is a straightforward wrapper around waveappstore.PublishDraft, with errors wrapped in context and the resulting PublishedAppId passed back to the client. For consistency with the other commands you might optionally add an explicit if data.AppId == "" guard, but functionally it’s already safe because PublishDraft validates the appId.

Also applies to: 1089-1096, 1121-1129

frontend/builder/builder-apppanel.tsx (2)

103-120: Consider improving error handling in the modal.

Currently, errors are only logged to the console (line 118), leaving the user without visible feedback if publishing fails. Consider displaying the error message within the modal UI.

Example enhancement:

+    const [error, setError] = useState<string | null>(null);
+
     const handlePublish = async () => {
         if (!builderAppId) {
             console.error("No builder app ID found");
             modalsModel.popModal();
             return;
         }
+        setError(null);

         try {
             const result = await RpcApi.PublishAppCommand(TabRpcClient, { appid: builderAppId });
             console.log("App published successfully:", result.publishedappid);
             modalsModel.popModal();
         } catch (error) {
             console.error("Failed to publish app:", error);
+            setError(error instanceof Error ? error.message : "Failed to publish app");
         }
     };

Then display the error in the modal UI if present.


219-221: Remove unused handleRestart function.

The handleRestart function is defined but never called, as the UI now uses handlePublishClick instead (line 283).

Apply this diff:

-    const handleRestart = useCallback(() => {
-        model.restartBuilder();
-    }, [model]);
-
     const handlePublishClick = useCallback(() => {
pkg/util/utilfn/partial_test.go (1)

92-96: Remove or document the commented-out test case.

This commented test case appears to demonstrate a known limitation where partial keywords (like fa for incomplete false) are not repaired. Either remove this commented code or add a comment explaining why this case is not supported.

Apply this diff to remove the commented code:

-		// {
-		// 	name:     "unclosed object with partial value",
-		// 	input:    `{"a": fa`,
-		// 	expected: `{"a": fa}`,
-		// },

Alternatively, if documenting the limitation, add an explanatory comment above it.

pkg/util/utilfn/partial.go (1)

88-94: Consider adding a comment to clarify the valueStart logic.

The boolean expression at line 91 checks for all possible starting characters of JSON values but is complex and could benefit from a brief comment explaining what each character/range represents (e.g., n/t/f for null/true/false, digits for numbers, etc.).

Apply this diff to add clarifying comments:

+		// Check if byte can start a JSON value:
+		// {/[ for objects/arrays, n/t/f for null/true/false, " for strings, 0-9/- for numbers
 		valueStart := b == '{' || b == '[' || b == 'n' || b == 't' || b == 'f' || b == '"' || (b >= '0' && b <= '9') || b == '-'
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 48c6b95 and 624ecff.

📒 Files selected for processing (34)
  • .roo/rules/rules.md (1 hunks)
  • emain/emain-menu.ts (1 hunks)
  • frontend/app/aipanel/aimessage.tsx (3 hunks)
  • frontend/app/aipanel/aipanel.tsx (1 hunks)
  • frontend/app/aipanel/aipanelmessages.tsx (1 hunks)
  • frontend/app/aipanel/aitooluse.tsx (6 hunks)
  • frontend/app/aipanel/aitypes.ts (1 hunks)
  • frontend/app/aipanel/thinkingmode.tsx (1 hunks)
  • frontend/app/modals/modalregistry.tsx (2 hunks)
  • frontend/app/store/wshclientapi.ts (2 hunks)
  • frontend/app/view/tsunami/tsunami.tsx (1 hunks)
  • frontend/builder/app-selection-modal.tsx (2 hunks)
  • frontend/builder/builder-app.tsx (2 hunks)
  • frontend/builder/builder-apppanel.tsx (5 hunks)
  • frontend/builder/builder-buildpanel.tsx (2 hunks)
  • frontend/builder/tabs/builder-previewtab.tsx (1 hunks)
  • frontend/types/gotypes.d.ts (6 hunks)
  • frontend/wave.ts (4 hunks)
  • pkg/aiusechat/openai/openai-backend.go (3 hunks)
  • pkg/aiusechat/tools_builder.go (6 hunks)
  • pkg/aiusechat/uctypes/usechat-types.go (2 hunks)
  • pkg/aiusechat/usechat.go (1 hunks)
  • pkg/blockcontroller/tsunamicontroller.go (3 hunks)
  • pkg/buildercontroller/buildercontroller.go (7 hunks)
  • pkg/util/fileutil/fileutil.go (2 hunks)
  • pkg/util/utilfn/partial.go (1 hunks)
  • pkg/util/utilfn/partial_test.go (1 hunks)
  • pkg/waveappstore/waveappstore.go (4 hunks)
  • pkg/waveobj/metaconsts.go (1 hunks)
  • pkg/waveobj/wtypemeta.go (1 hunks)
  • pkg/wshrpc/wshclient/wshclient.go (2 hunks)
  • pkg/wshrpc/wshrpctypes.go (5 hunks)
  • pkg/wshrpc/wshserver/wshserver.go (3 hunks)
  • tsunami/build/build.go (6 hunks)
🧰 Additional context used
🧠 Learnings (4)
📚 Learning: 2025-10-15T03:21:02.229Z
Learnt from: sawka
Repo: wavetermdev/waveterm PR: 2433
File: pkg/aiusechat/tools_readfile.go:197-197
Timestamp: 2025-10-15T03:21:02.229Z
Learning: In Wave Terminal's AI tool definitions (pkg/aiusechat/tools_*.go), the Description field should not mention approval requirements even when ToolApproval returns ApprovalNeedsApproval. This prevents the LLM from asking users for approval before calling the tool, avoiding redundant double-approval prompts since the runtime will enforce approval anyway.

Applied to files:

  • pkg/aiusechat/usechat.go
  • pkg/aiusechat/openai/openai-backend.go
  • frontend/app/aipanel/aitypes.ts
  • pkg/aiusechat/uctypes/usechat-types.go
  • pkg/aiusechat/tools_builder.go
  • frontend/app/aipanel/aitooluse.tsx
📚 Learning: 2025-10-14T06:30:54.763Z
Learnt from: sawka
Repo: wavetermdev/waveterm PR: 2430
File: frontend/app/aipanel/aimessage.tsx:137-144
Timestamp: 2025-10-14T06:30:54.763Z
Learning: In `frontend/app/aipanel/aimessage.tsx`, the `AIToolUseGroup` component splits file operation tool calls into separate batches (`fileOpsNeedApproval` and `fileOpsNoApproval`) based on their approval state before passing them to `AIToolUseBatch`. This ensures each batch has homogeneous approval states, making group-level approval handling valid.

Applied to files:

  • pkg/aiusechat/usechat.go
  • frontend/app/aipanel/aitypes.ts
  • frontend/app/aipanel/aimessage.tsx
  • frontend/app/aipanel/aitooluse.tsx
📚 Learning: 2025-01-22T01:28:41.417Z
Learnt from: esimkowitz
Repo: wavetermdev/waveterm PR: 1790
File: pkg/remote/fileshare/wshfs/wshfs.go:122-122
Timestamp: 2025-01-22T01:28:41.417Z
Learning: The RpcClient in pkg/remote/fileshare/wshfs/wshfs.go is initialized and handled downstream by either main-server or wshcmd-connserver, as documented in the package comment.

Applied to files:

  • pkg/wshrpc/wshclient/wshclient.go
  • pkg/buildercontroller/buildercontroller.go
📚 Learning: 2025-10-21T05:09:26.916Z
Learnt from: sawka
Repo: wavetermdev/waveterm PR: 2465
File: frontend/app/onboarding/onboarding-upgrade.tsx:13-21
Timestamp: 2025-10-21T05:09:26.916Z
Learning: In the waveterm codebase, clientData is loaded and awaited in wave.ts before React runs, ensuring it is always available when components mount. This means atoms.client will have data on first render.

Applied to files:

  • frontend/wave.ts
🧬 Code graph analysis (19)
frontend/builder/builder-buildpanel.tsx (1)
frontend/builder/store/builder-apppanel-model.ts (1)
  • BuilderAppPanelModel (21-294)
pkg/aiusechat/usechat.go (2)
pkg/aiusechat/uctypes/usechat-types.go (1)
  • ApprovalNeedsApproval (140-140)
pkg/aiusechat/toolapproval.go (1)
  • RegisterToolApproval (48-58)
pkg/aiusechat/openai/openai-backend.go (2)
pkg/util/utilfn/partial.go (1)
  • ParseParialJson (167-175)
pkg/aiusechat/uctypes/usechat-types.go (1)
  • UIMessageDataToolProgress (166-170)
frontend/builder/builder-apppanel.tsx (5)
frontend/app/store/global.ts (1)
  • atoms (816-816)
frontend/app/store/modalmodel.ts (1)
  • modalsModel (45-45)
frontend/app/store/wshclientapi.ts (1)
  • RpcApi (662-662)
frontend/app/store/wshrpcutil.ts (1)
  • TabRpcClient (37-37)
frontend/app/modals/modal.tsx (1)
  • Modal (116-116)
emain/emain-menu.ts (1)
emain/emain-platform.ts (1)
  • unamePlatform (273-273)
pkg/wshrpc/wshclient/wshclient.go (3)
frontend/app/store/wshclientapi.ts (2)
  • PublishAppCommand (386-388)
  • WriteAppSecretBindingsCommand (631-633)
pkg/wshutil/wshrpc.go (1)
  • WshRpc (47-61)
pkg/wshrpc/wshrpctypes.go (4)
  • CommandPublishAppData (1072-1074)
  • RpcOpts (357-363)
  • CommandPublishAppRtnData (1076-1078)
  • CommandWriteAppSecretBindingsData (1022-1025)
pkg/aiusechat/tools_builder.go (3)
pkg/util/fileutil/fileutil.go (1)
  • EditSpec (259-263)
pkg/aiusechat/uctypes/usechat-types.go (2)
  • ToolDefinition (80-95)
  • UIMessageDataToolUse (148-159)
pkg/waveappstore/waveappstore.go (1)
  • ReplaceInAppFilePartial (359-375)
frontend/app/store/wshclientapi.ts (1)
pkg/wshrpc/wshrpctypes.go (4)
  • CommandPublishAppData (1072-1074)
  • RpcOpts (357-363)
  • CommandPublishAppRtnData (1076-1078)
  • CommandWriteAppSecretBindingsData (1022-1025)
pkg/util/fileutil/fileutil.go (1)
pkg/aiusechat/tools_writefile.go (1)
  • MaxEditFileSize (18-18)
frontend/app/aipanel/aimessage.tsx (1)
frontend/app/aipanel/aitypes.ts (1)
  • WaveUIMessagePart (36-36)
frontend/app/aipanel/aitooluse.tsx (1)
frontend/app/aipanel/aitypes.ts (1)
  • WaveUIMessagePart (36-36)
pkg/wshrpc/wshrpctypes.go (2)
frontend/app/store/wshclientapi.ts (2)
  • WriteAppSecretBindingsCommand (631-633)
  • PublishAppCommand (386-388)
pkg/wshrpc/wshclient/wshclient.go (2)
  • WriteAppSecretBindingsCommand (753-756)
  • PublishAppCommand (468-471)
frontend/types/gotypes.d.ts (1)
pkg/wshrpc/wshrpctypes.go (5)
  • AppManifest (1046-1052)
  • SecretMeta (1041-1044)
  • CommandPublishAppData (1072-1074)
  • CommandPublishAppRtnData (1076-1078)
  • CommandWriteAppSecretBindingsData (1022-1025)
pkg/waveappstore/waveappstore.go (3)
pkg/util/fileutil/fileutil.go (3)
  • EditSpec (259-263)
  • EditResult (265-269)
  • ReplaceInFilePartial (380-406)
pkg/wshrpc/wshrpctypes.go (1)
  • AppManifest (1046-1052)
pkg/secretstore/secretstore.go (1)
  • GetSecret (251-263)
frontend/app/modals/modalregistry.tsx (1)
frontend/builder/builder-apppanel.tsx (1)
  • PublishAppModal (345-345)
frontend/builder/builder-app.tsx (1)
frontend/app/modals/modalsrenderer.tsx (1)
  • ModalsRenderer (57-57)
pkg/buildercontroller/buildercontroller.go (3)
pkg/wavebase/wavebase.go (1)
  • TryMkdirs (255-271)
pkg/waveappstore/waveappstore.go (3)
  • ReadAppManifest (672-694)
  • ReadAppSecretBindings (696-725)
  • BuildAppSecretEnv (754-792)
pkg/wshrpc/wshrpctypes.go (3)
  • BuilderStatusData (1054-1063)
  • AppManifest (1046-1052)
  • SecretMeta (1041-1044)
pkg/wshrpc/wshserver/wshserver.go (3)
pkg/wshrpc/wshclient/wshclient.go (2)
  • WriteAppSecretBindingsCommand (753-756)
  • PublishAppCommand (468-471)
pkg/wshrpc/wshrpctypes.go (3)
  • CommandWriteAppSecretBindingsData (1022-1025)
  • CommandPublishAppData (1072-1074)
  • CommandPublishAppRtnData (1076-1078)
pkg/waveappstore/waveappstore.go (2)
  • WriteAppSecretBindings (727-752)
  • PublishDraft (118-148)
pkg/blockcontroller/tsunamicontroller.go (5)
pkg/wconfig/filewatcher.go (1)
  • GetWatcher (33-57)
pkg/wavebase/wavebase.go (2)
  • GetWaveAppPath (113-115)
  • ExpandHomeDir (141-155)
tsunami/cmd/main-tsunami.go (1)
  • TsunamiSdkVersion (17-17)
pkg/waveobj/metaconsts.go (2)
  • MetaKey_TsunamiAppPath (129-129)
  • MetaKey_TsunamiAppId (130-130)
pkg/waveappstore/waveappstore.go (1)
  • GetAppDir (79-86)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: Build for TestDriver.ai
  • GitHub Check: Analyze (go)
  • GitHub Check: Analyze (javascript-typescript)
🔇 Additional comments (47)
emain/emain-menu.ts (1)

128-132: Verify the keyboard shortcut choice.

Command+Shift+B on macOS is the standard shortcut for toggling the bookmarks bar in most browsers (Chrome, Firefox, Safari). While this is a dev-only feature, developers using Wave Terminal may have muscle memory for this shortcut, potentially causing confusion or unintended behavior.

Consider whether an alternative shortcut might be more appropriate, or confirm that this conflict is acceptable for a dev-only builder window feature.

frontend/app/view/tsunami/tsunami.tsx (1)

238-238: LGTM!

The appId retrieval follows the same pattern as appPath and correctly uses optional chaining.

pkg/buildercontroller/buildercontroller.go (1)

101-116: Executable path helper and build wiring look correct

GetBuilderAppExecutablePath correctly targets <appPath>/bin/app(.exe) and ensures the bin directory exists. The subsequent usage in buildAndRun (as OutputFile, with MoveFileBack and the post-build os.Stat + executable bit check) is consistent and should work across platforms.

Also applies to: 193-203, 223-238, 249-258, 275-285

pkg/aiusechat/tools_builder.go (4)

171-191: formatEditDescriptions helper produces clear multi-edit summaries

The helper’s “header + per-edit bullet” format is easy to scan, and the desc fallback to edit #N avoids empty lines. The (+newLines -oldLines) notation should give both users and the UI a nice approximate sense of the edit impact without extra complexity. No issues spotted here.


197-222: Edit-tool description and schema accurately document sequential semantics

The updated Description and edits/old_str/desc field docs clearly explain the “exactly once” constraint and the new sequential behavior (partial success with later edits skipped). This should make the tool much easier for the model to use correctly, while keeping human-facing text concise and not mixing in approval semantics, which matches how ToolApproval is handled. Based on learnings.


231-244: No action needed—multi-line handling already implemented

The frontend's ToolDesc component (frontend/app/aipanel/aitooluse.tsx:63–75) is explicitly designed to handle multi-line descriptions. It splits strings on "\n" and renders each line through ToolDescLine. Both current usages (toolData.tooldesc at line 420 and progressData.statuslines at line 450) flow through this component, which correctly handles newline-separated output without wrapping or truncation issues.


245-273: Implementation matches documented semantics; consumer verification requires manual confirmation

The ReplaceInFilePartial implementation is correct:

  • Applies edits sequentially and stops on first failure (lines 322–345 in pkg/util/fileutil/fileutil.go)
  • Returns per-edit results with success/error status for each edit
  • Partial modifications are written to disk even on failure

The callback at lines 245–273 correctly returns {"edits": editResults, ...} matching this contract.

However, I could not locate frontend or integration tests in this codebase that validate consumers expect the new "edits" payload format. The result shape may differ from previous versions—verify that any client consuming builder_edit_app_file results has been updated to handle this structure.

.roo/rules/rules.md (1)

53-53: LGTM!

The styling guideline for accent buttons is clear and addresses the hover behavior issue. This will help maintain consistent button styling across the codebase.

pkg/waveobj/metaconsts.go (1)

130-130: LGTM!

The new constant follows the established naming convention and aligns with the tsunami metadata expansion described in the PR objectives.

pkg/waveobj/wtypemeta.go (1)

134-134: LGTM!

The new field follows the JSON naming convention (lowercase, no underscores) and integrates properly with the tsunami metadata infrastructure.

frontend/builder/tabs/builder-previewtab.tsx (1)

76-76: LGTM!

The button styling correctly implements the new accent button guideline from rules.md.

frontend/app/aipanel/aipanelmessages.tsx (1)

50-50: LGTM!

The repositioning of the ThinkingLevelDropdown to the left side is consistent with the alignment change in thinkingmode.tsx.

frontend/app/aipanel/thinkingmode.tsx (1)

78-78: LGTM!

The dropdown alignment change from right to left is consistent with the repositioning of the trigger button container in aipanelmessages.tsx and aipanel.tsx.

frontend/builder/builder-app.tsx (1)

6-6: LGTM!

The integration of ModalsRenderer is clean and enables the publish modal functionality described in the PR objectives.

Also applies to: 50-50

frontend/app/aipanel/aipanel.tsx (1)

500-500: LGTM!

The repositioning of the ThinkingLevelDropdown to the left side is consistent with the changes in aipanelmessages.tsx and thinkingmode.tsx.

pkg/aiusechat/usechat.go (1)

373-383: Early approval registration wiring is correct

Registering approval with chatOpts.RegisterToolApproval immediately after sending the initial data-tooluse packet ensures the approval workflow is set up before processToolCallInternal blocks on WaitForToolApproval. The nil check keeps this safe for callers that don’t use approvals. No issues from a correctness or concurrency standpoint.

frontend/app/modals/modalregistry.tsx (1)

7-18: PublishAppModal registration is consistent with existing modal patterns

Importing PublishAppModal and registering it under PublishAppModal.displayName || "PublishAppModal" matches the existing registry convention and gives a stable key even if displayName isn’t set. No issues here.

frontend/app/aipanel/aitypes.ts (1)

27-33: Tool progress typing matches backend payloads

The new toolprogress entry on WaveUIDataTypes (toolcallid/toolname/statuslines) lines up with UIMessageDataToolProgress on the Go side, so data-toolprogress packets from the backend will deserialize cleanly into these types. This is a minimal, correct extension of the existing message data union.

pkg/aiusechat/openai/openai-backend.go (1)

379-386: Streaming tool-progress implementation is well-structured; just ensure behavior matches OpenAI’s event semantics

The new logic to surface tool progress from streaming function-call arguments looks solid:

  • openaiBlockState.partialJSON tracks the accumulated arguments for each tool-use block, keyed by item_id, which is exactly what you want for handling ...arguments.delta events.
  • In the delta handler, you:
    • Append the latest Delta to partialJSON.
    • Look up the tool’s definition and, if ToolProgressDesc is provided, run it against a tolerant parse (utilfn.ParseParialJson) and emit data-toolprogress packets with UIMessageDataToolProgress. Parse/descriptor errors are swallowed, which prevents flaky progress parsing from impacting the main stream.
  • In the done handler, you still create ToolUseData via createToolUseData from the full Arguments string, and then, if ToolProgressDesc exists, parse the final JSON with json.Unmarshal and emit a final data-toolprogress update.

This keeps progress updates strictly derived from the arguments JSON without interfering with the existing tool-use or approval flow.

Please double-check against the current OpenAI Responses API streaming docs that:

  • response.function_call_arguments.delta and .done events are emitted exactly once per tool-call item and in the order assumed here, and
  • Delta/Arguments contain a contiguous JSON string for arguments, so that accumulating into partialJSON and parsing via your partial-JSON repair logic remains safe.

Also applies to: 855-880, 899-912

pkg/wshrpc/wshclient/wshclient.go (1)

467-471: LGTM! Generated RPC client wrappers follow the established pattern.

The new PublishAppCommand and WriteAppSecretBindingsCommand wrappers are consistent with the existing RPC command pattern in this generated file.

Also applies to: 752-756

frontend/app/store/wshclientapi.ts (1)

385-388: LGTM! Generated TypeScript RPC wrappers are consistent.

The new PublishAppCommand and WriteAppSecretBindingsCommand methods follow the established pattern and have appropriate return types.

Also applies to: 630-633

pkg/aiusechat/uctypes/usechat-types.go (2)

94-94: LGTM! New tool progress callback follows the pattern.

The ToolProgressDesc callback field is appropriately excluded from JSON serialization with the json:"-" tag, consistent with other callback fields like ToolAnyCallback and ToolApproval.


165-170: LGTM! Tool progress data type is well-structured.

The UIMessageDataToolProgress type provides a clean interface for tracking tool execution progress with status lines.

frontend/builder/builder-apppanel.tsx (1)

223-227: LGTM! Publish button integration is clean.

The new publish functionality is well-integrated, using the modal system appropriately and deriving the app name from the builderAppId.

Also applies to: 280-287

pkg/util/fileutil/fileutil.go (2)

320-345: Verify partial write behavior on failure is intentional.

ApplyEditsPartial continues to write the file even when some edits fail (line 344), which means partially modified content is persisted. Confirm this is the desired behavior, as it could leave files in an inconsistent state if later edits depend on earlier ones.

For example, if edit 1 succeeds but edit 2 fails:

  • File is written with only edit 1 applied
  • Edit 2's failure might be due to content that edit 1 should have set up
  • File now contains incomplete changes

If this behavior is intentional (e.g., for AI tools that make incremental progress), consider documenting this clearly in the function's godoc comment.


265-269: LGTM! Edit result tracking is well-structured.

The EditResult type and applyEdit helper provide clear per-edit status tracking with descriptive error messages.

Also applies to: 271-302

pkg/waveappstore/waveappstore.go (4)

754-792: LGTM! Secret environment builder has robust validation.

The BuildAppSecretEnv function properly handles:

  • Required vs. optional secrets (lines 768-770, 782-786)
  • Missing bindings (line 769)
  • Non-existent secrets in the store (lines 781-784)
  • Error propagation from secretstore (lines 777-778)

The logic correctly skips unbound optional secrets while failing on unbound or missing required secrets.


672-694: LGTM! Manifest reading follows consistent patterns.

The ReadAppManifest function properly validates the app ID, constructs the path, and handles JSON parsing errors with descriptive messages.


696-725: LGTM! Secret bindings I/O is well-implemented.

Both ReadAppSecretBindings and WriteAppSecretBindings handle the optional nature of secret bindings appropriately:

  • Reading returns an empty map if the file doesn't exist (lines 709-711)
  • Writing ensures a valid map before marshaling (lines 737-739)
  • Pretty-printed JSON for readability (line 741)

Also applies to: 727-752


359-375: LGTM! Partial edit wrapper maintains consistency.

The ReplaceInAppFilePartial function follows the same validation pattern as ReplaceInAppFile, ensuring path safety before delegating to fileutil.ReplaceInFilePartial.

frontend/types/gotypes.d.ts (1)

65-72: LGTM! Generated TypeScript types are accurate.

The new types (AppManifest, SecretMeta, command types) and BuilderStatusData extensions properly reflect the corresponding Go types, maintaining type safety across the frontend/backend boundary.

Also applies to: 136-138, 340-348, 507-511, 982-986

frontend/app/aipanel/aimessage.tsx (2)

143-149: LGTM! Tool progress grouping is well-integrated.

The changes properly extend the message part grouping to include data-toolprogress alongside data-tooluse, enabling progress updates to be displayed within tool execution groups. The implementation maintains type safety and follows the existing pattern.

Also applies to: 152-177


226-226: Note: User message width adjusted for better spacing.

The max-width change from calc(100%-20px) to calc(90%-10px) provides more whitespace around user messages, improving visual balance. This is a subjective UI adjustment.

frontend/app/aipanel/aitooluse.tsx (4)

16-77: LGTM! Clean implementation of highlighted signed numbers.

The regex pattern correctly identifies isolated signed numbers using negative lookbehind/lookahead, and the color coding (+/green, -/red) provides clear visual feedback. The multi-line support via ToolDesc is well-structured.


436-456: LGTM! Clean progress indicator component.

The AIToolProgress component follows the established patterns and correctly uses ToolDesc for status lines, maintaining consistency with the rest of the codebase.


420-420: LGTM! Consistent use of ToolDesc component.

Refactoring to use ToolDesc ensures consistent rendering and applies the signed number highlighting to all tool descriptions.


459-553: LGTM! Sound deduplication logic for tool progress.

The extension to handle toolprogress parts alongside tooluse parts is well-implemented. The deduplication logic (line 477) correctly filters out progress indicators for tools that have completed, preventing redundant display.

pkg/wshrpc/wshrpctypes.go (3)

160-173: LGTM! Builder command constants properly organized.

The formatting and organization of builder command constants is clean and follows the established naming conventions.


338-338: LGTM! New RPC interface methods follow established conventions.

Both WriteAppSecretBindingsCommand and PublishAppCommand follow the standard RPC method signature pattern and align with the client implementations shown in the codebase.

Also applies to: 345-345


1022-1078: LGTM! Well-structured types for app publishing infrastructure.

The new data types for secret bindings, manifests, and app publishing are well-designed:

  • Appropriate use of pointers for optional fields
  • map[string]any correctly handles arbitrary JSON schemas
  • JSON tags follow lowercase conventions
  • Field names are clear and descriptive
pkg/util/utilfn/partial_test.go (1)

11-135: Comprehensive test coverage for JSON repair.

The test cases thoroughly cover incomplete JSON scenarios including unclosed brackets, strings, trailing commas, and nested structures. The dual validation (exact string match + JSON validity) ensures both correctness and usability of the repair output.

pkg/util/utilfn/partial.go (1)

10-57: Stack implementation looks solid.

The stack operations correctly handle JSON structure tracking with appropriate state transitions. Note that stackQuote is defined but doesn't appear to be used in the repairJson function—this may be intentional for future use or could be removed if not needed.

tsunami/build/build.go (2)

746-806: LGTM on the signature change and path handling.

The updated signature correctly returns the absolute path to the built binary, and the logic properly handles both custom output paths and default temp directory paths. The distinction between outputPath (for the build command) and absOutputPath (for the return value) is well-handled.


731-741: LGTM on manifest copy logic.

The manifest.json copy logic follows the same pattern as go.sum (only copies if exists) and includes appropriate debug logging. The implementation is consistent with the existing code.

pkg/blockcontroller/tsunamicontroller.go (3)

139-159: LGTM on app path resolution logic.

The app path resolution correctly handles both tsunami:apppath and tsunami:appid metadata keys. The logic properly:

  • Derives the app directory from the app ID when apppath is not provided
  • Expands home directory references in provided paths
  • Validates that paths are absolute
  • Provides clear error messages for each failure case

188-190: Code under review is correct; however, inconsistency exists in other callers.

The SdkVersion, NodePath, and GoPath fields in pkg/blockcontroller/tsunamicontroller.go (lines 188-190) are correctly provided. However, verification reveals an inconsistency:

  • SdkVersion is present at all 4 BuildOpts construction sites (tsunamicontroller.go, buildercontroller.go, and 2 sites in main-tsunami.go) ✓
  • NodePath and GoPath are provided in tsunamicontroller.go and buildercontroller.go, but not in tsunami/cmd/main-tsunami.go (lines 71 and 100)

Both NodePath and GoPath are optional with sensible defaults (NodePath defaults to "node" via getNodePath(), GoPath defaults to system go executable via FindGoExecutable()). The missing fields in main-tsunami.go will simply use these defaults, so there is no compilation or runtime error. However, verify whether this inconsistency is intentional or if main-tsunami.go callers should also be updated for consistency.


26-26: golang.org/x/mod v0.27.0 requires Go 1.23.0, incompatible with minimum supported Go 1.22.

golang.org/x/mod@v0.27.0 specifies go 1.23.0 as its minimum requirement, which exceeds the codebase's minimum supported version of Go 1.22. This will cause the build to fail for users on Go 1.22. Either upgrade the minimum Go version requirement or downgrade golang.org/x/mod to a version compatible with Go 1.22.

⛔ Skipped due to learnings
Learnt from: esimkowitz
Repo: wavetermdev/waveterm PR: 1998
File: go.mod:37-37
Timestamp: 2025-02-20T00:10:31.048Z
Learning: The golang.org/x/sync package should remain at version v0.11.0 in go.mod, as explicitly confirmed by the maintainer.

frontend/wave.ts Outdated
Comment on lines 250 to 252
await WOS.reloadWaveObject<Client>(WOS.makeORef("client", savedBuilderInitOpts.clientId));
document.title = `Tsunami Builder - ${savedBuilderInitOpts.appId}`;
document.title = savedBuilderInitOpts.appId ? `WaveApp Builder (${savedBuilderInitOpts.appId})` : "WaveApp Builder";
getApi().setWindowInitStatus("wave-ready");
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

Builder window title can drift from the actual appId resolved from RTInfo

Right now:

  • initBuilder sets document.title from initOpts.appId (Line 276), then potentially overrides the active app via RTInfo (builder:appid) into appIdToUse and atoms.builderAppId (Lines 292–303).
  • reinitBuilder sets document.title from savedBuilderInitOpts.appId (Line 250), which never sees the RTInfo-based override.

If RTInfo has a different appId than initOpts.appId (e.g., user previously selected a different app in the builder and it was persisted), the builder window title can show one appId while the builder actually operates on another.

Consider driving the title from the resolved appIdToUse instead of the raw init opts, e.g.:

-    document.title = initOpts.appId ? `WaveApp Builder (${initOpts.appId})` : "WaveApp Builder";
+    let appIdToUse = initOpts.appId;
+    try {
+        const oref = WOS.makeORef("builder", initOpts.builderId);
+        const rtInfo = await RpcApi.GetRTInfoCommand(TabRpcClient, { oref });
+        if (rtInfo && rtInfo["builder:appid"]) {
+            appIdToUse = rtInfo["builder:appid"];
+        }
+    } catch (e) {
+        console.log("Could not load saved builder appId from rtinfo:", e);
+    }
+
+    document.title = appIdToUse ? `WaveApp Builder (${appIdToUse})` : "WaveApp Builder";
+    globalStore.set(atoms.builderAppId, appIdToUse);

and (optionally) mirroring the RTInfo lookup in reinitBuilder so the title stays consistent across re-inits.

Also applies to: 276-277, 292-303

🤖 Prompt for AI Agents
In frontend/wave.ts around lines 250-252 (also relevant at 276-277 and 292-303):
the window title is being set from the raw init opts (savedBuilderInitOpts.appId
/ initOpts.appId) which can differ from the resolved appId coming from RTInfo
(atoms.builderAppId / appIdToUse). Update reinitBuilder and initBuilder to set
document.title from the resolved appIdToUse (or atoms.builderAppId after RTInfo
resolution) instead of the raw init options; if reinitBuilder does not run the
RTInfo resolution, mirror the RTInfo lookup there or read atoms.builderAppId and
use that to compose the title so the UI title always reflects the actual appId
in use.

Comment on lines 127 to 137
settings := wconfig.GetWatcher().GetFullConfig().Settings
scaffoldPath := settings.TsunamiScaffoldPath
if scaffoldPath == "" {
return fmt.Errorf("tsunami:scaffoldpath is required")
scaffoldPath = filepath.Join(wavebase.GetWaveAppPath(), "tsunamiscaffold")
}
scaffoldPath, err := wavebase.ExpandHomeDir(scaffoldPath)
if err != nil {
return fmt.Errorf("tsunami:scaffoldpath invalid: %w", err)
}
if !filepath.IsAbs(scaffoldPath) {
return fmt.Errorf("tsunami:scaffoldpath must be absolute: %s", scaffoldPath)
}

sdkReplacePath := blockMeta.GetString(waveobj.MetaKey_TsunamiSdkReplacePath, "")
if sdkReplacePath == "" {
return fmt.Errorf("tsunami:sdkreplacepath is required")
}
sdkReplacePath, err = wavebase.ExpandHomeDir(sdkReplacePath)
if err != nil {
return fmt.Errorf("tsunami:sdkreplacepath invalid: %w", err)
}
if !filepath.IsAbs(sdkReplacePath) {
return fmt.Errorf("tsunami:sdkreplacepath must be absolute: %s", sdkReplacePath)
sdkReplacePath := settings.TsunamiSdkReplacePath
sdkVersion := settings.TsunamiSdkVersion
if sdkVersion == "" {
sdkVersion = "v0.12.2"
}
goPath := settings.TsunamiGoPath
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

Add nil checks and use constant for SDK version.

The settings retrieval lacks defensive nil checks and uses a hardcoded SDK version string.

Apply this diff to improve safety and maintainability:

+	watcher := wconfig.GetWatcher()
+	if watcher == nil {
+		return fmt.Errorf("config watcher not initialized")
+	}
+	fullConfig := watcher.GetFullConfig()
+	if fullConfig.Settings == nil {
+		return fmt.Errorf("settings not initialized")
+	}
+	
-	settings := wconfig.GetWatcher().GetFullConfig().Settings
+	settings := fullConfig.Settings
 	scaffoldPath := settings.TsunamiScaffoldPath
 	if scaffoldPath == "" {
 		scaffoldPath = filepath.Join(wavebase.GetWaveAppPath(), "tsunamiscaffold")
 	}
 	sdkReplacePath := settings.TsunamiSdkReplacePath
 	sdkVersion := settings.TsunamiSdkVersion
 	if sdkVersion == "" {
-		sdkVersion = "v0.12.2"
+		// TODO: Import and use constant from tsunami/cmd package
+		sdkVersion = "v0.12.2"  // Matches TsunamiSdkVersion in tsunami/cmd/main-tsunami.go
 	}
 	goPath := settings.TsunamiGoPath

Committable suggestion skipped: line range outside the PR's diff.

Comment on lines +298 to +320
func (bc *BuilderController) runBuilderApp(ctx context.Context, appId string, appBinPath string, builderEnv map[string]string) (*BuilderProcess, error) {
manifest, err := waveappstore.ReadAppManifest(appId)
if err != nil {
return nil, fmt.Errorf("failed to read app manifest: %w", err)
}

secretBindings, err := waveappstore.ReadAppSecretBindings(appId)
if err != nil {
return nil, fmt.Errorf("failed to read secret bindings: %w", err)
}

secretEnv, err := waveappstore.BuildAppSecretEnv(appId, manifest, secretBindings)
if err != nil {
return nil, fmt.Errorf("failed to build secret environment: %w", err)
}

if builderEnv == nil {
builderEnv = make(map[string]string)
}
for k, v := range secretEnv {
builderEnv[k] = v
}

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

Running apps now hard‑require a manifest; consider treating “no manifest” as optional

The new runBuilderApp path fails fast on any ReadAppManifest error:

manifest, err := waveappstore.ReadAppManifest(appId)
if err != nil {
    return nil, fmt.Errorf("failed to read app manifest: %w", err)
}

Given ReadAppManifest wraps os.ReadFile errors, this will break running existing apps that don’t yet ship a manifest (e.g., missing manifest.json), whereas BuildAppSecretEnv already handles manifest == nil gracefully.

If manifests are not guaranteed to exist for all apps at this point, this is a backward‑compatibility issue and will cause builder runs to fail unnecessarily.

You can keep missing manifests non‑fatal while still surfacing real errors by allowing the “file not found” case:

-    manifest, err := waveappstore.ReadAppManifest(appId)
-    if err != nil {
-        return nil, fmt.Errorf("failed to read app manifest: %w", err)
-    }
+    manifest, err := waveappstore.ReadAppManifest(appId)
+    if err != nil && !os.IsNotExist(err) {
+        return nil, fmt.Errorf("failed to read app manifest: %w", err)
+    }

This leaves manifest as nil when the file doesn’t exist, so BuildAppSecretEnv will no-op and you still get strict behavior for malformed or otherwise broken manifest files.

🤖 Prompt for AI Agents
In pkg/buildercontroller/buildercontroller.go around lines 298 to 320, change
the ReadAppManifest error handling so a missing manifest file is non‑fatal: call
ReadAppManifest, and if it returns an error check if the error is a
file-not-found (os.IsNotExist or wrapped equivalent); if so leave manifest as
nil and continue, otherwise return the wrapped error as today. This preserves
backward compatibility by allowing BuildAppSecretEnv to handle a nil manifest
while still surfacing real I/O or parse errors.

editStr = "edit"
}

result := make([]string, len(edits)+1)

Check failure

Code scanning / CodeQL

Size computation for allocation may overflow High

This operation, which is used in an
allocation
, involves a
potentially large value
and might overflow.

Copilot Autofix

AI 5 days ago

The best fix is to validate the size of the input before it is used to allocate memory, and reject unreasonably large input with a clear error.

  • The limit should be defined at a sensible maximum for your use-case. For an array of edit specifications, a value like 1000 is almost certainly enough for all normal operation and protects against both overflow and OOM attacks.
  • Change parseBuilderEditAppFileInput in tools_builder.go to check that len(result.Edits) does not exceed this maximum (e.g., 1000).
  • If exceeded, return a user-friendly error such as "too many edits requested (max 1000)".
  • No other regions need adjustment, since after this check, all downstream code can safely assume a reasonable length.

Suggested changeset 1
pkg/aiusechat/tools_builder.go

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/pkg/aiusechat/tools_builder.go b/pkg/aiusechat/tools_builder.go
--- a/pkg/aiusechat/tools_builder.go
+++ b/pkg/aiusechat/tools_builder.go
@@ -165,6 +165,11 @@
 		return nil, fmt.Errorf("missing edits parameter")
 	}
 
+	const maxEdits = 1000
+	if len(result.Edits) > maxEdits {
+		return nil, fmt.Errorf("too many edits requested (maximum %d allowed)", maxEdits)
+	}
+
 	return result, nil
 }
 
EOF
@@ -165,6 +165,11 @@
return nil, fmt.Errorf("missing edits parameter")
}

const maxEdits = 1000
if len(result.Edits) > maxEdits {
return nil, fmt.Errorf("too many edits requested (maximum %d allowed)", maxEdits)
}

return result, nil
}

Copilot is powered by AI and may make mistakes. Always verify output.
// ReplaceInFilePartial applies edits incrementally up to the first failure.
// Returns the results for each edit and writes the partially modified content.
func ReplaceInFilePartial(filePath string, edits []EditSpec) ([]EditResult, error) {
fileInfo, err := os.Stat(filePath)

Check failure

Code scanning / CodeQL

Uncontrolled data used in path expression High

This path depends on a
user-provided value
.

Copilot Autofix

AI 5 days ago

Copilot could not generate an autofix suggestion

Copilot could not generate an autofix suggestion for this alert. Try pushing a new commit or if the problem persists contact support.

return nil, fmt.Errorf("file too large for editing: %d bytes (max: %d)", fileInfo.Size(), MaxEditFileSize)
}

contents, err := os.ReadFile(filePath)

Check failure

Code scanning / CodeQL

Uncontrolled data used in path expression High

This path depends on a
user-provided value
.

Copilot Autofix

AI 5 days ago

Copilot could not generate an autofix suggestion

Copilot could not generate an autofix suggestion for this alert. Try pushing a new commit or if the problem persists contact support.


modifiedContents, results := ApplyEditsPartial(contents, edits)

if err := os.WriteFile(filePath, modifiedContents, fileInfo.Mode()); err != nil {

Check failure

Code scanning / CodeQL

Uncontrolled data used in path expression High

This path depends on a
user-provided value
.

Copilot Autofix

AI 5 days ago

Copilot could not generate an autofix suggestion

Copilot could not generate an autofix suggestion for this alert. Try pushing a new commit or if the problem persists contact support.

}

manifestPath := filepath.Join(appDir, ManifestFileName)
data, err := os.ReadFile(manifestPath)

Check failure

Code scanning / CodeQL

Uncontrolled data used in path expression High

This path depends on a
user-provided value
.

Copilot Autofix

AI 5 days ago

To fully mitigate path traversal risks, the code should enforce that any path constructed from user input is always strictly contained within a designated safe directory. In this situation, the home directory + "waveapps" is intended as the root for all app directories. The fix involves updating GetAppDir so that, after constructing the path using the validated appId components, it resolves the absolute path and checks that it starts with the absolute safe root directory. If the check fails, the function should return an error, rejecting the input no matter what. This complements existing regex and length validation, and makes the containment explicit and robust.

To implement this:

  • Update the GetAppDir function to use the absolute path for both the computed app directory and the apps root, and compare prefixes (using strings.HasPrefix or similar, taking care to avoid false matches for e.g. /home/foo/bar vs /home/foo/barbaz).
  • Return an error if the check fails.
  • No new imports are needed—existing imports suffice.
  • All changes are confined to pkg/waveappstore/waveappstore.go, specifically in the GetAppDir function.
Suggested changeset 1
pkg/waveappstore/waveappstore.go

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/pkg/waveappstore/waveappstore.go b/pkg/waveappstore/waveappstore.go
--- a/pkg/waveappstore/waveappstore.go
+++ b/pkg/waveappstore/waveappstore.go
@@ -82,7 +82,21 @@
 	}
 	appNS, appName, _ := ParseAppId(appId)
 	homeDir := wavebase.GetHomeDir()
-	return filepath.Join(homeDir, "waveapps", appNS, appName), nil
+	appsRoot := filepath.Join(homeDir, "waveapps")
+	appDir := filepath.Join(appsRoot, appNS, appName)
+	absAppsRoot, err := filepath.Abs(appsRoot)
+	if err != nil {
+		return "", fmt.Errorf("failed to get absolute apps root: %w", err)
+	}
+	absAppDir, err := filepath.Abs(appDir)
+	if err != nil {
+		return "", fmt.Errorf("failed to get absolute app dir: %w", err)
+	}
+	// Ensure absAppDir is strictly inside absAppsRoot
+	if absAppDir == absAppsRoot || !strings.HasPrefix(absAppDir, absAppsRoot+string(os.PathSeparator)) {
+		return "", fmt.Errorf("app directory resolves outside of apps root")
+	}
+	return absAppDir, nil
 }
 
 func copyDir(src, dst string) error {
EOF
@@ -82,7 +82,21 @@
}
appNS, appName, _ := ParseAppId(appId)
homeDir := wavebase.GetHomeDir()
return filepath.Join(homeDir, "waveapps", appNS, appName), nil
appsRoot := filepath.Join(homeDir, "waveapps")
appDir := filepath.Join(appsRoot, appNS, appName)
absAppsRoot, err := filepath.Abs(appsRoot)
if err != nil {
return "", fmt.Errorf("failed to get absolute apps root: %w", err)
}
absAppDir, err := filepath.Abs(appDir)
if err != nil {
return "", fmt.Errorf("failed to get absolute app dir: %w", err)
}
// Ensure absAppDir is strictly inside absAppsRoot
if absAppDir == absAppsRoot || !strings.HasPrefix(absAppDir, absAppsRoot+string(os.PathSeparator)) {
return "", fmt.Errorf("app directory resolves outside of apps root")
}
return absAppDir, nil
}

func copyDir(src, dst string) error {
Copilot is powered by AI and may make mistakes. Always verify output.
}

bindingsPath := filepath.Join(appDir, SecretBindingsFileName)
data, err := os.ReadFile(bindingsPath)

Check failure

Code scanning / CodeQL

Uncontrolled data used in path expression High

This path depends on a
user-provided value
.

Copilot Autofix

AI 5 days ago

To guarantee that paths constructed from a user-supplied appId are always contained within the desired app home, defensive validation should be applied after constructing the absolute path. After creating the appDir in GetAppDir, normalize it to absolute form and check that it is strictly a subdirectory of the intended apps root directory (e.g. $HOME/waveapps/). If the resolved path does not start with the canonical apps root prefix, reject it. This guards against any flaws in predecessor validation or possible future modifications.

Specifically, edit the function GetAppDir in pkg/waveappstore/waveappstore.go:

  • After constructing appDir, resolve it to an absolute path.
  • Also resolve the app home root itself to an absolute path.
  • Check that the absolute appDir begins with the root prefix and is not equal to the root itself.
  • Return an error if the check fails.

Add a testable helper for this containment check if desired.

No new imports are required; the necessary filepath and strings packages are available.


Suggested changeset 1
pkg/waveappstore/waveappstore.go

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/pkg/waveappstore/waveappstore.go b/pkg/waveappstore/waveappstore.go
--- a/pkg/waveappstore/waveappstore.go
+++ b/pkg/waveappstore/waveappstore.go
@@ -82,7 +82,23 @@
 	}
 	appNS, appName, _ := ParseAppId(appId)
 	homeDir := wavebase.GetHomeDir()
-	return filepath.Join(homeDir, "waveapps", appNS, appName), nil
+	appsRootDir := filepath.Join(homeDir, "waveapps")
+	appDir := filepath.Join(appsRootDir, appNS, appName)
+	absAppsRootDir, err := filepath.Abs(appsRootDir)
+	if err != nil {
+		return "", fmt.Errorf("failed to resolve apps root dir: %w", err)
+	}
+	absAppDir, err := filepath.Abs(appDir)
+	if err != nil {
+		return "", fmt.Errorf("failed to resolve app dir: %w", err)
+	}
+	absAppsRootDir = filepath.Clean(absAppsRootDir)
+	absAppDir = filepath.Clean(absAppDir)
+	// Ensure absAppDir is strictly a subdirectory; equal is not allowed
+	if !strings.HasPrefix(absAppDir, absAppsRootDir+string(os.PathSeparator)) {
+		return "", fmt.Errorf("resolved app dir escapes app root")
+	}
+	return absAppDir, nil
 }
 
 func copyDir(src, dst string) error {
EOF
@@ -82,7 +82,23 @@
}
appNS, appName, _ := ParseAppId(appId)
homeDir := wavebase.GetHomeDir()
return filepath.Join(homeDir, "waveapps", appNS, appName), nil
appsRootDir := filepath.Join(homeDir, "waveapps")
appDir := filepath.Join(appsRootDir, appNS, appName)
absAppsRootDir, err := filepath.Abs(appsRootDir)
if err != nil {
return "", fmt.Errorf("failed to resolve apps root dir: %w", err)
}
absAppDir, err := filepath.Abs(appDir)
if err != nil {
return "", fmt.Errorf("failed to resolve app dir: %w", err)
}
absAppsRootDir = filepath.Clean(absAppsRootDir)
absAppDir = filepath.Clean(absAppDir)
// Ensure absAppDir is strictly a subdirectory; equal is not allowed
if !strings.HasPrefix(absAppDir, absAppsRootDir+string(os.PathSeparator)) {
return "", fmt.Errorf("resolved app dir escapes app root")
}
return absAppDir, nil
}

func copyDir(src, dst string) error {
Copilot is powered by AI and may make mistakes. Always verify output.
func generateManifest(tempDir, exePath string, opts BuildOpts) error {
oc := opts.OutputCapture

manifestCmd := exec.Command(exePath, "--manifest")

Check failure

Code scanning / CodeQL

Command built from user-controlled sources Critical

This command depends on a
user-provided value
.
Copy link
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: 0

♻️ Duplicate comments (2)
frontend/wave.ts (1)

251-251: Builder window title can still drift from actual appId on reinit.

While initBuilder now correctly sets the title from the resolved appIdToUse (line 301), reinitBuilder still sets the title directly from savedBuilderInitOpts.appId without checking RTInfo. If the user previously selected a different app in the builder and it was persisted to builder:appid in RTInfo, the title will be stale after reinit.

Consider mirroring the RTInfo lookup from initBuilder or reading from atoms.builderAppId:

 async function reinitBuilder() {
     console.log("Reinit Builder");
     getApi().sendLog("Reinit Builder");
 
     // We use this hack to prevent a flicker of the previously-hovered tab when this view was last active.
     document.body.classList.add("nohover");
     requestAnimationFrame(() =>
         setTimeout(() => {
             document.body.classList.remove("nohover");
         }, 100)
     );
 
     await WOS.reloadWaveObject<Client>(WOS.makeORef("client", savedBuilderInitOpts.clientId));
-    document.title = savedBuilderInitOpts.appId ? `WaveApp Builder (${savedBuilderInitOpts.appId})` : "WaveApp Builder";
+    
+    let appIdToUse = savedBuilderInitOpts.appId;
+    try {
+        const oref = WOS.makeORef("builder", savedBuilderInitOpts.builderId);
+        const rtInfo = await RpcApi.GetRTInfoCommand(TabRpcClient, { oref });
+        if (rtInfo && rtInfo["builder:appid"]) {
+            appIdToUse = rtInfo["builder:appid"];
+        }
+    } catch (e) {
+        console.log("Could not load saved builder appId from rtinfo:", e);
+    }
+    
+    document.title = appIdToUse ? `WaveApp Builder (${appIdToUse})` : "WaveApp Builder";
+    globalStore.set(atoms.builderAppId, appIdToUse);
+    
     getApi().setWindowInitStatus("wave-ready");
     globalStore.set(atoms.reinitVersion, globalStore.get(atoms.reinitVersion) + 1);
     globalStore.set(atoms.updaterStatusAtom, getApi().getUpdaterStatus());
pkg/blockcontroller/tsunamicontroller.go (1)

129-139: Add nil checks to prevent potential panics.

The configuration retrieval chain lacks defensive nil checks, which could lead to panics if the watcher or config is not properly initialized.

Apply this diff to add defensive checks:

+	watcher := wconfig.GetWatcher()
+	if watcher == nil {
+		return fmt.Errorf("config watcher not initialized")
+	}
+	fullConfig := watcher.GetFullConfig()
+	if fullConfig.Settings == nil {
+		return fmt.Errorf("settings not initialized")
+	}
+	
-	settings := wconfig.GetWatcher().GetFullConfig().Settings
+	settings := fullConfig.Settings
🧹 Nitpick comments (2)
pkg/aiusechat/openai/openai-backend.go (1)

861-879: Consider extracting progress emission into a helper function.

The progress data construction and emission logic (lines 870-875) is duplicated in the done handler (lines 904-909). Extracting this into a helper reduces duplication and improves maintainability.

Example helper function you can add before handleOpenAIEvent:

// emitToolProgress sends tool progress data via SSE if parsing succeeds
func emitToolProgress(sse *sse.SSEHandlerCh, toolCallID, toolName string, toolDef *uctypes.ToolDefinition, jsonData any) {
	if toolDef == nil || toolDef.ToolProgressDesc == nil {
		return
	}
	statusLines, err := toolDef.ToolProgressDesc(jsonData)
	if err == nil {
		progressData := &uctypes.UIMessageDataToolProgress{
			ToolCallId:  toolCallID,
			ToolName:    toolName,
			StatusLines: statusLines,
		}
		_ = sse.AiMsgData("data-toolprogress", "progress-"+toolCallID, progressData)
	}
}

Then simplify the delta handler:

 		if st := state.blockMap[ev.ItemId]; st != nil && st.kind == openaiBlockToolUse {
 			st.partialJSON = append(st.partialJSON, []byte(ev.Delta)...)
 
-			toolDef := state.chatOpts.GetToolDefinition(st.toolName)
-			if toolDef != nil && toolDef.ToolProgressDesc != nil {
-				parsedJSON, err := utilfn.ParsePartialJson(st.partialJSON)
-				if err == nil {
-					statusLines, err := toolDef.ToolProgressDesc(parsedJSON)
-					if err == nil {
-						progressData := &uctypes.UIMessageDataToolProgress{
-							ToolCallId:  st.toolCallID,
-							ToolName:    st.toolName,
-							StatusLines: statusLines,
-						}
-						_ = sse.AiMsgData("data-toolprogress", "progress-"+st.toolCallID, progressData)
-					}
-				}
-			}
+			if parsedJSON, err := utilfn.ParsePartialJson(st.partialJSON); err == nil {
+				toolDef := state.chatOpts.GetToolDefinition(st.toolName)
+				emitToolProgress(sse, st.toolCallID, st.toolName, toolDef, parsedJSON)
+			}
 		}

And the done handler:

-			if toolDef != nil && toolDef.ToolProgressDesc != nil {
-				var parsedJSON any
-				if err := json.Unmarshal([]byte(ev.Arguments), &parsedJSON); err == nil {
-					statusLines, err := toolDef.ToolProgressDesc(parsedJSON)
-					if err == nil {
-						progressData := &uctypes.UIMessageDataToolProgress{
-							ToolCallId:  st.toolCallID,
-							ToolName:    st.toolName,
-							StatusLines: statusLines,
-						}
-						_ = sse.AiMsgData("data-toolprogress", "progress-"+st.toolCallID, progressData)
-					}
-				}
-			}
+			var parsedJSON any
+			if err := json.Unmarshal([]byte(ev.Arguments), &parsedJSON); err == nil {
+				toolDef := state.chatOpts.GetToolDefinition(st.toolName)
+				emitToolProgress(sse, st.toolCallID, st.toolName, toolDef, parsedJSON)
+			}
pkg/blockcontroller/tsunamicontroller.go (1)

32-33: Consider centralizing the SDK version constant.

The version string is now duplicated between this package and tsunami/cmd/main-tsunami.go. While importing from a cmd package is not ideal, consider moving this constant to a shared package (e.g., pkg/tsunamiutil or a new pkg/tsunamiversion) that both locations can import.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 624ecff and 5e892b3.

📒 Files selected for processing (5)
  • frontend/app/view/tsunami/tsunami.tsx (2 hunks)
  • frontend/wave.ts (3 hunks)
  • pkg/aiusechat/openai/openai-backend.go (3 hunks)
  • pkg/blockcontroller/tsunamicontroller.go (3 hunks)
  • pkg/util/utilfn/partial.go (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • frontend/app/view/tsunami/tsunami.tsx
  • pkg/util/utilfn/partial.go
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2025-10-21T05:09:26.916Z
Learnt from: sawka
Repo: wavetermdev/waveterm PR: 2465
File: frontend/app/onboarding/onboarding-upgrade.tsx:13-21
Timestamp: 2025-10-21T05:09:26.916Z
Learning: In the waveterm codebase, clientData is loaded and awaited in wave.ts before React runs, ensuring it is always available when components mount. This means atoms.client will have data on first render.

Applied to files:

  • frontend/wave.ts
📚 Learning: 2025-10-15T03:21:02.229Z
Learnt from: sawka
Repo: wavetermdev/waveterm PR: 2433
File: pkg/aiusechat/tools_readfile.go:197-197
Timestamp: 2025-10-15T03:21:02.229Z
Learning: In Wave Terminal's AI tool definitions (pkg/aiusechat/tools_*.go), the Description field should not mention approval requirements even when ToolApproval returns ApprovalNeedsApproval. This prevents the LLM from asking users for approval before calling the tool, avoiding redundant double-approval prompts since the runtime will enforce approval anyway.

Applied to files:

  • pkg/aiusechat/openai/openai-backend.go
🧬 Code graph analysis (2)
pkg/aiusechat/openai/openai-backend.go (2)
pkg/util/utilfn/partial.go (1)
  • ParsePartialJson (167-175)
pkg/aiusechat/uctypes/usechat-types.go (1)
  • UIMessageDataToolProgress (166-170)
pkg/blockcontroller/tsunamicontroller.go (5)
pkg/wconfig/filewatcher.go (1)
  • GetWatcher (33-57)
pkg/wavebase/wavebase.go (2)
  • GetWaveAppPath (113-115)
  • ExpandHomeDir (141-155)
tsunami/cmd/main-tsunami.go (1)
  • TsunamiSdkVersion (17-17)
pkg/waveobj/metaconsts.go (2)
  • MetaKey_TsunamiAppPath (129-129)
  • MetaKey_TsunamiAppId (130-130)
pkg/waveappstore/waveappstore.go (1)
  • GetAppDir (79-86)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: Analyze (go)
  • GitHub Check: Analyze (javascript-typescript)
  • GitHub Check: Build for TestDriver.ai
🔇 Additional comments (6)
pkg/aiusechat/openai/openai-backend.go (2)

385-385: LGTM!

The new partialJSON field is appropriately typed and well-documented for tracking accumulated JSON arguments during streaming.


898-912: Progress emission logic is correct.

This segment correctly emits final progress data using the complete arguments. The code duplication with the delta handler has been flagged in the previous comment.

frontend/wave.ts (1)

289-302: Good fix: title now uses resolved appId from RTInfo.

The implementation correctly resolves appIdToUse from RTInfo (builder:appid) before setting the document title. This ensures the title reflects the actual app being edited rather than the initial initOpts.appId, addressing the concern from the previous review for initBuilder.

pkg/blockcontroller/tsunamicontroller.go (3)

141-161: LGTM! Well-structured appPath resolution with proper validation.

The logic correctly handles both tsunami:apppath and tsunami:appid metadata with appropriate fallback behavior, home directory expansion, and absolute path validation. Error handling is comprehensive.


182-193: LGTM! BuildOpts correctly propagates new configuration fields.

The addition of SdkVersion and GoPath fields properly integrates the centralized configuration into the build flow.


226-229: LGTM! Appropriate async pattern for schema retrieval.

The asynchronous fetch ensures schema retrieval doesn't block the Start() method, and errors are properly logged within fetchAndSetSchemas.

@sawka sawka merged commit 7ac2f25 into main Nov 14, 2025
6 of 8 checks passed
@sawka sawka deleted the sawka/tsunami-manifest branch November 14, 2025 01:54
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.

2 participants