.claude/skills/tauri-2/tauri-impl/tauri-impl-multi-window/SKILL.md
Use when creating secondary windows, implementing splashscreen flows, or communicating between windows in Tauri 2. Prevents duplicate window label panics and orphaned window handles from missing lifecycle cleanup. Covers creating windows from Rust and JavaScript, inter-window events, show/hide patterns, splashscreen, and parent-child relationships. Keywords: tauri multi-window, secondary window, splashscreen, inter-window communication, window lifecycle, parent-child.
npx skillsauth add OpenAEC-Foundation/OpenAEC-Workspace-Composer tauri-impl-multi-windowInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
| Method | Context | Example |
|--------|---------|---------|
| WebviewWindowBuilder::new() | Rust (setup, commands) | WebviewWindowBuilder::new(app, "label", url).build()? |
| new Window() | TypeScript | new Window('label', { url: '/page' }) |
| tauri.conf.json windows array | Configuration | Main window auto-created on startup |
| Method | Context | Returns |
|--------|---------|---------|
| app.get_webview_window("label") | Rust | Option<WebviewWindow<R>> |
| app.webview_windows() | Rust | HashMap<String, WebviewWindow<R>> |
| getCurrentWindow() | TypeScript | Window |
| Window.getByLabel("label") | TypeScript | Promise<Window \| null> |
| getAllWindows() | TypeScript | Promise<Window[]> |
| Method | Direction | Scope |
|--------|-----------|-------|
| app.emit("event", payload) | Rust to all | Broadcast |
| app.emit_to("label", "event", payload) | Rust to specific window | Targeted |
| emit("event", payload) | JS to all | Broadcast |
| emitTo("label", "event", payload) | JS to specific window | Targeted |
| listen("event", handler) | JS receive | All events |
NEVER create two windows with the same label -- labels MUST be unique across the application. Duplicate labels cause a runtime error.
NEVER forget to await listen() before calling the unlisten function -- listen() returns Promise<UnlistenFn>, not UnlistenFn.
NEVER assume windows exist without checking -- ALWAYS use get_webview_window() and handle the None case.
ALWAYS clean up event listeners in component-based frameworks (React, Vue, Svelte) to prevent memory leaks.
ALWAYS use WebviewWindowBuilder (not WindowBuilder) for creating windows in Tauri 2 -- WindowBuilder is the v1 API.
ALWAYS use WebviewUrl::App() for local pages and WebviewUrl::External() for external URLs.
use tauri::webview::WebviewWindowBuilder;
use tauri::WebviewUrl;
// In setup() or any context with Manager access
.setup(|app| {
let settings_window = WebviewWindowBuilder::new(
app,
"settings", // unique label
WebviewUrl::App("settings.html".into()), // local page
)
.title("Settings")
.inner_size(600.0, 400.0)
.center()
.resizable(true)
.visible(false) // hidden by default
.build()?;
Ok(())
})
import { Window } from '@tauri-apps/api/window';
const settingsWindow = new Window('settings-window', {
url: '/settings',
title: 'Settings',
width: 600,
height: 400,
center: true,
decorations: true,
resizable: true,
});
#[tauri::command]
fn show_settings(app: tauri::AppHandle) {
if let Some(window) = app.get_webview_window("settings") {
window.show().unwrap();
window.set_focus().unwrap();
}
}
#[tauri::command]
fn hide_settings(app: tauri::AppHandle) {
if let Some(window) = app.get_webview_window("settings") {
window.hide().unwrap();
}
}
TypeScript equivalent:
import { Window } from '@tauri-apps/api/window';
async function toggleSettings() {
const settings = await Window.getByLabel('settings');
if (settings) {
const visible = await settings.isVisible();
if (visible) {
await settings.hide();
} else {
await settings.show();
await settings.setFocus();
}
}
}
Rust side -- emit to a specific window:
use tauri::Emitter;
#[tauri::command]
fn notify_settings(app: tauri::AppHandle, key: String, value: String) {
app.emit_to("settings", "config-changed", serde_json::json!({
"key": key,
"value": value,
})).unwrap();
}
TypeScript side -- send and receive between windows:
// In main window -- send to settings window
import { emitTo } from '@tauri-apps/api/event';
await emitTo('settings', 'config-changed', {
key: 'theme',
value: 'dark',
});
// In settings window -- listen for events
import { listen } from '@tauri-apps/api/event';
const unlisten = await listen<{ key: string; value: string }>('config-changed', (event) => {
console.log(`Config changed: ${event.payload.key} = ${event.payload.value}`);
});
Show a splashscreen while the main window loads, then swap:
// tauri.conf.json -- define both windows
{
"app": {
"windows": [
{
"label": "splashscreen",
"url": "/splashscreen",
"width": 400,
"height": 300,
"decorations": false,
"resizable": false,
"center": true
},
{
"label": "main",
"url": "/index.html",
"width": 1024,
"height": 768,
"visible": false
}
]
}
}
// src-tauri/src/lib.rs
#[tauri::command]
fn close_splashscreen(app: tauri::AppHandle) {
if let Some(splash) = app.get_webview_window("splashscreen") {
splash.close().unwrap();
}
if let Some(main) = app.get_webview_window("main") {
main.show().unwrap();
main.set_focus().unwrap();
}
}
// In main window's initialization code
import { invoke } from '@tauri-apps/api/core';
document.addEventListener('DOMContentLoaded', async () => {
// App is loaded, close splashscreen
await invoke('close_splashscreen');
});
// Rust -- prevent close and hide instead
.on_window_event(|window, event| {
if let tauri::WindowEvent::CloseRequested { api, .. } = event {
if window.label() == "settings" {
api.prevent_close();
window.hide().unwrap();
}
}
})
// TypeScript -- confirm before closing
import { getCurrentWindow } from '@tauri-apps/api/window';
const win = getCurrentWindow();
await win.onCloseRequested(async (event) => {
const confirmed = await confirm('Are you sure you want to close?');
if (!confirmed) {
event.preventDefault();
}
});
import { getCurrentWindow } from '@tauri-apps/api/window';
const win = getCurrentWindow();
// Resize events
await win.onResized(({ payload: size }) => {
console.log(`Window resized to ${size.width}x${size.height}`);
});
// Focus events
await win.onFocusChanged(({ payload: focused }) => {
console.log(`Window ${focused ? 'gained' : 'lost'} focus`);
});
// Theme change events (not supported on Linux)
await win.onThemeChanged(({ payload: theme }) => {
console.log(`Theme changed to: ${theme}`);
});
.inner_size(width: f64, height: f64) // Window content size
.min_inner_size(w: f64, h: f64) // Minimum size
.max_inner_size(w: f64, h: f64) // Maximum size
.position(x: f64, y: f64) // Window position
.center() // Center on screen
.title(title: impl Into<String>) // Window title
.visible(bool) // Show on creation
.focused(bool) // Focus on creation
.maximized(bool) // Start maximized
.fullscreen(bool) // Start fullscreen
.resizable(bool) // Allow resizing
.closable(bool) // Allow closing
.minimizable(bool) // Allow minimizing
.maximizable(bool) // Allow maximizing
.transparent(bool) // Transparent background
.initialization_script(script: impl Into<String>) // JS to run on page load
.user_agent(ua: &str) // Custom user agent
.devtools(enabled: bool) // Enable DevTools
| Event Variant | Fields | Description |
|---------------|--------|-------------|
| CloseRequested | api: CloseRequestApi | Close requested; api.prevent_close() to cancel |
| Destroyed | -- | Window destroyed |
| Focused | bool | Focus gained (true) or lost (false) |
| Resized | PhysicalSize<u32> | Window resized |
| Moved | PhysicalPosition<i32> | Window moved |
| ScaleFactorChanged | scale_factor, new_inner_size | DPI/display scale changed |
| DragDrop | DragDropEvent | File drag-and-drop event |
| ThemeChanged | Theme | System theme changed |
development
Use when integrating Vite with a backend framework, rendering Vite assets from server-side templates, or setting up dev/production HTML serving. Prevents incorrect manifest.json traversal and missing CSS chunk resolution in production. Covers build.manifest configuration, .vite/manifest.json structure, ManifestChunk properties, dev mode HTML setup, production rendering, CSS/JS chunk resolution, and modulepreload polyfill. Keywords: backend integration, manifest.json, ManifestChunk, Django, Laravel, Rails, modulepreload.
development
Use when encountering dev server startup failures, HMR issues, proxy errors, CORS blocks, or module not found errors during development. Prevents misconfiguring server.hmr behind reverse proxies and forgetting appType: 'custom' in middleware mode. Covers HMR full-reload debugging, proxy configuration, CORS setup, HTTPS certificates, server.fs.strict violations, port conflicts, WebSocket failures, file watcher issues, and middleware mode. Keywords: dev server, HMR, proxy, CORS, HTTPS, WebSocket, port conflict, server.fs.strict, middleware mode, file watcher.
development
Use when encountering pre-bundling errors, dependency resolution failures, stale cache issues, or slow development server startup. Prevents excluding CJS dependencies from pre-bundling (which breaks runtime module resolution) and misconfiguring optimizeDeps. Covers CJS/ESM conversion failures, missing dependency auto-discovery, optimizeDeps configuration, monorepo linked dependencies, cache invalidation, browser cache staleness, and large dependency tree performance. Keywords: pre-bundling, optimizeDeps, CJS, ESM, cache, dependency resolution, monorepo, node_modules/.vite.
development
Use when encountering Vite build failures, chunk size warnings, or version-specific build errors. Prevents the common mistake of using deprecated rollupOptions in v8 or misconfiguring build targets and minifiers. Covers Rolldown/Rollup bundling failures, CSS minification errors, sourcemap problems, library mode build failures, BundleError handling, and asset processing errors. Keywords: build error, Rolldown, chunk size, sourcemap, library mode, minify, BundleError, rollupOptions, build.target.