@forsion/tangu-computer-use 0.5.8

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.
Files changed (120) hide show
  1. package/CHANGELOG.md +223 -0
  2. package/LICENSE +26 -0
  3. package/LICENSE.upstream +21 -0
  4. package/README.md +169 -0
  5. package/UPSTREAM.md +277 -0
  6. package/check.mjs +369 -0
  7. package/icon.png +0 -0
  8. package/install.sh +112 -0
  9. package/main.js +541 -0
  10. package/manifest.json +47 -0
  11. package/native/linux/bridge-rs/Cargo.lock +1204 -0
  12. package/native/linux/bridge-rs/Cargo.toml +18 -0
  13. package/native/linux/bridge-rs/src/atspi.rs +640 -0
  14. package/native/linux/bridge-rs/src/error.rs +65 -0
  15. package/native/linux/bridge-rs/src/lib.rs +11 -0
  16. package/native/linux/bridge-rs/src/main.rs +1074 -0
  17. package/native/linux/bridge-rs/src/protocol.rs +49 -0
  18. package/native/linux/bridge-rs/src/state.rs +293 -0
  19. package/native/linux/bridge-rs/src/wayland.rs +89 -0
  20. package/native/linux/bridge-rs/src/x11.rs +909 -0
  21. package/native/linux/bridge-rs/tests/protocol_tests.rs +73 -0
  22. package/native/macos/agent_cursor.swift +184 -0
  23. package/native/macos/agent_cursor_motion.swift +252 -0
  24. package/native/macos/agent_cursor_tests.swift +128 -0
  25. package/native/macos/agent_highlight.swift +229 -0
  26. package/native/macos/agent_highlight_tests.swift +99 -0
  27. package/native/macos/bridge.swift +3852 -0
  28. package/native/macos/foreground_activity.swift +57 -0
  29. package/native/macos/foreground_activity_tests.swift +31 -0
  30. package/native/macos/live_stream.swift +289 -0
  31. package/native/windows/bridge-rs/Cargo.lock +396 -0
  32. package/native/windows/bridge-rs/Cargo.toml +30 -0
  33. package/native/windows/bridge-rs/src/capture.rs +518 -0
  34. package/native/windows/bridge-rs/src/error.rs +81 -0
  35. package/native/windows/bridge-rs/src/input.rs +501 -0
  36. package/native/windows/bridge-rs/src/lib.rs +16 -0
  37. package/native/windows/bridge-rs/src/main.rs +1683 -0
  38. package/native/windows/bridge-rs/src/protocol.rs +53 -0
  39. package/native/windows/bridge-rs/src/refs.rs +237 -0
  40. package/native/windows/bridge-rs/src/state.rs +55 -0
  41. package/native/windows/bridge-rs/src/uia.rs +1252 -0
  42. package/native/windows/bridge-rs/src/window.rs +718 -0
  43. package/native/windows/bridge-rs/tests/protocol_tests.rs +252 -0
  44. package/native/windows/bridge-rs/tests/refs_tests.rs +86 -0
  45. package/native/windows/bridge-rs/tests/state_tests.rs +33 -0
  46. package/package.json +74 -0
  47. package/prebuilt/linux/arm64/linux-bridge +0 -0
  48. package/prebuilt/linux/x64/linux-bridge +0 -0
  49. package/prebuilt/macos/arm64/bridge +0 -0
  50. package/prebuilt/macos/arm64/tangu-computer-use.app.json +10 -0
  51. package/prebuilt/macos/arm64/tangu-computer-use.app.zip +0 -0
  52. package/prebuilt/macos/x64/bridge +0 -0
  53. package/prebuilt/macos/x64/tangu-computer-use.app.json +10 -0
  54. package/prebuilt/macos/x64/tangu-computer-use.app.zip +0 -0
  55. package/prebuilt/windows/windows-bridge.exe +0 -0
  56. package/scripts/blind-click.check.mjs +128 -0
  57. package/scripts/build-native.mjs +302 -0
  58. package/scripts/build.mjs +84 -0
  59. package/scripts/calc-fixture.mjs +78 -0
  60. package/scripts/cli-exit.check.mjs +49 -0
  61. package/scripts/helper-path.check.mjs +82 -0
  62. package/scripts/helper-refresh.check.mjs +62 -0
  63. package/scripts/helper-signal.check.mjs +59 -0
  64. package/scripts/highlight.check.mjs +32 -0
  65. package/scripts/keychain-free-install.check.mjs +140 -0
  66. package/scripts/live-view.check.mjs +124 -0
  67. package/scripts/macos-bundle.d.mts +1 -0
  68. package/scripts/macos-bundle.mjs +119 -0
  69. package/scripts/make-signing-cert.sh +57 -0
  70. package/scripts/mini-foreground.check.mjs +11 -0
  71. package/scripts/no-foreground.check.mjs +81 -0
  72. package/scripts/overlay-visible.check.mjs +221 -0
  73. package/scripts/package-macos-app.mjs +39 -0
  74. package/scripts/permissions.check.mjs +199 -0
  75. package/scripts/platform-contract.check.mjs +30 -0
  76. package/scripts/setup-helper.mjs +154 -0
  77. package/scripts/tangu-computer-use.entitlements +10 -0
  78. package/scripts/verify-macos-bundles.mjs +25 -0
  79. package/scripts/verify-package.mjs +74 -0
  80. package/skills/computer-use/SKILL.md +110 -0
  81. package/src/foregroundNote.ts +43 -0
  82. package/src/helperState.ts +31 -0
  83. package/src/index.ts +63 -0
  84. package/src/onboarding.ts +198 -0
  85. package/src/pi-compat.ts +49 -0
  86. package/src/settings.ts +11 -0
  87. package/src/setup.ts +97 -0
  88. package/src/tools.ts +289 -0
  89. package/src/vendor/actions.ts +130 -0
  90. package/src/vendor/bridge.ts +2405 -0
  91. package/src/vendor/cdp.ts +658 -0
  92. package/src/vendor/config.ts +113 -0
  93. package/src/vendor/contract.ts +104 -0
  94. package/src/vendor/note.ts +195 -0
  95. package/src/vendor/outline.ts +651 -0
  96. package/src/vendor/output.ts +134 -0
  97. package/src/vendor/permissions.ts +111 -0
  98. package/src/vendor/platform/architecture.ts +23 -0
  99. package/src/vendor/platform/coerce.ts +16 -0
  100. package/src/vendor/platform/index.ts +59 -0
  101. package/src/vendor/platform/linux/backend.ts +186 -0
  102. package/src/vendor/platform/linux/helper.ts +238 -0
  103. package/src/vendor/platform/macos/backend.ts +131 -0
  104. package/src/vendor/platform/macos/browser.ts +110 -0
  105. package/src/vendor/platform/macos/helper-path.d.mts +9 -0
  106. package/src/vendor/platform/macos/helper-path.mjs +34 -0
  107. package/src/vendor/platform/macos/helper.ts +291 -0
  108. package/src/vendor/platform/macos/permissions.ts +146 -0
  109. package/src/vendor/platform/types.ts +222 -0
  110. package/src/vendor/platform/windows/backend.ts +142 -0
  111. package/src/vendor/platform/windows/helper.ts +140 -0
  112. package/src/vendor/root-selection.ts +25 -0
  113. package/src/vendor/runtime.ts +129 -0
  114. package/src/vendor/state.ts +146 -0
  115. package/src/vendor/view.ts +147 -0
  116. package/tangu-plugins/computer-use/dist/foregroundNote.js +19 -0
  117. package/tangu-plugins/computer-use/dist/index.js +5672 -0
  118. package/tangu-plugins/computer-use/tangu-plugin.json +9 -0
  119. package/tsconfig.json +22 -0
  120. package/types/tangu-agent.d.ts +142 -0
@@ -0,0 +1,501 @@
1
+ use serde_json::{json, Value};
2
+
3
+ use crate::error::{ErrorCode, ProtocolError};
4
+
5
+ #[derive(Debug, Clone, PartialEq)]
6
+ pub enum ActTarget {
7
+ Ref(String),
8
+ Point { x: f64, y: f64 },
9
+ }
10
+
11
+ #[derive(Debug, Clone, PartialEq)]
12
+ pub struct ParsedActRequest {
13
+ pub look_id: String,
14
+ pub pid: Option<u64>,
15
+ pub action: String,
16
+ pub policy: String,
17
+ pub target: ActTarget,
18
+ pub params: Value,
19
+ }
20
+
21
+ pub fn parse_act_request(args: &Value) -> Result<ParsedActRequest, ProtocolError> {
22
+ let look_id = args
23
+ .get("lookId")
24
+ .and_then(Value::as_str)
25
+ .filter(|value| !value.is_empty())
26
+ .ok_or_else(|| invalid("act requires lookId"))?
27
+ .to_owned();
28
+ let action = args
29
+ .get("action")
30
+ .and_then(Value::as_str)
31
+ .filter(|value| !value.is_empty())
32
+ .ok_or_else(|| invalid("act requires action"))?
33
+ .to_owned();
34
+ let policy = args
35
+ .get("policy")
36
+ .and_then(Value::as_str)
37
+ .unwrap_or("default")
38
+ .to_owned();
39
+ let target_value = args
40
+ .get("target")
41
+ .ok_or_else(|| invalid("act requires target"))?;
42
+ let target = if let Some(reference) = target_value.get("ref").and_then(Value::as_str) {
43
+ ActTarget::Ref(reference.to_owned())
44
+ } else if let (Some(x), Some(y)) = (
45
+ target_value.get("x").and_then(Value::as_f64),
46
+ target_value.get("y").and_then(Value::as_f64),
47
+ ) {
48
+ ActTarget::Point { x, y }
49
+ } else {
50
+ return Err(invalid("act target must include ref or x/y"));
51
+ };
52
+ Ok(ParsedActRequest {
53
+ look_id,
54
+ pid: args.get("pid").and_then(Value::as_u64),
55
+ action,
56
+ policy,
57
+ target,
58
+ params: args.get("params").cloned().unwrap_or_else(|| json!({})),
59
+ })
60
+ }
61
+
62
+ pub fn policy_allows_raw_input(request: &ParsedActRequest) -> Result<(), ProtocolError> {
63
+ if request.policy != "foreground" {
64
+ Err(ProtocolError::new(
65
+ "Background policy blocks global Windows coordinate/raw input grounding",
66
+ if request.policy == "ax_only" {
67
+ ErrorCode::CoordinateBlocked
68
+ } else {
69
+ ErrorCode::ForegroundRequired
70
+ },
71
+ ))
72
+ } else {
73
+ Ok(())
74
+ }
75
+ }
76
+
77
+ pub fn response_with_delta(mut response: Value, source: &str, delta: Vec<Value>) -> Value {
78
+ if !response
79
+ .get("performed")
80
+ .map(Value::is_object)
81
+ .unwrap_or(false)
82
+ {
83
+ response["performed"] = json!({});
84
+ }
85
+ response["performed"]["deltaSource"] = json!(source);
86
+ if !delta.is_empty() {
87
+ response["rootDelta"] = Value::Array(delta);
88
+ }
89
+ response
90
+ }
91
+
92
+ pub fn open_browser_location(url: &str) -> Result<(), ProtocolError> {
93
+ #[cfg(not(windows))]
94
+ {
95
+ let _ = url;
96
+ Ok(())
97
+ }
98
+ #[cfg(windows)]
99
+ {
100
+ native::open_browser_location(url)
101
+ }
102
+ }
103
+
104
+ pub fn act(args: &Value) -> Result<Value, ProtocolError> {
105
+ let request = parse_act_request(args)?;
106
+ let grounding = args
107
+ .get("resolvedPoint")
108
+ .map(|_| "coordinates")
109
+ .unwrap_or("description");
110
+
111
+ if matches!(
112
+ request.action.as_str(),
113
+ "typeText" | "keypress" | "drag" | "moveMouse"
114
+ ) {
115
+ policy_allows_raw_input(&request)?;
116
+ }
117
+ if grounding == "coordinates" {
118
+ policy_allows_raw_input(&request)?;
119
+ }
120
+
121
+ #[cfg(not(windows))]
122
+ {
123
+ Ok(json!({
124
+ "outcome": "unknown",
125
+ "performed": { "grounding": grounding, "delivery": if grounding == "description" { "ax" } else { "hid" } },
126
+ "evidence": { "platform": "non-windows-test-stub" }
127
+ }))
128
+ }
129
+
130
+ #[cfg(windows)]
131
+ {
132
+ native::act(&request, args, grounding)
133
+ }
134
+ }
135
+
136
+ fn invalid(message: impl Into<String>) -> ProtocolError {
137
+ ProtocolError::new(message.into(), ErrorCode::InvalidRequest)
138
+ }
139
+
140
+ #[cfg(windows)]
141
+ mod native {
142
+ use super::*;
143
+ use windows::Win32::UI::Input::KeyboardAndMouse::*;
144
+ use windows::Win32::UI::WindowsAndMessaging::SetCursorPos;
145
+
146
+ pub fn open_browser_location(url: &str) -> Result<(), ProtocolError> {
147
+ hotkey(&[VK_CONTROL], VK_L)?;
148
+ send_text(url)?;
149
+ send(&[key(VK_RETURN, false), key(VK_RETURN, true)])
150
+ }
151
+
152
+ pub fn act(
153
+ request: &ParsedActRequest,
154
+ args: &Value,
155
+ grounding: &str,
156
+ ) -> Result<Value, ProtocolError> {
157
+ let point = args.get("resolvedPoint").and_then(point_value);
158
+ match request.action.as_str() {
159
+ "press" | "click" => {
160
+ let (x, y) = point.ok_or_else(|| invalid("click requires resolvedPoint"))?;
161
+ click(
162
+ x,
163
+ y,
164
+ request
165
+ .params
166
+ .get("button")
167
+ .and_then(Value::as_str)
168
+ .unwrap_or("left"),
169
+ )?;
170
+ ok("unknown", grounding, "hid")
171
+ }
172
+ "moveMouse" => {
173
+ let (x, y) = point.ok_or_else(|| invalid("moveMouse requires resolvedPoint"))?;
174
+ unsafe { SetCursorPos(x, y) }.map_err(input_failed)?;
175
+ ok("unknown", "coordinates", "hid")
176
+ }
177
+ "drag" => {
178
+ let path = args
179
+ .get("resolvedPath")
180
+ .and_then(Value::as_array)
181
+ .ok_or_else(|| invalid("drag requires resolvedPath"))?;
182
+ let points = path
183
+ .iter()
184
+ .map(point_value_required)
185
+ .collect::<Result<Vec<_>, _>>()?;
186
+ drag(&points)?;
187
+ ok("unknown", "coordinates", "hid")
188
+ }
189
+ "scroll" => {
190
+ let (x, y) = point.ok_or_else(|| invalid("scroll requires resolvedPoint"))?;
191
+ let dy = request
192
+ .params
193
+ .get("scrollY")
194
+ .and_then(Value::as_f64)
195
+ .unwrap_or(0.0);
196
+ let dx = request
197
+ .params
198
+ .get("scrollX")
199
+ .and_then(Value::as_f64)
200
+ .unwrap_or(0.0);
201
+ scroll(x, y, dx, dy)?;
202
+ ok("unknown", grounding, "hid")
203
+ }
204
+ "typeText" => {
205
+ send_text(
206
+ request
207
+ .params
208
+ .get("text")
209
+ .and_then(Value::as_str)
210
+ .unwrap_or(""),
211
+ )?;
212
+ ok("unknown", "coordinates", "hid")
213
+ }
214
+ "setText" => {
215
+ if let Some((x, y)) = point {
216
+ click(x, y, "left")?;
217
+ }
218
+ hotkey(&[VK_CONTROL], VK_A)?;
219
+ let text = request
220
+ .params
221
+ .get("text")
222
+ .and_then(Value::as_str)
223
+ .unwrap_or("");
224
+ send_text(text)?;
225
+ Ok(
226
+ json!({ "outcome": "unknown", "performed": { "grounding": grounding, "delivery": "hid" }, "evidence": { "value": text } }),
227
+ )
228
+ }
229
+ "keypress" => {
230
+ let keys = request
231
+ .params
232
+ .get("keys")
233
+ .and_then(Value::as_array)
234
+ .ok_or_else(|| invalid("keypress requires keys"))?;
235
+ send_keys(keys)?;
236
+ ok("unknown", "coordinates", "hid")
237
+ }
238
+ other => Err(ProtocolError::new(
239
+ format!("Unsupported Windows action '{other}'"),
240
+ ErrorCode::UnsupportedCommand,
241
+ )),
242
+ }
243
+ }
244
+
245
+ fn ok(outcome: &str, grounding: &str, delivery: &str) -> Result<Value, ProtocolError> {
246
+ Ok(
247
+ json!({ "outcome": outcome, "performed": { "grounding": grounding, "delivery": delivery } }),
248
+ )
249
+ }
250
+
251
+ fn input_failed(error: impl std::fmt::Display) -> ProtocolError {
252
+ ProtocolError::new(format!("Input failed: {error}"), ErrorCode::CaptureFailed)
253
+ }
254
+
255
+ fn point_value(value: &Value) -> Option<(i32, i32)> {
256
+ Some((
257
+ value.get("x")?.as_f64()?.round() as i32,
258
+ value.get("y")?.as_f64()?.round() as i32,
259
+ ))
260
+ }
261
+
262
+ fn point_value_required(value: &Value) -> Result<(i32, i32), ProtocolError> {
263
+ point_value(value).ok_or_else(|| invalid("point requires x and y"))
264
+ }
265
+
266
+ fn send(inputs: &[INPUT]) -> Result<(), ProtocolError> {
267
+ let sent = unsafe { SendInput(inputs, std::mem::size_of::<INPUT>() as i32) };
268
+ if sent == inputs.len() as u32 {
269
+ Ok(())
270
+ } else {
271
+ Err(input_failed(format!(
272
+ "SendInput inserted {sent}/{} events",
273
+ inputs.len()
274
+ )))
275
+ }
276
+ }
277
+
278
+ fn mouse(flags: MOUSE_EVENT_FLAGS, data: u32) -> INPUT {
279
+ INPUT {
280
+ r#type: INPUT_MOUSE,
281
+ Anonymous: INPUT_0 {
282
+ mi: MOUSEINPUT {
283
+ dwFlags: flags,
284
+ mouseData: data,
285
+ ..Default::default()
286
+ },
287
+ },
288
+ }
289
+ }
290
+
291
+ fn key(vk: VIRTUAL_KEY, up: bool) -> INPUT {
292
+ INPUT {
293
+ r#type: INPUT_KEYBOARD,
294
+ Anonymous: INPUT_0 {
295
+ ki: KEYBDINPUT {
296
+ wVk: vk,
297
+ dwFlags: if up {
298
+ KEYEVENTF_KEYUP
299
+ } else {
300
+ KEYBD_EVENT_FLAGS(0)
301
+ },
302
+ ..Default::default()
303
+ },
304
+ },
305
+ }
306
+ }
307
+
308
+ fn unicode(ch: u16, up: bool) -> INPUT {
309
+ INPUT {
310
+ r#type: INPUT_KEYBOARD,
311
+ Anonymous: INPUT_0 {
312
+ ki: KEYBDINPUT {
313
+ wScan: ch,
314
+ dwFlags: KEYEVENTF_UNICODE
315
+ | if up {
316
+ KEYEVENTF_KEYUP
317
+ } else {
318
+ KEYBD_EVENT_FLAGS(0)
319
+ },
320
+ ..Default::default()
321
+ },
322
+ },
323
+ }
324
+ }
325
+
326
+ fn click(x: i32, y: i32, button: &str) -> Result<(), ProtocolError> {
327
+ unsafe { SetCursorPos(x, y) }.map_err(input_failed)?;
328
+ let (down, up) = match button {
329
+ "right" => (MOUSEEVENTF_RIGHTDOWN, MOUSEEVENTF_RIGHTUP),
330
+ "middle" => (MOUSEEVENTF_MIDDLEDOWN, MOUSEEVENTF_MIDDLEUP),
331
+ _ => (MOUSEEVENTF_LEFTDOWN, MOUSEEVENTF_LEFTUP),
332
+ };
333
+ send(&[mouse(down, 0), mouse(up, 0)])
334
+ }
335
+
336
+ fn drag(points: &[(i32, i32)]) -> Result<(), ProtocolError> {
337
+ if points.len() < 2 {
338
+ return Err(invalid("drag requires at least two points"));
339
+ }
340
+ unsafe { SetCursorPos(points[0].0, points[0].1) }.map_err(input_failed)?;
341
+ send(&[mouse(MOUSEEVENTF_LEFTDOWN, 0)])?;
342
+ for &(x, y) in &points[1..] {
343
+ unsafe { SetCursorPos(x, y) }.map_err(input_failed)?;
344
+ }
345
+ send(&[mouse(MOUSEEVENTF_LEFTUP, 0)])
346
+ }
347
+
348
+ fn scroll(x: i32, y: i32, dx: f64, dy: f64) -> Result<(), ProtocolError> {
349
+ unsafe { SetCursorPos(x, y) }.map_err(input_failed)?;
350
+ let mut inputs = Vec::new();
351
+ if dy != 0.0 {
352
+ inputs.push(mouse(
353
+ MOUSEEVENTF_WHEEL,
354
+ (-dy * 120.0).round() as i32 as u32,
355
+ ));
356
+ }
357
+ if dx != 0.0 {
358
+ inputs.push(mouse(
359
+ MOUSEEVENTF_HWHEEL,
360
+ (dx * 120.0).round() as i32 as u32,
361
+ ));
362
+ }
363
+ if inputs.is_empty() {
364
+ return Ok(());
365
+ }
366
+ send(&inputs)
367
+ }
368
+
369
+ fn send_text(text: &str) -> Result<(), ProtocolError> {
370
+ let mut inputs = Vec::new();
371
+ for unit in text.encode_utf16() {
372
+ inputs.push(unicode(unit, false));
373
+ inputs.push(unicode(unit, true));
374
+ }
375
+ send(&inputs)
376
+ }
377
+
378
+ fn send_keys(keys: &[Value]) -> Result<(), ProtocolError> {
379
+ let names = keys.iter().filter_map(Value::as_str).collect::<Vec<_>>();
380
+ if names.is_empty() {
381
+ return Err(invalid("keypress requires at least one key"));
382
+ }
383
+ let vks = names
384
+ .iter()
385
+ .map(|name| vk_for(name).ok_or_else(|| invalid(format!("Unsupported key '{name}'"))))
386
+ .collect::<Result<Vec<_>, _>>()?;
387
+ for vk in &vks[..vks.len().saturating_sub(1)] {
388
+ send(&[key(*vk, false)])?;
389
+ }
390
+ if let Some(last) = vks.last() {
391
+ send(&[key(*last, false), key(*last, true)])?;
392
+ }
393
+ for vk in vks[..vks.len().saturating_sub(1)].iter().rev() {
394
+ send(&[key(*vk, true)])?;
395
+ }
396
+ Ok(())
397
+ }
398
+
399
+ fn hotkey(mods: &[VIRTUAL_KEY], key_vk: VIRTUAL_KEY) -> Result<(), ProtocolError> {
400
+ for vk in mods {
401
+ send(&[key(*vk, false)])?;
402
+ }
403
+ send(&[key(key_vk, false), key(key_vk, true)])?;
404
+ for vk in mods.iter().rev() {
405
+ send(&[key(*vk, true)])?;
406
+ }
407
+ Ok(())
408
+ }
409
+
410
+ fn vk_for(name: &str) -> Option<VIRTUAL_KEY> {
411
+ match name.to_ascii_lowercase().as_str() {
412
+ "enter" | "return" => Some(VK_RETURN),
413
+ "escape" | "esc" => Some(VK_ESCAPE),
414
+ "tab" => Some(VK_TAB),
415
+ "backspace" => Some(VK_BACK),
416
+ "delete" => Some(VK_DELETE),
417
+ "space" => Some(VK_SPACE),
418
+ "left" | "arrowleft" => Some(VK_LEFT),
419
+ "right" | "arrowright" => Some(VK_RIGHT),
420
+ "up" | "arrowup" => Some(VK_UP),
421
+ "down" | "arrowdown" => Some(VK_DOWN),
422
+ "home" => Some(VK_HOME),
423
+ "end" => Some(VK_END),
424
+ "pageup" => Some(VK_PRIOR),
425
+ "pagedown" => Some(VK_NEXT),
426
+ "ctrl" | "control" => Some(VK_CONTROL),
427
+ "shift" => Some(VK_SHIFT),
428
+ "alt" | "option" => Some(VK_MENU),
429
+ "cmd" | "win" | "meta" => Some(VK_LWIN),
430
+ key if key.len() == 1 => {
431
+ Some(VIRTUAL_KEY(key.as_bytes()[0].to_ascii_uppercase() as u16))
432
+ }
433
+ key if key.starts_with('f') => key[1..]
434
+ .parse::<u16>()
435
+ .ok()
436
+ .filter(|n| (1..=24).contains(n))
437
+ .map(|n| VIRTUAL_KEY(VK_F1.0 + n - 1)),
438
+ _ => None,
439
+ }
440
+ }
441
+ }
442
+
443
+ #[cfg(test)]
444
+ mod tests {
445
+ use super::*;
446
+
447
+ #[test]
448
+ fn parses_discriminated_click_request() {
449
+ let parsed = parse_act_request(&json!({"lookId":"look_1","action":"click","policy":"default","target":{"ref":"@e1"},"params":{"button":"left"}})).unwrap();
450
+ assert_eq!(parsed.action, "click");
451
+ assert_eq!(parsed.target, ActTarget::Ref("@e1".to_owned()));
452
+ }
453
+
454
+ #[test]
455
+ fn non_foreground_policies_block_raw_input() {
456
+ let parsed = parse_act_request(&json!({"lookId":"look_1","action":"typeText","policy":"ax_only","target":{"ref":"@e1"},"params":{"text":"x"}})).unwrap();
457
+ let err = policy_allows_raw_input(&parsed).unwrap_err();
458
+ assert_eq!(err.code, ErrorCode::CoordinateBlocked);
459
+ let background = parse_act_request(&json!({"lookId":"look_1","action":"click","policy":"background","target":{"x":1,"y":1},"params":{}})).unwrap();
460
+ assert_eq!(
461
+ policy_allows_raw_input(&background).unwrap_err().code,
462
+ ErrorCode::ForegroundRequired
463
+ );
464
+ let default_policy = parse_act_request(&json!({"lookId":"look_1","action":"click","policy":"default","target":{"x":1,"y":1},"params":{}})).unwrap();
465
+ assert_eq!(
466
+ policy_allows_raw_input(&default_policy).unwrap_err().code,
467
+ ErrorCode::ForegroundRequired
468
+ );
469
+ let foreground = parse_act_request(&json!({"lookId":"look_1","action":"click","policy":"foreground","target":{"x":1,"y":1},"params":{}})).unwrap();
470
+ assert!(policy_allows_raw_input(&foreground).is_ok());
471
+ }
472
+
473
+ #[test]
474
+ fn attaches_delta_source_and_delta() {
475
+ let out = response_with_delta(
476
+ json!({"outcome":"worked","performed":{}}),
477
+ "snapshot",
478
+ vec![json!({"change":"focused","kind":"window","pid":1})],
479
+ );
480
+ assert_eq!(out["performed"]["deltaSource"], "snapshot");
481
+ assert_eq!(out["rootDelta"].as_array().unwrap().len(), 1);
482
+ }
483
+
484
+ #[test]
485
+ fn raw_coordinate_actions_do_not_claim_worked() {
486
+ let actions = ["click", "moveMouse", "scroll", "typeText", "keypress"];
487
+ for action in actions {
488
+ let params = match action {
489
+ "typeText" => json!({"text":"abc"}),
490
+ "keypress" => json!({"keys":["enter"]}),
491
+ "scroll" => json!({"scrollY":1,"scrollX":0}),
492
+ _ => json!({}),
493
+ };
494
+ let result = act(&json!({"lookId":"look_1","action":action,"policy":"foreground","target":{"x":1,"y":1},"params":params,"resolvedPoint":{"x":1,"y":1}})).unwrap();
495
+ assert_ne!(
496
+ result["outcome"], "worked",
497
+ "{action} must not report worked without verification"
498
+ );
499
+ }
500
+ }
501
+ }
@@ -0,0 +1,16 @@
1
+ //! Protocol types for the tangu-computer-use Windows helper.
2
+ //!
3
+ //! This crate provides the JSON-lines protocol types used to communicate
4
+ //! between the TypeScript host and the Rust helper binary.
5
+
6
+ pub mod capture;
7
+ pub mod error;
8
+ pub mod input;
9
+ pub mod protocol;
10
+ pub mod refs;
11
+ pub mod state;
12
+ pub mod uia;
13
+ pub mod window;
14
+
15
+ pub use error::{ErrorCode, ProtocolError};
16
+ pub use protocol::{Request, Response};