mcp-subagents-opencode 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +602 -0
- package/build/config/timeouts.d.ts +9 -0
- package/build/config/timeouts.d.ts.map +1 -0
- package/build/config/timeouts.js +18 -0
- package/build/config/timeouts.js.map +1 -0
- package/build/helpers.d.ts +6 -0
- package/build/helpers.d.ts.map +1 -0
- package/build/helpers.js +47 -0
- package/build/helpers.js.map +1 -0
- package/build/index.d.ts +3 -0
- package/build/index.d.ts.map +1 -0
- package/build/index.js +245 -0
- package/build/index.js.map +1 -0
- package/build/models.d.ts +32 -0
- package/build/models.d.ts.map +1 -0
- package/build/models.js +58 -0
- package/build/models.js.map +1 -0
- package/build/server/register-notifications.d.ts +3 -0
- package/build/server/register-notifications.d.ts.map +1 -0
- package/build/server/register-notifications.js +77 -0
- package/build/server/register-notifications.js.map +1 -0
- package/build/server/register-resources.d.ts +3 -0
- package/build/server/register-resources.d.ts.map +1 -0
- package/build/server/register-resources.js +210 -0
- package/build/server/register-resources.js.map +1 -0
- package/build/server/register-retry-execution.d.ts +2 -0
- package/build/server/register-retry-execution.d.ts.map +1 -0
- package/build/server/register-retry-execution.js +28 -0
- package/build/server/register-retry-execution.js.map +1 -0
- package/build/server/register-tasks.d.ts +3 -0
- package/build/server/register-tasks.d.ts.map +1 -0
- package/build/server/register-tasks.js +52 -0
- package/build/server/register-tasks.js.map +1 -0
- package/build/server/register-tools.d.ts +3 -0
- package/build/server/register-tools.d.ts.map +1 -0
- package/build/server/register-tools.js +32 -0
- package/build/server/register-tools.js.map +1 -0
- package/build/server/resource-helpers.d.ts +21 -0
- package/build/server/resource-helpers.d.ts.map +1 -0
- package/build/server/resource-helpers.js +84 -0
- package/build/server/resource-helpers.js.map +1 -0
- package/build/services/account-manager.d.ts +88 -0
- package/build/services/account-manager.d.ts.map +1 -0
- package/build/services/account-manager.js +239 -0
- package/build/services/account-manager.js.map +1 -0
- package/build/services/claude-code-runner.d.ts +15 -0
- package/build/services/claude-code-runner.d.ts.map +1 -0
- package/build/services/claude-code-runner.js +475 -0
- package/build/services/claude-code-runner.js.map +1 -0
- package/build/services/client-context.d.ts +31 -0
- package/build/services/client-context.d.ts.map +1 -0
- package/build/services/client-context.js +44 -0
- package/build/services/client-context.js.map +1 -0
- package/build/services/exhaustion-fallback.d.ts +27 -0
- package/build/services/exhaustion-fallback.d.ts.map +1 -0
- package/build/services/exhaustion-fallback.js +30 -0
- package/build/services/exhaustion-fallback.js.map +1 -0
- package/build/services/fallback-orchestrator.d.ts +16 -0
- package/build/services/fallback-orchestrator.d.ts.map +1 -0
- package/build/services/fallback-orchestrator.js +48 -0
- package/build/services/fallback-orchestrator.js.map +1 -0
- package/build/services/opencode-client.d.ts +40 -0
- package/build/services/opencode-client.d.ts.map +1 -0
- package/build/services/opencode-client.js +147 -0
- package/build/services/opencode-client.js.map +1 -0
- package/build/services/opencode-spawner.d.ts +56 -0
- package/build/services/opencode-spawner.d.ts.map +1 -0
- package/build/services/opencode-spawner.js +426 -0
- package/build/services/opencode-spawner.js.map +1 -0
- package/build/services/output-file.d.ts +24 -0
- package/build/services/output-file.d.ts.map +1 -0
- package/build/services/output-file.js +90 -0
- package/build/services/output-file.js.map +1 -0
- package/build/services/progress-registry.d.ts +12 -0
- package/build/services/progress-registry.d.ts.map +1 -0
- package/build/services/progress-registry.js +97 -0
- package/build/services/progress-registry.js.map +1 -0
- package/build/services/question-registry.d.ts +79 -0
- package/build/services/question-registry.d.ts.map +1 -0
- package/build/services/question-registry.js +249 -0
- package/build/services/question-registry.js.map +1 -0
- package/build/services/retry-queue.d.ts +41 -0
- package/build/services/retry-queue.d.ts.map +1 -0
- package/build/services/retry-queue.js +195 -0
- package/build/services/retry-queue.js.map +1 -0
- package/build/services/sdk-client-manager.d.ts +149 -0
- package/build/services/sdk-client-manager.d.ts.map +1 -0
- package/build/services/sdk-client-manager.js +632 -0
- package/build/services/sdk-client-manager.js.map +1 -0
- package/build/services/sdk-session-adapter.d.ts +203 -0
- package/build/services/sdk-session-adapter.d.ts.map +1 -0
- package/build/services/sdk-session-adapter.js +1088 -0
- package/build/services/sdk-session-adapter.js.map +1 -0
- package/build/services/sdk-spawner.d.ts +42 -0
- package/build/services/sdk-spawner.d.ts.map +1 -0
- package/build/services/sdk-spawner.js +488 -0
- package/build/services/sdk-spawner.js.map +1 -0
- package/build/services/session-hooks.d.ts +24 -0
- package/build/services/session-hooks.d.ts.map +1 -0
- package/build/services/session-hooks.js +130 -0
- package/build/services/session-hooks.js.map +1 -0
- package/build/services/session-snapshot.d.ts +19 -0
- package/build/services/session-snapshot.d.ts.map +1 -0
- package/build/services/session-snapshot.js +203 -0
- package/build/services/session-snapshot.js.map +1 -0
- package/build/services/subscription-registry.d.ts +12 -0
- package/build/services/subscription-registry.d.ts.map +1 -0
- package/build/services/subscription-registry.js +27 -0
- package/build/services/subscription-registry.js.map +1 -0
- package/build/services/task-manager.d.ts +150 -0
- package/build/services/task-manager.d.ts.map +1 -0
- package/build/services/task-manager.js +765 -0
- package/build/services/task-manager.js.map +1 -0
- package/build/services/task-persistence.d.ts +29 -0
- package/build/services/task-persistence.d.ts.map +1 -0
- package/build/services/task-persistence.js +159 -0
- package/build/services/task-persistence.js.map +1 -0
- package/build/services/task-status-mapper.d.ts +21 -0
- package/build/services/task-status-mapper.d.ts.map +1 -0
- package/build/services/task-status-mapper.js +171 -0
- package/build/services/task-status-mapper.js.map +1 -0
- package/build/templates/index.d.ts +22 -0
- package/build/templates/index.d.ts.map +1 -0
- package/build/templates/index.js +147 -0
- package/build/templates/index.js.map +1 -0
- package/build/templates/overlays/coder-csharp.mdx +58 -0
- package/build/templates/overlays/coder-go.mdx +53 -0
- package/build/templates/overlays/coder-java.mdx +54 -0
- package/build/templates/overlays/coder-kotlin.mdx +56 -0
- package/build/templates/overlays/coder-nextjs.mdx +65 -0
- package/build/templates/overlays/coder-python.mdx +53 -0
- package/build/templates/overlays/coder-react.mdx +55 -0
- package/build/templates/overlays/coder-ruby.mdx +59 -0
- package/build/templates/overlays/coder-rust.mdx +48 -0
- package/build/templates/overlays/coder-supabase.mdx +268 -0
- package/build/templates/overlays/coder-supastarter.mdx +313 -0
- package/build/templates/overlays/coder-swift.mdx +56 -0
- package/build/templates/overlays/coder-tauri.mdx +566 -0
- package/build/templates/overlays/coder-triggerdev.mdx +296 -0
- package/build/templates/overlays/coder-typescript.mdx +45 -0
- package/build/templates/overlays/coder-vue.mdx +62 -0
- package/build/templates/overlays/planner-architecture.mdx +78 -0
- package/build/templates/overlays/planner-bugfix.mdx +36 -0
- package/build/templates/overlays/planner-feature.mdx +38 -0
- package/build/templates/overlays/planner-migration.mdx +50 -0
- package/build/templates/overlays/planner-refactor.mdx +57 -0
- package/build/templates/overlays/researcher-library.mdx +59 -0
- package/build/templates/overlays/researcher-performance.mdx +68 -0
- package/build/templates/overlays/researcher-security.mdx +86 -0
- package/build/templates/overlays/tester-graphql.mdx +191 -0
- package/build/templates/overlays/tester-playwright.mdx +621 -0
- package/build/templates/overlays/tester-rest.mdx +101 -0
- package/build/templates/overlays/tester-suite.mdx +177 -0
- package/build/templates/super-coder.mdx +529 -0
- package/build/templates/super-planner.mdx +568 -0
- package/build/templates/super-researcher.mdx +406 -0
- package/build/templates/super-tester.mdx +243 -0
- package/build/tools/answer-question.d.ts +30 -0
- package/build/tools/answer-question.d.ts.map +1 -0
- package/build/tools/answer-question.js +108 -0
- package/build/tools/answer-question.js.map +1 -0
- package/build/tools/cancel-task.d.ts +44 -0
- package/build/tools/cancel-task.d.ts.map +1 -0
- package/build/tools/cancel-task.js +144 -0
- package/build/tools/cancel-task.js.map +1 -0
- package/build/tools/send-message.d.ts +39 -0
- package/build/tools/send-message.d.ts.map +1 -0
- package/build/tools/send-message.js +124 -0
- package/build/tools/send-message.js.map +1 -0
- package/build/tools/shared-spawn.d.ts +56 -0
- package/build/tools/shared-spawn.d.ts.map +1 -0
- package/build/tools/shared-spawn.js +114 -0
- package/build/tools/shared-spawn.js.map +1 -0
- package/build/tools/spawn-agent.d.ts +85 -0
- package/build/tools/spawn-agent.d.ts.map +1 -0
- package/build/tools/spawn-agent.js +133 -0
- package/build/tools/spawn-agent.js.map +1 -0
- package/build/tools/spawn-coder.d.ts +70 -0
- package/build/tools/spawn-coder.d.ts.map +1 -0
- package/build/tools/spawn-coder.js +71 -0
- package/build/tools/spawn-coder.js.map +1 -0
- package/build/tools/spawn-planner.d.ts +70 -0
- package/build/tools/spawn-planner.d.ts.map +1 -0
- package/build/tools/spawn-planner.js +71 -0
- package/build/tools/spawn-planner.js.map +1 -0
- package/build/tools/spawn-researcher.d.ts +70 -0
- package/build/tools/spawn-researcher.d.ts.map +1 -0
- package/build/tools/spawn-researcher.js +70 -0
- package/build/tools/spawn-researcher.js.map +1 -0
- package/build/tools/spawn-task.d.ts +74 -0
- package/build/tools/spawn-task.d.ts.map +1 -0
- package/build/tools/spawn-task.js +107 -0
- package/build/tools/spawn-task.js.map +1 -0
- package/build/tools/spawn-tester.d.ts +70 -0
- package/build/tools/spawn-tester.d.ts.map +1 -0
- package/build/tools/spawn-tester.js +69 -0
- package/build/tools/spawn-tester.js.map +1 -0
- package/build/types.d.ts +101 -0
- package/build/types.d.ts.map +1 -0
- package/build/types.js +28 -0
- package/build/types.js.map +1 -0
- package/build/utils/brief-validator.d.ts +30 -0
- package/build/utils/brief-validator.d.ts.map +1 -0
- package/build/utils/brief-validator.js +254 -0
- package/build/utils/brief-validator.js.map +1 -0
- package/build/utils/format.d.ts +34 -0
- package/build/utils/format.d.ts.map +1 -0
- package/build/utils/format.js +55 -0
- package/build/utils/format.js.map +1 -0
- package/build/utils/sanitize.d.ts +240 -0
- package/build/utils/sanitize.d.ts.map +1 -0
- package/build/utils/sanitize.js +89 -0
- package/build/utils/sanitize.js.map +1 -0
- package/build/utils/task-id-generator.d.ts +10 -0
- package/build/utils/task-id-generator.d.ts.map +1 -0
- package/build/utils/task-id-generator.js +22 -0
- package/build/utils/task-id-generator.js.map +1 -0
- package/package.json +62 -0
|
@@ -0,0 +1,566 @@
|
|
|
1
|
+
## TAURI 2.x GUIDELINES
|
|
2
|
+
|
|
3
|
+
You are working in a **Tauri 2.x** desktop application codebase (Rust backend + web frontend). Apply these patterns with zero exceptions. This is Tauri v2 -- do NOT use Tauri v1 APIs.
|
|
4
|
+
|
|
5
|
+
### Project Structure -- THE STANDARD LAYOUT
|
|
6
|
+
```
|
|
7
|
+
my-app/
|
|
8
|
+
src/ # Frontend (React/Vue/Svelte/etc.)
|
|
9
|
+
src-tauri/
|
|
10
|
+
Cargo.toml # Rust deps -- tauri, serde, thiserror
|
|
11
|
+
tauri.conf.json # App config, bundle ID, plugins, security
|
|
12
|
+
capabilities/ # ACL capability files (JSON/TOML)
|
|
13
|
+
default.json # Auto-enabled permissions for windows
|
|
14
|
+
icons/ # App icons (output of `tauri icon`)
|
|
15
|
+
build.rs # Must call tauri_build::build()
|
|
16
|
+
src/
|
|
17
|
+
lib.rs # Main entry -- Builder, plugins, state, commands
|
|
18
|
+
main.rs # Desktop-only: just calls app_lib::run()
|
|
19
|
+
commands/ # Split commands into modules by domain
|
|
20
|
+
mod.rs
|
|
21
|
+
files.rs
|
|
22
|
+
settings.rs
|
|
23
|
+
state.rs # AppState structs + type aliases
|
|
24
|
+
errors.rs # Custom error types with thiserror + Serialize
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
- **lib.rs is the real entry point** -- `main.rs` only calls `app_lib::run()`. This enables mobile support.
|
|
28
|
+
- **NEVER put all commands in lib.rs** for non-trivial apps. Use a `commands/` module.
|
|
29
|
+
- Cargo.toml must include `[lib]` section with `name = "app_lib"` and `crate-type = ["staticlib", "cdylib", "rlib"]` for mobile compatibility.
|
|
30
|
+
|
|
31
|
+
### lib.rs Entry Point Pattern
|
|
32
|
+
```rust
|
|
33
|
+
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
|
34
|
+
pub fn run() {
|
|
35
|
+
tauri::Builder::default()
|
|
36
|
+
.plugin(tauri_plugin_fs::init())
|
|
37
|
+
.plugin(tauri_plugin_shell::init())
|
|
38
|
+
.manage(std::sync::Mutex::new(AppState::default()))
|
|
39
|
+
.invoke_handler(tauri::generate_handler![
|
|
40
|
+
commands::files::read_project,
|
|
41
|
+
commands::files::save_project,
|
|
42
|
+
commands::settings::get_settings,
|
|
43
|
+
commands::settings::update_settings,
|
|
44
|
+
])
|
|
45
|
+
.run(tauri::generate_context!())
|
|
46
|
+
.expect("error while running tauri application");
|
|
47
|
+
}
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### Commands -- #[tauri::command]
|
|
51
|
+
|
|
52
|
+
**Basic command with arguments:**
|
|
53
|
+
```rust
|
|
54
|
+
#[tauri::command]
|
|
55
|
+
fn greet(name: String) -> String {
|
|
56
|
+
format!("Hello, {}!", name)
|
|
57
|
+
}
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
**Frontend invocation -- arguments are camelCase by default:**
|
|
61
|
+
```typescript
|
|
62
|
+
import { invoke } from '@tauri-apps/api/core';
|
|
63
|
+
const result = await invoke<string>('greet', { name: 'World' });
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
**Force snake_case arguments from frontend:**
|
|
67
|
+
```rust
|
|
68
|
+
#[tauri::command(rename_all = "snake_case")]
|
|
69
|
+
fn process_data(file_path: String) -> Result<(), AppError> { ... }
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
**Async commands -- MUST return Result:**
|
|
73
|
+
```rust
|
|
74
|
+
#[tauri::command]
|
|
75
|
+
async fn fetch_data(url: String) -> Result<Vec<DataItem>, AppError> {
|
|
76
|
+
let response = reqwest::get(&url).await.map_err(AppError::Network)?;
|
|
77
|
+
let items = response.json().await.map_err(AppError::Parse)?;
|
|
78
|
+
Ok(items)
|
|
79
|
+
}
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
**Accessing AppHandle, Window, WebviewWindow in commands:**
|
|
83
|
+
```rust
|
|
84
|
+
#[tauri::command]
|
|
85
|
+
fn do_work(app: tauri::AppHandle, window: tauri::WebviewWindow) -> Result<(), AppError> {
|
|
86
|
+
let label = window.label();
|
|
87
|
+
// Use app for emit, state, etc.
|
|
88
|
+
Ok(())
|
|
89
|
+
}
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
**Returning large binary data -- use ipc::Response to avoid JSON overhead:**
|
|
93
|
+
```rust
|
|
94
|
+
use tauri::ipc::Response;
|
|
95
|
+
|
|
96
|
+
#[tauri::command]
|
|
97
|
+
fn read_binary(path: String) -> Result<Response, AppError> {
|
|
98
|
+
let data = std::fs::read(&path).map_err(AppError::Io)?;
|
|
99
|
+
Ok(Response::new(data))
|
|
100
|
+
}
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
### Error Handling -- THE MANDATORY PATTERN
|
|
104
|
+
|
|
105
|
+
Standard Rust errors do NOT implement `serde::Serialize`. You MUST create a custom error type.
|
|
106
|
+
|
|
107
|
+
```rust
|
|
108
|
+
// src-tauri/src/errors.rs
|
|
109
|
+
#[derive(Debug, thiserror::Error)]
|
|
110
|
+
pub enum AppError {
|
|
111
|
+
#[error(transparent)]
|
|
112
|
+
Io(#[from] std::io::Error),
|
|
113
|
+
|
|
114
|
+
#[error("Database error: {0}")]
|
|
115
|
+
Database(String),
|
|
116
|
+
|
|
117
|
+
#[error("Not found: {0}")]
|
|
118
|
+
NotFound(String),
|
|
119
|
+
|
|
120
|
+
#[error("Validation failed: {0}")]
|
|
121
|
+
Validation(String),
|
|
122
|
+
|
|
123
|
+
#[error(transparent)]
|
|
124
|
+
Anyhow(#[from] anyhow::Error),
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// REQUIRED: Manual Serialize impl -- derive won't work for error types
|
|
128
|
+
impl serde::Serialize for AppError {
|
|
129
|
+
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
|
130
|
+
where S: serde::ser::Serializer {
|
|
131
|
+
serializer.serialize_str(self.to_string().as_ref())
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
- Add `thiserror = "2"` and `serde = { version = "1", features = ["derive"] }` to Cargo.toml.
|
|
137
|
+
- Use `#[from]` for automatic conversion with `?` operator.
|
|
138
|
+
- NEVER use `Result<T, String>` in production -- always use a typed error enum.
|
|
139
|
+
|
|
140
|
+
### State Management -- Mutex Patterns
|
|
141
|
+
|
|
142
|
+
**Setup state:**
|
|
143
|
+
```rust
|
|
144
|
+
use std::sync::Mutex;
|
|
145
|
+
|
|
146
|
+
#[derive(Default)]
|
|
147
|
+
struct AppState {
|
|
148
|
+
counter: u32,
|
|
149
|
+
config: Option<Config>,
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Type alias prevents State<'_, Mutex<AppState>> vs State<'_, AppState> runtime panics
|
|
153
|
+
type AppStateMutex = Mutex<AppState>;
|
|
154
|
+
|
|
155
|
+
// In lib.rs run():
|
|
156
|
+
.manage(Mutex::new(AppState::default()))
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
**Access state in commands:**
|
|
160
|
+
```rust
|
|
161
|
+
#[tauri::command]
|
|
162
|
+
fn increment(state: tauri::State<'_, AppStateMutex>) -> Result<u32, AppError> {
|
|
163
|
+
let mut s = state.lock().map_err(|_| AppError::Validation("poisoned lock".into()))?;
|
|
164
|
+
s.counter += 1;
|
|
165
|
+
Ok(s.counter)
|
|
166
|
+
}
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
**Async commands with state -- use tokio Mutex if holding lock across .await:**
|
|
170
|
+
```rust
|
|
171
|
+
use tauri::async_runtime::Mutex as AsyncMutex;
|
|
172
|
+
|
|
173
|
+
#[tauri::command]
|
|
174
|
+
async fn load_data(state: tauri::State<'_, AsyncMutex<AppState>>) -> Result<String, AppError> {
|
|
175
|
+
let mut s = state.lock().await;
|
|
176
|
+
s.config = Some(fetch_config().await?);
|
|
177
|
+
Ok("loaded".into())
|
|
178
|
+
}
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
**Access state outside commands via AppHandle:**
|
|
182
|
+
```rust
|
|
183
|
+
fn background_task(app: tauri::AppHandle) {
|
|
184
|
+
let state = app.state::<AppStateMutex>();
|
|
185
|
+
let mut s = state.lock().unwrap();
|
|
186
|
+
s.counter = 0;
|
|
187
|
+
}
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
- You do NOT need `Arc` for Tauri managed state -- Tauri wraps it in `Arc` internally.
|
|
191
|
+
- Each state type must be unique. You cannot `.manage()` two values of the same type.
|
|
192
|
+
- Use `std::sync::Mutex` by default. Only use `tauri::async_runtime::Mutex` when you hold the lock across `.await` points.
|
|
193
|
+
|
|
194
|
+
### Events -- Rust to Frontend Communication
|
|
195
|
+
|
|
196
|
+
**Emit global event from Rust:**
|
|
197
|
+
```rust
|
|
198
|
+
use tauri::Emitter;
|
|
199
|
+
|
|
200
|
+
#[tauri::command]
|
|
201
|
+
fn start_sync(app: tauri::AppHandle) {
|
|
202
|
+
std::thread::spawn(move || {
|
|
203
|
+
for progress in 0..=100 {
|
|
204
|
+
app.emit("sync-progress", progress).unwrap();
|
|
205
|
+
std::thread::sleep(std::time::Duration::from_millis(50));
|
|
206
|
+
}
|
|
207
|
+
app.emit("sync-complete", ()).unwrap();
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
**Listen in frontend:**
|
|
213
|
+
```typescript
|
|
214
|
+
import { listen } from '@tauri-apps/api/event';
|
|
215
|
+
|
|
216
|
+
const unlisten = await listen<number>('sync-progress', (event) => {
|
|
217
|
+
console.log(`Progress: ${event.payload}%`);
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
// ALWAYS unlisten on cleanup (React useEffect return, Vue onUnmounted, etc.)
|
|
221
|
+
unlisten();
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
**Emit to specific window:**
|
|
225
|
+
```rust
|
|
226
|
+
use tauri::Emitter;
|
|
227
|
+
app.emit_to("settings", "config-changed", &new_config).unwrap();
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
**Emit with filter:**
|
|
231
|
+
```rust
|
|
232
|
+
use tauri::{Emitter, EventTarget};
|
|
233
|
+
app.emit_filter("file-opened", &path, |target| match target {
|
|
234
|
+
EventTarget::WebviewWindow { label } => label == "main" || label == "editor",
|
|
235
|
+
_ => false,
|
|
236
|
+
}).unwrap();
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
### Channels -- For Streaming / High-Throughput Data
|
|
240
|
+
|
|
241
|
+
Use channels instead of events when you need ordered, fast streaming (download progress, log tailing, etc.).
|
|
242
|
+
|
|
243
|
+
**Rust side:**
|
|
244
|
+
```rust
|
|
245
|
+
use tauri::ipc::Channel;
|
|
246
|
+
use serde::Serialize;
|
|
247
|
+
|
|
248
|
+
#[derive(Clone, Serialize)]
|
|
249
|
+
#[serde(rename_all = "camelCase", tag = "event", content = "data")]
|
|
250
|
+
enum DownloadEvent {
|
|
251
|
+
#[serde(rename_all = "camelCase")]
|
|
252
|
+
Progress { download_id: usize, bytes_done: usize, total: usize },
|
|
253
|
+
#[serde(rename_all = "camelCase")]
|
|
254
|
+
Finished { download_id: usize },
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
#[tauri::command]
|
|
258
|
+
async fn download(url: String, on_event: Channel<DownloadEvent>) -> Result<(), AppError> {
|
|
259
|
+
// ... stream data ...
|
|
260
|
+
on_event.send(DownloadEvent::Progress { download_id: 1, bytes_done: 500, total: 1000 })?;
|
|
261
|
+
on_event.send(DownloadEvent::Finished { download_id: 1 })?;
|
|
262
|
+
Ok(())
|
|
263
|
+
}
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
**Frontend side:**
|
|
267
|
+
```typescript
|
|
268
|
+
import { invoke, Channel } from '@tauri-apps/api/core';
|
|
269
|
+
|
|
270
|
+
type DownloadEvent =
|
|
271
|
+
| { event: 'progress'; data: { downloadId: number; bytesDone: number; total: number } }
|
|
272
|
+
| { event: 'finished'; data: { downloadId: number } };
|
|
273
|
+
|
|
274
|
+
const onEvent = new Channel<DownloadEvent>();
|
|
275
|
+
onEvent.onmessage = (msg) => {
|
|
276
|
+
if (msg.event === 'progress') {
|
|
277
|
+
updateProgressBar(msg.data.bytesDone / msg.data.total);
|
|
278
|
+
}
|
|
279
|
+
};
|
|
280
|
+
await invoke('download', { url: 'https://example.com/file.bin', onEvent });
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
- Channels are faster than events and guarantee message ordering.
|
|
284
|
+
- Events are for pub/sub (multi-consumer). Channels are for point-to-point streaming.
|
|
285
|
+
|
|
286
|
+
### Capabilities & Permissions -- Tauri 2.x Security (Replaces v1 Allowlist)
|
|
287
|
+
|
|
288
|
+
Capabilities are JSON files in `src-tauri/capabilities/`. All files in this directory are auto-enabled.
|
|
289
|
+
|
|
290
|
+
**src-tauri/capabilities/default.json:**
|
|
291
|
+
```json
|
|
292
|
+
{
|
|
293
|
+
"$schema": "../gen/schemas/desktop-schema.json",
|
|
294
|
+
"identifier": "default",
|
|
295
|
+
"description": "Default capability for the main window",
|
|
296
|
+
"windows": ["main"],
|
|
297
|
+
"permissions": [
|
|
298
|
+
"core:default",
|
|
299
|
+
"fs:default",
|
|
300
|
+
"fs:allow-read-text-file",
|
|
301
|
+
"fs:allow-write-text-file",
|
|
302
|
+
"shell:allow-open",
|
|
303
|
+
{
|
|
304
|
+
"identifier": "fs:scope",
|
|
305
|
+
"allow": [{ "path": "$APPDATA" }, { "path": "$APPDATA/**" }]
|
|
306
|
+
}
|
|
307
|
+
]
|
|
308
|
+
}
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
**Platform-specific capabilities:**
|
|
312
|
+
```json
|
|
313
|
+
{
|
|
314
|
+
"identifier": "desktop-only",
|
|
315
|
+
"windows": ["main"],
|
|
316
|
+
"platforms": ["linux", "macOS", "windows"],
|
|
317
|
+
"permissions": ["global-shortcut:allow-register"]
|
|
318
|
+
}
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
**Multi-window with different capabilities:**
|
|
322
|
+
```json
|
|
323
|
+
{
|
|
324
|
+
"identifier": "settings-window",
|
|
325
|
+
"windows": ["settings"],
|
|
326
|
+
"permissions": ["core:default", "fs:allow-read-text-file"]
|
|
327
|
+
}
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
- A window with NO matching capability has ZERO access to IPC. This is intentional.
|
|
331
|
+
- Permission format: `plugin-name:permission-name` (e.g., `fs:allow-read-text-file`).
|
|
332
|
+
- Core permissions are prefixed with `core:` (e.g., `core:window:allow-set-title`).
|
|
333
|
+
- Deny scopes take precedence over allow scopes.
|
|
334
|
+
- NEVER use `"windows": ["*"]` unless every window truly needs those permissions.
|
|
335
|
+
|
|
336
|
+
### Plugin System
|
|
337
|
+
|
|
338
|
+
**Using official plugins:**
|
|
339
|
+
```bash
|
|
340
|
+
# Install from npm + cargo in one step
|
|
341
|
+
npm run tauri add fs
|
|
342
|
+
npm run tauri add shell
|
|
343
|
+
npm run tauri add updater
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
**Register in lib.rs:**
|
|
347
|
+
```rust
|
|
348
|
+
tauri::Builder::default()
|
|
349
|
+
.plugin(tauri_plugin_fs::init())
|
|
350
|
+
.plugin(tauri_plugin_shell::init())
|
|
351
|
+
.plugin(tauri_plugin_updater::init())
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
**Creating a custom plugin:**
|
|
355
|
+
```rust
|
|
356
|
+
use tauri::plugin::{Builder as PluginBuilder, TauriPlugin};
|
|
357
|
+
use tauri::Runtime;
|
|
358
|
+
|
|
359
|
+
#[tauri::command]
|
|
360
|
+
fn my_plugin_command(state: tauri::State<'_, MyPluginState>) -> Result<String, AppError> {
|
|
361
|
+
Ok("plugin response".into())
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
pub fn init<R: Runtime>() -> TauriPlugin<R> {
|
|
365
|
+
PluginBuilder::new("my-plugin")
|
|
366
|
+
.setup(|app, _api| {
|
|
367
|
+
app.manage(MyPluginState::default());
|
|
368
|
+
Ok(())
|
|
369
|
+
})
|
|
370
|
+
.invoke_handler(tauri::generate_handler![my_plugin_command])
|
|
371
|
+
.on_navigation(|window, url| {
|
|
372
|
+
// Return false to block navigation
|
|
373
|
+
true
|
|
374
|
+
})
|
|
375
|
+
.on_event(|app, event| {
|
|
376
|
+
// Handle app lifecycle events
|
|
377
|
+
})
|
|
378
|
+
.build()
|
|
379
|
+
}
|
|
380
|
+
```
|
|
381
|
+
|
|
382
|
+
**Frontend calls plugin commands with namespace prefix:**
|
|
383
|
+
```typescript
|
|
384
|
+
await invoke('plugin:my-plugin|my_plugin_command');
|
|
385
|
+
```
|
|
386
|
+
|
|
387
|
+
- Plugin commands require permissions in capabilities: `"my-plugin:allow-my-plugin-command"`.
|
|
388
|
+
- For inline plugins (defined in the app, not a crate), you must update `build.rs` to generate permissions.
|
|
389
|
+
|
|
390
|
+
### Window Management
|
|
391
|
+
|
|
392
|
+
**Create windows from Rust:**
|
|
393
|
+
```rust
|
|
394
|
+
use tauri::{WebviewWindowBuilder, WebviewUrl};
|
|
395
|
+
|
|
396
|
+
#[tauri::command]
|
|
397
|
+
fn open_settings(app: tauri::AppHandle) -> Result<(), AppError> {
|
|
398
|
+
let _window = WebviewWindowBuilder::new(&app, "settings", WebviewUrl::App("/settings".into()))
|
|
399
|
+
.title("Settings")
|
|
400
|
+
.inner_size(600.0, 400.0)
|
|
401
|
+
.build()
|
|
402
|
+
.map_err(|e| AppError::Validation(e.to_string()))?;
|
|
403
|
+
Ok(())
|
|
404
|
+
}
|
|
405
|
+
```
|
|
406
|
+
|
|
407
|
+
**Create windows from frontend (requires capability):**
|
|
408
|
+
```typescript
|
|
409
|
+
import { WebviewWindow } from '@tauri-apps/api/webviewWindow';
|
|
410
|
+
|
|
411
|
+
const settings = new WebviewWindow('settings', {
|
|
412
|
+
url: '/settings',
|
|
413
|
+
width: 600,
|
|
414
|
+
height: 400,
|
|
415
|
+
title: 'Settings',
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
settings.once('tauri://created', () => { /* success */ });
|
|
419
|
+
settings.once('tauri://error', (e) => { /* handle error */ });
|
|
420
|
+
```
|
|
421
|
+
|
|
422
|
+
- Requires `"core:webview:allow-create-webview-window"` permission.
|
|
423
|
+
- Inter-window communication: use `emit_to()` targeting window labels or global events.
|
|
424
|
+
- Use `WebviewWindow::getByLabel('label')` to get a handle to an existing window.
|
|
425
|
+
|
|
426
|
+
### File System Plugin
|
|
427
|
+
|
|
428
|
+
**Frontend file operations:**
|
|
429
|
+
```typescript
|
|
430
|
+
import { readTextFile, writeTextFile, BaseDirectory } from '@tauri-apps/plugin-fs';
|
|
431
|
+
import { appDataDir, join } from '@tauri-apps/api/path';
|
|
432
|
+
|
|
433
|
+
// Read from app data
|
|
434
|
+
const config = await readTextFile('config.json', { baseDir: BaseDirectory.AppData });
|
|
435
|
+
|
|
436
|
+
// Write to app data
|
|
437
|
+
await writeTextFile('config.json', JSON.stringify(settings), {
|
|
438
|
+
baseDir: BaseDirectory.AppData,
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
// Resolve absolute path
|
|
442
|
+
const dataDir = await appDataDir();
|
|
443
|
+
const filePath = await join(dataDir, 'projects', 'data.json');
|
|
444
|
+
```
|
|
445
|
+
|
|
446
|
+
**Rust path resolution:**
|
|
447
|
+
```rust
|
|
448
|
+
use tauri::Manager;
|
|
449
|
+
|
|
450
|
+
fn get_app_data(app: &tauri::AppHandle) -> Result<std::path::PathBuf, AppError> {
|
|
451
|
+
app.path().app_data_dir().map_err(|e| AppError::Io(e.into()))
|
|
452
|
+
}
|
|
453
|
+
```
|
|
454
|
+
|
|
455
|
+
- Paths using this API cannot traverse parent directories (`../` is blocked).
|
|
456
|
+
- BaseDirectory variables for scopes: `$APPDATA`, `$APPCONFIG`, `$APPLOG`, `$HOME`, `$RESOURCE`, `$TEMP`.
|
|
457
|
+
- App directories must be created at runtime before use -- they do not exist by default.
|
|
458
|
+
|
|
459
|
+
### Security -- CSP & IPC Safety
|
|
460
|
+
|
|
461
|
+
**tauri.conf.json CSP configuration:**
|
|
462
|
+
```json
|
|
463
|
+
{
|
|
464
|
+
"app": {
|
|
465
|
+
"security": {
|
|
466
|
+
"csp": {
|
|
467
|
+
"default-src": "'self' customprotocol: asset:",
|
|
468
|
+
"connect-src": "ipc: http://ipc.localhost",
|
|
469
|
+
"img-src": "'self' asset: http://asset.localhost blob: data:",
|
|
470
|
+
"style-src": "'unsafe-inline' 'self'"
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
```
|
|
476
|
+
|
|
477
|
+
- Tauri auto-injects nonces and hashes for bundled scripts/styles at compile time.
|
|
478
|
+
- NEVER use `'unsafe-eval'` unless using WASM (then use `'wasm-unsafe-eval'`).
|
|
479
|
+
- Avoid loading remote scripts. Any untrusted content is an attack vector.
|
|
480
|
+
- Sanitize ALL user input on the Rust side before processing.
|
|
481
|
+
- Defer business logic to Rust -- minimize logic in the frontend.
|
|
482
|
+
- Consider the Isolation pattern (`"isolation": { "pattern": "..." }`) for apps handling untrusted content.
|
|
483
|
+
|
|
484
|
+
### Build & Distribution
|
|
485
|
+
|
|
486
|
+
**Development:**
|
|
487
|
+
```bash
|
|
488
|
+
npm run tauri dev # Hot-reload development
|
|
489
|
+
npm run tauri build # Production build
|
|
490
|
+
npm run tauri icon # Generate icons from source image
|
|
491
|
+
```
|
|
492
|
+
|
|
493
|
+
**tauri.conf.json essentials:**
|
|
494
|
+
```json
|
|
495
|
+
{
|
|
496
|
+
"productName": "My App",
|
|
497
|
+
"mainBinaryName": "my-app",
|
|
498
|
+
"version": "1.0.0",
|
|
499
|
+
"identifier": "com.mycompany.myapp",
|
|
500
|
+
"build": {
|
|
501
|
+
"frontendDist": "../dist",
|
|
502
|
+
"devUrl": "http://localhost:5173",
|
|
503
|
+
"beforeBuildCommand": "npm run build",
|
|
504
|
+
"beforeDevCommand": "npm run dev"
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
```
|
|
508
|
+
|
|
509
|
+
**Updater setup (tauri.conf.json):**
|
|
510
|
+
```json
|
|
511
|
+
{
|
|
512
|
+
"plugins": {
|
|
513
|
+
"updater": {
|
|
514
|
+
"endpoints": ["https://releases.example.com/{{target}}/{{arch}}/{{current_version}}"],
|
|
515
|
+
"pubkey": "YOUR_PUBLIC_KEY_HERE"
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
```
|
|
520
|
+
|
|
521
|
+
**GitHub Actions CI/CD:**
|
|
522
|
+
```yaml
|
|
523
|
+
- uses: tauri-apps/tauri-action@v0
|
|
524
|
+
env:
|
|
525
|
+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
526
|
+
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
|
|
527
|
+
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
|
|
528
|
+
with:
|
|
529
|
+
tagName: v__VERSION__
|
|
530
|
+
releaseName: 'v__VERSION__'
|
|
531
|
+
includeUpdaterJson: true
|
|
532
|
+
```
|
|
533
|
+
|
|
534
|
+
- `TAURI_SIGNING_PRIVATE_KEY` and `TAURI_SIGNING_PRIVATE_KEY_PASSWORD` must be set as environment variables (not .env files).
|
|
535
|
+
- Generate signing keys with `npx tauri signer generate`.
|
|
536
|
+
- macOS requires code signing + notarization for distribution. Set `APPLE_SIGNING_IDENTITY` env var.
|
|
537
|
+
- `mainBinaryName` is REQUIRED in Tauri 2.x -- the binary is no longer auto-renamed to match `productName`.
|
|
538
|
+
|
|
539
|
+
### Common Gotchas -- AVOID THESE
|
|
540
|
+
|
|
541
|
+
1. **Serde serialization for errors:** Standard error types don't implement `Serialize`. You MUST create a custom error type (see Error Handling section above). This is the #1 newcomer mistake.
|
|
542
|
+
|
|
543
|
+
2. **State type mismatch:** `State<'_, AppState>` vs `State<'_, Mutex<AppState>>` gives a runtime panic, not a compile error. Always use a type alias for your wrapped state.
|
|
544
|
+
|
|
545
|
+
3. **Async commands with borrowed data:** `&str` and `State<'_, T>` in async commands cause "future is not Send" errors. Solution: async commands MUST return `Result<T, E>`. For `&str`, use `String` instead.
|
|
546
|
+
|
|
547
|
+
4. **Missing capabilities:** Commands silently fail (or return 400) if the capability file doesn't include the permission. Check `src-tauri/capabilities/` first when a command doesn't work.
|
|
548
|
+
|
|
549
|
+
5. **Frontend argument casing:** Rust `snake_case` args become `camelCase` on the frontend by default. Use `#[tauri::command(rename_all = "snake_case")]` to keep snake_case.
|
|
550
|
+
|
|
551
|
+
6. **v1 imports still in code:** `@tauri-apps/api/tauri` is now `@tauri-apps/api/core`. `@tauri-apps/api/fs` is now `@tauri-apps/plugin-fs`. There is no backward compatibility.
|
|
552
|
+
|
|
553
|
+
7. **Events are not type-safe:** Events use JSON payloads with no type checking at the Rust/TS boundary. Prefer commands for typed request/response and channels for typed streaming.
|
|
554
|
+
|
|
555
|
+
8. **Window without capability = zero IPC access.** If a webview or its window matches no capability, it has no access to the IPC layer at all. This is not a bug.
|
|
556
|
+
|
|
557
|
+
9. **Mutex poisoning:** Use `.lock().map_err(...)` instead of `.lock().unwrap()` in commands. A panicked thread poisons the mutex, and `.unwrap()` will crash subsequent calls.
|
|
558
|
+
|
|
559
|
+
10. **Plugin names cannot contain underscores.** Use hyphens (`my-plugin`, not `my_plugin`). Underscore names cause cryptic serde deserialization errors in capabilities.
|
|
560
|
+
|
|
561
|
+
### Naming & Conventions
|
|
562
|
+
- `snake_case` for Rust functions, modules, variables
|
|
563
|
+
- `PascalCase` for Rust types, traits, enums
|
|
564
|
+
- `camelCase` for TypeScript functions, variables, and invoke argument names
|
|
565
|
+
- Commands registered with `generate_handler!` use their Rust function name as the invoke name
|
|
566
|
+
- Plugin commands are invoked as `plugin:plugin-name|command_name`
|