Add definitive Tauri 2.0 guide, agent prompts, and WebSocket docs

- tauri2-guide/definitive-guide.md: Comprehensive Tauri 2.0 reference covering project structure, configuration, capabilities/permissions, commands/IPC, events/channels, window management, plugin ecosystem, WebSocket server, script injection, state management, v1->v2 migration, anti-patterns, and quick reference cheat sheet.

- tauri2-guide/agent-prompts.md: Copy-paste ready prompts for instructing AI agents to build Tauri 2.0 apps. Includes universal system prompt with sandbox constraints, 10 task-specific prompts, verification checklist, and v1->v2 API map.

- tauri2-docs/ws_server_client_inject_guide.md: Complete WebSocket server/client/script injection guide based on the moxie-app reference implementation. Covers the native Tokio WebSocket server pattern, frontend client connections, and script injection into external webview windows.

All code validated against Tauri 2.0 official docs (https://v2.tauri.app).
This commit is contained in:
Z User 2026-05-30 19:24:21 +00:00
parent 7f8eb17292
commit 04c128f509
3 changed files with 2879 additions and 0 deletions

View File

@ -0,0 +1,554 @@
# WebSocket Server, Client & Script Injection Guide
> Based on the working `moxie-app` reference implementation in this repository.
> This guide documents the complete pattern for running a native WebSocket server inside Tauri 2.0, connecting frontend clients, and injecting scripts into external webview windows.
---
## Overview
This guide covers three interrelated patterns used in the reference `moxie-app`:
1. **Native WebSocket Server** — A Tokio-backed WebSocket server running inside the Tauri process
2. **Frontend WebSocket Client** — JavaScript clients connecting to the local server for bidirectional communication
3. **Script Injection** — Injecting JavaScript into dynamically created webview windows (including external URLs) to bridge them into the WebSocket network
### Architecture Diagram
```text
┌─────────────────────────────────────────────────────────┐
│ Tauri Process │
│ │
│ ┌──────────────┐ ┌──────────────────────────────┐ │
│ │ lib.rs │ │ Tokio WebSocket Server │ │
│ │ │────▶│ (ws://127.0.0.1:8080) │ │
│ │ .setup() │ │ │ │
│ │ spawns │ │ broadcast channel (16 msg) │ │
│ │ server │ │ │ │
│ └──────────────┘ └───────────┬───────────────────┘ │
│ │ │
│ ┌─────────────────────────┼─────────────────┐ │
│ │ │ │ │
│ ┌──────▼──────┐ ┌───────────────▼──────────┐ │ │
│ │ Main Window │ │ Remote Window │ │ │
│ │ (index.html) │ │ (external URL) │ │ │
│ │ │ │ + injected script │ │ │
│ │ WebSocket │ │ + WebSocket client │ │ │
│ │ Client (JS) │ │ Client (injected JS) │ │ │
│ └──────┬──────┘ └───────────────┬──────────┘ │ │
└─────────┼─────────────────────────┼────────────────┘ │
│ │ │
│ WebSocket (local) │ │
└─────────────────────────┘ │
```
---
## Part 1: Native WebSocket Server (Rust)
### Why Run a WebSocket Server Inside Tauri?
Standard Tauri IPC (`invoke`, events, channels) only works between Tauri's own windows and its Rust backend. When you need to:
- Communicate with **external applications** running on the same machine
- Bridge **external URLs** loaded in webview windows back to the Tauri backend
- Enable **cross-window communication** at scale (beyond Tauri's event system)
- Create a **local API endpoint** for mobile apps or other tools to connect to
...a native WebSocket server running on localhost is the solution.
### Dependencies (`Cargo.toml`)
```toml
[dependencies]
tauri = { version = "2", features = [] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
tokio = { version = "1", features = ["net", "rt", "sync", "macros"] }
tokio-tungstenite = "0.24"
futures-util = "0.3"
```
**Key dependencies explained:**
- `tokio` — The async runtime Tauri uses internally. The `net` feature provides `TcpListener`, `sync` provides `broadcast` channels, `rt` provides the runtime, and `macros` provides `tokio::select!`.
- `tokio-tungstenite` — Industry-standard WebSocket implementation for Tokio. Provides `accept_async` for accepting WS connections and the `Message` type for frame construction.
- `futures-util` — Provides `StreamExt` (for `.next()` on streams) and `SinkExt` (for `.send()` on sinks).
### Server Implementation (`src-tauri/src/server.rs`)
```rust
use tokio::net::TcpListener;
use tokio_tungstenite::accept_async;
use futures_util::stream::StreamExt;
use futures_util::sink::SinkExt;
use tokio::sync::broadcast;
pub async fn start_websocket_server() {
// Bind to localhost only (security: not exposed to network)
let listener = TcpListener::bind("127.0.0.1:8080").await.unwrap();
println!("WebSocket Server listening on ws://127.0.0.1:8080");
// Broadcast channel: fans out messages to ALL connected clients
// Queue depth of 16 means if a slow client hasn't read 16 messages,
// the oldest undelivered message is dropped (lagging policy)
let (tx, _rx) = broadcast::channel::<String>(16);
// Accept connections in a loop
while let Ok((stream, _addr)) = listener.accept().await {
// Clone the broadcast transmitter for this connection's task
let tx = tx.clone();
// Create a dedicated receiver for this connection
let mut rx = tx.subscribe();
// Spawn a separate async task per client connection
tauri::async_runtime::spawn(async move {
// Perform the WebSocket handshake
if let Ok(ws_stream) = accept_async(stream).await {
println!("New WebSocket client connected");
let (mut ws_sender, mut ws_receiver) = ws_stream.split();
// Main message loop using tokio::select!
loop {
tokio::select! {
// Branch A: Message FROM this specific client
incoming = ws_receiver.next() => {
match incoming {
Some(Ok(msg)) if msg.is_text() => {
let text = msg.to_text().unwrap().to_string();
println!("Received from client: {}", text);
// Broadcast to ALL other connected clients
let _ = tx.send(text);
}
Some(Ok(msg)) if msg.is_binary() => {
// Handle binary data if needed
let _ = tx.send(format!("[binary:{}bytes]", msg.len()));
}
_ => {
// None = client disconnected, Err = error
println!("Client disconnected");
break;
}
}
}
// Branch B: Broadcast message FROM another client
broadcast_msg = rx.recv() => {
if let Ok(payload) = broadcast_msg {
// Construct a WebSocket text frame and send it
let frame = tokio_tungstenite::tungstenite::Message::Text(
payload.into()
);
if let Err(e) = ws_sender.send(frame).await {
eprintln!("Failed to send to client: {}", e);
break;
}
}
}
}
}
} else {
eprintln!("WebSocket handshake failed");
}
});
}
}
```
### How the Broadcast Pattern Works
```text
Client A sends "hello" ─────▶ Rust Server ─────▶ broadcast("hello") ─────▶ Client B receives "hello"
─────▶ Client C receives "hello"
─────▶ Client D receives "hello"
Client B sends "world" ─────▶ Rust Server ─────▶ broadcast("world") ─────▶ Client A receives "world"
─────▶ Client C receives "world"
─────▶ Client D receives "world"
```
Each client:
1. Gets its own `rx = tx.subscribe()` receiver
2. The main loop uses `tokio::select!` to wait on BOTH incoming messages AND broadcast messages simultaneously
3. When a client sends a message, it goes through `tx.send()` which broadcasts to all OTHER clients (the sender's own receiver won't get its own message because `subscribe()` only receives messages sent after subscription)
### Spawning the Server from `lib.rs`
```rust
// src-tauri/src/lib.rs
mod server;
use tauri::Manager;
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tauri::Builder::default()
.plugin(tauri_plugin_opener::init())
.setup(|_app| {
// Spawn the WebSocket server as a background task
// This runs on Tauri's managed Tokio runtime
tauri::async_runtime::spawn(server::start_websocket_server());
Ok(())
})
.invoke_handler(tauri::generate_handler![greet])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
```
**Key details:**
- `tauri::async_runtime::spawn()` is used instead of `tokio::spawn()` to ensure the task runs on Tauri's managed async runtime
- The server task runs for the entire lifetime of the application
- The `.setup()` closure returns immediately — the server runs in the background
---
## Part 2: Frontend WebSocket Client
### Main Window Connection (`src/main.js`)
The main application window connects to the WebSocket server using the standard browser `WebSocket` API:
```javascript
const { invoke } = window.__TAURI__.core;
// Connect to the local WebSocket server
const mainSocket = new WebSocket('ws://127.0.0.1:8080');
mainSocket.onopen = () => {
console.log('Main Control Window connected to the broadcast hub!');
};
mainSocket.onmessage = (event) => {
const data = JSON.parse(event.data);
console.log('Received broadcast:', data);
// Handle specific message types
if (data.localsession) {
document.querySelector('#session-display').textContent =
`Session: ${data.localsession}`;
}
};
mainSocket.onerror = (err) => {
console.error('WebSocket connection error:', err);
};
// Sending data to other connected windows
function sendToAllWindows(payload) {
mainSocket.send(JSON.stringify(payload));
}
// Example: button click sends data
document.querySelector('#broadcast-btn').addEventListener('click', () => {
sendToAllWindows({
sender: 'main-window',
type: 'notification',
content: 'Hello from the main window!'
});
});
```
### Message Protocol
While you can send any format over WebSocket, using JSON with a consistent structure is recommended:
```javascript
// Standard message envelope
{
"sender": "main-window", // Origin identifier
"type": "action-type", // Message category
"content": "payload data", // Message body
"timestamp": 1234567890 // Optional: for ordering
}
```
### Connection Lifecycle Management
For production apps, handle reconnection and cleanup:
```javascript
class TauriSocket {
constructor(url) {
this.url = url;
this.ws = null;
this.reconnectDelay = 1000;
this.maxReconnectDelay = 30000;
this.listeners = new Map();
}
connect() {
this.ws = new WebSocket(this.url);
this.ws.onopen = () => {
console.log('Connected to local WS server');
this.reconnectDelay = 1000; // Reset on successful connect
this.emit('connected');
};
this.ws.onmessage = (event) => {
try {
const data = JSON.parse(event.data);
this.emit('message', data);
} catch (e) {
console.error('Failed to parse WS message:', e);
}
};
this.ws.onclose = () => {
console.log('Disconnected, reconnecting...');
this.emit('disconnected');
setTimeout(() => this.connect(), this.reconnectDelay);
this.reconnectDelay = Math.min(
this.reconnectDelay * 1.5,
this.maxReconnectDelay
);
};
this.ws.onerror = (err) => {
console.error('WebSocket error:', err);
};
}
send(data) {
if (this.ws?.readyState === WebSocket.OPEN) {
this.ws.send(JSON.stringify(data));
}
}
on(event, callback) {
this.listeners.set(event, callback);
}
emit(event, data) {
this.listeners.get(event)?.(data);
}
disconnect() {
this.ws?.close();
}
}
// Usage
const socket = new TauriSocket('ws://127.0.0.1:8080');
socket.connect();
socket.on('message', (data) => console.log('Got:', data));
```
---
## Part 3: Script Injection into Webview Windows
### The Use Case
When your Tauri app opens windows with **external URLs** (e.g., a web-based admin panel, a third-party dashboard, or any remote page), those pages have no knowledge of Tauri or your local WebSocket server. Script injection allows you to:
1. **Bridge the external page** into your local communication network
2. **Extract data** from the page (localStorage, DOM, cookies) and relay it to Tauri
3. **Modify the page** (UI changes, injected controls, overlays)
4. **Share sessions** between windows (e.g., pass auth tokens)
### Injection via `initialization_script`
The `WebviewWindowBuilder::initialization_script()` method injects JavaScript that runs when the webview's page loads. Combined with `include_str!()`, you can load scripts from external files at compile time:
```rust
// src-tauri/src/lib.rs
use tauri::{AppHandle, Manager, WebviewUrl, WebviewWindowBuilder};
#[tauri::command]
async fn open_remote_session(app_handle: AppHandle, url: String) -> Result<String, String> {
let target_url = url.parse()
.map(WebviewUrl::External)
.map_err(|_| format!("Invalid URL: {}", url))?;
let window_label = "remote-session";
// Check if window already exists — focus it instead of duplicating
if let Some(existing) = app_handle.get_webview_window(window_label) {
let _ = existing.set_focus();
return Ok(format!("Focused existing window for {}", url));
}
// Load the injection script at compile time
let script_to_inject = include_str!("../extWebview.js");
let app_clone = app_handle.clone();
let url_clone = target_url.clone();
// Window creation MUST happen on the main thread
app_handle.run_on_main_thread(move || {
let _window = WebviewWindowBuilder::new(&app_clone, window_label, url_clone)
.title("Remote Session")
.inner_size(1024.0, 768.0)
.resizable(true)
.focused(true)
.initialization_script(script_to_inject) // ← Inject JS here
.build();
}).map_err(|e| format!("Failed to dispatch to main thread: {}", e))?;
Ok(format!("Dispatched window spawn for: {}", url))
}
```
### The Injected Script (`src-tauri/extWebview.js`)
```javascript
// extWebview.js — This script runs automatically when the webview loads
window.addEventListener('DOMContentLoaded', () => {
console.log('[TAURI] Initializing injected socket link...');
const ws = new WebSocket('ws://127.0.0.1:8080');
ws.onopen = () => {
console.log('[TAURI] Injected socket connected to backend!');
// Announce this window's identity to the Tauri backend
const payload = {
sender: 'remote-webview',
type: 'identify',
url: window.location.href,
title: document.title
};
ws.send(JSON.stringify(payload));
};
ws.onmessage = (event) => {
const data = JSON.parse(event.data);
console.log('[TAURI] RECEIVED FROM BACKEND:', data);
// Respond with session/token data if requested
if (data.type === 'request-session') {
ws.send(JSON.stringify({
sender: 'remote-webview',
type: 'session-response',
localsession: window.localStorage.getItem('auth-token') || 'no-session'
}));
}
// Handle commands from the backend
if (data.type === 'execute-action') {
// Perform actions on the remote page as instructed by Tauri
console.log('[TAURI] Executing action:', data.action);
}
};
ws.onerror = (err) => {
console.error('[TAURI] Injected socket error:', err);
};
// Expose the socket on window for the page's own code to use
window.localAppSocket = ws;
// Periodically send status updates
setInterval(() => {
if (ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({
sender: 'remote-webview',
type: 'heartbeat',
url: window.location.href
}));
}
}, 30000);
});
```
### CSP Considerations
Content Security Policy (CSP) can block script injection on external pages. If you need injection on external URLs:
```json
// tauri.conf.json — disable CSP (use with caution!)
{
"app": {
"security": {
"csp": null
}
}
}
```
For better security, craft a specific CSP that allows your injection patterns while blocking other sources:
```json
{
"app": {
"security": {
"csp": "default-src 'self' 'unsafe-inline' 'unsafe-eval'; connect-src ws://127.0.0.1:* http://127.0.0.1:* https:;"
}
}
}
```
---
## Complete Working Example Flow
### Step-by-step data flow through the entire system:
```text
1. App starts → lib.rs setup() → spawns WebSocket server on ws://127.0.0.1:8080
2. Main window (index.html) loads → main.js creates WebSocket client
→ connects to ws://127.0.0.1:8080
→ onopen fires → ready to communicate
3. User enters URL and clicks button → main.js calls invoke('greet', { name: url })
4. Rust greet() command:
→ parses URL as WebviewUrl::External
→ checks if window already exists (focus if so)
→ loads extWebview.js via include_str!()
→ dispatches window creation to main thread
→ creates new WebviewWindow with external URL + injected script
5. Remote window loads external page → extWebview.js runs on DOMContentLoaded
→ creates WebSocket client
→ connects to ws://127.0.0.1:8080
→ sends identification message
6. Server broadcasts identification to all clients
→ Main window receives it → displays session info
→ Other remote windows receive it (if any)
7. Main window user clicks "Send" → main.js sends JSON via WebSocket
→ Server broadcasts to all clients
→ All remote windows receive and log the message
```
---
## Choosing Between Approaches
| Requirement | Recommended Approach |
|-------------|---------------------|
| Connect to external WebSocket server | `tauri-plugin-websocket` (client plugin) |
| App IS the WebSocket server | Native Tokio server (this guide) |
| Tauri-to-frontend communication | Tauri events (`emit`/`listen`) |
| High-throughput streaming data | Tauri channels (`Channel<T>`) |
| Cross-window messaging (internal) | Tauri events or native WS server |
| External page needs to talk to Tauri | Native WS server + script injection |
| Multiple external pages sharing state | Native WS server + script injection |
---
## Troubleshooting
### "Address already in use" (port 8080)
- Another process is using port 8080
- Solution: Change the port in both `server.rs` and all frontend connection code
- Or kill the process using port 8080: `lsof -i :8080` / `netstat -ano | findstr :8080`
### WebSocket connection fails from injected scripts
- CSP may be blocking the connection → set `"csp": null` in config
- The injected script runs before the page's own scripts → ensure DOM is ready via `DOMContentLoaded`
- External HTTPS pages may refuse unencrypted WebSocket connections → page must allow mixed content
### Messages not broadcasting to other windows
- Each client needs its own `rx = tx.subscribe()` receiver
- The `tx.send()` call broadcasts to all subscribers EXCEPT the sender's own subscription
- Ensure `tokio::select!` has both branches (incoming AND broadcast)
### Window creation fails from async command
- Window creation MUST happen on the main thread
- Use `app_handle.run_on_main_thread(move || { ... })` for async contexts
- Not doing this will cause a panic or silent failure

View File

@ -0,0 +1,509 @@
# Tauri 2.0 Agent Prompts — Definitive Collection
> **Purpose**: Copy-paste ready prompts for instructing AI agents to build Tauri 2.0 applications.
> **Constraint**: These prompts are designed for sandbox environments where code compilation is NOT available. Agents must verify correctness using logic, API references, and pattern matching.
---
## Table of Contents
1. [Universal System Prompt (Required Prefix)](#1-universal-system-prompt-required-prefix)
2. [Prompt: Scaffold a New Tauri 2.0 Project](#2-prompt-scaffold-a-new-tauri-20-project)
3. [Prompt: Add a Tauri Plugin](#3-prompt-add-a-tauri-plugin)
4. [Prompt: Create a Custom Command (IPC)](#4-prompt-create-a-custom-command-ipc)
5. [Prompt: Add State Management](#5-prompt-add-state-management)
6. [Prompt: Implement Event-Based Communication](#6-prompt-implement-event-based-communication)
7. [Prompt: Create Multi-Window App](#7-prompt-create-multi-window-app)
8. [Prompt: Build a WebSocket Server in Rust](#8-prompt-build-a-websocket-server-in-rust)
9. [Prompt: Implement Script Injection](#9-prompt-implement-script-injection)
10. [Prompt: Set Up Capabilities & Permissions](#10-prompt-set-up-capabilities--permissions)
11. [Prompt: Migrate a v1 App to v2](#11-prompt-migrate-a-v1-app-to-v2)
12. [Prompt: Full App Build (End-to-End)](#12-prompt-full-app-build-end-to-end)
13. [Verification Checklist for Agents](#13-verification-checklist-for-agents)
14. [Quick Reference: v1 vs v2 API Map](#14-quick-reference-v1-vs-v2-api-map)
---
## 1. Universal System Prompt (Required Prefix)
> **Always include this as the system context before any task-specific prompt.**
```
You are a Tauri 2.0 expert developer. You build desktop and mobile applications using the Tauri 2.0 framework (Rust backend + web frontend).
CRITICAL ENVIRONMENT CONSTRAINTS:
- You CANNOT compile or run code in this sandbox. Do NOT attempt compilation.
- Verify correctness using logic, API pattern matching, and reference knowledge only.
- Cross-reference every code snippet against known Tauri 2.0 API patterns before outputting.
- If you are uncertain about an API or pattern, say so explicitly rather than guessing.
MANDATORY TAURI 2.0 RULES — NEVER VIOLATE:
1. This is Tauri 2.0 (NOT v1). The APIs are fundamentally different.
2. Use `tauri::WebviewWindow` (NOT `tauri::Window`).
3. Use `tauri::WebviewWindowBuilder` (NOT `tauri::WindowBuilder`).
4. Use `tauri::WebviewUrl` (NOT `tauri::WindowUrl`).
5. Use `Manager::get_webview_window()` (NOT `Manager::get_window()`).
6. Use `tauri::Emitter` trait for `emit()` and `emit_to()` (NOT the old window-only emit).
7. Use `tauri::Listener` trait for `listen()` and `once()` on the Rust side.
8. JS import `invoke` from `@tauri-apps/api/core` (NOT `@tauri-apps/api/tauri`).
9. JS import window utilities from `@tauri-apps/api/webviewWindow` (NOT `@tauri-apps/api/window`).
10. JS plugins import from `@tauri-apps/plugin-<name>` (NOT `@tauri-apps/api/<name>`).
11. Config uses `app` section (NOT `tauri` section), `build.frontendDist` (NOT `build.distDir`).
12. Security uses Capabilities + Permissions (NOT `allowlist`).
13. Every plugin requires: (a) Rust crate in Cargo.toml, (b) `.plugin()` registration in Builder, (c) npm package, (d) permissions in capabilities.
14. Use the `lib.rs` + `main.rs` pattern. Core logic goes in `lib.rs`. `main.rs` only calls `lib::run()`.
15. `#[cfg_attr(mobile, tauri::mobile_entry_point)]` attribute is REQUIRED on the `pub fn run()` function.
16. `invoke_handler` can only be called ONCE — all commands go in a single `tauri::generate_handler![]`.
17. Async commands cannot use borrowed references (`&str`, `&Path`) directly — use owned types or wrap return in `Result`.
18. Window creation from async contexts MUST use `app_handle.run_on_main_thread()`.
19. Always clean up event listeners (call `unlisten()`) on the frontend.
20. Use `tauri = { version = "2" }` and `tauri-build = { version = "2" }` (NOT version 1).
FILE STRUCTURE:
- Frontend source: `src/`
- Rust backend: `src-tauri/`
- Rust entry-point: `src-tauri/src/main.rs` (minimal)
- Core app logic: `src-tauri/src/lib.rs`
- Extra modules: `src-tauri/src/<module>.rs`
- Capabilities: `src-tauri/capabilities/default.json`
- Config: `src-tauri/tauri.conf.json`
- Rust deps: `src-tauri/Cargo.toml`
- Build script: `src-tauri/build.rs`
OFFICIAL DOCUMENTATION:
- Main docs: https://v2.tauri.app
- Rust API: https://docs.rs/tauri/latest/tauri/
- Migration guide: https://v2.tauri.app/start/migrate/from-tauri-1/
- Capabilities: https://v2.tauri.app/security/capabilities/
- Permissions: https://v2.tauri.app/security/permissions/
- Configuration: https://v2.tauri.app/develop/configuration-files/
- Calling Rust: https://v2.tauri.app/develop/calling-rust/
- Calling Frontend: https://v2.tauri.app/develop/calling-frontend/
- Plugin index: https://v2.tauri.app/plugin/
```
---
## 2. Prompt: Scaffold a New Tauri 2.0 Project
```
Create a new Tauri 2.0 project from scratch with vanilla JavaScript (no frontend framework).
Requirements:
- App name: [APP_NAME]
- Window title: [WINDOW_TITLE]
- Default window size: [WIDTH]x[HEIGHT]
- withGlobalTauri enabled (for window.__TAURI__ access without npm package)
Generate ALL of the following files with complete, correct Tauri 2.0 code:
1. `package.json` — with @tauri-apps/cli v2 devDependency and scripts (dev, build, tauri)
2. `src/index.html` — basic HTML5 boilerplate
3. `src/main.js` — demonstrate invoking a Rust command
4. `src/styles.css` — basic styling
5. `src-tauri/tauri.conf.json` — correct v2 structure (app section, build section, bundle section)
6. `src-tauri/Cargo.toml` — with tauri v2, tauri-build v2, serde, serde_json. Include [lib] with crate-type ["staticlib", "cdylib", "rlib"]
7. `src-tauri/build.rs` — standard tauri_build::build()
8. `src-tauri/capabilities/default.json` — with core:default permission
9. `src-tauri/src/main.rs` — minimal entry-point calling lib::run()
10. `src-tauri/src/lib.rs` — with Builder pattern, mobile_entry_point attribute, a sample "greet" command, and the command registered in invoke_handler
Verify every line follows Tauri 2.0 conventions. Cross-check config structure against the v2 schema.
```
---
## 3. Prompt: Add a Tauri Plugin
```
Add the [PLUGIN_NAME] plugin to an existing Tauri 2.0 project.
Plugin to add: [tauri-plugin-STORE/turi-plugin-FS/etc.]
JS package name: [@tauri-apps/plugin-store/etc.]
Instructions:
1. Add the Rust crate to `src-tauri/Cargo.toml` as `tauri-plugin-[name] = "2"`
2. Register the plugin in `src-tauri/src/lib.rs` using `.plugin(tauri_plugin_[name]::init())` or the Builder pattern if the plugin requires configuration
3. Add the npm package: `@tauri-apps/plugin-[name]`
4. Update `src-tauri/capabilities/default.json` to add the appropriate permission (usually `[name]:default` or specific allow permissions)
5. Provide a frontend usage example showing the correct v2 import from `@tauri-apps/plugin-[name]`
Show the exact changes needed for each file. Do NOT use v1 import paths or API patterns.
```
---
## 4. Prompt: Create a Custom Command (IPC)
```
Create a Tauri 2.0 command called "[COMMAND_NAME]" that [DESCRIBE WHAT IT DOES].
Requirements:
- Define the command using `#[tauri::command]` in a separate module file `src-tauri/src/commands.rs`
- The command must be `pub` (required for separate modules)
- Include proper error handling with `Result<T, String>` or a custom error type
- Register the command in `src-tauri/src/lib.rs` using `mod commands;` and including it in `generate_handler![commands::COMMAND_NAME]`
- Provide the frontend JavaScript code to invoke the command using `invoke()` from `@tauri-apps/api/core` or `window.__TAURI__.core`
- If the command is async, ensure all arguments are owned types (not borrowed references)
Show complete, correct code for both the Rust side and the JS side.
```
---
## 5. Prompt: Add State Management
```
Add shared state management to an existing Tauri 2.0 application.
Requirements:
1. Define a state struct in `src-tauri/src/lib.rs` using `std::sync::Mutex` for thread safety
2. Manage the state in the Builder using `.manage(MyState { ... })`
3. Create at least two commands that read and write the managed state using `tauri::State<'_, MyState>`
4. Show the frontend code for invoking these commands
If persistent storage is also needed:
- Add `tauri-plugin-store = "2"` to Cargo.toml
- Register the plugin
- Add `store:default` to capabilities
- Show frontend usage with `@tauri-apps/plugin-store`
```
---
## 6. Prompt: Implement Event-Based Communication
```
Implement bidirectional event communication in a Tauri 2.0 application.
Requirements:
Rust side:
- Use `use tauri::{Emitter, AppHandle}` to emit events from Rust
- Demonstrate both global emit (`app.emit()`) and targeted emit (`app.emit_to()`)
- If listening for events on the Rust side, use `use tauri::Listener` and `app.listen()` or `app.once()`
Frontend side:
- Import from `@tauri-apps/api/event` (NOT `@tauri-apps/api/tauri`)
- Show `listen()`, `once()`, `emit()`, and `emitTo()` usage
- Demonstrate proper cleanup with `unlisten()`
- Show targeted listening using `getCurrentWebviewWindow().listen()`
Show complete code for:
1. A Rust command that emits a progress event
2. A Rust command that emits a completion event to a specific window
3. Frontend listeners that handle these events
4. Frontend emitting an event back to Rust
Make sure `core:event:default` (or specific `core:event:allow-*` permissions) is included in capabilities.
```
---
## 7. Prompt: Create Multi-Window App
```
Create a Tauri 2.0 application with multiple windows.
Requirements:
1. Main window defined in `tauri.conf.json` with label "main"
2. A command `open_secondary` that creates a new window dynamically using `WebviewWindowBuilder`
3. The secondary window should load a local HTML file (`WebviewUrl::App("secondary.html".into())`)
4. If creating the window from an async context, use `app_handle.run_on_main_thread()`
5. Check for existing windows with the same label using `app_handle.get_webview_window(label)` and focus it if it exists
6. Show frontend code for creating windows using `new WebviewWindow(label, options)` from `@tauri-apps/api/webviewWindow`
CRITICAL: Use `tauri::WebviewWindowBuilder` and `tauri::WebviewUrl` (NOT the v1 `WindowBuilder`/`WindowUrl`).
CRITICAL: Use `app_handle.get_webview_window()` (NOT `app_handle.get_window()`).
```
---
## 8. Prompt: Build a WebSocket Server in Rust
```
Build a native WebSocket server that runs inside a Tauri 2.0 application using the Tokio async runtime.
Requirements:
Dependencies (Cargo.toml):
- `tokio = { version = "1", features = ["net", "rt", "sync", "macros"] }`
- `tokio-tungstenite = "0.24"`
- `futures-util = "0.3"`
Server implementation (`src-tauri/src/server.rs`):
- Bind a TcpListener to `127.0.0.1:[PORT]`
- Use `tokio::sync::broadcast` channel for message fan-out to all connected clients
- Accept WebSocket connections using `tokio_tungstenite::accept_async`
- Use `tokio::select!` to handle both incoming messages from clients AND broadcast messages from other clients
- Handle disconnections gracefully
- Spawn the server in `src-tauri/src/lib.rs` using `tauri::async_runtime::spawn(server::start_websocket_server())` inside the `.setup()` closure
Frontend client:
- Connect using standard `new WebSocket('ws://127.0.0.1:[PORT]')`
- Show send and receive handlers
Key pattern: Each client connection gets its own broadcast receiver via `tx.subscribe()`, enabling fan-out to all connected clients.
```
---
## 9. Prompt: Implement Script Injection
```
Implement JavaScript injection into a Tauri 2.0 WebviewWindow.
Requirements:
1. Create a JavaScript file (e.g., `src-tauri/inject.js`) that will be injected into webview windows
2. The injected script should:
- Connect to a local WebSocket server (ws://127.0.0.1:[PORT])
- Send data from the webview page (e.g., localStorage contents) back to the server
- Receive data from the server and process it
- Expose the socket connection on `window.localAppSocket` for the page's own code to use
3. Use `include_str!("../inject.js")` to load the script at compile time
4. Inject it using `WebviewWindowBuilder::initialization_script(script_content)`
5. Show a complete working example of creating a window with an external URL and injecting the script
IMPORTANT: Set `"csp": null` in the app security config if injecting scripts into external URLs, otherwise CSP may block the injection.
Show both the Rust window creation code with injection and the JavaScript injection script.
```
---
## 10. Prompt: Set Up Capabilities & Permissions
```
Set up the Tauri 2.0 Capabilities and Permissions system for an application.
The application needs access to:
- [LIST REQUIRED FEATURES: file system, dialogs, HTTP requests, clipboard, notifications, etc.]
Requirements:
1. Create `src-tauri/capabilities/default.json` with proper v2 structure
2. Include the `$schema` pointing to `../gen/schemas/desktop-schema.json`
3. Set the `identifier`, `description`, `windows` (list of window labels), and `platforms` fields
4. Add the correct permission identifiers for each required feature:
- File system: `fs:default` or specific `fs:allow-*`
- Dialogs: `dialog:default` or specific `dialog:allow-*`
- HTTP: `http:default` or specific `http:allow-*`
- Events: `core:event:default`
- Window management: `core:window:default`
5. If fine-grained scopes are needed (e.g., restricting file access to specific directories), show scoped permissions with `allow` and `deny` arrays using path variables like `$APPDATA`, `$HOME`, `$RESOURCE`
CRITICAL: Do NOT use the v1 `allowlist` in `tauri.conf.json`. In v2, all permissions are defined via capability files.
```
---
## 11. Prompt: Migrate a v1 App to v2
```
Migrate the following Tauri v1 code to Tauri 2.0.
[PASTE v1 CODE HERE]
Migration checklist — verify ALL of these changes:
Config (tauri.conf.json):
- [ ] `tauri` section renamed to `app`
- [ ] `build.distDir` renamed to `build.frontendDist`
- [ ] `build.devPath` renamed to `build.devUrl`
- [ ] `build.withGlobalTauri` moved to `app.withGlobalTauri`
- [ ] `tauri.allowlist` REMOVED → replaced by capabilities
- [ ] `tauri.bundle` promoted to top-level `bundle`
- [ ] `bundle.identifier` promoted to top-level `identifier`
- [ ] `tauri.updater` moved to `plugins.updater`
Project structure:
- [ ] `main.rs` logic moved to `lib.rs` with `pub fn run()`
- [ ] New minimal `main.rs` calling `lib::run()`
- [ ] `Cargo.toml` has `[lib]` section with `crate-type = ["staticlib", "cdylib", "rlib"]`
- [ ] `#[cfg_attr(mobile, tauri::mobile_entry_point)]` added to `run()`
Rust code:
- [ ] All `tauri::api::*` imports REMOVED (replaced by plugins)
- [ ] `tauri::Window``tauri::WebviewWindow`
- [ ] `tauri::WindowBuilder``tauri::WebviewWindowBuilder`
- [ ] `tauri::WindowUrl``tauri::WebviewUrl`
- [ ] `get_window()``get_webview_window()`
- [ ] `tauri::Emitter` trait imported for `emit()` / `emit_to()`
- [ ] `tauri::Listener` trait imported for `listen()` / `once()`
- [ ] All plugins registered with `.plugin()` in Builder
- [ ] `tauri` and `tauri-build` version set to "2"
JavaScript code:
- [ ] `@tauri-apps/api/tauri``@tauri-apps/api/core`
- [ ] `@tauri-apps/api/window``@tauri-apps/api/webviewWindow`
- [ ] All plugin imports → `@tauri-apps/plugin-<name>`
- [ ] `@tauri-apps/cli` version set to "^2"
Capabilities:
- [ ] Created `src-tauri/capabilities/default.json`
- [ ] All necessary permissions listed
- [ ] `core:default` included at minimum
Output the complete migrated code for every file that changed.
```
---
## 12. Prompt: Full App Build (End-to-End)
```
Build a complete Tauri 2.0 application with the following requirements:
App Name: [APP_NAME]
Purpose: [DESCRIBE APP PURPOSE]
Features needed:
- [FEATURE 1: e.g., File system read/write]
- [FEATURE 2: e.g., HTTP API client]
- [FEATURE 3: e.g., Persistent settings storage]
- [FEATURE 4: e.g., System tray icon]
- [FEATURE 5: e.g., Multi-window support]
- [FEATURE 6: e.g., Custom Rust commands]
Frontend: [Vanilla JS / React / Vue / Svelte / etc.]
Generate ALL files for a complete, working Tauri 2.0 application:
1. `package.json`
2. `src/index.html` (or appropriate entry point for the chosen framework)
3. Frontend source files (JavaScript/TypeScript/CSS)
4. `src-tauri/tauri.conf.json` — v2 structure with all necessary config
5. `src-tauri/Cargo.toml` — all dependencies including required plugins
6. `src-tauri/build.rs`
7. `src-tauri/capabilities/default.json` — all required permissions
8. `src-tauri/src/main.rs` — minimal entry-point
9. `src-tauri/src/lib.rs` — Builder with all plugins, commands, setup
10. Additional Rust module files as needed
11. Any injected scripts if required
For every file:
- Use ONLY valid Tauri 2.0 code
- Use the correct v2 import paths
- Use v2 type names (WebviewWindow, WebviewUrl, etc.)
- Include proper error handling
- Add appropriate comments explaining the v2 patterns used
Do NOT skip any file. Generate complete, production-ready code.
```
---
## 13. Verification Checklist for Agents
> Agents should run through this checklist mentally for EVERY code snippet they generate.
### Config Verification
- [ ] `tauri.conf.json` uses `app` section (not `tauri`)
- [ ] `build.frontendDist` (not `build.distDir`)
- [ ] `build.devUrl` (not `build.devPath`)
- [ ] `withGlobalTauri` is under `app` (not `build`)
- [ ] `identifier` is at top level (not under `bundle`)
- [ ] `bundle` is at top level (not under `tauri`)
- [ ] No `allowlist` present anywhere
### Rust Code Verification
- [ ] `tauri` version is `"2"` (not `"1"`)
- [ ] `tauri-build` version is `"2"` (not `"1"`)
- [ ] `Cargo.toml` has `[lib]` with `crate-type = ["staticlib", "cdylib", "rlib"]`
- [ ] `main.rs` only contains `#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]` and `fn main() { lib_name::run() }`
- [ ] `lib.rs` has `#[cfg_attr(mobile, tauri::mobile_entry_point)]` on `pub fn run()`
- [ ] No `tauri::api::*` imports anywhere
- [ ] `WebviewWindow` used (not `Window`)
- [ ] `WebviewWindowBuilder` used (not `WindowBuilder`)
- [ ] `WebviewUrl` used (not `WindowUrl`)
- [ ] `get_webview_window()` used (not `get_window()`)
- [ ] `Emitter` trait imported when using `emit()` or `emit_to()`
- [ ] `Listener` trait imported when using `listen()` or `once()`
- [ ] `invoke_handler` called only ONCE with ALL commands
- [ ] Async commands use owned types or Result wrapping (not bare `&str`)
- [ ] Window creation in async contexts uses `run_on_main_thread()`
- [ ] All plugins registered with `.plugin()` before `.run()`
### Frontend Code Verification
- [ ] `invoke` imported from `@tauri-apps/api/core` (not `@tauri-apps/api/tauri`)
- [ ] `WebviewWindow` imported from `@tauri-apps/api/webviewWindow` (not `@tauri-apps/api/window`)
- [ ] Event functions from `@tauri-apps/api/event` (not core or tauri)
- [ ] Plugin imports from `@tauri-apps/plugin-<name>` (not `@tauri-apps/api/<name>`)
- [ ] `@tauri-apps/cli` is version `^2`
- [ ] Event listeners cleaned up with `unlisten()`
- [ ] Argument names in `invoke()` calls match Rust command parameter names (camelCase by default)
### Capabilities Verification
- [ ] Capability files exist in `src-tauri/capabilities/`
- [ ] `$schema` points to correct schema path
- [ ] All used plugins have corresponding permissions
- [ ] Window labels in capabilities match actual window labels
- [ ] No `allowlist` in config files
### Plugin Verification
- [ ] Rust crate: `tauri-plugin-<name> = "2"` in Cargo.toml
- [ ] Registration: `.plugin(tauri_plugin_<name>::init())` in Builder
- [ ] JS package: `@tauri-apps/plugin-<name>` in dependencies
- [ ] Permissions: Listed in capabilities file
---
## 14. Quick Reference: v1 vs v2 API Map
> Keep this table handy when migrating or reviewing code.
### Rust Type Renames
| v1 | v2 |
|----|----|
| `tauri::Window` | `tauri::WebviewWindow` |
| `tauri::WindowBuilder` | `tauri::WebviewWindowBuilder` |
| `tauri::WindowUrl` | `tauri::WebviewUrl` |
| `Manager::get_window()` | `Manager::get_webview_window()` |
| `tauri::WindowEvent` | `tauri::WebviewWindowEvent` |
| `tauri::Manager::fs_scope()` | `tauri_plugin_fs::FsExt` |
### Rust API Removals (replaced by plugins)
| v1 | v2 Plugin |
|----|-----------|
| `tauri::api::dialog` | `tauri-plugin-dialog` |
| `tauri::api::fs` | `std::fs` / `tauri-plugin-fs` |
| `tauri::api::http` | `tauri-plugin-http` |
| `tauri::api::path` | `tauri::Manager::path()` |
| `tauri::api::process::Command` | `tauri-plugin-shell` |
| `tauri::api::clipboard` | `tauri-plugin-clipboard-manager` |
| `tauri::api::notification` | `tauri-plugin-notification` |
| `tauri::api::global_shortcut` | `tauri-plugin-global-shortcut` |
| `tauri::api::updater` | `tauri-plugin-updater` |
| `tauri::api::cli` | `tauri-plugin-cli` |
| `tauri::api::os` | `tauri-plugin-os` |
| `tauri::api::version` | `semver` crate |
### JavaScript Import Path Changes
| v1 | v2 |
|----|----|
| `@tauri-apps/api/tauri` | `@tauri-apps/api/core` |
| `@tauri-apps/api/window` | `@tauri-apps/api/webviewWindow` |
| `@tauri-apps/api/dialog` | `@tauri-apps/plugin-dialog` |
| `@tauri-apps/api/fs` | `@tauri-apps/plugin-fs` |
| `@tauri-apps/api/http` | `@tauri-apps/plugin-http` |
| `@tauri-apps/api/notification` | `@tauri-apps/plugin-notification` |
| `@tauri-apps/api/clipboard` | `@tauri-apps/plugin-clipboard-manager` |
| `@tauri-apps/api/shell` | `@tauri-apps/plugin-shell` |
| `@tauri-apps/api/global-shortcut` | `@tauri-apps/plugin-global-shortcut` |
| `@tauri-apps/api/updater` | `@tauri-apps/plugin-updater` |
| `@tauri-apps/api/os` | `@tauri-apps/plugin-os` |
| `@tauri-apps/api/process` | `@tauri-apps/plugin-process` |
| `@tauri-apps/api/cli` | `@tauri-apps/plugin-cli` |
### Config Path Changes
| v1 | v2 |
|----|----|
| `build.distDir` | `build.frontendDist` |
| `build.devPath` | `build.devUrl` |
| `build.withGlobalTauri` | `app.withGlobalTauri` |
| `tauri.*` | `app.*` |
| `tauri.allowlist.*` | Capabilities system |
| `tauri.windows.fileDropEnabled` | `app.windows.dragDropEnabled` |
| `tauri.bundle` | `bundle` (top-level) |
| `bundle.identifier` | `identifier` (top-level) |
| `tauri.updater` | `plugins.updater` |
| `tauri.systemTray` | `app.trayIcon` |
| `tauri.cli` | `plugins.cli` |

File diff suppressed because it is too large Load Diff