@ycheung5/pi-computer-use 0.0.0-stage → 0.1.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.
Files changed (92) hide show
  1. package/GLOSSARY.md +38 -0
  2. package/LICENSE +21 -0
  3. package/README.md +165 -2
  4. package/assets/accessibility.cs +208 -0
  5. package/assets/browser-indicators.cs +37 -0
  6. package/assets/capture.cs +215 -0
  7. package/assets/clipboard.cs +197 -0
  8. package/assets/controls.cs +249 -0
  9. package/assets/indicator-preview.png +0 -0
  10. package/assets/indicators.cs +252 -0
  11. package/assets/node-LICENSE.txt +2732 -0
  12. package/assets/ocr.cs +86 -0
  13. package/assets/ownership.cs +59 -0
  14. package/assets/pointer.cs +229 -0
  15. package/assets/puppeteer-LICENSE.txt +202 -0
  16. package/assets/readme-banner.svg +33 -0
  17. package/assets/stop.cs +197 -0
  18. package/assets/text.cs +249 -0
  19. package/assets/wgc.cs +167 -0
  20. package/assets/window-management.cs +217 -0
  21. package/assets/window-marker.cs +23 -0
  22. package/assets/window-references.cs +80 -0
  23. package/assets/windows.cs +127 -0
  24. package/dist/accessibility.d.ts +29 -0
  25. package/dist/accessibility.js +258 -0
  26. package/dist/browser-actions.d.ts +21 -0
  27. package/dist/browser-actions.js +213 -0
  28. package/dist/browser-indicators.d.ts +23 -0
  29. package/dist/browser-indicators.js +69 -0
  30. package/dist/browser-profiles.d.ts +10 -0
  31. package/dist/browser-profiles.js +174 -0
  32. package/dist/browser.d.ts +11 -0
  33. package/dist/browser.js +2071 -0
  34. package/dist/capture.d.ts +74 -0
  35. package/dist/capture.js +199 -0
  36. package/dist/client.d.ts +38 -0
  37. package/dist/client.js +331 -0
  38. package/dist/desktop.d.ts +20 -0
  39. package/dist/desktop.js +42 -0
  40. package/dist/extension.d.ts +4 -0
  41. package/dist/extension.js +409 -0
  42. package/dist/launcher.d.ts +6 -0
  43. package/dist/launcher.js +85 -0
  44. package/dist/native.d.ts +11 -0
  45. package/dist/native.js +145 -0
  46. package/dist/ocr.d.ts +52 -0
  47. package/dist/ocr.js +246 -0
  48. package/dist/ownership.d.ts +4 -0
  49. package/dist/ownership.js +24 -0
  50. package/dist/pointer.d.ts +108 -0
  51. package/dist/pointer.js +174 -0
  52. package/dist/protocol.d.ts +15 -0
  53. package/dist/protocol.js +1 -0
  54. package/dist/runtime.d.ts +8 -0
  55. package/dist/runtime.js +72 -0
  56. package/dist/settings.d.ts +85 -0
  57. package/dist/settings.js +449 -0
  58. package/dist/stop.d.ts +24 -0
  59. package/dist/stop.js +156 -0
  60. package/dist/text.d.ts +100 -0
  61. package/dist/text.js +183 -0
  62. package/dist/waits.d.ts +19 -0
  63. package/dist/waits.js +262 -0
  64. package/dist/window-management.d.ts +25 -0
  65. package/dist/window-management.js +167 -0
  66. package/dist/worker.d.ts +1 -0
  67. package/dist/worker.js +540 -0
  68. package/docs/README.md +54 -0
  69. package/docs/adr/0001-preserve-reference-behavior.md +9 -0
  70. package/docs/adr/0002-window-lifetime-references.md +11 -0
  71. package/docs/adr/0003-exclusive-desktop-control.md +11 -0
  72. package/docs/adr/0004-pi-first-acceptance.md +9 -0
  73. package/docs/adr/0005-default-apps-and-workspace-reuse.md +7 -0
  74. package/docs/agents/domain.md +9 -0
  75. package/docs/agents/issue-tracker.md +10 -0
  76. package/docs/agents/triage-labels.md +9 -0
  77. package/docs/browser.md +113 -0
  78. package/docs/implementation-plan.md +139 -0
  79. package/docs/indicators.md +53 -0
  80. package/docs/native-interop-research.md +17 -0
  81. package/docs/ownership.md +25 -0
  82. package/docs/pi-extension-research.md +57 -0
  83. package/docs/pointer.md +65 -0
  84. package/docs/runtime.md +42 -0
  85. package/docs/settings.md +33 -0
  86. package/docs/spec.md +161 -0
  87. package/docs/stopping.md +83 -0
  88. package/docs/text.md +79 -0
  89. package/docs/tickets.md +84 -0
  90. package/docs/validation.md +1372 -0
  91. package/docs/windows.md +171 -0
  92. package/package.json +134 -4
package/dist/worker.js ADDED
@@ -0,0 +1,540 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { createInterface } from "node:readline";
3
+ import { controlTools, executeAccessibility, observationTools, } from "./accessibility.js";
4
+ import { browse, browserListing, clearBrowserIndicators, configureBrowser, disposeBrowser, isBrowserRequest, } from "./browser.js";
5
+ import { browserActionTools } from "./browser-actions.js";
6
+ import { screenshot, screenshotParameters } from "./capture.js";
7
+ import { disposeDesktop, listWindows, listWindowsParameters, retireDesktop, } from "./desktop.js";
8
+ import { runNativeOperation } from "./native.js";
9
+ import { disposeOcr, ocrDesktop, ocrParameters } from "./ocr.js";
10
+ import { acquireDesktop, disposeOwnership, releaseDesktop, } from "./ownership.js";
11
+ import { awaitPointerCleanup, executePointer, pointerTools, validatePointer, } from "./pointer.js";
12
+ import { PersonalSettingsManager } from "./settings.js";
13
+ import { StopListener } from "./stop.js";
14
+ import { executeText, prepareTextInput, textTools, validateText, } from "./text.js";
15
+ import { waitDesktop, waitTools } from "./waits.js";
16
+ import { manageWindow, windowTools } from "./window-management.js";
17
+ const settings = new PersonalSettingsManager(new Map([
18
+ [
19
+ "health",
20
+ {
21
+ description: "Check automation worker health",
22
+ parameters: {
23
+ type: "object",
24
+ properties: {},
25
+ additionalProperties: false,
26
+ },
27
+ },
28
+ ],
29
+ [
30
+ "list_windows",
31
+ {
32
+ description: "Discover native windows and browser tabs",
33
+ parameters: listWindowsParameters,
34
+ },
35
+ ],
36
+ [
37
+ "screenshot",
38
+ {
39
+ description: "Capture desktop, native window or browser page",
40
+ parameters: screenshotParameters,
41
+ },
42
+ ],
43
+ [
44
+ "ocr",
45
+ {
46
+ description: "Read literal screen text and coordinates",
47
+ parameters: ocrParameters,
48
+ },
49
+ ],
50
+ ...[
51
+ ...observationTools,
52
+ ...controlTools,
53
+ ...pointerTools,
54
+ ...textTools,
55
+ ...waitTools,
56
+ ...windowTools,
57
+ ...browserActionTools,
58
+ ].map((tool) => [
59
+ tool.action,
60
+ {
61
+ description: `Windows computer use ${tool.action}`,
62
+ parameters: tool.parameters,
63
+ },
64
+ ]),
65
+ ]));
66
+ const input = createInterface({ input: process.stdin, crlfDelay: Infinity });
67
+ let queue = Promise.resolve();
68
+ const requests = new Map();
69
+ const blocked = new Set();
70
+ const lastTargets = new Map();
71
+ function rememberTarget(operationId, result) {
72
+ const activated = result.action === "activate_window" && result.activated === true;
73
+ const handle = activated
74
+ ? result.nativeWindowHandle
75
+ : result.inputTargetHandle;
76
+ const owner = activated ? result.processId : result.inputTargetProcessId;
77
+ if (result.ok && Number.isSafeInteger(handle) && Number.isSafeInteger(owner))
78
+ lastTargets.set(operationId, {
79
+ nativeWindowHandle: Number(handle),
80
+ processId: Number(owner),
81
+ ...(typeof result.windowRef === "string"
82
+ ? { windowRef: result.windowRef }
83
+ : {}),
84
+ });
85
+ }
86
+ let stopping = Promise.resolve();
87
+ const stopListener = new StopListener((operationId) => {
88
+ void stopOperation(operationId).catch((error) => process.stderr.write(`${String(error)}\n`));
89
+ });
90
+ let presentationUpdating = false;
91
+ let presentationError;
92
+ const presentationPoll = setInterval(() => {
93
+ if (!stopListener.hasStarted || presentationUpdating)
94
+ return;
95
+ presentationUpdating = true;
96
+ void settings
97
+ .synchronize()
98
+ .then(async ({ settings: current }) => {
99
+ await stopListener.configure(current);
100
+ presentationError = undefined;
101
+ })
102
+ .catch((error) => {
103
+ const operation = stopListener.currentOperation;
104
+ if (operation)
105
+ void stopOperation(operation).catch(() => undefined);
106
+ const message = String(error);
107
+ if (message !== presentationError)
108
+ process.stderr.write(`PRESENTATION_SETTINGS_FAILED: ${message}\n`);
109
+ presentationError = message;
110
+ })
111
+ .finally(() => {
112
+ presentationUpdating = false;
113
+ });
114
+ }, 250);
115
+ presentationPoll.unref();
116
+ async function stopOperation(operationId) {
117
+ if (blocked.has(operationId))
118
+ return stopping;
119
+ blocked.add(operationId);
120
+ process.stdout.write(`${JSON.stringify({ event: "stopped", operationId })}\n`);
121
+ stopping = stopping.then(async () => {
122
+ try {
123
+ await stopListener.signal(operationId);
124
+ }
125
+ finally {
126
+ for (const request of requests.values())
127
+ if (request.operationId === operationId &&
128
+ request.action !== "operation_end")
129
+ request.controller.abort(new Error("COMPUTER_USE_STOPPED"));
130
+ await awaitPointerCleanup();
131
+ await clearBrowserIndicators();
132
+ await retireDesktop();
133
+ }
134
+ });
135
+ return stopping;
136
+ }
137
+ function isObject(value) {
138
+ return value !== null && typeof value === "object" && !Array.isArray(value);
139
+ }
140
+ const inputPreparations = new Map();
141
+ async function acknowledgeInputPreparation(requestId, operationId, signal) {
142
+ signal.throwIfAborted();
143
+ const token = randomUUID();
144
+ await new Promise((resolve, reject) => {
145
+ const cleanup = () => {
146
+ inputPreparations.delete(requestId);
147
+ signal.removeEventListener("abort", abort);
148
+ input.removeListener("close", abort);
149
+ };
150
+ const abort = () => {
151
+ cleanup();
152
+ process.stdout.write(`${JSON.stringify({ event: "input_state", active: false })}\n`);
153
+ reject(signal.reason ?? new Error("COMPUTER_USE_STOPPED"));
154
+ };
155
+ inputPreparations.set(requestId, {
156
+ token,
157
+ accept: () => {
158
+ cleanup();
159
+ resolve();
160
+ },
161
+ });
162
+ signal.addEventListener("abort", abort, { once: true });
163
+ input.once("close", abort);
164
+ process.stdout.write(`${JSON.stringify({ event: "input_prepare", requestId, operationId, token })}\n`);
165
+ if (signal.aborted)
166
+ abort();
167
+ });
168
+ }
169
+ async function guardedInput(action, args, operationId, signal, requestId, quietScroll = false) {
170
+ if (quietScroll)
171
+ return executeAccessibility(action, args, signal, (normalized) => guardedInput(action, { ...normalized, _quietScroll: true }, operationId, signal, requestId));
172
+ const armed = await stopListener.arm(operationId, signal);
173
+ if (!armed.ok || typeof armed.cancelEvent !== "string")
174
+ return { ...armed, action };
175
+ const prepared = ["type_text", "keypress"].includes(action)
176
+ ? await prepareTextInput(action, args, signal)
177
+ : args;
178
+ await acknowledgeInputPreparation(requestId, operationId, signal);
179
+ try {
180
+ signal.throwIfAborted();
181
+ if (prepared._quietScroll !== true) {
182
+ const control = await stopListener.acquire(operationId, signal);
183
+ if (!control.ok) {
184
+ process.stdout.write(`${JSON.stringify({ event: "input_state", active: false })}\n`);
185
+ return { ...control, action, cleanupConfirmed: true };
186
+ }
187
+ signal.throwIfAborted();
188
+ }
189
+ }
190
+ catch (error) {
191
+ process.stdout.write(`${JSON.stringify({ event: "input_state", active: false })}\n`);
192
+ throw error;
193
+ }
194
+ let result;
195
+ try {
196
+ result = await executePointer(action, prepared, armed.cancelEvent, operationId, Number(armed.listenerProcessId), signal);
197
+ }
198
+ catch (error) {
199
+ void stopOperation(operationId).catch(() => undefined);
200
+ throw error;
201
+ }
202
+ // Failure leaves the input lease pending; only the native cleanup ACK permits
203
+ // this notification, worker retirement and eventual desktop ownership release.
204
+ process.stdout.write(`${JSON.stringify({ event: "input_state", active: false })}\n`);
205
+ if (!result.ok &&
206
+ /^(COMPUTER_USE_STOPPED|EXCLUSIVE_CONTROL_LOST|STOP_LISTENER_UNAVAILABLE)/.test(String(result.error)))
207
+ await stopOperation(operationId);
208
+ return result;
209
+ }
210
+ async function dispatch(request, signal) {
211
+ signal.throwIfAborted();
212
+ const operationId = request.operationId ?? `worker-${process.pid}`;
213
+ if (request.action === "operation_end") {
214
+ await stopping;
215
+ await awaitPointerCleanup();
216
+ await stopListener.disarm(operationId);
217
+ await clearBrowserIndicators();
218
+ const result = await releaseDesktop(operationId);
219
+ if (result.ok)
220
+ lastTargets.delete(operationId);
221
+ process.stdout.write(`${JSON.stringify({ id: request.id, result })}\n`);
222
+ return;
223
+ }
224
+ if (request.action === "indicator_preview") {
225
+ if (Object.keys(request.args).some((key) => key !== "show") ||
226
+ (request.args.show !== undefined &&
227
+ typeof request.args.show !== "boolean")) {
228
+ process.stdout.write(`${JSON.stringify({ id: request.id, result: { ok: false, action: request.action, error: "INVALID_ARGUMENTS: preview accepts boolean show only" } })}\n`);
229
+ return;
230
+ }
231
+ const current = await settings.synchronize();
232
+ const result = await stopListener.preview(current.settings, request.args.show !== false);
233
+ process.stdout.write(`${JSON.stringify({ id: request.id, result: { ...result, action: request.action } })}\n`);
234
+ return;
235
+ }
236
+ if (["settings_get", "settings_update", "tools"].includes(request.action)) {
237
+ await stopListener.configure((await settings.synchronize()).settings);
238
+ const result = await settings.handle(request.action, request.args);
239
+ if (result.ok && settings.currentSettings)
240
+ await stopListener.configure(settings.currentSettings);
241
+ process.stdout.write(`${JSON.stringify({ id: request.id, result })}\n`);
242
+ return;
243
+ }
244
+ if (request.args.windowRef !== undefined &&
245
+ (typeof request.args.windowRef !== "string" ||
246
+ !request.args.windowRef.trim() ||
247
+ isBrowserRequest(request.args) ||
248
+ request.args.scope === "desktop")) {
249
+ process.stdout.write(`${JSON.stringify({ id: request.id, result: { ok: false, action: request.action, error: "INVALID_ARGUMENTS: windowRef requires a nonempty native-window reference and cannot target desktop/browser channel" } })}\n`);
250
+ return;
251
+ }
252
+ const denial = await settings.authorize(request.action);
253
+ if (denial) {
254
+ process.stdout.write(`${JSON.stringify({ id: request.id, result: { ok: false, action: request.action, error: denial } })}\n`);
255
+ return;
256
+ }
257
+ const configuration = settings.currentSettings;
258
+ if (configuration)
259
+ await stopListener.configure(configuration);
260
+ if (configuration &&
261
+ process.platform === "win32" &&
262
+ settings.currentFileExists)
263
+ await configureBrowser({
264
+ browserApp: configuration.browserApp,
265
+ browserVisible: configuration.browserVisible,
266
+ browserSignIn: configuration.browserSignIn,
267
+ });
268
+ const suspendCapture = !isBrowserRequest(request.args) &&
269
+ ["screenshot", "snapshot", "ocr", "find", "wait_for"].includes(request.action);
270
+ if (suspendCapture)
271
+ await stopListener.suspendCapture(true);
272
+ try {
273
+ if (request.action !== "health" && blocked.has(operationId)) {
274
+ process.stdout.write(`${JSON.stringify({ id: request.id, result: { ok: false, action: request.action, error: "COMPUTER_USE_STOPPED: wait for a new user operation" } })}\n`);
275
+ return;
276
+ }
277
+ if (request.action !== "health" && process.platform === "win32") {
278
+ const ownership = await acquireDesktop(operationId, signal);
279
+ if (!ownership.ok) {
280
+ process.stdout.write(`${JSON.stringify({ id: request.id, result: { ...ownership, action: request.action } })}\n`);
281
+ return;
282
+ }
283
+ }
284
+ const previous = lastTargets.get(operationId);
285
+ if (previous &&
286
+ !isBrowserRequest(request.args) &&
287
+ request.args.scope !== "desktop" &&
288
+ ![
289
+ "list_windows",
290
+ "health",
291
+ "operation_end",
292
+ "wait",
293
+ "launch_app",
294
+ "activate_window",
295
+ "move_window",
296
+ "close_window",
297
+ ].includes(request.action) &&
298
+ ![
299
+ "windowRef",
300
+ "windowTitle",
301
+ "processId",
302
+ "nativeWindowHandle",
303
+ "elementId",
304
+ ].some((key) => request.args[key] !== undefined))
305
+ request.args = { ...request.args, ...previous };
306
+ if (request.action === "prepare_input") {
307
+ const result = await stopListener.arm(operationId, signal);
308
+ process.stdout.write(`${JSON.stringify({ id: request.id, result })}\n`);
309
+ return;
310
+ }
311
+ if (["type_text", "keypress"].includes(request.action) &&
312
+ !isBrowserRequest(request.args)) {
313
+ const invalid = validateText(request.action, request.args);
314
+ if (invalid) {
315
+ process.stdout.write(`${JSON.stringify({ id: request.id, result: invalid })}\n`);
316
+ return;
317
+ }
318
+ let result;
319
+ if ((request.action === "type_text" &&
320
+ ["auto", "background"].includes(String(request.args.method ?? "auto"))) ||
321
+ request.args.dispatch === "background") {
322
+ result = await executeText(request.action, request.args, "", operationId, undefined, signal);
323
+ if (result.inputRequired !== true) {
324
+ rememberTarget(operationId, result);
325
+ process.stdout.write(`${JSON.stringify({ id: request.id, result })}\n`);
326
+ return;
327
+ }
328
+ }
329
+ result = await guardedInput(request.action, request.args, operationId, signal, request.id);
330
+ rememberTarget(operationId, result);
331
+ process.stdout.write(`${JSON.stringify({ id: request.id, result })}\n`);
332
+ return;
333
+ }
334
+ if (!isBrowserRequest(request.args) &&
335
+ (["click", "double_click", "move", "drag"].includes(request.action) ||
336
+ (request.action === "scroll" && !request.args.elementId))) {
337
+ const invalid = validatePointer(request.action, request.args);
338
+ if (invalid) {
339
+ process.stdout.write(`${JSON.stringify({ id: request.id, result: invalid })}\n`);
340
+ return;
341
+ }
342
+ // Only the explicit needs-input result permits escalation/retry. No timeout,
343
+ // stale/disabled/covered-point error can trigger replay.
344
+ if ((request.args.dispatch === "background" ||
345
+ (request.action === "click" &&
346
+ request.args.elementId &&
347
+ (request.args.button ?? "left") === "left" &&
348
+ !request.args.activate &&
349
+ request.args.dispatch !== "foreground")) &&
350
+ ["click", "double_click"].includes(request.action)) {
351
+ const quiet = await executePointer(request.action, request.args, "", operationId, undefined, signal);
352
+ if (!quiet.inputRequired || request.args.dispatch === "background") {
353
+ rememberTarget(operationId, quiet);
354
+ process.stdout.write(`${JSON.stringify({ id: request.id, result: quiet })}\n`);
355
+ return;
356
+ }
357
+ }
358
+ const result = await guardedInput(request.action, request.args, operationId, signal, request.id);
359
+ rememberTarget(operationId, result);
360
+ process.stdout.write(`${JSON.stringify({ id: request.id, result })}\n`);
361
+ return;
362
+ }
363
+ const response = {
364
+ id: request.id,
365
+ result: request.action === "health"
366
+ ? {
367
+ ok: true,
368
+ action: "health",
369
+ processId: process.pid,
370
+ platform: process.platform,
371
+ nodeVersion: process.version,
372
+ control: await stopListener.status(),
373
+ settings: {
374
+ preset: settings.currentSettings?.toolPreset,
375
+ notices: settings.currentNotices,
376
+ source: settings.currentSource,
377
+ },
378
+ }
379
+ : isBrowserRequest(request.args)
380
+ ? await browse(request.action, request.args, signal)
381
+ : windowTools.some((tool) => tool.action === request.action)
382
+ ? await manageWindow(request.action, request.args, signal)
383
+ : ["wait", "wait_for"].includes(request.action)
384
+ ? await waitDesktop(request.action, request.args, signal)
385
+ : request.action === "list_windows"
386
+ ? {
387
+ ...(await listWindows(request.args, signal)),
388
+ browserLane: await browserListing(),
389
+ }
390
+ : request.action === "ocr"
391
+ ? await ocrDesktop(request.args, signal)
392
+ : request.action === "screenshot"
393
+ ? await screenshot(request.args, signal)
394
+ : [
395
+ "snapshot",
396
+ "accessibility_tree",
397
+ "find",
398
+ "element_info",
399
+ "focus",
400
+ "invoke",
401
+ "set_value",
402
+ "scroll",
403
+ ].includes(request.action)
404
+ ? request.action === "scroll"
405
+ ? await guardedInput(request.action, request.args, operationId, signal, request.id, true)
406
+ : await executeAccessibility(request.action, request.args, signal)
407
+ : {
408
+ ok: false,
409
+ action: request.action,
410
+ error: "UNKNOWN_ACTION",
411
+ },
412
+ };
413
+ if (!isBrowserRequest(request.args) &&
414
+ response.result.inputRequired === true &&
415
+ ["invoke", "scroll"].includes(request.action)) {
416
+ const pointerAction = request.action === "invoke" ? "click" : "scroll";
417
+ const result = await guardedInput(pointerAction, { ...request.args, dispatch: "foreground" }, operationId, signal, request.id);
418
+ response.result = {
419
+ ...result,
420
+ action: request.action,
421
+ method: result.ok ? `fallback:${String(result.method)}` : result.method,
422
+ };
423
+ }
424
+ if (!isBrowserRequest(request.args) &&
425
+ request.action === "set_value" &&
426
+ response.result.inputRequired === true) {
427
+ const textArgs = {
428
+ text: request.args.value,
429
+ method: "clipboard",
430
+ focusMode: "preserve",
431
+ restoreClipboard: request.args.restoreClipboard ?? true,
432
+ _elementId: request.args.elementId,
433
+ _replaceAll: true,
434
+ };
435
+ for (const key of [
436
+ "windowRef",
437
+ "windowTitle",
438
+ "processId",
439
+ "nativeWindowHandle",
440
+ "activate",
441
+ ])
442
+ if (request.args[key] !== undefined)
443
+ textArgs[key] = request.args[key];
444
+ const result = await guardedInput("type_text", textArgs, operationId, signal, request.id);
445
+ response.result = {
446
+ ...result,
447
+ action: "set_value",
448
+ method: result.ok ? `fallback:${String(result.method)}` : result.method,
449
+ };
450
+ }
451
+ rememberTarget(operationId, response.result);
452
+ process.stdout.write(`${JSON.stringify(response)}\n`);
453
+ }
454
+ finally {
455
+ if (suspendCapture)
456
+ await stopListener.suspendCapture(false);
457
+ }
458
+ }
459
+ input.on("line", (line) => {
460
+ let parsed;
461
+ try {
462
+ parsed = JSON.parse(line);
463
+ }
464
+ catch {
465
+ process.stderr.write("WORKER_PROTOCOL_ERROR: invalid JSON\n");
466
+ return;
467
+ }
468
+ if (isObject(parsed) &&
469
+ typeof parsed.inputAck === "string" &&
470
+ Number.isSafeInteger(parsed.requestId)) {
471
+ const preparation = inputPreparations.get(Number(parsed.requestId));
472
+ if (preparation && preparation.token === parsed.inputAck)
473
+ preparation.accept();
474
+ return;
475
+ }
476
+ if (isObject(parsed) && typeof parsed.stop === "string") {
477
+ void stopOperation(parsed.stop).catch((error) => process.stderr.write(`${String(error)}\n`));
478
+ return;
479
+ }
480
+ if (isObject(parsed) && "cancel" in parsed) {
481
+ if (typeof parsed.cancel === "number") {
482
+ const request = requests.get(parsed.cancel);
483
+ if (request)
484
+ void stopOperation(request.operationId).catch((error) => process.stderr.write(`${String(error)}\n`));
485
+ }
486
+ return;
487
+ }
488
+ if (!isObject(parsed) || !Number.isSafeInteger(parsed.id)) {
489
+ process.stderr.write("WORKER_PROTOCOL_ERROR: request requires a numeric id\n");
490
+ return;
491
+ }
492
+ if (typeof parsed.action !== "string" ||
493
+ !isObject(parsed.args) ||
494
+ (parsed.operationId !== undefined &&
495
+ (typeof parsed.operationId !== "string" ||
496
+ parsed.operationId.length < 1 ||
497
+ parsed.operationId.length > 200))) {
498
+ process.stdout.write(`${JSON.stringify({ id: parsed.id, result: { ok: false, action: typeof parsed.action === "string" ? parsed.action : "invalid_request", error: "INVALID_REQUEST: action must be a string and args must be an object" } })}\n`);
499
+ return;
500
+ }
501
+ const request = parsed;
502
+ if (requests.has(request.id)) {
503
+ process.stderr.write("WORKER_PROTOCOL_ERROR: duplicate request id\n");
504
+ return;
505
+ }
506
+ const controller = new AbortController();
507
+ requests.set(request.id, {
508
+ controller,
509
+ operationId: request.operationId ?? `worker-${process.pid}`,
510
+ action: request.action,
511
+ });
512
+ queue = queue
513
+ .then(() => runNativeOperation(request.operationId ?? `worker-${process.pid}`, () => dispatch(request, controller.signal)))
514
+ .catch((error) => {
515
+ process.stdout.write(`${JSON.stringify({ id: request.id, result: { ok: false, action: request.action, error: controller.signal.aborted ? "COMPUTER_USE_STOPPED" : String(error) } })}\n`);
516
+ })
517
+ .finally(() => {
518
+ requests.delete(request.id);
519
+ });
520
+ });
521
+ input.on("close", () => {
522
+ clearInterval(presentationPoll);
523
+ process.stdout.write(`${JSON.stringify({ event: "shutdown_state", phase: "started" })}\n`);
524
+ void queue
525
+ .then(async () => {
526
+ await stopping;
527
+ await disposeBrowser();
528
+ await awaitPointerCleanup();
529
+ await disposeDesktop();
530
+ await disposeOcr();
531
+ await stopListener.dispose();
532
+ await disposeOwnership();
533
+ process.stdout.write(`${JSON.stringify({ event: "shutdown_state", phase: "complete" })}\n`);
534
+ })
535
+ .catch((error) => {
536
+ // Keep native handles and ownership alive if safe exit cannot be proved.
537
+ process.stderr.write(`CLEANUP_UNCONFIRMED: ${String(error)}\n`);
538
+ process.stdout.write(`${JSON.stringify({ event: "shutdown_state", phase: "failed", error: String(error) })}\n`);
539
+ });
540
+ });
package/docs/README.md ADDED
@@ -0,0 +1,54 @@
1
+ # Documentation index
2
+
3
+ Read the row matching the task; follow section links before reading whole documents.
4
+
5
+ | Task | Read |
6
+ | --- | --- |
7
+ | Requirements and scope | [Spec: solution](spec.md#solution), [user stories](spec.md#user-stories), [out of scope](spec.md#out-of-scope) |
8
+ | Implementation contracts | [Spec: implementation decisions](spec.md#implementation-decisions) |
9
+ | Delivery, design review and feasibility | [Design revision](implementation-plan.md#design-revision), [plan: detailed milestones](implementation-plan.md#detailed-milestones), [feasibility gates](implementation-plan.md#feasibility-gates) |
10
+ | Test design and release validation | [Spec: testing decisions](spec.md#testing-decisions), [plan: coverage matrix](implementation-plan.md#coverage-matrix) |
11
+ | Pi acceptance, phased testing and agent recovery | [Current phases](implementation-plan.md#detailed-milestones), [baseline audit](validation.md#baseline-identity-and-schema-evidence), [guidance inventory](validation.md#model-visible-guidance-inventory), [scenario catalog](validation.md#alignment-scenario-catalog) |
12
+ | Release checks, capability coverage and pending acceptance | [0.1.0 release preparation](validation.md#first-release-preparation), [current release audit](validation.md#final-release-audit), [capability coverage](validation.md#capability-coverage), [validation history](validation.md) |
13
+ | Pi interfaces and sources | [Pi extension research](pi-extension-research.md) (research context; current spec takes precedence over earlier proposals) |
14
+ | Domain terminology | [Glossary](../GLOSSARY.md) |
15
+ | Current Pi-first acceptance scope | [ADR 0004](adr/0004-pi-first-acceptance.md) |
16
+ | Windows default apps, existing windows and foreground | [Accepted policy](spec.md#default-apps-workspaces-and-foreground), [ADR 0005](adr/0005-default-apps-and-workspace-reuse.md), [delivery cases](implementation-plan.md#default-app-and-workspace-delivery), [current support](windows.md#default-apps-and-workspaces) |
17
+ | Historical reference-parity decision | [Superseded ADR 0001](adr/0001-preserve-reference-behavior.md) |
18
+ | Stronger native window identity design | [Lifetime plan](implementation-plan.md#window-lifetime-design), [ADR 0002](adr/0002-window-lifetime-references.md) |
19
+ | Publish or read issues | [Issue tracker](agents/issue-tracker.md), [triage labels](agents/triage-labels.md) |
20
+ | What works, remaining outcomes, next work and cleanup decisions | [Progress summary](tickets.md#current-progress), [delivery map](tickets.md#delivery-outcomes-and-ownership), [execution queue](tickets.md#execution-queue), [cleanup dispositions](tickets.md#cleanup-dispositions), [original obligations](tickets.md#original-issue-disposition) |
21
+ | Worker setup, protocol and test evidence | [Runtime and validation](runtime.md) |
22
+ | Exclusive control, human handback and emergency stopping | [Control contract](spec.md#exclusive-control), [delivery/test slices](implementation-plan.md#exclusive-control-delivery), [ADR 0003](adr/0003-exclusive-desktop-control.md), [current stopping implementation](stopping.md), [control acceptance](validation.md#control-interruption-acceptance) |
23
+ | Control indicators, preview and recording acceptance | [Indicators](indicators.md), [Snipping Tool receipt](validation.md#snipping-tool-recording-acceptance) |
24
+ | Desktop ownership and operation lifecycle | [Ownership](ownership.md) |
25
+ | Native window discovery and validation | [Windows discovery](windows.md), [accessibility observations](windows.md#accessibility-observations), [targeting acceptance reconciliation](validation.md#pi-targeting-and-accessibility-reconciliation), [OCR failures](validation.md#pi-ocr-failure-acceptance), [locked sessions](windows.md#locked-session-refusals) |
26
+ | Scoped browser/mixed agent acceptance | [Held-navigation cancellation](validation.md#scoped-held-navigation-cancellation), [Partial-observation recovery](validation.md#scoped-partial-observation-recovery), [mixed outcome evidence](validation.md#scoped-mixed-browsernative-outcome), [runner prerequisites](validation.md#reproduce-automated-checks), [ordinary workspace actor outcomes](validation.md#ordinary-pi-workspace-actor-preparation) |
27
+ | Scoped native document and visual agent acceptance | [Document workflow](validation.md#native-document-workflow), [handback continuation](validation.md#native-cancellation-and-handback-continuation), [opt-in visual runner](windows.md#scoped-visual-agent-check), [A04 evidence](validation.md#visual-ocr-association-preflight) |
28
+ | Lifetime-bound native targeting and stale-reference behavior | [Window references](windows.md#window-lifetime-references), [accepted design](implementation-plan.md#window-lifetime-design) |
29
+ | Native interop alternatives and tradeoffs | [Native interop research](native-interop-research.md) |
30
+ | Guarded desktop pointer and dedicated input validation | [Pointer actions](pointer.md), [popup/menu/cross-process drop and restoration acceptance](validation.md#scoped-pointer-and-restoration-acceptance) |
31
+ | Text, chords, clipboard preservation and validation | [Text entry](text.md), [chord/editor-focus acceptance](validation.md#pi-chords-and-editor-focus-acceptance), [clipboard acceptance](validation.md#pi-clipboard-contention-and-privacy-acceptance) |
32
+ | Personal settings, commands and tool loading | [Settings](settings.md) |
33
+ | Browser reads/actions, dedicated profiles, idle cleanup and fixtures | [Browser](browser.md) |
34
+ | Domain-document consumer rules | [Domain docs](agents/domain.md) |
35
+
36
+ ## Repository layout
37
+
38
+ ```text
39
+ README.md project entry point
40
+ AGENTS.md short contributor rules and routing
41
+ GLOSSARY.md domain terms
42
+ src/ flat TypeScript modules
43
+ tests/ tests and native smoke fixtures
44
+ assets/ packaged resources and runtime notices
45
+ docs/
46
+ README.md this index
47
+ spec.md authoritative requirements
48
+ implementation-plan.md delivery, feasibility and coverage
49
+ pi-extension-research.md cited Pi interface research
50
+ adr/ durable decision rationale
51
+ agents/ issue tracker, triage and domain configuration
52
+ ```
53
+
54
+ Use the existing source, test and asset directories. Keep responsibility-named modules such as extension, launcher, worker, desktop, browser, settings and stop; split by demonstrated complexity rather than pre-creating a directory per module.
@@ -0,0 +1,9 @@
1
+ ---
2
+ status: superseded by ADR-0004
3
+ ---
4
+
5
+ # Preserve reference behavior while implementing for Pi
6
+
7
+ Historical decision. [ADR 0004](0004-pi-first-acceptance.md) replaces mandatory DSH parity with Pi-first acceptance; the Pi capability and safety requirements remain in the current specification.
8
+
9
+ Implement fresh TypeScript code for native Pi tools and Windows/WSL2 hosts, using dsh-computer-use-win 0.5.3 as the behavioral baseline. Preserve its safeguards, browser post-action observations and experimental installed-Firefox compatibility rather than changing behavior to fit a simpler browser library; simplification remains welcome where these contracts are retained. [ADR 0003](0003-exclusive-desktop-control.md) supersedes shared physical-user-input behavior for exclusive-control periods; other reference contracts remain the baseline. This trades some implementation freedom for a faithful Pi version and supersedes the proposed managed-Firefox and observation changes.
@@ -0,0 +1,11 @@
1
+ ---
2
+ status: accepted
3
+ ---
4
+
5
+ # Bind observed targets to a window lifetime
6
+
7
+ The user chose stronger window-lifetime tracking beyond DSH's HWND/PID checks; this records the reviewed implementation design, not delivered feature acceptance. Keep the reference authority beside the existing native UIA cache, using a unique private window property as a lifetime witness; expose one opaque `windowRef` target field and retain raw selectors as explicit compatibility selection. This avoids a second registry and asynchronous event-delivery assumptions, at the cost of window-metadata writes, UIPI restrictions, conservative invalidation after native-child restart and possible scalar-marker residue after abrupt native-child exit.
8
+
9
+ The owned-window probe confirmed marker isolation, removal and replacement behavior; it did not achieve identical-HWND reuse. [UIA RuntimeIds can be reused](https://learn.microsoft.com/en-us/windows/win32/api/uiautomationclient/nf-uiautomationclient-iuiautomationelement-getruntimeid), [WinEvent delivery can be asynchronous](https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-setwineventhook), and [SetProp requires ownership hygiene and respects UIPI](https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-setpropw). Validation cannot atomically lock a foreign window through a later action; the contract rejects already-lost proof and prevents subsequent steps rather than promising to undo racing effects. Implementation and acceptance follow [the #28 plan](../implementation-plan.md#window-lifetime-design).
10
+
11
+ Unavailable binding leaves read metadata available without actionable native reference IDs. Existing desktop-image snapshots retain separate desktop provenance; stronger window identity does not change their image scope.
@@ -0,0 +1,11 @@
1
+ ---
2
+ status: accepted
3
+ ---
4
+
5
+ # Use exclusive control for physical desktop automation
6
+
7
+ The user chose suppression of ordinary physical keyboard/mouse input while the agent controls the desktop, with a blue border and explicit stop or human handback. This departs from DSH's selectively handled stop keys and Pi's earlier interference-aborts behavior: accidental interaction should not disrupt an agent task. It superseded that part of [ADR 0001](0001-preserve-reference-behavior.md), trading simultaneous desktop use for a simpler control contract; full acceptance is pending and all Pi safeguards remain. [ADR 0004](0004-pi-first-acceptance.md) now governs the broader Pi-first acceptance scope.
8
+
9
+ Requirements live in the [spec](../spec.md#exclusive-control); feasibility, responsibility boundaries and acceptance live in the [plan](../implementation-plan.md#exclusive-control-delivery). [BlockInput](https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-blockinput) restricts injection to its owning thread, while [keyboard hooks](https://learn.microsoft.com/en-us/windows/win32/winmsg/lowlevelkeyboardproc) can suppress events but can be removed on timeout. The candidate uses hooks in the existing independent listener, which allows separately tagged agent input. The native observer ignores physical input only when a downstream hook confirms it consumed the event and the retained listener/protection event remain valid; unconsumed or foreign injected input still invalidates the action. Tagged absolute pointer movement replaces the old expected-SetCursorPos queue.
10
+
11
+ On 2026-11-01, after the grouped physical check passed and a nonelevated BlockInput probe returned access denied, the user chose hooks with documented Windows limits for the first release. This avoids requiring additional privileges or isolation. Silent hook removal can allow physical events through and make physical stop keys unavailable; neither a live listener nor a stored hook handle proves otherwise. The first release accepts that specific platform limit, without claiming absolute input blocking. Detected protection loss must still stop automation and preserve uncertain-input safeguards. The decision does not waive remaining physical, failure, host or cleanup acceptance, or make #31 complete.
@@ -0,0 +1,9 @@
1
+ ---
2
+ status: accepted
3
+ ---
4
+
5
+ # Judge release readiness by Pi functionality
6
+
7
+ On 2026-11-02 the user prioritized a working standalone Pi extension over copying DSH and discarded the remaining DSH manual comparisons. This supersedes ADR 0001's mandatory behavioral-parity gate: DSH remains design/source context, while the current Pi specification defines supported behavior and acceptance. DSH installation, credentials, host/model runs, side-by-side manual checks and comparative performance are no longer release prerequisites.
8
+
9
+ Keep the 25 implemented tool capabilities, supported browsers/hosts, preservation and cleanup safeguards, exclusive control and the accepted Windows hook limitation. Reuse existing Pi evidence; preserve historical comparisons as historical evidence, with reference-backed developer utilities optional. Close a Pi deliverable only when its remaining Pi criteria are supported; transfer any shared Pi obligations explicitly before retiring an alignment/diagnosis umbrella. Independent Pi correctness, failure handling and useful responsiveness still matter.
@@ -0,0 +1,7 @@
1
+ ---
2
+ status: accepted
3
+ ---
4
+
5
+ # Follow Windows defaults and reuse the user's workspace
6
+
7
+ On 2026-11-02 the user chose Windows default apps and a new task tab/document in the intended existing window, with coherent foreground handling, because separate browser launches and repeated focus changes disrupted normal use. This supersedes the specification's dedicated-browser-profile default: existing profile acceptance remains valid for the explicit dedicated mode, while ordinary-app reuse may require native UI rather than the current browser protocol channel. Requirements live in the [spec](../spec.md#default-apps-workspaces-and-foreground); #30 owns implementation and acceptance, with editor-focus behavior shared with #26.
@@ -0,0 +1,9 @@
1
+ # Domain docs
2
+
3
+ This is a single-context project.
4
+
5
+ Read the root [GLOSSARY.md](../../GLOSSARY.md) for domain terminology and relevant decisions in [docs/adr/](../adr/) before exploring or changing the affected behavior. Use the glossary's vocabulary and surface conflicts with existing ADRs explicitly.
6
+
7
+ If a domain document is absent, proceed; create new terms or decisions through domain-modeling only when needed.
8
+
9
+ Use [docs/README.md](../README.md) to find task-specific documentation.