Skip to content

feat(v3,macos): NSWindow tabbing mode configuration - #5708

Merged
taliesin-ai merged 13 commits into
masterfrom
mac-window-tabbing-4966
Jul 1, 2026
Merged

taliesin-ai merged 13 commits into
masterfrom
mac-window-tabbing-4966

Conversation

@taliesin-ai

@taliesin-ai taliesin-ai commented Jul 1, 2026 •

Copy link
Copy Markdown
Collaborator

Adds MacWindow.TabbingMode (MacWindowTabbingMode) to configure macOS window tabbing (NSWindowTabbingMode).

Supersedes #4966 by @Eriyc — the original fork was deleted, so this re-homes that work on a maintainer branch (full credit to @Eriyc). De-conflicted against master's restructured options.mdx (kept both the new TabbingMode docs and master's WebviewPreferences section) and ran go mod tidy. darwin build + options test pass; includes the mac-window-tabs example.

Summary by CodeRabbit

  • New Features
    • Added macOS window tabbing control via a new TabbingMode option (Default/Automatic/Preferred/Disallowed).
    • Added a macOS example demonstrating tabbed vs non-tabbed window opening.
    • Introduced build/run/packaging/signing automation for the new example across macOS, Windows, and Linux.
  • Documentation
    • Updated Go window-close and singleton cleanup examples formatting.
    • Added documentation for macOS tabbing mode behavior and usage examples.
  • Tests
    • Added a test validating the TabbingMode constant values.

Eriyc and others added 9 commits February 7, 2026 00:09
- Add MacWindowTabbingMode type and constants to webview_window_options.go
- Add TabbingMode field to MacWindow struct
- Implement windowSetTabbingMode C function with macOS 10.12+ availability check
- Apply tabbing mode in macosWebviewWindow.run() method
- Add test for MacWindowTabbingMode constants
…c/wails-fork into v3-alpha-feature/mac-window-tabbing
…mac-window-tabbing

# Conflicts:
#	docs/src/content/docs/features/windows/options.mdx
# Conflicts:
#	docs/src/content/docs/features/windows/options.mdx
@coderabbitai

coderabbitai Bot commented Jul 1, 2026 •

Copy link
Copy Markdown
Contributor

Caution

Review failed

An error occurred during the review process. Please try again later.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch mac-window-tabbing-4966

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.

NSWindow tabbing is a macOS-only API with no Android/iOS equivalent, so the
build/android + build/ios scaffolding (auto-generated by wails3 init) is
irrelevant boilerplate. Remove both dirs (45 files), their Taskfile includes,
and the trailing ios:device tasks — matching the desktop-only convention used
by other examples (e.g. notifications: windows/darwin/linux only).

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 14

🧹 Nitpick comments (10)
v3/examples/mac-window-tabs/frontend/package.json (1)

12-14: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider pinning @wailsio/runtime instead of "latest".

Using "latest" makes builds non-reproducible across time; pinning to the current runtime version used elsewhere in the repo would improve consistency.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@v3/examples/mac-window-tabs/frontend/package.json` around lines 12 - 14, The
package.json dependency for `@wailsio/runtime` is using "latest", which makes
installs non-reproducible. Update the dependency in the dependencies block to a
pinned version that matches the runtime version used elsewhere in the repo, so
this frontend stays consistent with the rest of the codebase.
v3/examples/mac-window-tabs/frontend/public/style.css (2)

56-59: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Duplicate .result selector.

The .result rule is declared twice; the second declaration (Lines 103-108) overrides/duplicates the first (Lines 56-59). Consolidate into one rule.

Also applies to: 103-108

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@v3/examples/mac-window-tabs/frontend/public/style.css` around lines 56 - 59,
The `.result` CSS rule is duplicated, with a second declaration later in the
stylesheet overriding the first; consolidate the styles into a single `.result`
block and remove the redundant one so the selector is defined only once. Update
the shared `.result` rule in the stylesheet and keep the combined styles
together to avoid conflicting declarations.

1-28: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Stylelint flags quoting/casing issues.

Static analysis reports: unnecessary quotes around font-family names ("Inter", "Roboto", "Oxygen", "Ubuntu", "Cantarell") and optimizeLegibility should be lowercased.

🎨 Proposed fix
 :root {
-    font-family: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto",
-    "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
+    font-family: Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
+    Oxygen, Ubuntu, Cantarell, "Fira Sans", "Droid Sans", "Helvetica Neue",
     sans-serif;
     ...
-    text-rendering: optimizeLegibility;
+    text-rendering: optimizelegibility;
     ...
 }

 `@font-face` {
-    font-family: "Inter";
+    font-family: Inter;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@v3/examples/mac-window-tabs/frontend/public/style.css` around lines 1 - 28,
The CSS in the `:root` and `@font-face` blocks needs cleanup for Stylelint:
remove unnecessary quotes from font-family values like the `font-family` stack
and the `@font-face` `font-family` declaration, and change `text-rendering:
optimizeLegibility` to the lowercase form expected by the linter. Update the
`:root` styles in `style.css` so the font stack uses unquoted generic/custom
names where allowed and the `@font-face` definition remains consistent with the
same `Inter` naming.

Source: Linters/SAST tools

v3/pkg/application/webview_window_darwin.go (1)

1498-1504: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Default resolution duplicated between run() and setTabbingMode.

run() already normalizes MacWindowTabbingModeDefault to Disallowed before calling w.setTabbingMode, but setTabbingMode (Line 1360-1362) performs the identical normalization again. This is harmless today but the duplicated defaulting logic could drift out of sync if only one call site is updated later.

See related comment on webview_window_options.go Lines 594-609 regarding whether "Default → Disallowed" is the intended default-behavior change for this feature.

♻️ Optional consolidation
-		// Set tabbing mode (macOS 10.12+)
-		// Default to disallowed unless explicitly configured.
-		if macOptions.TabbingMode == MacWindowTabbingModeDefault {
-			macOptions.TabbingMode = MacWindowTabbingModeDisallowed
-		}
-		w.setTabbingMode(macOptions.TabbingMode)
+		// Set tabbing mode (macOS 10.12+); setTabbingMode normalizes the
+		// Default sentinel to Disallowed internally.
+		w.setTabbingMode(macOptions.TabbingMode)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@v3/pkg/application/webview_window_darwin.go` around lines 1498 - 1504, The
default-to-Disallowed normalization for MacWindowTabbingModeDefault is
duplicated between run() and setTabbingMode, so consolidate it in one place
only. Update run() and setTabbingMode in webview_window_darwin.go so that only
one of them performs the Default → Disallowed fallback, and keep the other path
assuming the mode is already normalized. Use the existing symbols
MacWindowTabbingModeDefault, MacWindowTabbingModeDisallowed, and
w.setTabbingMode to locate the logic and avoid future drift.
docs/src/content/docs/features/windows/options.mdx (1)

809-809: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Docs accurately reflect the implementation.

The new TabbingMode subsection's description of MacWindowTabbingModeDefault "defaults to disallowing tabbing" correctly documents the implementation in webview_window_darwin.go. See the related comment on webview_window_options.go about whether this default-disallow behavior (which diverges from macOS's native Automatic default) is the intended tradeoff — if so, it may be worth calling out explicitly here that this changes default behavior versus a plain unmanaged NSWindow.

Also applies to: 882-907

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/src/content/docs/features/windows/options.mdx` at line 809, The
TabbingMode docs currently say `MacWindowTabbingModeDefault` “defaults to
disallowing tabbing” but do not make clear that this differs from macOS’s native
`Automatic` default. Update the `TabbingMode` subsection in the `options.mdx`
docs to explicitly call out that the default behavior follows
`application.MacWindowTabbingModeDisallowed` and is an intentional divergence
from a plain unmanaged `NSWindow`, matching the implementation in
`webview_window_darwin.go` and the default noted in `webview_window_options.go`.
v3/examples/mac-window-tabs/build/Taskfile.yml (2)

46-79: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

frontend:vendor:puppertino task appears unused.

This task isn't listed as a dependency of build:frontend or any other task in this file, so it will never run automatically. Also, since sources and generates point to the exact same file, Task's checksum-based up-to-date check means once the file exists it will always be considered up-to-date and skipped, making the "attempt to fetch" logic largely dead outside the first manual run.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@v3/examples/mac-window-tabs/build/Taskfile.yml` around lines 46 - 79, The
frontend:vendor:puppertino task is effectively unreachable and self-skipping
because nothing depends on it and its sources/generates point to the same file.
Update the Taskfile so a relevant build task (such as build:frontend) depends on
frontend:vendor:puppertino, and adjust its sources/generates to reflect the
actual fetched artifact or remove the checksum trigger if the task should always
attempt the fetch. Keep the task name and the build:frontend dependency wiring
as the main symbols to locate and fix this.

71-78: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Unconditional sed/awk mutation of index.html on every invocation.

The script rewrites index.html in-place (inserting a stylesheet link and replacing class="btn" with Puppertino classes) whenever this task runs, with no idempotency guard beyond the grep -q check for the link tag. If a developer customizes index.html and later re-runs this task, the blind sed replacement of btn classes could silently overwrite manual edits.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@v3/examples/mac-window-tabs/build/Taskfile.yml` around lines 71 - 78, The
Taskfile step is mutating index.html unconditionally by rewriting the file with
awk/sed, which can overwrite manual changes. Update the logic around the
index.html patching block to make the Puppertino injection idempotent and
narrowly scoped, using the existing grep check plus an additional guard before
the class replacement in the same task. In the task that edits INDEX_HTML, only
transform the specific default markup when it matches the expected original
content, and skip the sed replacement if the file has already been customized or
already contains Puppertino classes.
v3/examples/mac-window-tabs/build/config.yml (1)

62-62: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Placeholder/broken doc link.

The URL https://v3.wails.io/noit/done/yet is clearly a placeholder and not a real documentation page. This is likely inherited from the Wails scaffolding template rather than authored in this PR, but since it ships in this example's generated config, it should be corrected.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@v3/examples/mac-window-tabs/build/config.yml` at line 62, The comment in the
example config contains a placeholder Wails documentation URL that should be
replaced with a real, valid link. Update the documentation reference in the
generated config comment so it points to an actual Wails docs page, keeping the
change within the mac-window-tabs build config example and preserving the
surrounding comment structure.
v3/examples/mac-window-tabs/build/android/app/src/main/java/com/wails/app/WailsJSBridge.java (1)

47-61: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Unbounded thread-per-call for invokeAsync.

Each invokeAsync spawns a brand-new Thread; under bursty invoke traffic this can exhaust resources. Consider a shared bounded ExecutorService instead.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@v3/examples/mac-window-tabs/build/android/app/src/main/java/com/wails/app/WailsJSBridge.java`
around lines 47 - 61, The invokeAsync method in WailsJSBridge currently creates
a new Thread for every call, which can exhaust resources under heavy traffic.
Replace the per-call thread creation with a shared bounded ExecutorService (or
similar pooled executor) and submit the message handling work there. Keep the
existing async behavior in invokeAsync, but route bridge.handleMessage and
sendCallback through the shared executor so concurrency is controlled.
v3/examples/mac-window-tabs/build/android/app/src/main/java/com/wails/app/WailsPathHandler.java (1)

22-33: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Unconditional debug logging.

Unlike the upstream template, this handler omits the DEBUG flag guard on Log.d/Log.w calls in handle(), so every asset request is logged even in release builds.

🔧 Proposed fix
 public class WailsPathHandler implements WebViewAssetLoader.PathHandler {
     private static final String TAG = "WailsPathHandler";
+    private static final boolean DEBUG = BuildConfig.DEBUG;
 
     private final WailsBridge bridge;
 
     public WailsPathHandler(WailsBridge bridge) {
         this.bridge = bridge;
     }
 
     `@Nullable`
     `@Override`
     public WebResourceResponse handle(`@NonNull` String path) {
-        Log.d(TAG, "Handling path: " + path);
+        if (DEBUG) Log.d(TAG, "Handling path: " + path);
 
         // Normalize path
         if (path.isEmpty() || path.equals("/")) {
             path = "/index.html";
         }
 
         // Get asset from Go
         byte[] data = bridge.serveAsset(path, "GET", "{}");
 
         if (data == null || data.length == 0) {
-            Log.w(TAG, "Asset not found: " + path);
+            if (DEBUG) Log.w(TAG, "Asset not found: " + path);
             return null; // Return null to let WebView handle 404
         }
 
         // Determine MIME type
         String mimeType = bridge.getAssetMimeType(path);
-        Log.d(TAG, "Serving " + path + " with type " + mimeType + " (" + data.length + " bytes)");
+        if (DEBUG) Log.d(TAG, "Serving " + path + " with type " + mimeType + " (" + data.length + " bytes)");

Also applies to: 43-50

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@v3/examples/mac-window-tabs/build/android/app/src/main/java/com/wails/app/WailsPathHandler.java`
around lines 22 - 33, The logging in WailsPathHandler.handle() is missing the
DEBUG guard, so asset requests are always written in release builds. Update the
handle() method to wrap the Log.d and any Log.w calls with the same DEBUG flag
check used by the upstream template, keeping the logging behavior only for debug
builds while preserving the existing path handling logic in WailsPathHandler and
its bridge usage.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@v3/examples/mac-window-tabs/build/android/app/src/main/AndroidManifest.xml`:
- Around line 8-16: The AndroidManifest application config is enabling app-wide
cleartext traffic unnecessarily. Remove the usesCleartextTraffic attribute from
the application entry in the manifest, since MainActivity.setupWebView serves
content through the virtual https://wails.localhost asset loader and does not
need permissive HTTP. Keep the rest of the application setup unchanged.

In
`@v3/examples/mac-window-tabs/build/android/app/src/main/java/com/wails/app/WailsJSBridge.java`:
- Around line 70-89: Guard the WailsJSBridge.log() JavaScript bridge against a
null level before calling level.toLowerCase(), since `@JavascriptInterface`
callers can pass null/undefined and trigger an NPE. Update log(String level,
String message) to normalize a missing level to a safe default (for example the
existing "verbose" path) and keep the switch logic in place for the valid cases.

In `@v3/examples/mac-window-tabs/build/android/Taskfile.yml`:
- Around line 107-121: The package tasks are still producing the debug APK
instead of a production/release artifact. Update the Taskfile targets for
package and package:fat so they invoke the release Gradle variant through
assemble:apk, and make sure package:fat also passes PRODUCTION through
compile:go:all-archs. If the APK filename changes, adjust any references in
deploy-emulator and run from the debug name to the release name so the
downstream tasks use the correct artifact.
- Around line 21-45: The build task only passes ARCH into compile:go:shared, so
that nested task can ignore the parent’s PRODUCTION setting and resolve
BUILD_FLAGS incorrectly. Update the build command to forward BUILD_FLAGS or
PRODUCTION into the task: compile:go:shared invocation, using the existing
BUILD_FLAGS and PRODUCTION vars defined in build so the shared compile step
matches the Android package mode.

In `@v3/examples/mac-window-tabs/build/docker/Dockerfile.cross`:
- Around line 1-16: Add the Dockerfile frontend directive at the very top of
Dockerfile.cross so the heredoc-based COPY instructions are parsed consistently
across Docker/BuildKit versions. Update the file header before the existing FROM
line, keeping the rest of the cross-compile setup unchanged.

In `@v3/examples/mac-window-tabs/build/docker/Dockerfile.server`:
- Around line 25-41: The server image is still using the root-running distroless
base, so update the base image in Dockerfile.server to the nonroot distroless
variant while keeping the existing COPY, EXPOSE, ENV, and ENTRYPOINT behavior
unchanged. Use the current distroless stage as the anchor and switch it to the
matching nonroot image so the server container runs without root privileges.

In `@v3/examples/mac-window-tabs/build/ios/main.m`:
- Around line 16-21: `main()` is starting `WailsIOSMain()` too early and twice;
remove the asynchronous dispatch from `main.m` and let `WailsAppDelegate` invoke
`WailsIOSMain()` from `didFinishLaunchingWithOptions` as designed. Keep
`UIApplicationMain(argc, argv, nil, @"WailsAppDelegate")` as the only launch
path so Go startup happens exactly once and only after the app delegate is
ready.

In `@v3/examples/mac-window-tabs/build/ios/project.pbxproj`:
- Line 158: The prebuild shell script in the Xcode project uses the generated
archive path with a space unquoted, so the file check and build invocation can
break due to word splitting. Update the shellScript block to treat the output
path as a single argument in both the existence test and the go build command,
using the same bin/My Product.a location referenced there.

In `@v3/examples/mac-window-tabs/build/ios/Taskfile.yml`:
- Around line 6-7: The simctl bundle ID values used by the Taskfile do not match
the actual CFBundleIdentifier values shipped in the app plists, so
deploy-simulator and run can target the wrong app. Update the BUNDLE_ID default
and the hardcoded identifier in the ios tasks so they resolve to the same bundle
IDs defined in build/ios/Info.plist and Info.dev.plist. Use the existing
deploy-simulator and run task entries, along with the BUNDLE_ID var, to keep the
simulator commands aligned with the packaged app identifiers.

In `@v3/examples/mac-window-tabs/build/linux/appimage/build.sh`:
- Around line 33-35: The AppImage rename in the build script is using a quoted
glob, so `mv` cannot match the generated file from `linuxdeploy`. Update the
`mv` command in the AppImage packaging step to allow wildcard expansion when
renaming the output, using the existing `APP_NAME` variable so it can find the
arch-suffixed `.AppImage` file. Keep the change localized to the rename logic in
`build.sh`.

In `@v3/examples/mac-window-tabs/build/linux/desktop`:
- Line 3: The desktop entry Name field is still the scaffold placeholder “My
Product” while the rest of the entry uses mac-window-tabs. Update the Name value
in the desktop entry alongside Exec, Icon, and StartupWMClass so the launcher
label matches the actual application name. Use the desktop entry’s Name key as
the locator for this change.

In `@v3/examples/mac-window-tabs/build/linux/nfpm/nfpm.yaml`:
- Around line 33-44: The RPM override comment is stale and conflicts with the
actual dependency pinned in the rpm block. In nfpm.yaml, update the comment
under the rpm override to match the WebKit version required by the depends entry
(using the same version as the archlinux override if 4.1 is correct), so the
package metadata is consistent and unambiguous.

In `@v3/examples/mac-window-tabs/go.mod`:
- Around line 7-47: The example module still pins older indirect go-git
dependencies, so update the require entries for github.com/go-git/go-git/v5 and
github.com/go-git/go-billy/v5 to match the patched versions already used in v3.
Keep the change limited to the go.mod dependency list so this example aligns
with the newer secure versions and removes the advisory exposure.

In `@v3/pkg/application/webview_window_options.go`:
- Around line 594-609: The MacWindowTabbingModeDefault sentinel in
MacWindowTabbingMode currently resolves to Disallowed, which changes the
behavior for callers of webview window options that never set TabbingMode.
Update the MacWindowTabbingMode constants or the runtime mapping used by
webview_window_options.go so the zero value preserves macOS’s native automatic
tabbing behavior, or otherwise make the breaking change explicit in the
TabbingMode handling and related docs.

---

Nitpick comments:
In `@docs/src/content/docs/features/windows/options.mdx`:
- Line 809: The TabbingMode docs currently say `MacWindowTabbingModeDefault`
“defaults to disallowing tabbing” but do not make clear that this differs from
macOS’s native `Automatic` default. Update the `TabbingMode` subsection in the
`options.mdx` docs to explicitly call out that the default behavior follows
`application.MacWindowTabbingModeDisallowed` and is an intentional divergence
from a plain unmanaged `NSWindow`, matching the implementation in
`webview_window_darwin.go` and the default noted in `webview_window_options.go`.

In
`@v3/examples/mac-window-tabs/build/android/app/src/main/java/com/wails/app/WailsJSBridge.java`:
- Around line 47-61: The invokeAsync method in WailsJSBridge currently creates a
new Thread for every call, which can exhaust resources under heavy traffic.
Replace the per-call thread creation with a shared bounded ExecutorService (or
similar pooled executor) and submit the message handling work there. Keep the
existing async behavior in invokeAsync, but route bridge.handleMessage and
sendCallback through the shared executor so concurrency is controlled.

In
`@v3/examples/mac-window-tabs/build/android/app/src/main/java/com/wails/app/WailsPathHandler.java`:
- Around line 22-33: The logging in WailsPathHandler.handle() is missing the
DEBUG guard, so asset requests are always written in release builds. Update the
handle() method to wrap the Log.d and any Log.w calls with the same DEBUG flag
check used by the upstream template, keeping the logging behavior only for debug
builds while preserving the existing path handling logic in WailsPathHandler and
its bridge usage.

In `@v3/examples/mac-window-tabs/build/config.yml`:
- Line 62: The comment in the example config contains a placeholder Wails
documentation URL that should be replaced with a real, valid link. Update the
documentation reference in the generated config comment so it points to an
actual Wails docs page, keeping the change within the mac-window-tabs build
config example and preserving the surrounding comment structure.

In `@v3/examples/mac-window-tabs/build/Taskfile.yml`:
- Around line 46-79: The frontend:vendor:puppertino task is effectively
unreachable and self-skipping because nothing depends on it and its
sources/generates point to the same file. Update the Taskfile so a relevant
build task (such as build:frontend) depends on frontend:vendor:puppertino, and
adjust its sources/generates to reflect the actual fetched artifact or remove
the checksum trigger if the task should always attempt the fetch. Keep the task
name and the build:frontend dependency wiring as the main symbols to locate and
fix this.
- Around line 71-78: The Taskfile step is mutating index.html unconditionally by
rewriting the file with awk/sed, which can overwrite manual changes. Update the
logic around the index.html patching block to make the Puppertino injection
idempotent and narrowly scoped, using the existing grep check plus an additional
guard before the class replacement in the same task. In the task that edits
INDEX_HTML, only transform the specific default markup when it matches the
expected original content, and skip the sed replacement if the file has already
been customized or already contains Puppertino classes.

In `@v3/examples/mac-window-tabs/frontend/package.json`:
- Around line 12-14: The package.json dependency for `@wailsio/runtime` is using
"latest", which makes installs non-reproducible. Update the dependency in the
dependencies block to a pinned version that matches the runtime version used
elsewhere in the repo, so this frontend stays consistent with the rest of the
codebase.

In `@v3/examples/mac-window-tabs/frontend/public/style.css`:
- Around line 56-59: The `.result` CSS rule is duplicated, with a second
declaration later in the stylesheet overriding the first; consolidate the styles
into a single `.result` block and remove the redundant one so the selector is
defined only once. Update the shared `.result` rule in the stylesheet and keep
the combined styles together to avoid conflicting declarations.
- Around line 1-28: The CSS in the `:root` and `@font-face` blocks needs cleanup
for Stylelint: remove unnecessary quotes from font-family values like the
`font-family` stack and the `@font-face` `font-family` declaration, and change
`text-rendering: optimizeLegibility` to the lowercase form expected by the
linter. Update the `:root` styles in `style.css` so the font stack uses unquoted
generic/custom names where allowed and the `@font-face` definition remains
consistent with the same `Inter` naming.

In `@v3/pkg/application/webview_window_darwin.go`:
- Around line 1498-1504: The default-to-Disallowed normalization for
MacWindowTabbingModeDefault is duplicated between run() and setTabbingMode, so
consolidate it in one place only. Update run() and setTabbingMode in
webview_window_darwin.go so that only one of them performs the Default →
Disallowed fallback, and keep the other path assuming the mode is already
normalized. Use the existing symbols MacWindowTabbingModeDefault,
MacWindowTabbingModeDisallowed, and w.setTabbingMode to locate the logic and
avoid future drift.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

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

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: a6836ad4-0b18-4095-b88b-8d0682704d5d

📥 Commits

Reviewing files that changed from the base of the PR and between d247852 and 48776b5.

⛔ Files ignored due to path filters (19)
  • v3/examples/mac-window-tabs/build/android/app/src/main/res/mipmap-hdpi/ic_launcher.png is excluded by !**/*.png
  • v3/examples/mac-window-tabs/build/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png is excluded by !**/*.png
  • v3/examples/mac-window-tabs/build/android/app/src/main/res/mipmap-mdpi/ic_launcher.png is excluded by !**/*.png
  • v3/examples/mac-window-tabs/build/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png is excluded by !**/*.png
  • v3/examples/mac-window-tabs/build/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png is excluded by !**/*.png
  • v3/examples/mac-window-tabs/build/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png is excluded by !**/*.png
  • v3/examples/mac-window-tabs/build/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png is excluded by !**/*.png
  • v3/examples/mac-window-tabs/build/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png is excluded by !**/*.png
  • v3/examples/mac-window-tabs/build/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png is excluded by !**/*.png
  • v3/examples/mac-window-tabs/build/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png is excluded by !**/*.png
  • v3/examples/mac-window-tabs/build/android/gradle/wrapper/gradle-wrapper.jar is excluded by !**/*.jar
  • v3/examples/mac-window-tabs/build/appicon.icon/Assets/wails_icon_vector.svg is excluded by !**/*.svg
  • v3/examples/mac-window-tabs/build/appicon.png is excluded by !**/*.png
  • v3/examples/mac-window-tabs/build/ios/icon.png is excluded by !**/*.png
  • v3/examples/mac-window-tabs/build/windows/icon.ico is excluded by !**/*.ico
  • v3/examples/mac-window-tabs/frontend/public/Inter-Medium.ttf is excluded by !**/*.ttf
  • v3/examples/mac-window-tabs/frontend/public/javascript.svg is excluded by !**/*.svg
  • v3/examples/mac-window-tabs/frontend/public/wails.png is excluded by !**/*.png
  • v3/examples/mac-window-tabs/go.sum is excluded by !**/*.sum
📒 Files selected for processing (75)
  • docs/src/content/docs/features/windows/options.mdx
  • v3/examples/mac-window-tabs/.gitignore
  • v3/examples/mac-window-tabs/README.md
  • v3/examples/mac-window-tabs/Taskfile.yml
  • v3/examples/mac-window-tabs/build/Taskfile.yml
  • v3/examples/mac-window-tabs/build/android/Taskfile.yml
  • v3/examples/mac-window-tabs/build/android/app/build.gradle
  • v3/examples/mac-window-tabs/build/android/app/proguard-rules.pro
  • v3/examples/mac-window-tabs/build/android/app/src/main/AndroidManifest.xml
  • v3/examples/mac-window-tabs/build/android/app/src/main/java/com/wails/app/MainActivity.java
  • v3/examples/mac-window-tabs/build/android/app/src/main/java/com/wails/app/WailsBridge.java
  • v3/examples/mac-window-tabs/build/android/app/src/main/java/com/wails/app/WailsJSBridge.java
  • v3/examples/mac-window-tabs/build/android/app/src/main/java/com/wails/app/WailsPathHandler.java
  • v3/examples/mac-window-tabs/build/android/app/src/main/res/layout/activity_main.xml
  • v3/examples/mac-window-tabs/build/android/app/src/main/res/values/colors.xml
  • v3/examples/mac-window-tabs/build/android/app/src/main/res/values/strings.xml
  • v3/examples/mac-window-tabs/build/android/app/src/main/res/values/themes.xml
  • v3/examples/mac-window-tabs/build/android/build.gradle
  • v3/examples/mac-window-tabs/build/android/gradle.properties
  • v3/examples/mac-window-tabs/build/android/gradle/wrapper/gradle-wrapper.properties
  • v3/examples/mac-window-tabs/build/android/gradlew
  • v3/examples/mac-window-tabs/build/android/gradlew.bat
  • v3/examples/mac-window-tabs/build/android/main_android.go
  • v3/examples/mac-window-tabs/build/android/scripts/deps/install_deps.go
  • v3/examples/mac-window-tabs/build/android/settings.gradle
  • v3/examples/mac-window-tabs/build/appicon.icon/icon.json
  • v3/examples/mac-window-tabs/build/config.yml
  • v3/examples/mac-window-tabs/build/darwin/Assets.car
  • v3/examples/mac-window-tabs/build/darwin/Info.dev.plist
  • v3/examples/mac-window-tabs/build/darwin/Info.plist
  • v3/examples/mac-window-tabs/build/darwin/Taskfile.yml
  • v3/examples/mac-window-tabs/build/darwin/icons.icns
  • v3/examples/mac-window-tabs/build/docker/Dockerfile.cross
  • v3/examples/mac-window-tabs/build/docker/Dockerfile.server
  • v3/examples/mac-window-tabs/build/ios/Assets.xcassets
  • v3/examples/mac-window-tabs/build/ios/Info.dev.plist
  • v3/examples/mac-window-tabs/build/ios/Info.plist
  • v3/examples/mac-window-tabs/build/ios/LaunchScreen.storyboard
  • v3/examples/mac-window-tabs/build/ios/Taskfile.yml
  • v3/examples/mac-window-tabs/build/ios/app_options_default.go
  • v3/examples/mac-window-tabs/build/ios/app_options_ios.go
  • v3/examples/mac-window-tabs/build/ios/build.sh
  • v3/examples/mac-window-tabs/build/ios/entitlements.plist
  • v3/examples/mac-window-tabs/build/ios/main.m
  • v3/examples/mac-window-tabs/build/ios/main_ios.go
  • v3/examples/mac-window-tabs/build/ios/project.pbxproj
  • v3/examples/mac-window-tabs/build/ios/scripts/deps/install_deps.go
  • v3/examples/mac-window-tabs/build/linux/Taskfile.yml
  • v3/examples/mac-window-tabs/build/linux/appimage/build.sh
  • v3/examples/mac-window-tabs/build/linux/desktop
  • v3/examples/mac-window-tabs/build/linux/nfpm/nfpm.yaml
  • v3/examples/mac-window-tabs/build/linux/nfpm/scripts/postinstall.sh
  • v3/examples/mac-window-tabs/build/linux/nfpm/scripts/postremove.sh
  • v3/examples/mac-window-tabs/build/linux/nfpm/scripts/preinstall.sh
  • v3/examples/mac-window-tabs/build/linux/nfpm/scripts/preremove.sh
  • v3/examples/mac-window-tabs/build/windows/Taskfile.yml
  • v3/examples/mac-window-tabs/build/windows/info.json
  • v3/examples/mac-window-tabs/build/windows/msix/app_manifest.xml
  • v3/examples/mac-window-tabs/build/windows/msix/template.xml
  • v3/examples/mac-window-tabs/build/windows/nsis/project.nsi
  • v3/examples/mac-window-tabs/build/windows/nsis/wails_tools.nsh
  • v3/examples/mac-window-tabs/build/windows/wails.exe.manifest
  • v3/examples/mac-window-tabs/frontend/Inter Font License.txt
  • v3/examples/mac-window-tabs/frontend/index.html
  • v3/examples/mac-window-tabs/frontend/package.json
  • v3/examples/mac-window-tabs/frontend/public/style.css
  • v3/examples/mac-window-tabs/frontend/src/main.js
  • v3/examples/mac-window-tabs/frontend/tsconfig.json
  • v3/examples/mac-window-tabs/frontend/vite.config.js
  • v3/examples/mac-window-tabs/go.mod
  • v3/examples/mac-window-tabs/main.go
  • v3/examples/mac-window-tabs/windowservice.go
  • v3/pkg/application/webview_window_darwin.go
  • v3/pkg/application/webview_window_options.go
  • v3/pkg/application/webview_window_options_test.go

Comment thread v3/examples/mac-window-tabs/build/android/app/src/main/AndroidManifest.xml Outdated
Comment on lines +70 to +89
@JavascriptInterface
public void log(String level, String message) {
switch (level.toLowerCase()) {
case "debug":
Log.d(TAG + "/JS", message);
break;
case "info":
Log.i(TAG + "/JS", message);
break;
case "warn":
Log.w(TAG + "/JS", message);
break;
case "error":
Log.e(TAG + "/JS", message);
break;
default:
Log.v(TAG + "/JS", message);
break;
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

NPE risk: log() doesn't guard against a null level.

This method is exposed via @JavascriptInterface, so JS can call wails.log(null, msg) (or pass undefined, coerced to null), causing level.toLowerCase() to throw an NPE.

🐛 Proposed fix
     `@JavascriptInterface`
     public void log(String level, String message) {
-        switch (level.toLowerCase()) {
+        switch (level == null ? "" : level.toLowerCase()) {
📝 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
@JavascriptInterface
public void log(String level, String message) {
switch (level.toLowerCase()) {
case "debug":
Log.d(TAG + "/JS", message);
break;
case "info":
Log.i(TAG + "/JS", message);
break;
case "warn":
Log.w(TAG + "/JS", message);
break;
case "error":
Log.e(TAG + "/JS", message);
break;
default:
Log.v(TAG + "/JS", message);
break;
}
}
`@JavascriptInterface`
public void log(String level, String message) {
switch (level == null ? "" : level.toLowerCase()) {
case "debug":
Log.d(TAG + "/JS", message);
break;
case "info":
Log.i(TAG + "/JS", message);
break;
case "warn":
Log.w(TAG + "/JS", message);
break;
case "error":
Log.e(TAG + "/JS", message);
break;
default:
Log.v(TAG + "/JS", message);
break;
}
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@v3/examples/mac-window-tabs/build/android/app/src/main/java/com/wails/app/WailsJSBridge.java`
around lines 70 - 89, Guard the WailsJSBridge.log() JavaScript bridge against a
null level before calling level.toLowerCase(), since `@JavascriptInterface`
callers can pass null/undefined and trigger an NPE. Update log(String level,
String message) to normalize a missing level to a safe default (for example the
existing "verbose" path) and keep the switch logic in place for the valid cases.

Comment on lines +21 to +45
build:
summary: Creates a build of the application for Android
deps:
- task: common:go:mod:tidy
- task: generate:android:bindings
vars:
BUILD_FLAGS:
ref: .BUILD_FLAGS
- task: common:build:frontend
vars:
BUILD_FLAGS:
ref: .BUILD_FLAGS
PRODUCTION:
ref: .PRODUCTION
- task: common:generate:icons
cmds:
- echo "Building Android app {{.APP_NAME}}..."
- task: compile:go:shared
vars:
ARCH: '{{.ARCH | default "arm64"}}'
vars:
BUILD_FLAGS: '{{if eq .PRODUCTION "true"}}-tags production,android -trimpath -buildvcs=false -ldflags="-w -s"{{else}}-tags android,debug -buildvcs=false -gcflags=all="-l"{{end}}'
env:
PRODUCTION: '{{.PRODUCTION | default "false"}}'

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate the relevant Taskfiles and inspect the referenced sections with line numbers.
git ls-files 'v3/examples/mac-window-tabs/build/Taskfile.yml' 'v3/examples/mac-window-tabs/build/android/Taskfile.yml' '**/Taskfile.yml' | sed -n '1,20p'

echo
echo '--- android Taskfile ---'
nl -ba v3/examples/mac-window-tabs/build/android/Taskfile.yml | sed -n '1,140p'

echo
echo '--- search for compile:go:shared ---'
rg -n --hidden --glob 'Taskfile.yml' 'compile:go:shared|BUILD_FLAGS|PRODUCTION' v3/examples/mac-window-tabs

Repository: wailsapp/wails

Length of output: 1063


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- candidate files ---'
git ls-files 'v3/examples/android/build/android/Taskfile.yml' 'v3/examples/android/build/Taskfile.yml' 'v3/examples/android/Taskfile.yml' 'v3/examples/mac-window-tabs/build/android/Taskfile.yml' 2>/dev/null || true

echo
echo '--- android build/android Taskfile ---'
sed -n '1,180p' v3/examples/android/build/android/Taskfile.yml

echo
echo '--- search compile:go:shared references ---'
rg -n --hidden --glob 'Taskfile.yml' 'compile:go:shared|BUILD_FLAGS|PRODUCTION' v3/examples/android

Repository: wailsapp/wails

Length of output: 12000


🌐 Web query:

go-task nested task inherits vars env task: call parent task PRODUCTION BUILD_FLAGS documentation

💡 Result:

In Task, tasks do not automatically inherit variables or environment variables from their parent tasks [1][2][3]. Variables have defined scopes, and a subtask (or a called task) only receives variables that are explicitly passed to it [1][3][4]. To pass variables from a parent task to a subtask, you must define them within the vars block of the task: call [1][3][5]. For example, to pass a BUILD_FLAGS variable to a subtask named production: tasks: parent: vars: BUILD_FLAGS: "--verbose" cmds: - task: production vars: BUILD_FLAGS: "{{.BUILD_FLAGS}}" production: cmds: - echo "Running production with {{.BUILD_FLAGS}}" If you need to propagate these values into the subtask's environment (the env block), you must explicitly map them in the subtask definition or during the task call [1][6]. Because there is no native inheritance of the parent's environment, common strategies include: 1. Explicit Mapping: Map the variable within the subtask's env block using the passed-in variable: production: vars: BUILD_FLAGS: "" env: BUILD_FLAGS: "{{.BUILD_FLAGS}}" cmds: - echo "Environment variable BUILD_FLAGS is $BUILD_FLAGS" 2. YAML Anchors: Use YAML anchors/aliases if the variables and environment mapping are complex and shared within the same Taskfile to avoid duplication [1][6]. 3. Env Files: For more complex setups, consider using .env files, which can be loaded by tasks to provide a consistent environment without manual repetition [1]. Variable precedence in Task follows a specific order, where variables explicitly passed in the task: call override global or included variables, but are themselves overridden by variables declared directly in the subtask's own vars block [4][7].

Citations:


Pass BUILD_FLAGS into compile:go:shared
build only forwards ARCH to the nested task: compile:go:shared, so that task falls back to its own default .PRODUCTION value and uses the debug BUILD_FLAGS branch even when package sets PRODUCTION: "true". Add BUILD_FLAGS (or PRODUCTION) to this call.

🐛 Proposed fix
     cmds:
       - echo "Building Android app {{.APP_NAME}}..."
       - task: compile:go:shared
         vars:
           ARCH: '{{.ARCH | default .HOST_ARCH}}'
+          BUILD_FLAGS:
+            ref: .BUILD_FLAGS
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@v3/examples/mac-window-tabs/build/android/Taskfile.yml` around lines 21 - 45,
The build task only passes ARCH into compile:go:shared, so that nested task can
ignore the parent’s PRODUCTION setting and resolve BUILD_FLAGS incorrectly.
Update the build command to forward BUILD_FLAGS or PRODUCTION into the task:
compile:go:shared invocation, using the existing BUILD_FLAGS and PRODUCTION vars
defined in build so the shared compile step matches the Android package mode.

Comment on lines +107 to +121
package:
summary: Packages a production build of the application into an APK
deps:
- task: build
vars:
PRODUCTION: "true"
cmds:
- task: assemble:apk

package:fat:
summary: Packages a production build for all architectures (fat APK)
cmds:
- task: compile:go:all-archs
- task: assemble:apk

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

package/package:fat assemble the debug Gradle variant, not release.

package (Line 107) builds with PRODUCTION: "true" but then calls assemble:apk (Line 114), which runs ./gradlew assembleDebug and copies app-debug.apk — not assemble:apk:release. Same issue in package:fat (Line 116-121), which additionally never threads PRODUCTION into compile:go:all-archs. The net effect: tasks named/described as producing a "production build"/"packages ... into an APK" ship a debug-signed, debug-tagged artifact.

🐛 Proposed fix
   package:
     summary: Packages a production build of the application into an APK
     deps:
       - task: build
         vars:
           PRODUCTION: "true"
     cmds:
-      - task: assemble:apk
+      - task: assemble:apk:release

   package:fat:
     summary: Packages a production build for all architectures (fat APK)
     cmds:
-      - task: compile:go:all-archs
-      - task: assemble:apk
+      - task: compile:go:all-archs
+        vars:
+          PRODUCTION: "true"
+      - task: assemble:apk:release

Note: deploy-emulator/run reference {{.APP_NAME}}.apk (debug filename); if switching package to the release variant, update those references to {{.APP_NAME}}-release.apk accordingly.

📝 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
package:
summary: Packages a production build of the application into an APK
deps:
- task: build
vars:
PRODUCTION: "true"
cmds:
- task: assemble:apk
package:fat:
summary: Packages a production build for all architectures (fat APK)
cmds:
- task: compile:go:all-archs
- task: assemble:apk
package:
summary: Packages a production build of the application into an APK
deps:
- task: build
vars:
PRODUCTION: "true"
cmds:
- task: assemble:apk:release
package:fat:
summary: Packages a production build for all architectures (fat APK)
cmds:
- task: compile:go:all-archs
vars:
PRODUCTION: "true"
- task: assemble:apk:release
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@v3/examples/mac-window-tabs/build/android/Taskfile.yml` around lines 107 -
121, The package tasks are still producing the debug APK instead of a
production/release artifact. Update the Taskfile targets for package and
package:fat so they invoke the release Gradle variant through assemble:apk, and
make sure package:fat also passes PRODUCTION through compile:go:all-archs. If
the APK filename changes, adjust any references in deploy-emulator and run from
the debug name to the release name so the downstream tasks use the correct
artifact.

Comment on lines +1 to +16
# Cross-compile Wails v3 apps to any platform
#
# Darwin: Zig + macOS SDK
# Linux: Native GCC when host matches target, Zig for cross-arch
# Windows: Zig + bundled mingw
#
# Usage:
# docker build -t wails-cross -f Dockerfile.cross .
# docker run --rm -v $(pwd):/app wails-cross darwin arm64
# docker run --rm -v $(pwd):/app wails-cross darwin amd64
# docker run --rm -v $(pwd):/app wails-cross linux amd64
# docker run --rm -v $(pwd):/app wails-cross linux arm64
# docker run --rm -v $(pwd):/app wails-cross windows amd64
# docker run --rm -v $(pwd):/app wails-cross windows arm64

FROM golang:1.25-bookworm

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

set -e
printf '\n== File ==\n'
cat -n v3/examples/mac-window-tabs/build/docker/Dockerfile.cross | sed -n '1,220p'
printf '\n== syntax directives in build/docker ==\n'
rg -n --glob 'Dockerfile*' '^# syntax=' v3/examples/mac-window-tabs/build/docker || true

Repository: wailsapp/wails

Length of output: 7154


🏁 Script executed:

cat -n v3/examples/mac-window-tabs/build/docker/Dockerfile.cross | sed -n '1,220p'

Repository: wailsapp/wails

Length of output: 7101


Add # syntax=docker/dockerfile:1 at the top of v3/examples/mac-window-tabs/build/docker/Dockerfile.cross.
This file uses heredoc COPY instructions later on, and pinning the frontend avoids parser incompatibilities across Docker/BuildKit versions.

🧰 Tools
🪛 Trivy (0.69.3)

[error] 1-1: Image user should not be 'root'

Specify at least 1 USER command in Dockerfile with non-root user as argument

Rule: DS-0002

Learn more

(IaC/Dockerfile)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@v3/examples/mac-window-tabs/build/docker/Dockerfile.cross` around lines 1 -
16, Add the Dockerfile frontend directive at the very top of Dockerfile.cross so
the heredoc-based COPY instructions are parsed consistently across
Docker/BuildKit versions. Update the file header before the existing FROM line,
keeping the rest of the cross-compile setup unchanged.

Comment on lines +33 to +35
# Rename the generated AppImage
mv "${APP_NAME}*.AppImage" "${APP_NAME}.AppImage"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win

Quoted glob prevents mv from finding the AppImage.

Double-quoting "${APP_NAME}*.AppImage" disables glob expansion in bash — the * is matched literally, not as a wildcard. Since linuxdeploy outputs a file with an arch suffix (not a literal *), this mv will fail and (with set -e) abort the packaging step.

🐛 Proposed fix
 # Rename the generated AppImage
-mv "${APP_NAME}*.AppImage" "${APP_NAME}.AppImage"
+mv ${APP_NAME}*.AppImage "${APP_NAME}.AppImage"
📝 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
# Rename the generated AppImage
mv "${APP_NAME}*.AppImage" "${APP_NAME}.AppImage"
# Rename the generated AppImage
mv ${APP_NAME}*.AppImage "${APP_NAME}.AppImage"
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@v3/examples/mac-window-tabs/build/linux/appimage/build.sh` around lines 33 -
35, The AppImage rename in the build script is using a quoted glob, so `mv`
cannot match the generated file from `linuxdeploy`. Update the `mv` command in
the AppImage packaging step to allow wildcard expansion when renaming the
output, using the existing `APP_NAME` variable so it can find the arch-suffixed
`.AppImage` file. Keep the change localized to the rename logic in `build.sh`.

@@ -0,0 +1,13 @@
[Desktop Entry]
Version=1.0
Name=My Product

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Desktop entry Name left as scaffold placeholder.

Every other field in this file (Exec, Icon, StartupWMClass) uses mac-window-tabs, but Name still says "My Product" — the unfilled template default. This is the label shown in Linux application launchers.

✏️ Proposed fix
-Name=My Product
+Name=Mac Window Tabs
📝 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
Name=My Product
Name=Mac Window Tabs
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@v3/examples/mac-window-tabs/build/linux/desktop` at line 3, The desktop entry
Name field is still the scaffold placeholder “My Product” while the rest of the
entry uses mac-window-tabs. Update the Name value in the desktop entry alongside
Exec, Icon, and StartupWMClass so the launcher label matches the actual
application name. Use the desktop entry’s Name key as the locator for this
change.

Comment on lines +33 to +44
overrides:
# RPM packages for RHEL/CentOS/AlmaLinux/Rocky Linux (WebKit 4.0)
rpm:
depends:
- gtk3
- webkit2gtk4.1

# Arch Linux packages (WebKit 4.1)
archlinux:
depends:
- gtk3
- webkit2gtk-4.1

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

RPM override comment contradicts the pinned dependency.

The comment says (WebKit 4.0) but the RPM depends list pins webkit2gtk4.1 — the same version noted in the Arch override below. Confirm which WebKit version is actually required and fix the stale comment.

✏️ Proposed fix (if 4.1 is correct)
-  # RPM packages for RHEL/CentOS/AlmaLinux/Rocky Linux (WebKit 4.0)
+  # RPM packages for RHEL/CentOS/AlmaLinux/Rocky Linux (WebKit 4.1)
   rpm:
     depends:
       - gtk3
       - webkit2gtk4.1
📝 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
overrides:
# RPM packages for RHEL/CentOS/AlmaLinux/Rocky Linux (WebKit 4.0)
rpm:
depends:
- gtk3
- webkit2gtk4.1
# Arch Linux packages (WebKit 4.1)
archlinux:
depends:
- gtk3
- webkit2gtk-4.1
overrides:
# RPM packages for RHEL/CentOS/AlmaLinux/Rocky Linux (WebKit 4.1)
rpm:
depends:
- gtk3
- webkit2gtk4.1
# Arch Linux packages (WebKit 4.1)
archlinux:
depends:
- gtk3
- webkit2gtk-4.1
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@v3/examples/mac-window-tabs/build/linux/nfpm/nfpm.yaml` around lines 33 - 44,
The RPM override comment is stale and conflicts with the actual dependency
pinned in the rpm block. In nfpm.yaml, update the comment under the rpm override
to match the WebKit version required by the depends entry (using the same
version as the archlinux override if 4.1 is correct), so the package metadata is
consistent and unambiguous.

Comment on lines +7 to +47
require (
dario.cat/mergo v1.0.2 // indirect
github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/ProtonMail/go-crypto v1.3.0 // indirect
github.com/adrg/xdg v0.5.3 // indirect
github.com/bep/debounce v1.2.1 // indirect
github.com/cloudflare/circl v1.6.3 // indirect
github.com/coder/websocket v1.8.14 // indirect
github.com/cyphar/filepath-securejoin v0.6.1 // indirect
github.com/ebitengine/purego v0.9.1 // indirect
github.com/emirpasic/gods v1.18.1 // indirect
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
github.com/go-git/go-billy/v5 v5.7.0 // indirect
github.com/go-git/go-git/v5 v5.16.4 // indirect
github.com/go-ole/go-ole v1.3.0 // indirect
github.com/godbus/dbus/v5 v5.2.2 // indirect
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
github.com/jchv/go-winloader v0.0.0-20250406163304-c1995be93bd1 // indirect
github.com/kevinburke/ssh_config v1.4.0 // indirect
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
github.com/leaanthony/go-ansi-parser v1.6.1 // indirect
github.com/leaanthony/u v1.1.1 // indirect
github.com/lmittmann/tint v1.1.2 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/pjbgf/sha1cd v0.5.0 // indirect
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/samber/lo v1.52.0 // indirect
github.com/sergi/go-diff v1.4.0 // indirect
github.com/skeema/knownhosts v1.3.2 // indirect
github.com/wailsapp/go-webview2 v1.0.23 // indirect
github.com/xanzy/ssh-agent v0.3.3 // indirect
golang.org/x/crypto v0.47.0 // indirect
golang.org/x/net v0.49.0 // indirect
golang.org/x/sys v0.40.0 // indirect
golang.org/x/text v0.33.0 // indirect
gopkg.in/warnings.v0 v0.1.2 // indirect
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check latest patched versions for go-git and go-billy
curl -s https://api.github.com/repos/go-git/go-git/releases/latest | jq '.tag_name'
curl -s https://api.github.com/repos/go-git/go-billy/releases/latest | jq '.tag_name' 2>/dev/null

Repository: wailsapp/wails

Length of output: 219


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== v3/examples/mac-window-tabs/go.mod =="
cat -n v3/examples/mac-window-tabs/go.mod | sed -n '1,120p'

echo
echo "== Search for go-git / go-billy usage in the repo =="
rg -n --hidden --glob '!*node_modules*' --glob '!*.sum' 'go-git|go-billy' v3 | sed -n '1,200p'

echo
echo "== Latest release tags from GitHub =="
python3 - <<'PY'
import json, urllib.request

repos = [
    ("go-git/go-git", "https://api.github.com/repos/go-git/go-git/releases/latest"),
    ("go-git/go-billy", "https://api.github.com/repos/go-git/go-billy/releases/latest"),
]
for name, url in repos:
    try:
        with urllib.request.urlopen(url, timeout=20) as r:
            data = json.load(r)
        print(name, data.get("tag_name"))
    except Exception as e:
        print(name, "ERROR:", e)
PY

echo
echo "== Proxy versions (fallback) =="
python3 - <<'PY'
import json, urllib.request
for module in ["github.com/go-git/go-git/v5", "github.com/go-git/go-billy/v5"]:
    url = f"https://proxy.golang.org/{module}/@v/list"
    try:
        with urllib.request.urlopen(url, timeout=20) as r:
            versions = r.read().decode().strip().splitlines()
        print(module, versions[-5:])
    except Exception as e:
        print(module, "ERROR:", e)
PY

Repository: wailsapp/wails

Length of output: 7478


Bump the indirect go-git/go-billy versions here

github.com/go-git/go-git/v5 v5.16.4 and github.com/go-git/go-billy/v5 v5.7.0 are older than the patched versions already used elsewhere in v3 (v5.19.1 / v5.9.0). Updating this example to match removes the known HIGH-severity advisories.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@v3/examples/mac-window-tabs/go.mod` around lines 7 - 47, The example module
still pins older indirect go-git dependencies, so update the require entries for
github.com/go-git/go-git/v5 and github.com/go-git/go-billy/v5 to match the
patched versions already used in v3. Keep the change limited to the go.mod
dependency list so this example aligns with the newer secure versions and
removes the advisory exposure.

Comment on lines +594 to +609
// MacWindowTabbingMode controls window tabbing behavior (macOS 10.12+).
// Values map to NSWindowTabbingMode (offset by 1 so the zero value is an "unset" sentinel).
type MacWindowTabbingMode int

const (
// MacWindowTabbingModeDefault is the zero-value sentinel meaning "not explicitly set".
// At runtime it resolves to Disallowed.
MacWindowTabbingModeDefault MacWindowTabbingMode = iota
// MacWindowTabbingModeAutomatic allows the system to determine tabbing behavior
MacWindowTabbingModeAutomatic
// MacWindowTabbingModePreferred indicates the window prefers to be in tabbing mode
MacWindowTabbingModePreferred
// MacWindowTabbingModeDisallowed prevents the window from being tabbed
MacWindowTabbingModeDisallowed
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

Does macOS window tabbing (NSWindow tabbingMode automatic) get enabled by default for non-document-based NSWindow subclasses without NSDocument support?

💡 Result:

Yes, macOS window tabbing (via NSWindow.TabbingMode.automatic) is enabled by default for all NSWindow instances, including those that are not document-based and do not use NSDocument [1][2]. When tabbingMode is set to its default value of .automatic, the system consults the user's global tabbing preference (configured in System Settings) to determine if a window should be allowed to join a tab group [1][2][3]. While the tabbing mechanism is enabled by default, successfully implementing a functional tabbed interface outside of the NSDocument framework requires additional development work to ensure the system-level interactions function correctly [4][5]: 1. Responder Chain: You must ensure that newWindowForTab(_:) is implemented somewhere in the responder chain (e.g., in your NSWindowController or NSApplicationDelegate) [6]. The system calls this method to create a new window when the user clicks the "plus" button in the tab bar; if it is not implemented, the "plus" button may not appear or may be non-functional [4][6]. 2. Window Management: Because the system treats each tab as an individual window, you are responsible for managing the lifecycle of the NSWindowController instances associated with those tabs to keep them alive and responsive to menu actions [4][5][7]. 3. Tabbing Identifier: To group specific types of windows together, you may need to set a common tabbingIdentifier on your windows [8]. In summary, while the platform-level support for "automatic" tabbing is active on all windows by default, the developer remains responsible for handling the creation of new windows and maintaining the responder chain to support the native tab bar interactions [4][6][7].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate the relevant symbols and nearby commentary.
rg -n "MacWindowTabbingMode|CollectionBehavior|tabbingMode|setTabbingMode|user tabbing" v3/pkg/application -S

# Show the specific file section around the new enum.
sed -n '560,640p' v3/pkg/application/webview_window_options.go

# Show the Darwin runtime handling around run() and any defaults commentary.
fd -a 'webview_window_darwin.go' v3/pkg/application

Repository: wailsapp/wails

Length of output: 14346


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate the relevant symbols and nearby commentary.
rg -n "MacWindowTabbingMode|CollectionBehavior|tabbingMode|setTabbingMode|user tabbing" v3/pkg/application -S

# Show the specific file section around the new enum.
sed -n '560,640p' v3/pkg/application/webview_window_options.go

# Find the Darwin implementation file.
fd -a 'webview_window_darwin.go' v3/pkg/application

Repository: wailsapp/wails

Length of output: 14346


🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n "MacWindowTabbingMode|CollectionBehavior|tabbingMode|setTabbingMode|user tabbing" v3/pkg/application -S
printf '\n--- webview_window_options.go ---\n'
sed -n '560,640p' v3/pkg/application/webview_window_options.go
printf '\n--- webview_window_darwin.go path ---\n'
fd -a 'webview_window_darwin.go' v3/pkg/application

Repository: wailsapp/wails

Length of output: 14420


Keep the zero value aligned with macOS tabbing defaults
MacWindowTabbingModeDefault now maps to Disallowed, so existing apps that never set TabbingMode will lose macOS’s automatic tabbing behavior on upgrade. Either preserve the native default or call this out as a breaking change.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@v3/pkg/application/webview_window_options.go` around lines 594 - 609, The
MacWindowTabbingModeDefault sentinel in MacWindowTabbingMode currently resolves
to Disallowed, which changes the behavior for callers of webview window options
that never set TabbingMode. Update the MacWindowTabbingMode constants or the
runtime mapping used by webview_window_options.go so the zero value preserves
macOS’s native automatic tabbing behavior, or otherwise make the breaking change
explicit in the TabbingMode handling and related docs.

- go.mod: add replace => ../../ so the example builds against the local Wails
  module (MacWindowTabbingMode is not in a published release), matching the
  print/mobile examples.
- build/Taskfile.yml: bind vite dev server to 127.0.0.1 so the wails3 dev
  tcp4 proxy connects (was: connection refused / blank webview).
- README: run with `task dev` (generates bindings + builds frontend); `go run .`
  alone does not work. Document the four windows (2 tabbing-preferred, 2
  disallowed) and that this is macOS-only.

Verified: task dev builds against local v3, opens the four windows, content
loads with 0 proxy errors.
…ndow

Spawning four windows at launch was confusing: the two Preferred windows
auto-tabbed and the two Disallowed ones stacked at the same frame, so the
demo appeared to start with two windows and mis-counted after Merge All
Windows. The bound WindowService buttons (Open tabbed / non-tabbed) were
dead weight behind them.

Open a single Preferred window at startup and drive the demo from the
buttons, giving clear cause-and-effect: tabbed windows join the current
window, non-tabbed windows stay standalone. Also rename the module from the
default 'changeme' to 'mac-window-tabs'.
The example is a standalone module (own go.mod + replace => ../../) nested
under v3/, which is a member of the repo root go.work. With the workspace
active, 'wails3 generate bindings' resolved the dir as v3 and emitted 0
services (breaking the frontend import), and 'go mod tidy' would target v3
rather than the example. Set GOWORK=off at the Taskfile level so the example
always builds as itself; it propagates through the wails3 dev/build tree and
is a no-op outside the monorepo.
@taliesin-ai
taliesin-ai merged commit fef40d8 into master Jul 1, 2026
46 of 47 checks passed
@taliesin-ai
taliesin-ai deleted the mac-window-tabbing-4966 branch July 1, 2026 12:20
leaanthony pushed a commit that referenced this pull request Jul 1, 2026
@leaanthony

Copy link
Copy Markdown
Member

Thanks for this @Eriyc 🙏

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.

3 participants