.claude/skills/tauri-2/tauri-core/tauri-core-runtime/SKILL.md
Use when configuring app initialization, using setup hooks, spawning background tasks, or managing app lifecycle. Prevents misconfigured Builder chains and missing plugin registration that cause silent runtime failures. Covers Builder configuration, setup hook, AppHandle usage, Manager trait, tokio async runtime, and app exit/restart. Keywords: tauri builder, setup hook, AppHandle, Manager trait, tokio runtime, app lifecycle, window events.
npx skillsauth add OpenAEC-Foundation/OpenAEC-Workspace-Composer tauri-core-runtimeInstall 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 | Purpose | Example |
|--------|---------|---------|
| Builder::default() | Create default builder | tauri::Builder::default() |
| .setup(F) | App initialization hook | .setup(\|app\| { Ok(()) }) |
| .invoke_handler(F) | Register command handlers | .invoke_handler(generate_handler![cmd1]) |
| .manage(T) | Register managed state | .manage(MyState::default()) |
| .plugin(P) | Register a plugin | .plugin(my_plugin::init()) |
| .menu(F) | Set application menu | .menu(\|app\| MenuBuilder::new(app).build()) |
| .on_window_event(F) | Window event handler | .on_window_event(\|window, event\| { }) |
| .on_menu_event(F) | Menu event handler | .on_menu_event(\|app, event\| { }) |
| .on_webview_event(F) | Webview event handler | .on_webview_event(\|webview, event\| { }) |
| .on_page_load(F) | Page load handler | .on_page_load(\|webview, payload\| { }) |
| .on_tray_icon_event(F) | Tray icon event handler | .on_tray_icon_event(\|app, event\| { }) |
| .any_thread() | Allow running on non-main thread | .any_thread() |
| .build(Context) | Build without running (returns App) | .build(generate_context!()) |
| .run(Context) | Build and run the app | .run(generate_context!()) |
| Method | Returns | Available On |
|--------|---------|-------------|
| app_handle() | &AppHandle<R> | App, AppHandle, Window, WebviewWindow, Webview |
| state::<T>() | State<'_, T> | All Manager implementors |
| try_state::<T>() | Option<State<'_, T>> | All Manager implementors |
| manage(T) | bool | All Manager implementors |
| get_webview_window(label) | Option<WebviewWindow<R>> | All Manager implementors |
| webview_windows() | HashMap<String, WebviewWindow<R>> | All Manager implementors |
| get_window(label) | Option<Window<R>> | All Manager implementors |
| get_focused_window() | Option<Window<R>> | All Manager implementors |
| config() | &Config | All Manager implementors |
| path() | &PathResolver<R> | All Manager implementors |
| package_info() | &PackageInfo | All Manager implementors |
| Property | Description |
|----------|-------------|
| Clone | Can be cloned freely |
| Send + Sync | Safe to pass across threads |
| Implements Manager | Full access to state, windows, config |
| Implements Emitter | Can emit events |
| Implements Listener | Can listen for events |
NEVER call .invoke_handler() more than once on a Builder -- only the last call takes effect. Put ALL commands in a single generate_handler![].
NEVER use Arc to wrap managed state -- Tauri already wraps state in Arc internally. Use app.manage(data), not app.manage(Arc::new(data)).
NEVER access managed state with the wrong wrapper type -- State<'_, MyState> when you registered Mutex<MyState> causes a runtime panic, not a compile error.
ALWAYS clone AppHandle before moving it into a thread or async block -- app.handle().clone() is the correct pattern.
ALWAYS use #[cfg_attr(mobile, tauri::mobile_entry_point)] on the run() function in lib.rs when targeting mobile platforms.
NEVER perform heavy I/O in setup() synchronously without spawning a thread -- it blocks app startup.
// src-tauri/src/lib.rs
mod commands;
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tauri::Builder::default()
.manage(AppState::default())
.plugin(tauri_plugin_opener::init())
.invoke_handler(tauri::generate_handler![
commands::greet,
commands::fetch_data,
])
.setup(|app| {
let handle = app.handle().clone();
// Initialization logic here
Ok(())
})
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
// src-tauri/src/main.rs
fn main() {
app_lib::run();
}
The setup hook runs after the app is initialized but before windows are shown. It receives &mut App<R> and returns Result<(), Box<dyn Error>>. Any error aborts app startup.
.setup(|app| {
// 1. Access AppHandle for thread-safe operations
let handle = app.handle().clone();
// 2. Register state that depends on app paths
let db_path = app.path().app_data_dir()?.join("data.db");
app.manage(Database::new(&db_path)?);
// 3. Spawn a background thread (uses cloned AppHandle)
std::thread::spawn(move || {
loop {
handle.emit("heartbeat", ()).unwrap();
std::thread::sleep(std::time::Duration::from_secs(30));
}
});
// 4. Create additional windows programmatically
let _settings = tauri::webview::WebviewWindowBuilder::new(
app,
"settings",
tauri::WebviewUrl::App("settings.html".into()),
)
.title("Settings")
.inner_size(600.0, 400.0)
.visible(false)
.build()?;
Ok(())
})
AppHandle is Clone + Send + Sync, making it the primary handle for background work.
// In setup() or from a command
let handle = app.handle().clone();
// Std thread
std::thread::spawn(move || {
let state = handle.state::<MyState>();
handle.emit("background-update", "data").unwrap();
if let Some(window) = handle.get_webview_window("main") {
window.set_title("Updated").unwrap();
}
});
// Tokio async task
let handle = app.handle().clone();
tokio::spawn(async move {
let result = some_async_work().await;
handle.emit("async-done", result).unwrap();
});
.on_window_event(|window, event| {
match event {
tauri::WindowEvent::CloseRequested { api, .. } => {
// Prevent close and hide instead (e.g., tray app)
api.prevent_close();
window.hide().unwrap();
}
tauri::WindowEvent::Focused(focused) => {
if *focused {
println!("{} gained focus", window.label());
}
}
tauri::WindowEvent::Destroyed => {
println!("{} was destroyed", window.label());
}
_ => {}
}
})
Use .build() when you need access to the App instance before running, or when you need the run callback for lifecycle events like exit.
// .run() -- simple, no run callback
tauri::Builder::default()
.run(tauri::generate_context!())
.expect("error running app");
// .build() + .run() -- access to RunEvent for exit handling
let app = tauri::Builder::default()
.build(tauri::generate_context!())
.expect("error building app");
app.run(|app_handle, event| {
match event {
tauri::RunEvent::ExitRequested { api, .. } => {
// Prevent exit (e.g., keep running in tray)
api.prevent_exit();
}
tauri::RunEvent::Exit => {
// Final cleanup before process ends
println!("Application exiting");
}
_ => {}
}
});
// Exit from a command
#[tauri::command]
fn quit_app(app: tauri::AppHandle) {
app.exit(0); // 0 = success exit code
}
// Restart (requires tauri-plugin-process)
#[tauri::command]
fn restart_app(app: tauri::AppHandle) {
app.restart();
}
// Frontend exit/restart (requires @tauri-apps/plugin-process)
import { exit, relaunch } from '@tauri-apps/plugin-process';
await exit(0);
await relaunch();
Tauri 2.x uses tokio as the async runtime. Async commands are automatically spawned on the tokio runtime -- you do NOT need to configure tokio yourself.
The tauri::async_runtime module provides direct access when needed:
tauri::async_runtime::spawn(async {
// Runs on the tokio runtime
});
ALWAYS use async commands for I/O, network, or long-running operations. Sync commands run on the main thread and will freeze the UI if they block.
ALWAYS use #[tauri::command(async)] on a sync function if you want it to run on the async runtime instead of the main thread:
#[tauri::command(async)]
fn heavy_sync_computation(data: Vec<u8>) -> Vec<u8> {
// Runs on tokio runtime, not main thread
expensive_transform(data)
}
| Variant | Fields | Description |
|---------|--------|-------------|
| Resized | PhysicalSize<u32> | Window client area resized |
| Moved | PhysicalPosition<i32> | Window position changed |
| CloseRequested | api: CloseRequestApi | Close requested; call api.prevent_close() to cancel |
| Destroyed | -- | Window has been destroyed |
| Focused | bool | true = gained focus, false = lost |
| ScaleFactorChanged | scale_factor, new_inner_size | DPI/display scale changed |
| DragDrop | DragDropEvent | File drag-and-drop event |
| ThemeChanged | Theme | System theme changed (not on Linux) |
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.