Skip to content

Conversation

@sawka
Copy link
Member

@sawka sawka commented Nov 19, 2025

  • Kebab Menu + Switch App Functionality
  • Updated Secrets Tab (still more to do, but closer)
  • Better Error Handling (Switch to Secrets Tab)
  • Add AppInit functionality to Tsunami (fixes initialization issues and races with embedded assets, and error handling)
  • Tsunami - ListStaticFiles
  • Tsunami - More Robust Manifest generation (even when there are extra stdout messages)

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Nov 19, 2025

Caution

Review failed

The pull request is closed.

Walkthrough

The diff migrates builder appId storage from saved init options into a new BuilderWindowType.builderAppId field and RT info; adds a "do-refresh" IPC path and exposes preload.doRefresh; introduces StopBuilderCommand across RPC server, client, and frontend APIs; replaces the builder "Env" tab with a Secrets UI and modal plus secret-related RPCs and error handling; adjusts builder UI styling and adds a dev badge; and refactors the tsunami build/init flow: adds AppInit(fn) that returns error, AST validation for AppInit, changes scaffold/template copy behavior and manifest extraction, and adds static-file listing utilities.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

  • High-attention areas:

    • frontend/builder/store/builder-apppanel-model.ts — new atoms, switchBuilderApp flow, restartBuilder changes, StopBuilderCommand and RT info interactions.
    • tsunami/build/* and tsunami/engine/* — AppInit signature change, build AST validation, copyScaffoldFS signature and template-copy behavior, manifest extraction, and build CLI/taskfile impacts.
    • Secrets UI vs removed Env UI — frontend/builder/tabs/* (deleted builder-envtab.tsx, added builder-secrettab.tsx) — UX parity, RPC error handling (ERR-SECRET), modal accessibility, and state persistence.
    • IPC/preload/appId migration — emain/emain-builder.ts, emain/emain-ipc.ts, emain/preload.ts, frontend/types/custom.d.ts, frontend/wave.ts — consistency of builderAppId usage and RT info writes/reads.
    • RPC surface additions — pkg/wshrpc (types, client, server) and frontend wshclientapi.ts — parameter shapes, error propagation, and lifecycle semantics.
  • Secondary checks:

    • tsunami/app/defaultclient.go RegisterAppInitFn change and ListStaticFiles behavior.
    • Taskfile.yml and other build scripts referencing scaffold/template names or copyScaffoldFS signature.

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 25.00% 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 title 'More Builder Improvements + Tsunami AppInit' accurately captures the main changes: builder UI enhancements and new Tsunami AppInit functionality across multiple files.
Description check ✅ Passed The description relates directly to the changeset, covering kebab menu, secrets tab updates, error handling, AppInit functionality, ListStaticFiles, and manifest generation improvements.

📜 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 629213c and 08b4bcb.

📒 Files selected for processing (1)
  • tsunami/build/build.go (12 hunks)

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: 6

🧹 Nitpick comments (9)
pkg/buildercontroller/buildercontroller.go (1)

156-156: Clarify or remove cryptic debug log.

The log format "*** BC %s" is unclear. If this is temporary debug code, consider removing it. If it's intended to remain, make it more descriptive with proper context.

Apply this diff for a more descriptive log:

-	log.Printf("*** BC %s\n", appId)
+	log.Printf("BuilderController[%s]: starting app %s\n", bc.builderId, appId)

Or remove it entirely if it's temporary:

-	log.Printf("*** BC %s\n", appId)
frontend/builder/tabs/builder-previewtab.tsx (1)

33-89: Secret‑error handling and navigation to Secrets tab are wired correctly

Detecting "ERR-SECRET" in displayMsg and short‑circuiting to a dedicated “Secrets Required” view, then switching tabs via BuilderAppPanelModel.setActiveTab("secrets"), is coherent with the new secrets flow. Only minor nit: substring matching on the raw message is a bit brittle long‑term but acceptable for now.

emain/emain-builder.ts (1)

35-49: Consider isolating RT‑info failures from window creation

Right now, a failure in SetRTInfoCommand will reject createBuilderWindow and prevent the window from appearing. If you want the builder window to still open even when RT‑info cannot be persisted, you could wrap this block in a try/catch and log instead of propagating.

frontend/builder/store/builder-apppanel-model.ts (1)

214-229: switchBuilderApp flow is reasonable, but relies on fixed delays

Stopping the builder, clearing "builder:appid" via SetRTInfoCommand, syncing the main process with setBuilderWindowAppId(null), and finally calling doRefresh() is a coherent way to bounce back to the app‑selection state. The fixed 500 ms/100 ms waits are heuristic; if you see flakiness, consider wiring this to explicit status/route events instead.

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

200-201: Guard secrets tab content when hasSecrets is false

The "Secrets" tab button is gated on hasSecrets, but the content panel is only gated on activeTab === "secrets". If activeTab ever remains "secrets" while hasSecrets becomes false (e.g., manifest change), users could end up on a hidden tab. Either enforce in BuilderAppPanelModel that activeTab is never "secrets" when hasSecretsAtom is false, or mirror the guard here:

-                <div className="w-full h-full" style={{ display: activeTab === "secrets" ? "block" : "none" }}>
-                    <ErrorBoundary>
-                        <BuilderEnvTab />
-                    </ErrorBoundary>
-                </div>
+                {hasSecrets && (
+                    <div className="w-full h-full" style={{ display: activeTab === "secrets" ? "block" : "none" }}>
+                        <ErrorBoundary>
+                            <BuilderEnvTab />
+                        </ErrorBoundary>
+                    </div>
+                )}

Also applies to: 320-328, 364-368

frontend/builder/tabs/builder-secrettab.tsx (3)

25-47: Clarify status/tooltip for invalid mappings

SecretRow distinguishes between “not mapped” and “mapped & valid”, but the tooltip text always says “Secret Has a Valid Mapping” whenever isMapped is true, even when isValid is false (no checkmark, but also no warning icon or different tooltip).

Consider explicitly handling the “mapped but not found in availableSecrets” case so users see that as an error/warning instead of a valid mapping. For example, add a separate branch with a warning icon + “Mapped to unknown secret” tooltip keyed off isMapped && !isValid.


174-190: Avoid calling setLocalBindings inside render; derive from secretBindings in an effect

The initialization:

if (!localBindings || Object.keys(localBindings).length === 0) {
    if (Object.keys(secretBindings).length > 0) {
        setLocalBindings({ ...secretBindings });
    }
}

runs during render and mutates state, which is generally discouraged and can be brittle with future React behavior. It also makes the relationship between builderStatus.secretbindings and localBindings less explicit.

Recommend moving this into a useEffect that depends on secretBindings and only seeds localBindings when they’re empty, e.g.:

useEffect(() => {
    if (Object.keys(secretBindings).length > 0 && Object.keys(localBindings).length === 0) {
        setLocalBindings({ ...secretBindings });
    }
}, [secretBindings, localBindings]);

This keeps state transitions in effects and makes updates predictable.


230-235: Consider deduping availableSecrets when adding a new secret

handleSetAndMap unconditionally appends secretName:

setAvailableSecrets((prev) => [...prev, secretName]);

If SetSecretsCommand is retried or the user goes through the dialog multiple times for the same name, this can introduce duplicates in availableSecrets, which may affect any future UI that enumerates it.

Minor, but you can cheaply guard against this with a set-style update:

setAvailableSecrets((prev) =>
    prev.includes(secretName) ? prev : [...prev, secretName],
);
tsunami/build/build.go (1)

1071-1169: Scaffold copy behavior is correct; small nit on duplicate JSON copies

The revised copyScaffoldFS does the right high-level things:

  • Handles nm via symlink-or-copy.
  • Copies dist/.
  • Always copies app-main.go.tmplapp-main.go.
  • Conditionally copies app-init.go.tmplapp-init.go when hasAppInit is true.
  • Then copies *.md, *.json, and tailwind.css via fs.Glob.

Functionally this is fine, but note that package.json and package-lock.json are copied twice: once explicitly in packageFiles, and again when matching *.json. It’s harmless (the second copy overwrites the first, only fileCount is inflated), but if you care about efficiency or accurate stats you could exclude those from the globbed patterns or special-case them when iterating matches.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 44a0688 and af1f898.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (27)
  • Taskfile.yml (2 hunks)
  • emain/emain-builder.ts (3 hunks)
  • emain/emain-ipc.ts (3 hunks)
  • emain/preload.ts (1 hunks)
  • frontend/app/aipanel/thinkingmode.tsx (3 hunks)
  • frontend/app/modals/modalregistry.tsx (2 hunks)
  • frontend/app/store/wshclientapi.ts (1 hunks)
  • frontend/builder/builder-app.tsx (2 hunks)
  • frontend/builder/builder-apppanel.tsx (5 hunks)
  • frontend/builder/builder-buildpanel.tsx (1 hunks)
  • frontend/builder/builder-workspace.tsx (1 hunks)
  • frontend/builder/store/builder-apppanel-model.ts (5 hunks)
  • frontend/builder/tabs/builder-envtab.tsx (0 hunks)
  • frontend/builder/tabs/builder-previewtab.tsx (2 hunks)
  • frontend/builder/tabs/builder-secrettab.tsx (1 hunks)
  • frontend/types/custom.d.ts (1 hunks)
  • frontend/wave.ts (1 hunks)
  • pkg/buildercontroller/buildercontroller.go (2 hunks)
  • pkg/wshrpc/wshclient/wshclient.go (1 hunks)
  • pkg/wshrpc/wshrpctypes.go (1 hunks)
  • pkg/wshrpc/wshserver/wshserver.go (1 hunks)
  • tsunami/app/defaultclient.go (5 hunks)
  • tsunami/build/build-ast.go (1 hunks)
  • tsunami/build/build.go (10 hunks)
  • tsunami/build/buildutil.go (1 hunks)
  • tsunami/engine/clientimpl.go (4 hunks)
  • tsunami/templates/app-init.go.tmpl (1 hunks)
💤 Files with no reviewable changes (1)
  • frontend/builder/tabs/builder-envtab.tsx
🧰 Additional context used
🧠 Learnings (3)
📚 Learning: 2025-10-15T03:18:52.647Z
Learnt from: sawka
Repo: wavetermdev/waveterm PR: 2433
File: emain/emain-window.ts:811-828
Timestamp: 2025-10-15T03:18:52.647Z
Learning: In emain/emain-window.ts, within the relaunchBrowserWindows function, ClientService.GetClientData() is guaranteed to return a valid client object and never null/undefined. The backend ensures a client is initialized before startup, so no null-guard is needed when accessing clientData.windowids.

Applied to files:

  • emain/emain-builder.ts
  • emain/emain-ipc.ts
📚 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
📚 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/builder/store/builder-apppanel-model.ts
🧬 Code graph analysis (13)
frontend/builder/tabs/builder-previewtab.tsx (1)
frontend/builder/store/builder-apppanel-model.ts (1)
  • BuilderAppPanelModel (21-316)
frontend/app/store/wshclientapi.ts (2)
frontend/app/store/wshclient.ts (1)
  • WshClient (159-159)
pkg/wshrpc/wshrpctypes.go (1)
  • RpcOpts (364-370)
emain/emain-builder.ts (2)
frontend/app/store/wshclientapi.ts (1)
  • RpcApi (682-682)
emain/emain-wsh.ts (1)
  • ElectronWshClient (116-116)
pkg/wshrpc/wshclient/wshclient.go (3)
frontend/app/store/wshclientapi.ts (1)
  • StopBuilderCommand (551-553)
pkg/wshutil/wshrpc.go (1)
  • WshRpc (47-61)
pkg/wshrpc/wshrpctypes.go (1)
  • RpcOpts (364-370)
pkg/wshrpc/wshserver/wshserver.go (3)
frontend/app/store/wshclientapi.ts (1)
  • StopBuilderCommand (551-553)
pkg/wshrpc/wshclient/wshclient.go (1)
  • StopBuilderCommand (662-665)
pkg/buildercontroller/buildercontroller.go (1)
  • GetOrCreateController (72-89)
pkg/wshrpc/wshrpctypes.go (2)
frontend/app/store/wshclientapi.ts (1)
  • StopBuilderCommand (551-553)
pkg/wshrpc/wshclient/wshclient.go (1)
  • StopBuilderCommand (662-665)
tsunami/build/build-ast.go (1)
tsunami/build/build.go (1)
  • MainAppFileName (32-32)
frontend/builder/tabs/builder-secrettab.tsx (7)
frontend/app/element/tooltip.tsx (1)
  • Tooltip (143-176)
frontend/app/store/modalmodel.ts (1)
  • modalsModel (45-45)
frontend/app/modals/modal.tsx (1)
  • Modal (116-116)
frontend/builder/store/builder-apppanel-model.ts (1)
  • BuilderAppPanelModel (21-316)
frontend/app/store/wshclientapi.ts (1)
  • RpcApi (682-682)
frontend/app/store/wshrpcutil.ts (1)
  • TabRpcClient (37-37)
frontend/app/store/global.ts (1)
  • atoms (816-816)
tsunami/app/defaultclient.go (1)
tsunami/engine/clientimpl.go (1)
  • GetDefaultClient (116-118)
tsunami/engine/clientimpl.go (1)
tsunami/app/defaultclient.go (1)
  • RegisterAppInitFn (55-57)
pkg/buildercontroller/buildercontroller.go (1)
pkg/waveappstore/waveappstore.go (1)
  • BuildAppSecretEnv (804-842)
tsunami/build/build.go (1)
tsunami/build/buildutil.go (1)
  • CopyFileFromFS (153-188)
frontend/builder/store/builder-apppanel-model.ts (3)
frontend/app/store/wshclientapi.ts (1)
  • RpcApi (682-682)
pkg/wshrpc/wshrpctypes.go (1)
  • BuilderStatusData (1077-1086)
frontend/app/store/global.ts (3)
  • atoms (816-816)
  • WOS (866-866)
  • getApi (826-826)
⏰ 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). (5)
  • GitHub Check: Analyze (javascript-typescript)
  • GitHub Check: Analyze (go)
  • GitHub Check: Build Docsite
  • GitHub Check: Build for TestDriver.ai
  • GitHub Check: merge-gatekeeper
🔇 Additional comments (32)
pkg/buildercontroller/buildercontroller.go (1)

305-310: LGTM! Error messages improved for better secret error identification.

The addition of (ERR-SECRET) markers makes it easier to identify and handle secret-related errors, which aligns well with the PR's objective of improved error handling and routing users to the Secrets Tab.

tsunami/build/buildutil.go (1)

1-3: LGTM!

Standard license header addition with no functional changes.

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

88-88: LGTM!

The addition of rounded-br-2 class provides consistent bottom-right border radius styling, coordinating with the parent container changes in builder-workspace.tsx.

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

130-130: LGTM!

The doRefresh method addition extends the Electron API to support window refresh functionality, appropriately using a void return type for this fire-and-forget operation.

pkg/wshrpc/wshrpctypes.go (1)

346-346: LGTM!

The StopBuilderCommand interface method is properly declared and well-positioned among related builder lifecycle methods. The signature is consistent with the implementation.

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

104-111: LGTM!

The border radius styling update to 8px on the outer container coordinates well with the panel styling to ensure consistent visual appearance.


118-123: LGTM!

The explicit borderBottomRightRadius: 8 style on the Panel ensures the build panel has consistent bottom-right corner rounding with its parent container.

Taskfile.yml (2)

542-542: LGTM!

Expanding the template copy pattern from a specific file to all *.go.tmpl files is more maintainable and supports the new AppInit functionality. This ensures all Go templates are included in the scaffold without requiring task file updates for each new template.


563-563: LGTM!

The Windows version mirrors the Unix template copy expansion, ensuring cross-platform consistency in scaffold generation.

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

48-48: Verify the hideQuick logic rationale.

The condition model.inBuilder && hasPremium hides the "quick" thinking mode for builder users with premium access. This removes user choice and might be unexpected.

Consider:

  1. Why should builder users with premium be prevented from using quick mode?
  2. Is there a performance/quality reason specific to builder context?
  3. Should this be a recommendation rather than enforced hiding?

The UX might be confusing if users in builder wonder why quick mode disappeared.


64-66: Conditional mapping is correct given the hideQuick behavior.

The logic properly maps "quick" to "balanced" when hideQuick is active, preventing an invalid UI state. However, this is contingent on the verification of the hideQuick rationale itself.


89-123: Implementation is technically correct.

The filtering and rendering logic properly handles the conditional hiding of "quick" mode, including correct recalculation of isFirst and isLast based on the filtered array. The implementation is sound, though it depends on the verification of the hideQuick business logic.

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

7-48: Dev badge and header tweaks look good

Importing isDev and conditionally rendering the dev badge in the drag-region header is consistent with existing patterns and side‑effect free. No functional issues spotted.

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

661-665: StopBuilderCommand wrapper matches existing RPC patterns

The new StopBuilderCommand helper mirrors other simple sendRpcRequestCallHelper[any] wrappers and aligns with the TypeScript StopBuilderCommand signature.

emain/emain-builder.ts (2)

14-18: Adding builderAppId to BuilderWindowType is consistent with new appId flow

Storing builderAppId directly on the BrowserWindow subtype gives main‑process IPC a clear source of truth and aligns with the RT‑info based propagation.


85-88: Window‑local builderAppId assignment matches main RT‑info state

Assigning typedBuilderWindow.builderAppId = appId right after creation keeps the Electron side in sync with the "builder:appid" RT‑info key and the renderer’s atoms.builderAppId.

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

545-553: StopBuilderCommand TS API is consistent with other RPC helpers

The new StopBuilderCommand method correctly delegates to wshRpcCall("stopbuilder", data, opts) and mirrors the existing StartBuilderCommand pattern.

frontend/wave.ts (1)

260-274: Builder appId initialization from RT info looks correct

Reading "builder:appid" from GetRTInfoCommand and deriving both document.title and atoms.builderAppId from that single source cleans up the previous dual‑source behaviour. The try/catch fallback to a neutral title on RT‑info failure is reasonable.

frontend/builder/store/builder-apppanel-model.ts (3)

8-34: Secrets tab and model wiring are aligned

Extending TabType with "secrets" and adding hasSecretsAtom into the panel model matches the new Secrets tab UX and gives a clean, atom‑backed flag for whether the manifest declares secrets.


81-98: Status subscription correctly updates secrets latch

Hooking updateSecretsLatch(status) both in the event handler and after the initial GetBuilderStatusCommand ensures hasSecretsAtom is populated as soon as a manifest with secrets arrives. Since this is a latch, it’s fine that it only flips from false → true.


116-122: updateSecretsLatch implementation matches latch semantics

Guarding on status?.manifest?.secrets and only setting hasSecretsAtom when the secrets map is non‑empty is simple and avoids unnecessary writes. Behaviour is monotonic, as implied by the “latch” name.

tsunami/templates/app-init.go.tmpl (1)

1-7: AppInit registration template looks consistent with new AppInitFn API

Using an init function to call app.RegisterAppInitFn(AppInit) ensures the app’s AppInit hook is wired automatically; just make sure generated/user AppInit uses the func() error signature expected by RegisterAppInitFn.

emain/preload.ts (1)

68-71: doRefresh IPC exposure aligns with backend handler

The new doRefresh API cleanly forwards to the "do-refresh" IPC channel and matches the handler added in emain-ipc.ts; just ensure the ElectronApi type in custom.d.ts includes this method.

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

8-22: SetSecretDialog modal registration matches existing pattern

Importing SetSecretDialog and registering it under SetSecretDialog.displayName || "SetSecretDialog" is consistent with the other entries and should make the modal available via modalsModel.

emain/emain-ipc.ts (1)

29-40: Builder window appId tracking and refresh IPC look correct

  • openBuilderWindow now de-duplicates by builderAppId, which matches the new field on BuilderWindowType and avoids multiple windows per appId (with "" covering the generic builder).
  • "set-builder-window-appid" updates bw.builderAppId, keeping the in-memory index consistent with the RT info written from createBuilderWindow.
  • "do-refresh" simply calls event.sender.reloadIgnoringCache(), which is an appropriate per‑webContents refresh hook for the new preload bridge.

Also applies to: 424-431, 455-457

tsunami/engine/clientimpl.go (1)

76-96: AppInitFn lifecycle and manifest markers look well-integrated

  • Storing an AppInitFn func() error on ClientImpl and invoking it at the start of runMainE with error propagation cleanly integrates initialization into the server lifecycle.
  • RegisterAppInitFn is a simple hook that exposes this to callers without extra complexity.
  • Wrapping PrintAppManifest output in <AppManifest>/</AppManifest> markers should make downstream parsing more robust when other stdout is present; just ensure any existing consumers of the plain JSON are updated accordingly.

Also applies to: 182-199, 514-524

tsunami/app/defaultclient.go (1)

52-57: AppInit registration and static FS helpers are wired sensibly

  • RegisterAppInitFn(fn func() error) cleanly delegates to the engine’s client and matches the updated ClientImpl API.
  • ReadStaticFile/OpenStaticFile now return clear errors when StaticFS is nil and when the path does not start with "static/", and the guidance to use AppInit for initialization‑time access is helpful for callers.

Also applies to: 206-236

tsunami/build/build-ast.go (2)

18-41: buildImportsMap behavior looks solid; note shallow scan and ignored parse errors

The import collection logic is straightforward and safe: it only inspects *.go files in the given directory, trims quotes from import paths, and skips files that fail parser.ParseFile with ImportsOnly. Just calling out that this is a shallow scan (no subdirs) and treats parse failures as “no imports from that file”, which seems fine for a heuristic/import-check use case but is worth keeping in mind if it’s later used for stricter validation.


47-94: AppInit/init validation is consistent with the new AppInit contract

parseAndValidateAppFile:

  • Gracefully handles missing/unparsable app.go by returning HasAppInit: false and no error, relying on other checks to enforce file presence.
  • Explicitly rejects a spec-compliant init() (no params/results), steering users to AppInit instead.
  • Enforces AppInit to be func AppInit() error exactly (no params, exactly one result of type error).

This matches the described behavior for the new AppInit contract and should fail fast with clear messages when signatures are wrong. No issues from a correctness standpoint.

tsunami/build/build.go (3)

450-476: MainAppFileName refactor improves consistency

Switching hard-coded "app.go" uses in verifyAppPathFs and GetAppModTime to the MainAppFileName constant is a nice cleanup and keeps the main app filename centralized. No behavioral issues here.


598-626: AppInit validation wiring into scaffold copy looks correct

TsunamiBuildInternal now calls:

appInfo, err := parseAndValidateAppFile(appFS)
...
scaffoldCount, err := copyScaffoldFS(scaffoldFS, tempDir, appInfo.HasAppInit, opts.Verbose, oc)

This cleanly feeds the AST-derived HasAppInit flag into scaffold copying, so copyScaffoldFS can decide whether to copy app-init.go.tmplapp-init.go. Failure to parse/validate app.go yields HasAppInit=false but still lets the build proceed (with Go catching syntax issues later), while invalid init/AppInit signatures cause immediate errors. The overall flow is consistent and there are no obvious nil or error-handling pitfalls here.


806-822: Manifest delimiter extraction is correct; consider diagnostics if delimiters missing

generateManifest now extracts only the content between <AppManifest> and </AppManifest>:

startIdx := strings.Index(manifestStr, startTag)
endIdx := strings.Index(manifestStr, endTag)

if startIdx == -1 || endIdx == -1 {
    return fmt.Errorf("manifest delimiters not found in output")
}

manifestJSON := manifestStr[startIdx+len(startTag) : endIdx]
manifestJSON = strings.TrimSpace(manifestJSON)

Semantics are sound: it fails fast when delimiters are absent and writes only the trimmed JSON region. If you run into debugging issues later, you might consider logging a small prefix of manifestStr to oc on delimiter failures, but as-is this is functionally correct.

Comment on lines +5 to 6
import { ContextMenuModel } from "@/app/store/contextmenu";
import { modalsModel } from "@/app/store/modalmodel";
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

ContextMenuItem type likely needs an explicit import

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

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

Also applies to: 259-277

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

Comment on lines 199 to 212
async restartBuilder() {
return this.restartBuilder();
}
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 | 🔴 Critical

restartBuilder is infinite recursion and will crash

restartBuilder currently calls return this.restartBuilder();, which recurses indefinitely and will blow the stack as soon as this.debouncedRestart() fires.

You likely meant to call the backend RestartBuilderAndWaitCommand here. A minimal fix could look like:

-    async restartBuilder() {
-        return this.restartBuilder();
-    }
+    async restartBuilder() {
+        const builderId = globalStore.get(atoms.builderId);
+        try {
+            await RpcApi.RestartBuilderAndWaitCommand(TabRpcClient, { builderid: builderId });
+        } catch (err) {
+            console.error("Failed to restart builder:", err);
+            globalStore.set(this.errorAtom, `Failed to restart builder: ${err.message || "Unknown error"}`);
+        }
+    }

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

🤖 Prompt for AI Agents
In frontend/builder/store/builder-apppanel-model.ts around lines 210 to 212, the
async method restartBuilder currently calls itself (return
this.restartBuilder()), causing infinite recursion; replace that self-call with
the intended backend invocation (call the RestartBuilderAndWaitCommand via your
RPC/command client or service), await its result, handle errors and return the
appropriate response/void. Ensure you do not re-enter this method and that any
debouncedRestart trigger calls this corrected method.

Comment on lines +237 to +263
const allRequiredBound =
sortedSecretEntries.filter(([_, meta]) => !meta.optional).every(([name]) => localBindings[name]?.trim()) ||
false;

return (
<div className="w-full h-full flex flex-col p-4">
<div className="flex items-center justify-between mb-2">
<h2 className="text-lg font-semibold">Secret Bindings</h2>
<button
className="px-3 py-1 text-sm font-medium rounded bg-accent/80 text-primary hover:bg-accent transition-colors cursor-pointer disabled:opacity-50 disabled:cursor-not-allowed"
onClick={handleSave}
disabled={!isDirty || isSaving}
>
{isSaving ? "Saving..." : "Save"}
</button>
</div>

<div className="mb-4 p-2 bg-blue-500/10 border border-blue-500/30 rounded text-sm text-secondary">
Map app secrets to Wave secret store names. Required secrets must be bound before the app can run
successfully.
</div>

{!allRequiredBound && (
<div className="mb-4 p-2 bg-yellow-500/10 border border-yellow-500/30 rounded text-sm text-yellow-600">
Some required secrets are not bound yet.
</div>
)}
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Treat required secrets as “bound” only when mapped to an existing secret

allRequiredBound currently treats any non-empty binding as satisfying the requirement:

const allRequiredBound =
    sortedSecretEntries
        .filter(([_, meta]) => !meta.optional)
        .every(([name]) => localBindings[name]?.trim()) || false;

This ignores whether the binding points to a real secret in availableSecrets, so a required secret bound to a non-existent name will still suppress the “Some required secrets are not bound yet” warning.

You likely want to incorporate validity here, e.g. reuse the same logic as SecretRow:

const allRequiredBound = sortedSecretEntries
    .filter(([_, meta]) => !meta.optional)
    .every(([name]) => {
        const binding = localBindings[name]?.trim();
        return binding && availableSecrets.includes(binding);
    });

That keeps the banner aligned with what will actually work at runtime.

🤖 Prompt for AI Agents
In frontend/builder/tabs/builder-secrettab.tsx around lines 237 to 263, the
allRequiredBound calculation currently treats any non-empty string as a valid
binding; change it to only consider a required secret "bound" when the trimmed
binding exists and matches an entry in availableSecrets. Update the .every check
to trim the binding, ensure it's non-empty, and verify
availableSecrets.includes(binding) (preserving the false fallback if needed) so
the warning banner reflects actual usable secret mappings.

Comment on lines 238 to 270
// ListStaticFiles returns FileInfo for all files in the embedded static filesystem.
// The Name() of each FileInfo will be the full path prefixed with "static/" (e.g., "static/config.json"),
// which can be passed directly to ReadStaticFile or OpenStaticFile.
// Returns an empty slice if StaticFS is nil or on error.
func ListStaticFiles() ([]fs.FileInfo, error) {
client := engine.GetDefaultClient()
if client.StaticFS == nil {
return nil, nil
}

var fileInfos []fs.FileInfo
err := fs.WalkDir(client.StaticFS, ".", func(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
if !d.IsDir() {
info, err := d.Info()
if err != nil {
return err
}
fullPath := "static/" + path
fileInfos = append(fileInfos, &staticFileInfo{
fullPath: fullPath,
info: info,
})
}
return nil
})
if err != nil {
return nil, err
}
return fileInfos, nil
}
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

Align ListStaticFiles behavior with its docstring

The comment says “Returns an empty slice if StaticFS is nil or on error”, but the implementation:

  • returns (nil, nil) when StaticFS is nil, and
  • propagates err from fs.WalkDir instead of returning an empty slice.

To avoid surprises, either update the comment to describe the current behavior, or adjust the implementation (e.g., return []fs.FileInfo{} when StaticFS is nil and decide explicitly whether WalkDir errors should be surfaced or converted to an empty result).

🤖 Prompt for AI Agents
In tsunami/app/defaultclient.go around lines 238 to 270, the docstring promises
"Returns an empty slice if StaticFS is nil or on error" but the code returns
(nil, nil) when StaticFS is nil and propagates WalkDir errors; change the
implementation to return an empty slice (e.g., []fs.FileInfo{}) and nil error
when client.StaticFS is nil, and if fs.WalkDir returns an error convert that
into returning an empty slice and nil error (instead of propagating the error)
so the runtime behavior matches the docstring.

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: 1

🧹 Nitpick comments (6)
frontend/builder/store/builder-apppanel-model.ts (2)

81-98: Ensure hasSecretsAtom is cleared when no secrets are present

updateSecretsLatch only ever sets hasSecretsAtom to true (when secrets exist) and does nothing when status.manifest.secrets is missing or empty. This means once hasSecretsAtom becomes true, it will never be reset to false within the same JS lifetime, even if a later manifest has no secrets (e.g., after switching apps without a full reload or when secrets are removed).

Consider explicitly setting the atom based on the current status, e.g.:

-    updateSecretsLatch(status: BuilderStatusData) {
-        if (!status?.manifest?.secrets) return;
-        const secrets = status.manifest.secrets;
-        if (Object.keys(secrets).length > 0) {
-            globalStore.set(this.hasSecretsAtom, true);
-        }
-    }
+    updateSecretsLatch(status: BuilderStatusData) {
+        const secrets = status?.manifest?.secrets;
+        const hasSecrets = !!secrets && Object.keys(secrets).length > 0;
+        globalStore.set(this.hasSecretsAtom, hasSecrets);
+    }

This keeps the secrets tab state in sync with whatever the latest manifest reports.

Also applies to: 116-122


215-231: Consider replacing fixed sleeps in switchBuilderApp with event/ack-based coordination

The setTimeout-style waits (500 ms and 100 ms) around StopBuilderCommand, SetRTInfoCommand, and doRefresh() can be brittle: they add fixed latency and may still race on slower systems or future changes to the stop semantics.

If StopBuilderCommand already waits for the builder to fully stop, the 500 ms delay might be unnecessary. Otherwise, it would be more robust to key off a builderstatus event (or another explicit ack) rather than a hardcoded delay, and similarly avoid the 100 ms sleep before doRefresh() if possible.

Not a blocker, but worth tightening when you next touch this flow.

tsunami/app/defaultclient.go (3)

6-16: Static file helpers: consistent behavior and clear errors, with minor DRY opportunity

The new ReadStaticFile / OpenStaticFile guards and error messages look solid: they fail fast when StaticFS is nil and clearly enforce the "static/" prefix.

You repeat the same “static files not available before app initialization…” error string three times; if callers might want to detect this condition, consider promoting it to a shared var ErrStaticFilesUnavailable = errors.New(...) (or at least a shared helper) so checks and future updates stay consistent.

Also applies to: 206-220, 222-236


28-38: staticFileInfo + ListStaticFiles design looks correct; note the non-standard Name()

The staticFileInfo wrapper and ListStaticFiles implementation look correct: fs.WalkDir covers the tree, skips dirs, and you wrap entries so Name() returns "static/"+path as documented, which is convenient for feeding into ReadStaticFile/OpenStaticFile.

One nuance: returning the full path from Name() is intentionally non-standard for fs.FileInfo (which usually exposes only the base name). Since this API is new and the comment already calls this out, that’s acceptable, but it’d be good to keep this in mind when adding future callers so they don’t assume Name() is just the basename.

Also applies to: 238-269


52-56: Document what happens when RegisterAppInitFn returns an error

RegisterAppInitFn(fn func() error) is a nice upgrade, but the comment still talks about a “setup function” without explaining how a non-nil error is treated (e.g., abort app start, logged and ignored, etc.).

Consider extending the docstring to spell out the error semantics so app authors know whether returning an error will actually prevent startup or just surface as a logged warning.

tsunami/build/build.go (1)

1135-1149: Consider renaming shadowed destPath variable.

The variable destPath is declared twice (lines 1136 and 1144). While valid Go, this shadowing could reduce clarity.

Consider using distinct names:

-	// Always copy app-main.go.tmpl => app-main.go
-	destPath := filepath.Join(destDir, "app-main.go")
-	if err := CopyFileFromFS(scaffoldFS, "app-main.go.tmpl", destPath); err != nil {
+	// Always copy app-main.go.tmpl => app-main.go
+	mainDestPath := filepath.Join(destDir, "app-main.go")
+	if err := CopyFileFromFS(scaffoldFS, "app-main.go.tmpl", mainDestPath); err != nil {
 		return 0, fmt.Errorf("failed to copy app-main.go.tmpl: %w", err)
 	}
 	fileCount++
 
 	// Conditionally copy app-init.go.tmpl => app-init.go
 	if hasAppInit {
-		destPath := filepath.Join(destDir, "app-init.go")
-		if err := CopyFileFromFS(scaffoldFS, "app-init.go.tmpl", destPath); err != nil {
+		initDestPath := filepath.Join(destDir, "app-init.go")
+		if err := CopyFileFromFS(scaffoldFS, "app-init.go.tmpl", initDestPath); err != nil {
 			return 0, fmt.Errorf("failed to copy app-init.go.tmpl: %w", err)
 		}
 		fileCount++
 	}
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between af1f898 and 629213c.

📒 Files selected for processing (5)
  • frontend/builder/store/builder-apppanel-model.ts (5 hunks)
  • pkg/buildercontroller/buildercontroller.go (2 hunks)
  • pkg/wshrpc/wshserver/wshserver.go (1 hunks)
  • tsunami/app/defaultclient.go (5 hunks)
  • tsunami/build/build.go (12 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • pkg/wshrpc/wshserver/wshserver.go
🧰 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/builder/store/builder-apppanel-model.ts
🧬 Code graph analysis (4)
pkg/buildercontroller/buildercontroller.go (1)
pkg/waveappstore/waveappstore.go (1)
  • BuildAppSecretEnv (804-842)
frontend/builder/store/builder-apppanel-model.ts (4)
frontend/app/store/wshclientapi.ts (1)
  • RpcApi (682-682)
frontend/app/store/wshrpcutil.ts (1)
  • TabRpcClient (37-37)
pkg/wshrpc/wshrpctypes.go (1)
  • BuilderStatusData (1077-1086)
frontend/app/store/global.ts (3)
  • atoms (816-816)
  • WOS (866-866)
  • getApi (826-826)
tsunami/app/defaultclient.go (1)
tsunami/engine/clientimpl.go (1)
  • GetDefaultClient (116-118)
tsunami/build/build.go (1)
tsunami/build/buildutil.go (1)
  • CopyFileFromFS (153-188)
⏰ 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). (4)
  • GitHub Check: Build for TestDriver.ai
  • GitHub Check: Analyze (go)
  • GitHub Check: Analyze (javascript-typescript)
  • GitHub Check: merge-gatekeeper
🔇 Additional comments (8)
pkg/buildercontroller/buildercontroller.go (2)

309-309: LGTM! Error tagging enables better UX for secret failures.

The (ERR-SECRET) suffix allows the UI to identify secret-related errors and switch to the Secrets tab, which aligns with the PR objectives for improved error handling.

Also applies to: 314-314


91-95: No action required—code is safe.

Verification confirms the usage is properly protected:

  • The only caller (StopBuilderCommand) checks for nil immediately after GetController and calls Stop() in the same scope, avoiding holding references across operations
  • Stop() is idempotent: subsequent calls see bc.process == nil and return early (line 486-487)
  • stopProcess_nolock() sets bc.process = nil after cleanup (line 498), making concurrent calls safe
  • All stop operations are serialized by bc.lock
frontend/builder/store/builder-apppanel-model.ts (2)

8-34: Secrets tab type and base state wiring look consistent

Extending TabType with "secrets" and introducing hasSecretsAtom fit cleanly with the existing atom pattern and tab handling; no issues from a typing or state-ownership perspective.


210-213: restartBuilder fix (delegation to startBuilder) looks good

Having restartBuilder delegate to startBuilder (which already wraps StartBuilderCommand in a try/catch and handles errors) removes the previous recursion issue and keeps restart semantics centralized.

tsunami/build/build.go (4)

33-33: Good practice: Centralized filename constant.

Introducing MainAppFileName eliminates hardcoded "app.go" strings throughout the codebase, improving maintainability.


491-498: Past review issue resolved.

The copy-paste error mentioned in the previous review has been correctly fixed. The code now properly validates both app-main.go.tmpl and app-init.go.tmpl with appropriate error messages.


1072-1173: Well-structured conditional scaffold copying.

The refactored copyScaffoldFS properly handles the new hasAppInit parameter, always copying the main template while conditionally copying the init template. The use of slices.Contains to avoid duplicate package file copies is a clean solution.


599-602: No issues found. Function exists and is properly accessible.

The parseAndValidateAppFile function is defined in build-ast.go (line 47) within the same package and is correctly called in build.go. The function signature matches the call site, and it handles all edge cases appropriately: missing files return a safe default, parse errors return a safe default, and invalid app initialization functions return descriptive errors.

Comment on lines 807 to 824
// Extract manifest between delimiters
manifestStr := string(manifestOutput)
startTag := "<AppManifest>"
endTag := "</AppManifest>"
startIdx := strings.Index(manifestStr, startTag)
endIdx := strings.Index(manifestStr, endTag)

if startIdx == -1 || endIdx == -1 {
return fmt.Errorf("manifest delimiters not found in output")
}

manifestJSON := manifestStr[startIdx+len(startTag) : endIdx]
manifestJSON = strings.TrimSpace(manifestJSON)

manifestPath := filepath.Join(tempDir, "manifest.json")
if err := os.WriteFile(manifestPath, manifestOutput, 0644); err != nil {
if err := os.WriteFile(manifestPath, []byte(manifestJSON), 0644); err != nil {
return fmt.Errorf("failed to write manifest.json: %w", err)
}
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 bounds validation for manifest delimiter extraction.

The code checks if delimiter indices are -1 but doesn't validate that endIdx > startIdx. If the delimiters appear in reverse order or overlap, the slice operation on Line 818 could panic or extract incorrect content.

Apply this diff to add bounds validation:

 	if startIdx == -1 || endIdx == -1 {
 		return fmt.Errorf("manifest delimiters not found in output")
 	}
+	if endIdx <= startIdx {
+		return fmt.Errorf("manifest delimiters are malformed (end tag before start tag)")
+	}
 
 	manifestJSON := manifestStr[startIdx+len(startTag) : endIdx]
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Extract manifest between delimiters
manifestStr := string(manifestOutput)
startTag := "<AppManifest>"
endTag := "</AppManifest>"
startIdx := strings.Index(manifestStr, startTag)
endIdx := strings.Index(manifestStr, endTag)
if startIdx == -1 || endIdx == -1 {
return fmt.Errorf("manifest delimiters not found in output")
}
manifestJSON := manifestStr[startIdx+len(startTag) : endIdx]
manifestJSON = strings.TrimSpace(manifestJSON)
manifestPath := filepath.Join(tempDir, "manifest.json")
if err := os.WriteFile(manifestPath, manifestOutput, 0644); err != nil {
if err := os.WriteFile(manifestPath, []byte(manifestJSON), 0644); err != nil {
return fmt.Errorf("failed to write manifest.json: %w", err)
}
// Extract manifest between delimiters
manifestStr := string(manifestOutput)
startTag := "<AppManifest>"
endTag := "</AppManifest>"
startIdx := strings.Index(manifestStr, startTag)
endIdx := strings.Index(manifestStr, endTag)
if startIdx == -1 || endIdx == -1 {
return fmt.Errorf("manifest delimiters not found in output")
}
if endIdx <= startIdx {
return fmt.Errorf("manifest delimiters are malformed (end tag before start tag)")
}
manifestJSON := manifestStr[startIdx+len(startTag) : endIdx]
manifestJSON = strings.TrimSpace(manifestJSON)
manifestPath := filepath.Join(tempDir, "manifest.json")
if err := os.WriteFile(manifestPath, []byte(manifestJSON), 0644); err != nil {
return fmt.Errorf("failed to write manifest.json: %w", err)
}
🤖 Prompt for AI Agents
In tsunami/build/build.go around lines 807 to 824, the code checks for missing
delimiters but doesn't validate that the end delimiter comes after the start
delimiter, which can cause an invalid slice or wrong content; update the logic
to compute contentStart := startIdx + len(startTag) and then verify endIdx >
contentStart (or at minimum endIdx > startIdx) and return a descriptive error if
not, before slicing and writing manifest.json.

@sawka sawka merged commit ce978c1 into main Nov 19, 2025
5 of 7 checks passed
@sawka sawka deleted the sawka/builder-secrets branch November 19, 2025 17:41
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