tauri-agent-tools 0.1.0 → 0.2.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.
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: tauri-agent-tools
3
3
  description: CLI for inspecting Tauri desktop apps — DOM queries, screenshots, IPC/console monitoring, storage, and page state
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  tags: [tauri, desktop, debugging, screenshot, dom, inspection]
6
6
  ---
7
7
 
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: tauri-bridge-setup
3
3
  description: How to add the tauri-agent-tools Rust dev bridge to a Tauri application
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  tags: [tauri, rust, bridge, setup, integration]
6
6
  ---
7
7
 
@@ -21,6 +21,7 @@ serde = { version = "1", features = ["derive"] }
21
21
  serde_json = "1"
22
22
  scopeguard = "1"
23
23
  rand = "0.8"
24
+ uuid = { version = "1", features = ["v4"] }
24
25
  ```
25
26
 
26
27
  ## Step 2 — Copy the bridge module
@@ -43,13 +44,21 @@ cp node_modules/tauri-agent-tools/examples/tauri-bridge/src/dev_bridge.rs src-ta
43
44
 
44
45
  ## Step 3 — Wire up in main.rs
45
46
 
46
- Add the module declaration and start the bridge in your `src-tauri/src/main.rs`:
47
+ Add the module declaration, register the bridge command, and start the bridge in your `src-tauri/src/main.rs`:
47
48
 
48
49
  ```rust
49
50
  mod dev_bridge;
50
51
 
51
52
  fn main() {
52
- tauri::Builder::default()
53
+ let mut builder = tauri::Builder::default();
54
+
55
+ if cfg!(debug_assertions) {
56
+ builder = builder.invoke_handler(tauri::generate_handler![
57
+ dev_bridge::__dev_bridge_result
58
+ ]);
59
+ }
60
+
61
+ builder
53
62
  .setup(|app| {
54
63
  if cfg!(debug_assertions) {
55
64
  if let Err(e) = dev_bridge::start_bridge(app.handle()) {
@@ -63,6 +72,16 @@ fn main() {
63
72
  }
64
73
  ```
65
74
 
75
+ If you already have an `.invoke_handler()` with your own commands, merge them into one handler:
76
+
77
+ ```rust
78
+ builder = builder.invoke_handler(tauri::generate_handler![
79
+ your_command_one,
80
+ your_command_two,
81
+ dev_bridge::__dev_bridge_result,
82
+ ]);
83
+ ```
84
+
66
85
  If you already have a `.setup()` call, add the `if cfg!(debug_assertions) { ... }` block inside it.
67
86
 
68
87
  ## Step 4 — Verify
@@ -11,3 +11,4 @@ serde = { version = "1", features = ["derive"] }
11
11
  serde_json = "1"
12
12
  scopeguard = "1"
13
13
  rand = "0.8"
14
+ uuid = { version = "1", features = ["v4"] }
@@ -1,8 +1,9 @@
1
1
  use rand::Rng;
2
2
  use serde::{Deserialize, Serialize};
3
+ use std::collections::HashMap;
3
4
  use std::fs;
4
5
  use std::io::Write;
5
- use std::sync::Arc;
6
+ use std::sync::{Arc, Condvar, Mutex};
6
7
  use std::thread;
7
8
  use tauri::{AppHandle, Manager};
8
9
  use tiny_http::{Header, Response, Server};
@@ -25,10 +26,31 @@ struct TokenFile {
25
26
  pid: u32,
26
27
  }
27
28
 
29
+ /// Shared state for pending eval results.
30
+ /// The HTTP handler thread waits on the Condvar; the Tauri command inserts
31
+ /// the result and signals.
32
+ pub struct PendingResults {
33
+ results: Mutex<HashMap<String, serde_json::Value>>,
34
+ notify: Condvar,
35
+ }
36
+
37
+ /// Tauri command invoked from injected JS to deliver eval results back to Rust.
38
+ #[tauri::command]
39
+ pub fn __dev_bridge_result(
40
+ id: String,
41
+ value: serde_json::Value,
42
+ state: tauri::State<'_, Arc<PendingResults>>,
43
+ ) {
44
+ let mut results = state.results.lock().unwrap();
45
+ results.insert(id, value);
46
+ state.notify.notify_all();
47
+ }
48
+
28
49
  /// Start the development bridge HTTP server.
29
50
  /// Returns the port number on success.
30
51
  pub fn start_bridge(app: &AppHandle) -> Result<u16, String> {
31
- let server = Server::http("127.0.0.1:0").map_err(|e| format!("Failed to start bridge: {e}"))?;
52
+ let server =
53
+ Server::http("127.0.0.1:0").map_err(|e| format!("Failed to start bridge: {e}"))?;
32
54
  let port = server
33
55
  .server_addr()
34
56
  .to_ip()
@@ -58,6 +80,13 @@ pub fn start_bridge(app: &AppHandle) -> Result<u16, String> {
58
80
  let _ = fs::remove_file(&cleanup_path);
59
81
  });
60
82
 
83
+ // Create shared pending-results state and register it with Tauri
84
+ let pending = Arc::new(PendingResults {
85
+ results: Mutex::new(HashMap::new()),
86
+ notify: Condvar::new(),
87
+ });
88
+ app.manage(pending.clone());
89
+
61
90
  let app_handle = app.clone();
62
91
  let expected_token = token.clone();
63
92
 
@@ -96,45 +125,88 @@ pub fn start_bridge(app: &AppHandle) -> Result<u16, String> {
96
125
  continue;
97
126
  }
98
127
 
99
- // Evaluate JS in webview
100
- let (tx, rx) = std::sync::mpsc::channel();
101
- let js = eval_req.js.clone();
128
+ // Evaluate JS in webview via callback pattern
129
+ let request_id = uuid::Uuid::new_v4().to_string();
102
130
 
103
131
  if let Some(window) = app_handle.get_webview_window("main") {
104
- let _ = window.eval(&format!(
132
+ // Build JS that evaluates the expression, then calls back into Rust
133
+ // via __TAURI__.core.invoke() to deliver the result.
134
+ let callback_js = format!(
105
135
  r#"
106
- try {{
107
- const __result = eval({js});
108
- window.__tauriDevBridgeResult = __result;
109
- }} catch(e) {{
110
- window.__tauriDevBridgeResult = "ERROR: " + e.message;
111
- }}
136
+ (async () => {{
137
+ try {{
138
+ let __result = await eval({js});
139
+ if (typeof __result === "undefined") {{
140
+ __result = null;
141
+ }} else if (typeof __result === "object" && __result !== null) {{
142
+ __result = JSON.stringify(__result);
143
+ }} else if (typeof __result !== "string") {{
144
+ __result = String(__result);
145
+ }}
146
+ await window.__TAURI__.core.invoke("__dev_bridge_result", {{
147
+ id: {id},
148
+ value: __result
149
+ }});
150
+ }} catch(e) {{
151
+ await window.__TAURI__.core.invoke("__dev_bridge_result", {{
152
+ id: {id},
153
+ value: "ERROR: " + e.message
154
+ }});
155
+ }}
156
+ }})();
112
157
  "#,
113
- js = serde_json::to_string(&js).unwrap()
114
- ));
115
-
116
- // Give the webview a moment to evaluate
117
- thread::sleep(std::time::Duration::from_millis(50));
118
-
119
- // For simplicity, return the JS expression in production,
120
- // use a Tauri command callback to get the actual result
121
- let _ = tx.send(serde_json::Value::String(js));
122
- } else {
123
- let _ = tx.send(serde_json::Value::Null);
124
- }
125
-
126
- match rx.recv_timeout(std::time::Duration::from_secs(5)) {
127
- Ok(result) => {
128
- let resp = EvalResponse { result };
129
- let json = serde_json::to_string(&resp).unwrap();
130
- let header =
131
- Header::from_bytes("Content-Type", "application/json").unwrap();
132
- let _ = request.respond(Response::from_string(json).with_header(header));
133
- }
134
- Err(_) => {
135
- let _ = request
136
- .respond(Response::from_string("Eval timeout").with_status_code(504));
158
+ js = serde_json::to_string(&eval_req.js).unwrap(),
159
+ id = serde_json::to_string(&request_id).unwrap(),
160
+ );
161
+
162
+ let _ = window.eval(&callback_js);
163
+
164
+ // Wait for the result with a 5-second timeout
165
+ let mut results = pending.results.lock().unwrap();
166
+ let deadline = std::time::Duration::from_secs(5);
167
+ let start = std::time::Instant::now();
168
+
169
+ loop {
170
+ if let Some(value) = results.remove(&request_id) {
171
+ let resp = EvalResponse { result: value };
172
+ let json = serde_json::to_string(&resp).unwrap();
173
+ let header =
174
+ Header::from_bytes("Content-Type", "application/json").unwrap();
175
+ let _ =
176
+ request.respond(Response::from_string(json).with_header(header));
177
+ break;
178
+ }
179
+
180
+ let elapsed = start.elapsed();
181
+ if elapsed >= deadline {
182
+ // Timeout — clean up and respond with 504
183
+ results.remove(&request_id);
184
+ let _ = request.respond(
185
+ Response::from_string("Eval timeout").with_status_code(504),
186
+ );
187
+ break;
188
+ }
189
+
190
+ let remaining = deadline - elapsed;
191
+ let (guard, timeout_result) =
192
+ pending.notify.wait_timeout(results, remaining).unwrap();
193
+ results = guard;
194
+
195
+ if timeout_result.timed_out() && !results.contains_key(&request_id) {
196
+ results.remove(&request_id);
197
+ let _ = request.respond(
198
+ Response::from_string("Eval timeout").with_status_code(504),
199
+ );
200
+ break;
201
+ }
137
202
  }
203
+ } else {
204
+ let resp = EvalResponse {
205
+ result: serde_json::Value::Null,
206
+ };
207
+ let json = serde_json::to_string(&resp).unwrap();
208
+ let header = Header::from_bytes("Content-Type", "application/json").unwrap();
209
+ let _ = request.respond(Response::from_string(json).with_header(header));
138
210
  }
139
211
  }
140
212
  });
@@ -1,9 +1,16 @@
1
1
  mod dev_bridge;
2
2
 
3
3
  fn main() {
4
- tauri::Builder::default()
4
+ let mut builder = tauri::Builder::default();
5
+
6
+ if cfg!(debug_assertions) {
7
+ builder = builder.invoke_handler(tauri::generate_handler![
8
+ dev_bridge::__dev_bridge_result
9
+ ]);
10
+ }
11
+
12
+ builder
5
13
  .setup(|app| {
6
- // Only start bridge in development
7
14
  if cfg!(debug_assertions) {
8
15
  if let Err(e) = dev_bridge::start_bridge(app.handle()) {
9
16
  eprintln!("Warning: Failed to start dev bridge: {e}");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tauri-agent-tools",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Agent-driven inspection toolkit for Tauri desktop apps",
5
5
  "type": "module",
6
6
  "bin": {
@@ -13,21 +13,30 @@ serde = { version = "1", features = ["derive"] }
13
13
  serde_json = "1"
14
14
  scopeguard = "1"
15
15
  rand = "0.8"
16
+ uuid = { version = "1", features = ["v4"] }
16
17
  ```
17
18
 
18
19
  ### 2. Copy the bridge module
19
20
 
20
21
  Copy `examples/tauri-bridge/src/dev_bridge.rs` into your Tauri project's `src/` directory.
21
22
 
22
- ### 3. Start the bridge in your app
23
+ ### 3. Wire up in main.rs
23
24
 
24
- In your `main.rs` or app setup:
25
+ In your `main.rs`, register the bridge's Tauri command and start the bridge during setup:
25
26
 
26
27
  ```rust
27
28
  mod dev_bridge;
28
29
 
29
30
  fn main() {
30
- tauri::Builder::default()
31
+ let mut builder = tauri::Builder::default();
32
+
33
+ if cfg!(debug_assertions) {
34
+ builder = builder.invoke_handler(tauri::generate_handler![
35
+ dev_bridge::__dev_bridge_result
36
+ ]);
37
+ }
38
+
39
+ builder
31
40
  .setup(|app| {
32
41
  if cfg!(debug_assertions) {
33
42
  if let Err(e) = dev_bridge::start_bridge(app.handle()) {
@@ -41,6 +50,16 @@ fn main() {
41
50
  }
42
51
  ```
43
52
 
53
+ > **Note:** If your app already uses `.invoke_handler()` with its own commands, merge them into one handler:
54
+ > ```rust
55
+ > builder = builder.invoke_handler(tauri::generate_handler![
56
+ > your_command_one,
57
+ > your_command_two,
58
+ > dev_bridge::__dev_bridge_result,
59
+ > ]);
60
+ > ```
61
+ > Tauri only supports a single `invoke_handler` per builder — commands from multiple calls won't merge.
62
+
44
63
  ### 4. Use tauri-agent-tools
45
64
 
46
65
  The bridge writes a token file to `/tmp/tauri-dev-bridge-<pid>.token` which `tauri-agent-tools` auto-discovers:
@@ -61,8 +80,10 @@ tauri-agent-tools eval "document.title"
61
80
  1. Bridge starts an HTTP server on a random localhost port
62
81
  2. A token file with `{ port, token, pid }` is written to `/tmp/`
63
82
  3. `tauri-agent-tools` discovers the token file and authenticates via the token
64
- 4. Requests are `POST /eval { js, token }` — the bridge evaluates JS in the webview
65
- 5. The token file is cleaned up when the app exits
83
+ 4. Requests are `POST /eval { js, token }` — the bridge injects JS into the webview
84
+ 5. The injected JS evaluates the expression, then calls back into Rust via `window.__TAURI__.core.invoke("__dev_bridge_result", { id, value })` to deliver the result
85
+ 6. The HTTP handler thread waits for the result (up to 5 seconds) and returns it as JSON
86
+ 7. The token file is cleaned up when the app exits
66
87
 
67
88
  ## Security
68
89