Claude Skill

desktop-multiwindow-tauri

Tauri 2.x multi-window creation, event system, window state persistence, parent/child and modal windows

LLM Mart · 0 points · 0 views 0 listing impressions 0 install-command copies
Virus-scanned Reviewed automatically before listing.

Full trust report

Download agents-inc-skills-dist_plugins_desktop-multiwindow-tauri_skills_desktop-multiwindow-tauri-3a51ef5.zip · 16 KB
Part of agents-inc/skills — 130 skills

Install

skills CLI npx skills add https://github.com/agents-inc/skills/tree/main/dist/plugins/desktop-multiwindow-tauri/skills/desktop-multiwindow-tauri
Claude Code claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install agents-inc-skills@llmmart
Git git clone https://github.com/agents-inc/skills.git

The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole agents-inc/skills collection as a plugin from our marketplace. Git is the plain clone.

Skill manifest

Tauri Multi-Window & Events

Quick Guide: Create windows from JS with new WebviewWindow(label, options) or from Rust with WebviewWindowBuilder. Communicate across windows using events: emit() broadcasts globally, emitTo(label, event, payload) targets a specific window, emit_filter() targets multiple windows by predicate. Always call the unlisten function returned by listen(). Use tauri-plugin-window-state to persist window position/size across sessions. Window labels must be unique and are used for both event targeting and permission scoping.

Current version: Tauri 2.x (stable). Multi-webview in a single window requires the unstable feature flag.


<critical_requirements>

CRITICAL: Before Using This Skill

All code must follow project conventions in CLAUDE.md (kebab-case, named exports, import ordering, import type, named constants)

(You MUST give every window a unique label -- labels identify windows for event targeting, permissions, and retrieval)

(You MUST always call the unlisten function returned by listen() / once() -- leaked listeners cause memory leaks in long-running apps)

(You MUST check if a window already exists before creating it -- duplicate labels cause runtime errors)

(You MUST add window labels to capability file windows array -- windows without permissions cannot use plugins or core APIs)

(You MUST use @tauri-apps/api/event for global events and @tauri-apps/api/webviewWindow for window-scoped events -- mixing them causes missed events)

</critical_requirements>


Auto-detection: WebviewWindow, WebviewWindowBuilder, emit_to, emitTo, emit_filter, EventTarget, window label, multi-window, onCloseRequested, tauri-plugin-window-state, parent window, modal window, WebviewBuilder, add_child, getCurrentWebviewWindow, window.listen, window.emit, cross-window communication

When to use:

  • Creating secondary windows (settings, preferences, about, detached panels)
  • Communicating between windows via the Tauri event system
  • Handling window close confirmation (unsaved changes dialogs)
  • Persisting window size/position across app restarts
  • Setting up parent/child or modal window relationships
  • Building multi-panel layouts with multiple webviews in a single window

When NOT to use:

  • Single-window apps with no inter-window communication (use the base Tauri framework skill)
  • General Tauri commands, IPC, or plugin setup (use the base Tauri framework skill)
  • Frontend framework state management within a single window (use respective framework skills)

Key patterns covered:

Detailed resources:




<decision_framework>

Decision Framework

Event Targeting

Who should receive this event?
|-- ALL windows in the app?
|   +-- emit() (global broadcast)
|-- ONE specific window?
|   +-- emitTo(label, event, payload) (targeted)
|-- MULTIPLE specific windows (but not all)?
|   +-- emit_filter() with predicate (Rust only)
+-- The backend (Rust side)?
    +-- emit() from frontend + app.listen() in Rust

Window Architecture

Does this UI surface need to be a separate window?
|-- Is it a settings/preferences panel?
|   +-- YES: separate window with its own label
|-- Is it a modal confirmation dialog?
|   +-- YES: child window with parent set
|-- Is it a dockable/undockable panel (IDE-style)?
|   +-- YES: multi-webview in single window (unstable)
|-- Is it content that can live in the same DOM?
|   +-- NO: don't create a new window -- use in-app routing/tabs
+-- Does it need to persist across window close?
    +-- YES: use tauri-plugin-window-state

Where to Create Windows

Where does window creation belong?
|-- Triggered by user action in UI (button click)?
|   +-- Frontend: new WebviewWindow(label, options)
|-- Triggered by backend logic (file watcher, system event)?
|   +-- Rust: WebviewWindowBuilder::new(app, label, url)
|-- Need multiple webviews in one window?
|   +-- Rust: WindowBuilder + window.add_child(WebviewBuilder)
+-- Declared at startup in config?
    +-- tauri.conf.json "windows" array

See reference.md for the complete API quick reference.

</decision_framework>


<red_flags>

RED FLAGS

High Priority Issues:

  • Duplicate window labels -- creating a window with a label that already exists throws a runtime error; always check first with getByLabel() or app.get_webview_window()
  • Missing unlisten calls -- listen() returns an unlisten function; not calling it leaks memory, especially in long-running apps or components that mount/unmount
  • Window label not in capability file windows array -- the new window silently lacks permissions for plugins and core APIs
  • Using @tauri-apps/api/tauri for event imports -- removed in v2; use @tauri-apps/api/event for global events and @tauri-apps/api/webviewWindow for window-scoped events
  • Calling emit() when you mean emitTo() -- broadcasting sensitive events (auth tokens, user data) to all windows is a security risk

Medium Priority Issues:

  • Not handling tauri://error event on window creation -- creation can fail silently without this listener
  • Using close() after preventDefault() in close handler -- triggers the close-requested event again; use destroy() to force-close without re-triggering
  • Hardcoding window dimensions without named constants -- magic numbers make layouts harder to maintain
  • Not using center: true or explicit position -- windows appear at OS-default position, which may be off-screen on multi-monitor setups

Gotchas & Edge Cases:

  • onCloseRequested on dynamic windows: there is a known issue where event.preventDefault() may not work on windows created at runtime; use the Rust on_window_event approach as a fallback
  • Event payload serialization: payloads must be serializable (JSON-compatible from JS, serde::Serialize from Rust); complex types like functions or DOM nodes cannot be sent
  • Window labels: must be alphanumeric with -, /, :, _ characters only -- no spaces or special characters
  • Multi-monitor: center() centers on the primary monitor; for specific monitor positioning use setPosition() with PhysicalPosition
  • emit_filter is Rust-only: there is no JavaScript equivalent; use multiple emitTo() calls from the frontend
  • Multi-webview is unstable: requires features = ["unstable"] in Cargo.toml and is desktop-only (no mobile support)
  • Window state plugin: does not restore maximized/fullscreen state by default -- use StateFlags::all() to include all state dimensions
  • Platform differences: parent/child behavior varies -- macOS creates child windows, Linux uses transient windows, Windows creates owned windows

</red_flags>


<critical_reminders>

CRITICAL REMINDERS

All code must follow project conventions in CLAUDE.md (kebab-case, named exports, import ordering, import type, named constants)

(You MUST give every window a unique label -- labels identify windows for event targeting, permissions, and retrieval)

(You MUST always call the unlisten function returned by listen() / once() -- leaked listeners cause memory leaks in long-running apps)

(You MUST check if a window already exists before creating it -- duplicate labels cause runtime errors)

(You MUST add window labels to capability file windows array -- windows without permissions cannot use plugins or core APIs)

(You MUST use @tauri-apps/api/event for global events and @tauri-apps/api/webviewWindow for window-scoped events -- mixing them causes missed events)

Failure to follow these rules will cause runtime errors, memory leaks, silent permission failures, and missed events.

</critical_reminders>

Files (skills)
  • examples
    • advanced.md 7.8 KB
      # Tauri Multi-Window & Events - Advanced Patterns
      
      > Parent/child windows, modal dialogs, and multi-webview layouts. See [SKILL.md](../SKILL.md) for decision frameworks. See [core.md](core.md) for basic window creation and events. See [persistence.md](persistence.md) for window state plugin.
      
      ---
      
      ## Parent/Child Windows
      
      ### JavaScript: Create a Child Window
      
      ```typescript
      import {
        WebviewWindow,
        getCurrentWebviewWindow,
      } from "@tauri-apps/api/webviewWindow";
      
      const DIALOG_LABEL = "confirm-dialog";
      const DIALOG_WIDTH = 400;
      const DIALOG_HEIGHT = 200;
      
      async function openChildDialog(): Promise<void> {
        const existing = await WebviewWindow.getByLabel(DIALOG_LABEL);
        if (existing) {
          await existing.setFocus();
          return;
        }
      
        const parent = getCurrentWebviewWindow();
      
        const dialog = new WebviewWindow(DIALOG_LABEL, {
          url: "confirm.html",
          title: "Confirm Action",
          width: DIALOG_WIDTH,
          height: DIALOG_HEIGHT,
          parent: parent,
          center: true,
          resizable: false,
        });
      
        dialog.once("tauri://error", (e) => {
          console.error("Failed to create dialog:", e);
        });
      }
      ```
      
      **Key point:** setting `parent` creates an owned window. On macOS, the child moves with the parent. On Linux, the child is transient (stays above parent). On Windows, the child is hidden when the parent is minimized.
      
      ### Rust: Create a Child Window
      
      ```rust
      use tauri::{Manager, WebviewWindowBuilder};
      
      const DIALOG_LABEL: &str = "confirm-dialog";
      const DIALOG_WIDTH: f64 = 400.0;
      const DIALOG_HEIGHT: f64 = 200.0;
      
      #[tauri::command]
      async fn open_child_dialog(app: tauri::AppHandle) -> Result<(), String> {
          if let Some(window) = app.get_webview_window(DIALOG_LABEL) {
              window.set_focus().map_err(|e| e.to_string())?;
              return Ok(());
          }
      
          let parent = app.get_webview_window("main")
              .ok_or("Parent window not found")?;
      
          WebviewWindowBuilder::new(
              &app,
              DIALOG_LABEL,
              tauri::WebviewUrl::App("confirm.html".into()),
          )
          .title("Confirm Action")
          .inner_size(DIALOG_WIDTH, DIALOG_HEIGHT)
          .parent(&parent)
          .map_err(|e| e.to_string())?
          .center()
          .resizable(false)
          .build()
          .map_err(|e| e.to_string())?;
      
          Ok(())
      }
      ```
      
      **Key point:** `.parent()` returns a `Result` because parent window support varies by platform -- handle the error.
      
      ---
      
      ## Dialog Pattern: Child Communicates Result to Parent
      
      A common pattern for confirmation dialogs: the child emits a result event, the parent listens.
      
      ### Parent Window
      
      ```typescript
      import { listen } from "@tauri-apps/api/event";
      
      // Open child and listen for its response
      await openChildDialog();
      
      const unlisten = await listen<{ confirmed: boolean }>(
        "dialog-result",
        (event) => {
          if (event.payload.confirmed) {
            performAction();
          }
          unlisten();
        },
      );
      ```
      
      ### Child Window (confirm.html)
      
      ```typescript
      import { emitTo } from "@tauri-apps/api/event";
      import { getCurrentWindow } from "@tauri-apps/api/window";
      
      const PARENT_LABEL = "main";
      
      async function handleConfirm(confirmed: boolean): Promise<void> {
        await emitTo(PARENT_LABEL, "dialog-result", { confirmed });
        const currentWindow = getCurrentWindow();
        await currentWindow.destroy();
      }
      ```
      
      **Why good:** child uses `emitTo` to send result only to the parent (not all windows), then closes itself with `destroy()`
      
      ---
      
      ## Window Position Management
      
      ### Position Relative to Parent
      
      ```typescript
      import {
        getCurrentWebviewWindow,
        WebviewWindow,
      } from "@tauri-apps/api/webviewWindow";
      import { PhysicalPosition, PhysicalSize } from "@tauri-apps/api/dpi";
      
      const OFFSET_X = 50;
      const OFFSET_Y = 50;
      const CHILD_WIDTH = 400;
      const CHILD_HEIGHT = 300;
      
      async function openOffsetChild(label: string, url: string): Promise<void> {
        const parent = getCurrentWebviewWindow();
        const parentPos = await parent.outerPosition();
        const parentSize = await parent.outerSize();
      
        const childX = parentPos.x + OFFSET_X;
        const childY = parentPos.y + OFFSET_Y;
      
        const child = new WebviewWindow(label, {
          url,
          width: CHILD_WIDTH,
          height: CHILD_HEIGHT,
          x: childX,
          y: childY,
        });
      }
      ```
      
      **Key point:** use `outerPosition()` and `outerSize()` for pixel-accurate positioning. These return `PhysicalPosition`/`PhysicalSize` which account for display scaling (HiDPI).
      
      ---
      
      ## Multi-Webview in a Single Window (Unstable)
      
      Create multiple webviews within a single window for panel/splitter layouts. Requires the `unstable` feature flag in Cargo.toml.
      
      ### Setup
      
      ```toml
      # Cargo.toml
      [dependencies]
      tauri = { version = "2", features = ["unstable"] }
      ```
      
      ### Create a Split Layout
      
      ```rust
      use tauri::{LogicalPosition, LogicalSize};
      use tauri::webview::WebviewBuilder;
      use tauri::window::WindowBuilder;
      
      const WINDOW_WIDTH: f64 = 1200.0;
      const WINDOW_HEIGHT: f64 = 800.0;
      const SIDEBAR_WIDTH: f64 = 300.0;
      
      fn setup_split_layout(app: &mut tauri::App) -> Result<(), Box<dyn std::error::Error>> {
          let window = WindowBuilder::new(app, "main")
              .title("My App")
              .inner_size(WINDOW_WIDTH, WINDOW_HEIGHT)
              .build()?;
      
          let content_width = WINDOW_WIDTH - SIDEBAR_WIDTH;
      
          // Left sidebar panel
          window.add_child(
              WebviewBuilder::new("sidebar", tauri::WebviewUrl::App("sidebar.html".into())),
              LogicalPosition::new(0.0, 0.0),
              LogicalSize::new(SIDEBAR_WIDTH, WINDOW_HEIGHT),
          )?;
      
          // Main content panel
          window.add_child(
              WebviewBuilder::new("content", tauri::WebviewUrl::App("content.html".into())),
              LogicalPosition::new(SIDEBAR_WIDTH, 0.0),
              LogicalSize::new(content_width, WINDOW_HEIGHT),
          )?;
      
          Ok(())
      }
      ```
      
      **Key point:** each child webview gets its own label and can communicate with other webviews via the event system (`emitTo`). Position and size use `LogicalPosition`/`LogicalSize` for DPI-aware layout.
      
      ### Cross-Panel Communication
      
      ```typescript
      // In sidebar.html: notify content panel of selection
      import { emitTo } from "@tauri-apps/api/event";
      
      async function selectItem(itemId: string): Promise<void> {
        await emitTo("content", "item-selected", { itemId });
      }
      ```
      
      ```typescript
      // In content.html: listen for sidebar selections
      import { getCurrentWebview } from "@tauri-apps/api/webview";
      
      const webview = getCurrentWebview();
      const unlisten = await webview.listen<{ itemId: string }>(
        "item-selected",
        (event) => {
          loadItem(event.payload.itemId);
        },
      );
      ```
      
      **Key point:** in multi-webview setups, use `getCurrentWebview()` from `@tauri-apps/api/webview` (not `getCurrentWebviewWindow()`). Each webview within the window has its own label and event scope.
      
      ### Resizing Panels Programmatically
      
      ```rust
      use tauri::{LogicalPosition, LogicalSize, Manager};
      
      #[tauri::command]
      fn resize_sidebar(app: tauri::AppHandle, new_width: f64) -> Result<(), String> {
          let window = app.get_webview_window("main")
              .ok_or("Main window not found")?;
      
          let window_size = window.inner_size().map_err(|e| e.to_string())?;
          let height = window_size.height as f64;
          let content_width = window_size.width as f64 - new_width;
      
          if let Some(sidebar) = app.get_webview("sidebar") {
              sidebar.set_size(LogicalSize::new(new_width, height))
                  .map_err(|e| e.to_string())?;
          }
      
          if let Some(content) = app.get_webview("content") {
              content.set_position(LogicalPosition::new(new_width, 0.0))
                  .map_err(|e| e.to_string())?;
              content.set_size(LogicalSize::new(content_width, height))
                  .map_err(|e| e.to_string())?;
          }
      
          Ok(())
      }
      ```
      
      **Limitations of multi-webview:**
      
      - Desktop-only (no mobile support)
      - Requires `unstable` feature flag -- API may change between Tauri minor versions
      - No built-in drag-to-resize between panels -- must implement resize handles in the webview or via Rust commands
      - Each webview runs in its own web context (no shared DOM, no shared JS globals)
      
      ---
      
      See [core.md](core.md) for window creation patterns and [persistence.md](persistence.md) for state persistence.
      
    • core.md 10 KB
      # Tauri Multi-Window & Events - Core Examples
      
      > Window creation, event system, close confirmation, and cross-window communication. See [SKILL.md](../SKILL.md) for decision frameworks and red flags. See [persistence.md](persistence.md) for window state plugin. See [advanced.md](advanced.md) for parent/child and multi-webview.
      
      ---
      
      ## Window Creation from JavaScript
      
      ### Basic Creation with Existence Check
      
      ```typescript
      import { WebviewWindow } from "@tauri-apps/api/webviewWindow";
      
      const SETTINGS_LABEL = "settings";
      const SETTINGS_WIDTH = 600;
      const SETTINGS_HEIGHT = 400;
      
      async function openSettingsWindow(): Promise<void> {
        // Always check if window already exists
        const existing = await WebviewWindow.getByLabel(SETTINGS_LABEL);
        if (existing) {
          await existing.setFocus();
          return;
        }
      
        const settingsWindow = new WebviewWindow(SETTINGS_LABEL, {
          url: "settings.html",
          title: "Settings",
          width: SETTINGS_WIDTH,
          height: SETTINGS_HEIGHT,
          resizable: false,
          center: true,
        });
      
        settingsWindow.once("tauri://created", () => {
          console.log("Settings window created");
        });
      
        settingsWindow.once("tauri://error", (e) => {
          console.error("Failed to create settings window:", e);
        });
      }
      ```
      
      **Why good:** existence check prevents duplicate-label runtime errors, named constants for dimensions, handles both success and error events, focuses existing window instead of creating duplicate
      
      ```typescript
      // BAD: No existence check, magic numbers, no error handling
      const win = new WebviewWindow("settings", {
        url: "settings.html",
        width: 600,
        height: 400,
      });
      ```
      
      **Why bad:** creating a second window with the same label throws a runtime error, magic numbers are undocumented, silent failure if creation fails
      
      ---
      
      ## Window Creation from Rust
      
      ```rust
      use tauri::WebviewWindowBuilder;
      
      const SETTINGS_LABEL: &str = "settings";
      const SETTINGS_WIDTH: f64 = 600.0;
      const SETTINGS_HEIGHT: f64 = 400.0;
      
      #[tauri::command]
      async fn open_settings(app: tauri::AppHandle) -> Result<(), String> {
          // Check if window already exists
          if let Some(window) = app.get_webview_window(SETTINGS_LABEL) {
              window.set_focus().map_err(|e| e.to_string())?;
              return Ok(());
          }
      
          WebviewWindowBuilder::new(
              &app,
              SETTINGS_LABEL,
              tauri::WebviewUrl::App("settings.html".into()),
          )
          .title("Settings")
          .inner_size(SETTINGS_WIDTH, SETTINGS_HEIGHT)
          .resizable(false)
          .center()
          .build()
          .map_err(|e| e.to_string())?;
      
          Ok(())
      }
      ```
      
      **Why good:** same existence-check pattern as JS side, named constants, returns Result for error propagation to frontend
      
      ---
      
      ## Event System
      
      ### Global Events (All Windows)
      
      Use for app-wide broadcasts where every window should react.
      
      ```typescript
      import { emit, listen } from "@tauri-apps/api/event";
      
      // Emitting: all windows with a "theme-changed" listener receive this
      await emit("theme-changed", { theme: "dark" });
      
      // Listening: returns an unlisten function -- MUST be called on cleanup
      const unlisten = await listen<{ theme: string }>("theme-changed", (event) => {
        applyTheme(event.payload.theme);
      });
      
      // Clean up when component unmounts or listener is no longer needed
      unlisten();
      ```
      
      **Why good:** typed payload with generics, unlisten stored and called on cleanup
      
      ```typescript
      // BAD: Listener leak -- unlisten never called
      await listen("theme-changed", (event) => {
        applyTheme(event.payload.theme);
      });
      ```
      
      **Why bad:** listener accumulates on every mount/navigation, memory leak in long-running apps
      
      ---
      
      ### Targeted Events (One Specific Window)
      
      Use when you know exactly which window should receive the event.
      
      ```typescript
      import { emitTo } from "@tauri-apps/api/event";
      
      // Send to the "editor" window only
      await emitTo("editor", "file-opened", { path: "/docs/readme.md" });
      ```
      
      ```typescript
      // Listening in the editor window
      import { getCurrentWebviewWindow } from "@tauri-apps/api/webviewWindow";
      
      const appWindow = getCurrentWebviewWindow();
      const unlisten = await appWindow.listen<{ path: string }>(
        "file-opened",
        (event) => {
          loadFile(event.payload.path);
        },
      );
      ```
      
      **Key point:** `emitTo` accepts either a string label or an `EventTarget` object. Using the string label is simpler for most cases.
      
      ---
      
      ### Filtered Events (Multiple Specific Windows -- Rust Only)
      
      Use when targeting a subset of windows by predicate. Only available from Rust.
      
      ```rust
      use tauri::{Emitter, EventTarget};
      
      #[tauri::command]
      fn notify_viewers(app: tauri::AppHandle, path: String) -> Result<(), String> {
          app.emit_filter("file-changed", &path, |target| match target {
              EventTarget::WebviewWindow { label } =>
                  label == "main" || label == "file-viewer" || label == "diff-viewer",
              _ => false,
          })
          .map_err(|e| e.to_string())?;
      
          Ok(())
      }
      ```
      
      **Why good:** single emission to multiple windows without calling `emit_to` three times, predicate-based filtering is flexible
      
      **Key point:** there is no JavaScript equivalent of `emit_filter`. From the frontend, use multiple `emitTo()` calls.
      
      ---
      
      ### One-Time Events
      
      Use `once()` for events you only need to handle once (initialization, window-ready signals).
      
      ```typescript
      import { once } from "@tauri-apps/api/event";
      
      // Listen only for the first occurrence
      const unlisten = await once<{ ready: boolean }>("app-initialized", (event) => {
        console.log("App ready:", event.payload.ready);
      });
      ```
      
      ```rust
      use tauri::Listener;
      
      app.once("frontend-ready", |event| {
          println!("Frontend reports ready: {:?}", event.payload());
      });
      ```
      
      ---
      
      ## Cross-Window State Synchronization
      
      Use events to keep state in sync across windows. One window emits changes, others listen and update.
      
      ```typescript
      // In the settings window: emit preference changes
      import { emit } from "@tauri-apps/api/event";
      
      interface PreferenceUpdate {
        key: string;
        value: unknown;
      }
      
      async function updatePreference(key: string, value: unknown): Promise<void> {
        // Save to backend/store first
        await savePreference(key, value);
        // Broadcast to all windows
        await emit("preference-updated", { key, value } satisfies PreferenceUpdate);
      }
      ```
      
      ```typescript
      // In every other window: listen for preference changes
      import { listen } from "@tauri-apps/api/event";
      
      interface PreferenceUpdate {
        key: string;
        value: unknown;
      }
      
      const unlisten = await listen<PreferenceUpdate>(
        "preference-updated",
        (event) => {
          const { key, value } = event.payload;
          applyPreference(key, value);
        },
      );
      
      // Clean up on unmount
      unlisten();
      ```
      
      **Key point:** the emitting window also receives its own global events unless you use `emitTo` to target only other windows. Filter in the listener if needed, or use `emitTo` to each window except `getCurrentWebviewWindow().label`.
      
      ---
      
      ## Window Close Confirmation
      
      ### JavaScript (onCloseRequested)
      
      ```typescript
      import { getCurrentWindow } from "@tauri-apps/api/window";
      
      const currentWindow = getCurrentWindow();
      const unlisten = await currentWindow.onCloseRequested(async (event) => {
        if (hasUnsavedChanges()) {
          event.preventDefault();
          // Show your confirmation UI
          const confirmed = await showConfirmDialog(
            "You have unsaved changes. Close anyway?",
          );
          if (confirmed) {
            // Use destroy() to force-close without retriggering close-requested
            await currentWindow.destroy();
          }
        }
      });
      ```
      
      **Why good:** prevents accidental data loss, uses `destroy()` instead of `close()` to avoid retriggering the close handler
      
      ```typescript
      // BAD: Using close() after preventDefault causes infinite loop
      const unlisten = await currentWindow.onCloseRequested(async (event) => {
        event.preventDefault();
        const confirmed = await showConfirmDialog("Close?");
        if (confirmed) {
          await currentWindow.close(); // Triggers onCloseRequested again!
        }
      });
      ```
      
      **Why bad:** `close()` fires the close-requested event again, creating an infinite confirmation loop
      
      ### Rust (on_window_event)
      
      ```rust
      use tauri::Manager;
      
      tauri::Builder::default()
          .setup(|app| {
              let window = app.get_webview_window("main").unwrap();
              window.on_window_event(move |event| {
                  if let tauri::WindowEvent::CloseRequested { api, .. } = event {
                      api.prevent_close();
                      // Emit to frontend for confirmation UI
                      // Frontend calls window.destroy() if confirmed
                  }
              });
              Ok(())
          })
      ```
      
      **Key point:** the Rust `on_window_event` approach is more reliable for dynamically created windows where the JS `onCloseRequested` may have known issues.
      
      ---
      
      ## Retrieving Windows
      
      ### Get All Open Windows
      
      ```typescript
      import { WebviewWindow } from "@tauri-apps/api/webviewWindow";
      
      const allWindows = WebviewWindow.getAll();
      for (const win of allWindows) {
        console.log(`Window: ${win.label}`);
      }
      ```
      
      ### Get Current Window
      
      ```typescript
      import { getCurrentWebviewWindow } from "@tauri-apps/api/webviewWindow";
      
      const currentWindow = getCurrentWebviewWindow();
      console.log(`I am: ${currentWindow.label}`);
      ```
      
      ### Get Window by Label
      
      ```typescript
      import { WebviewWindow } from "@tauri-apps/api/webviewWindow";
      
      const editor = await WebviewWindow.getByLabel("editor");
      if (editor) {
        await editor.setFocus();
      }
      ```
      
      ```rust
      use tauri::Manager;
      
      if let Some(editor) = app.get_webview_window("editor") {
          editor.set_focus().unwrap();
      }
      ```
      
      ---
      
      ## Capability File for Multi-Window
      
      Every window label must appear in a capability file's `windows` array to receive permissions.
      
      ```json
      {
        "$schema": "../gen/schemas/desktop-schema.json",
        "identifier": "multi-window-capability",
        "description": "Permissions for all app windows",
        "windows": ["main", "settings", "editor", "file-viewer"],
        "permissions": [
          "core:default",
          "event:default",
          "window:default",
          "window:allow-create",
          "window:allow-close",
          "window:allow-set-focus"
        ]
      }
      ```
      
      **Key point:** a window not listed in any capability's `windows` array silently lacks permissions. If a new window can't use plugins or core APIs, check this first.
      
      ---
      
      See [persistence.md](persistence.md) for window state persistence and [advanced.md](advanced.md) for parent/child and multi-webview patterns.
      
    • persistence.md 4.3 KB
      # Tauri Multi-Window & Events - Window State Persistence
      
      > Save and restore window position, size, and visibility across app restarts using `tauri-plugin-window-state`. See [SKILL.md](../SKILL.md) for decision frameworks. See [core.md](core.md) for window creation and events.
      
      ---
      
      ## Plugin Setup
      
      Install with `npm run tauri add window-state` (handles both Rust crate and JS bindings).
      
      ### Register in Rust
      
      ```rust
      #[cfg_attr(mobile, tauri::mobile_entry_point)]
      pub fn run() {
          tauri::Builder::default()
              .setup(|app| {
                  #[cfg(desktop)]
                  app.handle().plugin(
                      tauri_plugin_window_state::Builder::default().build()
                  );
                  Ok(())
              })
              .run(tauri::generate_context!())
              .expect("error while running tauri application");
      }
      ```
      
      **Key point:** wrap in `#[cfg(desktop)]` because window-state is desktop-only. After registration, all windows automatically save state on close and restore on next launch.
      
      ### Add Permission
      
      ```json
      {
        "permissions": ["window-state:default"]
      }
      ```
      
      The default permission set includes `allow-filename`, `allow-restore-state`, and `allow-save-window-state`.
      
      ---
      
      ## Preventing Flash on Restore
      
      Set `visible: false` in the window config so the plugin can restore position/size before showing the window.
      
      ```json
      {
        "app": {
          "windows": [
            {
              "label": "main",
              "title": "My App",
              "width": 1024,
              "height": 768,
              "visible": false
            }
          ]
        }
      }
      ```
      
      **Why this matters:** without this, the window briefly appears at its default position/size before the plugin restores the saved state, causing a visible jump.
      
      ---
      
      ## Manual Save and Restore (Rust)
      
      Override the automatic behavior when you need to save state at specific points (e.g., before a settings migration).
      
      ```rust
      use tauri_plugin_window_state::{AppHandleExt, StateFlags};
      
      // Save all window states to disk
      app.save_window_state(StateFlags::all())
          .expect("failed to save window state");
      ```
      
      ```rust
      use tauri_plugin_window_state::{WindowExt, StateFlags};
      
      // Restore a specific window's state
      let window = app.get_webview_window("main").unwrap();
      window.restore_state(StateFlags::all())
          .expect("failed to restore window state");
      ```
      
      ---
      
      ## Manual Save and Restore (JavaScript)
      
      ```typescript
      import {
        saveWindowState,
        restoreStateCurrent,
        StateFlags,
      } from "@tauri-apps/plugin-window-state";
      
      // Save all windows
      await saveWindowState(StateFlags.ALL);
      
      // Restore the current window
      await restoreStateCurrent(StateFlags.ALL);
      ```
      
      ---
      
      ## StateFlags
      
      Control which dimensions of window state are persisted.
      
      | Flag          | What it persists              |
      | ------------- | ----------------------------- |
      | `POSITION`    | Window x/y coordinates        |
      | `SIZE`        | Window width/height           |
      | `MAXIMIZED`   | Whether window is maximized   |
      | `VISIBLE`     | Whether window is visible     |
      | `DECORATIONS` | Whether decorations are shown |
      | `FULLSCREEN`  | Whether window is fullscreen  |
      | `ALL`         | All of the above              |
      
      ```rust
      use tauri_plugin_window_state::StateFlags;
      
      // Save only position and size (not maximized/fullscreen state)
      let flags = StateFlags::POSITION | StateFlags::SIZE;
      app.save_window_state(flags).unwrap();
      ```
      
      **Key point:** the default behavior uses `StateFlags::all()`. If you only want to persist position and size, use a custom flags combination.
      
      ---
      
      ## Multi-Window State Persistence
      
      The plugin automatically handles all windows. Each window's state is saved under its label, so labels must be consistent across app launches.
      
      ```rust
      // All windows declared in tauri.conf.json OR created programmatically
      // will have their state saved/restored automatically.
      // The label used at creation time is the key for persistence.
      
      // Dynamic windows: use consistent labels
      WebviewWindowBuilder::new(&app, "editor", url)  // "editor" is the persistence key
          .visible(false)  // Let plugin handle visibility after restore
          .build()?;
      ```
      
      **Gotcha:** if you create windows with dynamic labels (e.g., `editor-{file-id}`), the plugin saves state per label. Restarting the app with different file IDs means old state won't match. Use stable labels for windows that should persist state.
      
      ---
      
      See [core.md](core.md) for window creation patterns and [advanced.md](advanced.md) for parent/child windows.
      
  • reference.md 8.2 KB
    # Tauri Multi-Window & Events Quick Reference
    
    > Quick-lookup tables and decision frameworks. See [SKILL.md](../SKILL.md) for patterns and red flags. See [examples/core.md](examples/core.md) for full code examples.
    
    ---
    
    ## Event Method Comparison
    
    | Method                                   | Scope                  | Available From | Use Case                                  |
    | ---------------------------------------- | ---------------------- | -------------- | ----------------------------------------- |
    | `emit(event, payload)`                   | Global (all windows)   | JS + Rust      | App-wide broadcasts (theme, logout)       |
    | `emitTo(label, event, payload)`          | One window             | JS + Rust      | Targeted messages (file opened in editor) |
    | `emit_filter(event, payload, predicate)` | Multiple windows       | Rust only      | Subset targeting (notify all viewers)     |
    | `window.emit(event, payload)`            | Window-scoped (Rust)   | Rust           | Emit from a `WebviewWindow` handle        |
    | `listen(event, handler)`                 | Global listener        | JS + Rust      | Receive from any emitter                  |
    | `window.listen(event, handler)`          | Window-scoped listener | JS + Rust      | Receive only for this window              |
    | `once(event, handler)`                   | One-time global        | JS + Rust      | Initialization, one-shot signals          |
    
    ---
    
    ## JavaScript Import Map
    
    | Module                            | Import                                                               | Purpose                                   |
    | --------------------------------- | -------------------------------------------------------------------- | ----------------------------------------- |
    | `@tauri-apps/api/event`           | `emit`, `emitTo`, `listen`, `once`                                   | Global event functions                    |
    | `@tauri-apps/api/webviewWindow`   | `WebviewWindow`, `getCurrentWebviewWindow`                           | Window creation + window-scoped events    |
    | `@tauri-apps/api/webview`         | `Webview`, `getCurrentWebview`                                       | Webview-scoped operations (multi-webview) |
    | `@tauri-apps/api/window`          | `Window`, `getCurrentWindow`                                         | Window control (close, focus, position)   |
    | `@tauri-apps/api/dpi`             | `PhysicalPosition`, `PhysicalSize`, `LogicalPosition`, `LogicalSize` | Position/size types                       |
    | `@tauri-apps/plugin-window-state` | `saveWindowState`, `restoreStateCurrent`, `StateFlags`               | Window state persistence                  |
    
    ---
    
    ## Rust Trait Map
    
    | Trait      | Provides                                             | Import                 |
    | ---------- | ---------------------------------------------------- | ---------------------- |
    | `Emitter`  | `emit()`, `emit_to()`, `emit_filter()`               | `use tauri::Emitter;`  |
    | `Listener` | `listen()`, `once()`, `unlisten()`                   | `use tauri::Listener;` |
    | `Manager`  | `get_webview_window()`, `get_webview()`, `windows()` | `use tauri::Manager;`  |
    
    ---
    
    ## Window Creation Options (JavaScript)
    
    | Option        | Type      | Default     | Purpose                                        |
    | ------------- | --------- | ----------- | ---------------------------------------------- |
    | `url`         | `string`  | -           | HTML file or URL to load                       |
    | `title`       | `string`  | `""`        | Window title bar text                          |
    | `width`       | `number`  | `800`       | Window width in logical pixels                 |
    | `height`      | `number`  | `600`       | Window height in logical pixels                |
    | `x`           | `number`  | OS default  | Window x position                              |
    | `y`           | `number`  | OS default  | Window y position                              |
    | `center`      | `boolean` | `false`     | Center on primary monitor                      |
    | `resizable`   | `boolean` | `true`      | Allow user resize                              |
    | `decorations` | `boolean` | `true`      | Show OS title bar                              |
    | `alwaysOnTop` | `boolean` | `false`     | Stay above other windows                       |
    | `visible`     | `boolean` | `true`      | Show immediately (set false for state restore) |
    | `focused`     | `boolean` | `true`      | Focus on creation                              |
    | `parent`      | `Window`  | `undefined` | Parent window for child/owned relationship     |
    | `closable`    | `boolean` | `true`      | Allow close button                             |
    | `minimizable` | `boolean` | `true`      | Allow minimize button                          |
    | `maximizable` | `boolean` | `true`      | Allow maximize button                          |
    
    ---
    
    ## EventTarget Type (JavaScript)
    
    Used with `emitTo()` for explicit target specification.
    
    ```typescript
    type EventTarget =
      | { kind: "Any" }
      | { kind: "AnyLabel"; label: string }
      | { kind: "Webview"; label: string }
      | { kind: "WebviewWindow"; label: string };
    ```
    
    Most common usage: pass the label string directly (shorthand for `WebviewWindow` target).
    
    ---
    
    ## Built-in Tauri Events
    
    | Event                     | Trigger                             |
    | ------------------------- | ----------------------------------- |
    | `tauri://created`         | Window/webview successfully created |
    | `tauri://error`           | Window/webview creation failed      |
    | `tauri://close-requested` | User clicked close button           |
    | `tauri://destroyed`       | Window/webview destroyed            |
    | `tauri://focus`           | Window gained focus                 |
    | `tauri://blur`            | Window lost focus                   |
    | `tauri://resize`          | Window resized                      |
    | `tauri://move`            | Window moved                        |
    | `tauri://scale-change`    | Display scale factor changed        |
    | `tauri://theme-changed`   | OS theme changed (light/dark)       |
    | `tauri://webview-created` | A new webview was created           |
    
    ---
    
    ## Window State Plugin Permissions
    
    | Permission                             | Purpose                                             |
    | -------------------------------------- | --------------------------------------------------- |
    | `window-state:default`                 | Includes filename, restore-state, save-window-state |
    | `window-state:allow-filename`          | Access state file path                              |
    | `window-state:allow-restore-state`     | Restore window state                                |
    | `window-state:allow-save-window-state` | Save window state                                   |
    
    ---
    
    ## Decision Quick Reference
    
    | Question                         | Answer                                                                          |
    | -------------------------------- | ------------------------------------------------------------------------------- |
    | How to target one window?        | `emitTo(label, event, payload)`                                                 |
    | How to target all windows?       | `emit(event, payload)`                                                          |
    | How to target multiple windows?  | `emit_filter()` (Rust) or multiple `emitTo()` (JS)                              |
    | How to prevent close?            | `onCloseRequested` + `event.preventDefault()`                                   |
    | How to force-close?              | `window.destroy()` (skips close-requested)                                      |
    | How to persist window state?     | `tauri-plugin-window-state`                                                     |
    | How to avoid flash on restore?   | Set `visible: false` in config                                                  |
    | How to make a child window?      | Set `parent` option in creation                                                 |
    | How to split window into panels? | Multi-webview with `unstable` feature                                           |
    | How to check if window exists?   | `WebviewWindow.getByLabel(label)` (JS) / `app.get_webview_window(label)` (Rust) |
    
    ---
    
    See [SKILL.md](SKILL.md) for the full decision framework and red flags.
    
  • SKILL.md 16 KB
    ---
    name: desktop-multiwindow-tauri
    description: Tauri 2.x multi-window creation, event system, window state persistence, parent/child and modal windows
    ---
    
    # Tauri Multi-Window & Events
    
    > **Quick Guide:** Create windows from JS with `new WebviewWindow(label, options)` or from Rust with `WebviewWindowBuilder`. Communicate across windows using events: `emit()` broadcasts globally, `emitTo(label, event, payload)` targets a specific window, `emit_filter()` targets multiple windows by predicate. Always call the unlisten function returned by `listen()`. Use `tauri-plugin-window-state` to persist window position/size across sessions. Window labels must be unique and are used for both event targeting and permission scoping.
    >
    > **Current version:** Tauri 2.x (stable). Multi-webview in a single window requires the `unstable` feature flag.
    
    ---
    
    <critical_requirements>
    
    ## CRITICAL: Before Using This Skill
    
    > **All code must follow project conventions in CLAUDE.md** (kebab-case, named exports, import ordering, `import type`, named constants)
    
    **(You MUST give every window a unique label -- labels identify windows for event targeting, permissions, and retrieval)**
    
    **(You MUST always call the unlisten function returned by `listen()` / `once()` -- leaked listeners cause memory leaks in long-running apps)**
    
    **(You MUST check if a window already exists before creating it -- duplicate labels cause runtime errors)**
    
    **(You MUST add window labels to capability file `windows` array -- windows without permissions cannot use plugins or core APIs)**
    
    **(You MUST use `@tauri-apps/api/event` for global events and `@tauri-apps/api/webviewWindow` for window-scoped events -- mixing them causes missed events)**
    
    </critical_requirements>
    
    ---
    
    **Auto-detection:** WebviewWindow, WebviewWindowBuilder, emit_to, emitTo, emit_filter, EventTarget, window label, multi-window, onCloseRequested, tauri-plugin-window-state, parent window, modal window, WebviewBuilder, add_child, getCurrentWebviewWindow, window.listen, window.emit, cross-window communication
    
    **When to use:**
    
    - Creating secondary windows (settings, preferences, about, detached panels)
    - Communicating between windows via the Tauri event system
    - Handling window close confirmation (unsaved changes dialogs)
    - Persisting window size/position across app restarts
    - Setting up parent/child or modal window relationships
    - Building multi-panel layouts with multiple webviews in a single window
    
    **When NOT to use:**
    
    - Single-window apps with no inter-window communication (use the base Tauri framework skill)
    - General Tauri commands, IPC, or plugin setup (use the base Tauri framework skill)
    - Frontend framework state management within a single window (use respective framework skills)
    
    **Key patterns covered:**
    
    - Window creation from JS and Rust ([examples/core.md](examples/core.md))
    - Event system: emit, emitTo, emit_filter for targeted messaging ([examples/core.md](examples/core.md))
    - Cross-window state synchronization via events ([examples/core.md](examples/core.md))
    - Window close confirmation with onCloseRequested ([examples/core.md](examples/core.md))
    - Window state persistence with tauri-plugin-window-state ([examples/persistence.md](examples/persistence.md))
    - Parent/child windows and modal dialogs ([examples/advanced.md](examples/advanced.md))
    - Multi-webview in a single window (unstable) ([examples/advanced.md](examples/advanced.md))
    
    **Detailed resources:**
    
    - [examples/core.md](examples/core.md) - Window creation, event system, close confirmation, cross-window sync
    - [examples/persistence.md](examples/persistence.md) - Window state plugin setup, manual save/restore, StateFlags
    - [examples/advanced.md](examples/advanced.md) - Parent/child windows, modals, multi-webview layouts
    - [reference.md](reference.md) - API quick reference, event method comparison, decision framework
    
    ---
    
    <philosophy>
    
    ## Philosophy
    
    Tauri's multi-window architecture is built on two key concepts: **window labels** for identification and **events** for communication.
    
    Every window has a unique string label assigned at creation. This label is used everywhere: event targeting with `emitTo`, permission scoping in capability files, and retrieval with `app.get_webview_window(label)`. Labels are the window's address.
    
    The event system follows a pub-sub model with three tiers of targeting:
    
    1. **Global** (`emit`) -- all listeners in all windows receive the event
    2. **Targeted** (`emitTo`) -- only listeners in the specified window receive the event
    3. **Filtered** (`emit_filter`) -- listeners matching a predicate receive the event
    
    **When to use multi-window patterns:**
    
    - App needs separate UI surfaces (settings panel, file viewer, log output)
    - Need to decouple UI concerns into independent windows
    - Need modal dialogs that block interaction with the parent window
    - Need persistent panel layouts (IDE-style split views)
    
    **When NOT to use multi-window:**
    
    - A tabbed interface within a single window handles the use case
    - The secondary UI is a simple overlay/modal that lives in the same DOM
    - You only need to show/hide sections of a single-page app
    
    </philosophy>
    
    ---
    
    <patterns>
    
    ## Core Patterns
    
    ### Pattern 1: Window Creation
    
    Create windows from JavaScript or Rust. Always check if the window exists first to avoid duplicate-label errors.
    
    ```typescript
    import { WebviewWindow } from "@tauri-apps/api/webviewWindow";
    
    const SETTINGS_WINDOW_LABEL = "settings";
    const SETTINGS_WIDTH = 600;
    const SETTINGS_HEIGHT = 400;
    
    // Check if already open, focus it instead of creating duplicate
    const existing = await WebviewWindow.getByLabel(SETTINGS_WINDOW_LABEL);
    if (existing) {
      await existing.setFocus();
      return;
    }
    
    const settingsWindow = new WebviewWindow(SETTINGS_WINDOW_LABEL, {
      url: "settings.html",
      title: "Settings",
      width: SETTINGS_WIDTH,
      height: SETTINGS_HEIGHT,
      resizable: false,
      center: true,
    });
    
    settingsWindow.once("tauri://created", () => {
      console.log("Settings window created");
    });
    
    settingsWindow.once("tauri://error", (e) => {
      console.error("Failed to create window:", e);
    });
    ```
    
    **Why good:** checks for existing window first, uses named constants for dimensions, handles both creation success and error events
    
    See [examples/core.md](examples/core.md) for Rust-side creation with `WebviewWindowBuilder` and a full reusable helper.
    
    ---
    
    ### Pattern 2: Event System (Global, Targeted, Filtered)
    
    Three tiers of event emission -- choose the narrowest scope that fits.
    
    ```typescript
    import { emit, emitTo, listen } from "@tauri-apps/api/event";
    
    // Global: all windows receive
    await emit("theme-changed", { theme: "dark" });
    
    // Targeted: only the "editor" window receives
    await emitTo("editor", "file-opened", { path: "/docs/readme.md" });
    ```
    
    ```rust
    use tauri::{Emitter, EventTarget};
    
    // Targeted: send to one window
    app.emit_to("editor", "file-opened", payload)?;
    
    // Filtered: send to multiple specific windows
    app.emit_filter("file-opened", payload, |target| match target {
        EventTarget::WebviewWindow { label } =>
            label == "main" || label == "file-viewer",
        _ => false,
    })?;
    ```
    
    **Key decision:** Use `emit()` for app-wide broadcasts (theme changes, user logout). Use `emitTo()` when you know the exact target window. Use `emit_filter()` when targeting multiple specific windows.
    
    See [examples/core.md](examples/core.md) for the full listening pattern with cleanup and cross-window state sync.
    
    ---
    
    ### Pattern 3: Window Close Confirmation
    
    Intercept the close request to show a confirmation dialog (e.g., unsaved changes).
    
    ```typescript
    import { getCurrentWindow } from "@tauri-apps/api/window";
    
    const unlisten = await getCurrentWindow().onCloseRequested(async (event) => {
      if (hasUnsavedChanges()) {
        event.preventDefault();
        // Show your confirmation UI, then call window.close() or window.destroy() if confirmed
      }
    });
    
    // Clean up listener when no longer needed
    unlisten();
    ```
    
    **Key point:** `event.preventDefault()` stops the window from closing. You must then either close it programmatically when the user confirms or leave it open. `destroy()` force-closes without triggering close-requested again.
    
    See [examples/core.md](examples/core.md) for the Rust-side equivalent using `on_window_event`.
    
    ---
    
    ### Pattern 4: Window State Persistence
    
    Use `tauri-plugin-window-state` to automatically save and restore window position/size across sessions.
    
    ```rust
    tauri::Builder::default()
        .setup(|app| {
            #[cfg(desktop)]
            app.handle().plugin(
                tauri_plugin_window_state::Builder::default().build()
            );
            Ok(())
        })
    ```
    
    **Key point:** Set `visible: false` in window config and let the plugin show the window after restoring state -- prevents a flash of the default position before restore. See [examples/persistence.md](examples/persistence.md) for manual save/restore and StateFlags.
    
    ---
    
    ### Pattern 5: Parent/Child and Modal Windows
    
    Create owned windows that are tied to a parent. Modal windows block interaction with the parent until closed.
    
    ```typescript
    import {
      WebviewWindow,
      getCurrentWebviewWindow,
    } from "@tauri-apps/api/webviewWindow";
    
    const DIALOG_LABEL = "confirm-dialog";
    const DIALOG_WIDTH = 400;
    const DIALOG_HEIGHT = 200;
    
    const parent = getCurrentWebviewWindow();
    
    const dialog = new WebviewWindow(DIALOG_LABEL, {
      url: "confirm.html",
      title: "Confirm Action",
      width: DIALOG_WIDTH,
      height: DIALOG_HEIGHT,
      parent: parent,
      center: true,
    });
    ```
    
    ```rust
    use tauri::WebviewWindowBuilder;
    
    const DIALOG_WIDTH: f64 = 400.0;
    const DIALOG_HEIGHT: f64 = 200.0;
    
    WebviewWindowBuilder::new(&app, "confirm-dialog", tauri::WebviewUrl::App("confirm.html".into()))
        .title("Confirm Action")
        .inner_size(DIALOG_WIDTH, DIALOG_HEIGHT)
        .parent(&parent_window)?
        .center()
        .build()?;
    ```
    
    **Key point:** On macOS, parent makes the child a child window. On Linux, it makes it transient. Owned windows are hidden when the parent is minimized. See [examples/advanced.md](examples/advanced.md) for modal patterns.
    
    ---
    
    ### Pattern 6: Multi-Webview in a Single Window (Unstable)
    
    Create multiple webviews within a single window for panel/splitter layouts. Requires the `unstable` Cargo feature.
    
    ```rust
    // Cargo.toml: tauri = { version = "2", features = ["unstable"] }
    
    use tauri::{LogicalPosition, LogicalSize};
    use tauri::webview::WebviewBuilder;
    use tauri::window::WindowBuilder;
    
    let window = WindowBuilder::new(&app, "main")
        .inner_size(1200.0, 800.0)
        .build()?;
    
    let sidebar_width = 300.0;
    let main_width = 900.0;
    let height = 800.0;
    
    window.add_child(
        WebviewBuilder::new("sidebar", tauri::WebviewUrl::App("sidebar.html".into())),
        LogicalPosition::new(0.0, 0.0),
        LogicalSize::new(sidebar_width, height),
    )?;
    
    window.add_child(
        WebviewBuilder::new("content", tauri::WebviewUrl::App("content.html".into())),
        LogicalPosition::new(sidebar_width, 0.0),
        LogicalSize::new(main_width, height),
    )?;
    ```
    
    **Key point:** This is behind the `unstable` feature flag and is desktop-only. Each child webview gets its own label and can communicate via the event system. See [examples/advanced.md](examples/advanced.md) for resizing patterns.
    
    </patterns>
    
    ---
    
    <decision_framework>
    
    ## Decision Framework
    
    ### Event Targeting
    
    ```
    Who should receive this event?
    |-- ALL windows in the app?
    |   +-- emit() (global broadcast)
    |-- ONE specific window?
    |   +-- emitTo(label, event, payload) (targeted)
    |-- MULTIPLE specific windows (but not all)?
    |   +-- emit_filter() with predicate (Rust only)
    +-- The backend (Rust side)?
        +-- emit() from frontend + app.listen() in Rust
    ```
    
    ### Window Architecture
    
    ```
    Does this UI surface need to be a separate window?
    |-- Is it a settings/preferences panel?
    |   +-- YES: separate window with its own label
    |-- Is it a modal confirmation dialog?
    |   +-- YES: child window with parent set
    |-- Is it a dockable/undockable panel (IDE-style)?
    |   +-- YES: multi-webview in single window (unstable)
    |-- Is it content that can live in the same DOM?
    |   +-- NO: don't create a new window -- use in-app routing/tabs
    +-- Does it need to persist across window close?
        +-- YES: use tauri-plugin-window-state
    ```
    
    ### Where to Create Windows
    
    ```
    Where does window creation belong?
    |-- Triggered by user action in UI (button click)?
    |   +-- Frontend: new WebviewWindow(label, options)
    |-- Triggered by backend logic (file watcher, system event)?
    |   +-- Rust: WebviewWindowBuilder::new(app, label, url)
    |-- Need multiple webviews in one window?
    |   +-- Rust: WindowBuilder + window.add_child(WebviewBuilder)
    +-- Declared at startup in config?
        +-- tauri.conf.json "windows" array
    ```
    
    See [reference.md](reference.md) for the complete API quick reference.
    
    </decision_framework>
    
    ---
    
    <red_flags>
    
    ## RED FLAGS
    
    **High Priority Issues:**
    
    - Duplicate window labels -- creating a window with a label that already exists throws a runtime error; always check first with `getByLabel()` or `app.get_webview_window()`
    - Missing unlisten calls -- `listen()` returns an unlisten function; not calling it leaks memory, especially in long-running apps or components that mount/unmount
    - Window label not in capability file `windows` array -- the new window silently lacks permissions for plugins and core APIs
    - Using `@tauri-apps/api/tauri` for event imports -- removed in v2; use `@tauri-apps/api/event` for global events and `@tauri-apps/api/webviewWindow` for window-scoped events
    - Calling `emit()` when you mean `emitTo()` -- broadcasting sensitive events (auth tokens, user data) to all windows is a security risk
    
    **Medium Priority Issues:**
    
    - Not handling `tauri://error` event on window creation -- creation can fail silently without this listener
    - Using `close()` after `preventDefault()` in close handler -- triggers the close-requested event again; use `destroy()` to force-close without re-triggering
    - Hardcoding window dimensions without named constants -- magic numbers make layouts harder to maintain
    - Not using `center: true` or explicit position -- windows appear at OS-default position, which may be off-screen on multi-monitor setups
    
    **Gotchas & Edge Cases:**
    
    - **`onCloseRequested` on dynamic windows:** there is a known issue where `event.preventDefault()` may not work on windows created at runtime; use the Rust `on_window_event` approach as a fallback
    - **Event payload serialization:** payloads must be serializable (JSON-compatible from JS, `serde::Serialize` from Rust); complex types like functions or DOM nodes cannot be sent
    - **Window labels:** must be alphanumeric with `-`, `/`, `:`, `_` characters only -- no spaces or special characters
    - **Multi-monitor:** `center()` centers on the primary monitor; for specific monitor positioning use `setPosition()` with `PhysicalPosition`
    - **`emit_filter` is Rust-only:** there is no JavaScript equivalent; use multiple `emitTo()` calls from the frontend
    - **Multi-webview is unstable:** requires `features = ["unstable"]` in Cargo.toml and is desktop-only (no mobile support)
    - **Window state plugin:** does not restore maximized/fullscreen state by default -- use `StateFlags::all()` to include all state dimensions
    - **Platform differences:** parent/child behavior varies -- macOS creates child windows, Linux uses transient windows, Windows creates owned windows
    
    </red_flags>
    
    ---
    
    <critical_reminders>
    
    ## CRITICAL REMINDERS
    
    > **All code must follow project conventions in CLAUDE.md** (kebab-case, named exports, import ordering, `import type`, named constants)
    
    **(You MUST give every window a unique label -- labels identify windows for event targeting, permissions, and retrieval)**
    
    **(You MUST always call the unlisten function returned by `listen()` / `once()` -- leaked listeners cause memory leaks in long-running apps)**
    
    **(You MUST check if a window already exists before creating it -- duplicate labels cause runtime errors)**
    
    **(You MUST add window labels to capability file `windows` array -- windows without permissions cannot use plugins or core APIs)**
    
    **(You MUST use `@tauri-apps/api/event` for global events and `@tauri-apps/api/webviewWindow` for window-scoped events -- mixing them causes missed events)**
    
    **Failure to follow these rules will cause runtime errors, memory leaks, silent permission failures, and missed events.**
    
    </critical_reminders>
    

Comments (0)

Sign in to join the conversation.

No comments yet.

Reviews (0)

No reviews yet.

Related