@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
@@ -0,0 +1,449 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { mkdir, readFile, rename, rm, stat, writeFile } from "node:fs/promises";
3
+ import { release } from "node:os";
4
+ import { dirname, join } from "node:path";
5
+ import { windowsHostEnvironment } from "./launcher.js";
6
+ export const TOOL_PREFIX = "windows_computer_use_";
7
+ export const TOOL_NAMES = [
8
+ "health",
9
+ "snapshot",
10
+ "accessibility_tree",
11
+ "list_windows",
12
+ "find",
13
+ "element_info",
14
+ "click",
15
+ "double_click",
16
+ "move",
17
+ "drag",
18
+ "scroll",
19
+ "type_text",
20
+ "keypress",
21
+ "focus",
22
+ "invoke",
23
+ "set_value",
24
+ "activate_window",
25
+ "wait",
26
+ "ocr",
27
+ "screenshot",
28
+ "wait_for",
29
+ "close_window",
30
+ "move_window",
31
+ "launch_app",
32
+ "tools",
33
+ ];
34
+ const LITE = [
35
+ "list_windows",
36
+ "find",
37
+ "click",
38
+ "scroll",
39
+ "type_text",
40
+ "keypress",
41
+ "ocr",
42
+ "screenshot",
43
+ "wait_for",
44
+ "launch_app",
45
+ "tools",
46
+ ];
47
+ export const DEFAULT_SETTINGS = {
48
+ browserFirst: true,
49
+ browserVisible: true,
50
+ browserApp: "default",
51
+ browserSignIn: false,
52
+ mode: "full",
53
+ toolPreset: "full",
54
+ enabledTools: TOOL_NAMES,
55
+ indicatorEnabled: true,
56
+ cursorIndicatorEnabled: true,
57
+ remoteIndicatorVisible: true,
58
+ indicatorText: "Pi is controlling the computer",
59
+ indicatorPosition: "top",
60
+ indicatorIdleMs: 800,
61
+ stopShortcut: "",
62
+ resumeToken: 0,
63
+ previewToken: 0,
64
+ };
65
+ const choices = {
66
+ browserApp: ["default", "edge", "chrome", "firefox"],
67
+ mode: ["full", "lite"],
68
+ toolPreset: ["auto", "full", "lite", "lazy", "custom"],
69
+ indicatorPosition: ["top", "bottom"],
70
+ };
71
+ const switches = [
72
+ "browserFirst",
73
+ "browserVisible",
74
+ "browserSignIn",
75
+ "indicatorEnabled",
76
+ "cursorIndicatorEnabled",
77
+ "remoteIndicatorVisible",
78
+ ];
79
+ const object = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
80
+ function validate(input, repair) {
81
+ if (!object(input))
82
+ throw new Error("SETTINGS_INVALID: settings must be an object");
83
+ const settings = {
84
+ ...DEFAULT_SETTINGS,
85
+ enabledTools: [...TOOL_NAMES],
86
+ }, notices = [];
87
+ const invalid = (key) => {
88
+ throw new Error(`SETTINGS_INVALID: invalid ${key}`);
89
+ };
90
+ for (const [key, value] of Object.entries(input)) {
91
+ if (!(key in DEFAULT_SETTINGS)) {
92
+ if (repair) {
93
+ notices.push(`Ignored unknown saved setting: ${key}`);
94
+ continue;
95
+ }
96
+ invalid(key);
97
+ }
98
+ if (choices[key]) {
99
+ if (!choices[key]?.includes(String(value)) || typeof value !== "string") {
100
+ if (repair) {
101
+ notices.push(`Reset unsupported saved ${key} to default`);
102
+ continue;
103
+ }
104
+ invalid(key);
105
+ }
106
+ }
107
+ else if (switches.includes(key)) {
108
+ if (typeof value !== "boolean") {
109
+ if (repair) {
110
+ notices.push(`Reset invalid saved ${key} to default`);
111
+ continue;
112
+ }
113
+ invalid(key);
114
+ }
115
+ }
116
+ else if (key === "enabledTools") {
117
+ if (!Array.isArray(value)) {
118
+ if (repair) {
119
+ notices.push("Reset invalid saved enabledTools to default");
120
+ continue;
121
+ }
122
+ invalid(key);
123
+ }
124
+ const names = value;
125
+ if (names.length > 25 ||
126
+ names.some((name) => typeof name !== "string" || !TOOL_NAMES.includes(name))) {
127
+ if (!repair)
128
+ invalid(key);
129
+ const known = TOOL_NAMES.filter((name) => names.includes(name));
130
+ settings.enabledTools = known.length ? known : [...TOOL_NAMES];
131
+ notices.push("Dropped unknown saved tools or restored the default selection");
132
+ continue;
133
+ }
134
+ settings.enabledTools = TOOL_NAMES.filter((name) => names.includes(name));
135
+ continue;
136
+ }
137
+ else if (key === "indicatorText") {
138
+ if (typeof value !== "string" ||
139
+ !value.trim() ||
140
+ value.trim().length > 120 ||
141
+ [...value].some((character) => character.charCodeAt(0) < 32 || character.charCodeAt(0) === 127))
142
+ invalid(key);
143
+ settings[key] = value.trim();
144
+ continue;
145
+ }
146
+ else if (key === "stopShortcut") {
147
+ if (typeof value !== "string")
148
+ invalid(key);
149
+ if (value !== "") {
150
+ const parts = value.split("+"), keyName = parts.pop() ?? "", mods = ["Ctrl", "Alt", "Shift", "Win"];
151
+ if (parts.some((part) => !mods.includes(part)) ||
152
+ new Set(parts).size !== parts.length ||
153
+ !/^(?:[A-Z0-9]|F(?:[1-9]|1[0-9]|2[0-4])|Space|Enter|Tab|Backspace|Delete|Insert|Home|End|PageUp|PageDown|Up|Down|Left|Right|Pause|CapsLock|NumLock|ScrollLock)$/u.test(keyName))
154
+ invalid(key);
155
+ settings[key] = [
156
+ ...mods.filter((part) => parts.includes(part)),
157
+ keyName,
158
+ ].join("+");
159
+ continue;
160
+ }
161
+ }
162
+ else if (key === "indicatorIdleMs") {
163
+ if (!Number.isSafeInteger(value) ||
164
+ Number(value) < 200 ||
165
+ Number(value) > 3000)
166
+ invalid(key);
167
+ }
168
+ else if (key === "resumeToken" || key === "previewToken") {
169
+ if (!Number.isSafeInteger(value) || Number(value) < 0)
170
+ invalid(key);
171
+ }
172
+ settings[key] = value;
173
+ }
174
+ if (settings.browserApp === "firefox" && settings.browserVisible) {
175
+ settings.browserVisible = false;
176
+ notices.push("Experimental Firefox automatic launches are headless");
177
+ }
178
+ if (settings.toolPreset === "auto")
179
+ settings.toolPreset =
180
+ settings.enabledTools.length === TOOL_NAMES.length
181
+ ? "full"
182
+ : "custom";
183
+ return { settings: settings, notices };
184
+ }
185
+ export async function personalSettingsPath(host = false) {
186
+ if (process.env.PI_CU_SETTINGS_FILE)
187
+ return process.env.PI_CU_SETTINGS_FILE;
188
+ if (process.platform === "win32") {
189
+ if (!process.env.LOCALAPPDATA)
190
+ throw new Error("SETTINGS_PATH_FAILED: Windows LOCALAPPDATA is missing");
191
+ return join(process.env.LOCALAPPDATA, "pi-computer-use", "settings.json");
192
+ }
193
+ if (host &&
194
+ process.platform === "linux" &&
195
+ /microsoft.*wsl2/i.test(release())) {
196
+ const { localData } = await windowsHostEnvironment();
197
+ return join(localData, "pi-computer-use", "settings.json");
198
+ }
199
+ return undefined;
200
+ }
201
+ export async function readPersonalSettings(host = false) {
202
+ const path = await personalSettingsPath(host);
203
+ if (!path)
204
+ return {
205
+ ...validate({}, false),
206
+ path: undefined,
207
+ exists: false,
208
+ source: "unsupported-host-defaults",
209
+ };
210
+ try {
211
+ const parsed = JSON.parse(await readFile(path, "utf8"));
212
+ return {
213
+ ...validate(parsed, true),
214
+ path,
215
+ exists: true,
216
+ source: "personal-file",
217
+ };
218
+ }
219
+ catch (error) {
220
+ if (object(error) && error.code === "ENOENT")
221
+ return {
222
+ ...validate({}, false),
223
+ path,
224
+ exists: false,
225
+ source: "new-personal-defaults",
226
+ };
227
+ throw new Error(`SETTINGS_READ_FAILED: ${error instanceof Error ? error.message : String(error)}`);
228
+ }
229
+ }
230
+ async function updatePersonalSettings(changes) {
231
+ const path = await personalSettingsPath();
232
+ if (!path)
233
+ throw new Error("SETTINGS_PATH_FAILED: settings require a Windows data directory or explicit test file");
234
+ if (!object(changes))
235
+ throw new Error("SETTINGS_INVALID: changes must be an object");
236
+ await mkdir(dirname(path), { recursive: true });
237
+ const lock = `${path}.lock`;
238
+ let locked = false;
239
+ try {
240
+ try {
241
+ await mkdir(lock);
242
+ locked = true;
243
+ }
244
+ catch (error) {
245
+ if (!object(error) || error.code !== "EEXIST")
246
+ throw error;
247
+ let stale = false;
248
+ try {
249
+ const owner = JSON.parse(await readFile(join(lock, "owner.json"), "utf8"));
250
+ if (!Number.isSafeInteger(owner.pid) || owner.pid < 1)
251
+ throw new Error("invalid lock owner");
252
+ try {
253
+ process.kill(owner.pid, 0);
254
+ }
255
+ catch (cause) {
256
+ if (object(cause) && cause.code === "ESRCH")
257
+ stale = true;
258
+ }
259
+ }
260
+ catch {
261
+ stale = Date.now() - (await stat(lock)).mtimeMs > 30000;
262
+ }
263
+ if (!stale)
264
+ throw new Error("SETTINGS_BUSY: another process is saving personal settings");
265
+ await rm(lock, { recursive: true, force: true });
266
+ await mkdir(lock);
267
+ locked = true;
268
+ }
269
+ await writeFile(join(lock, "owner.json"), JSON.stringify({ pid: process.pid }), { flag: "wx" });
270
+ const current = await readPersonalSettings();
271
+ const validated = validate({ ...current.settings, ...changes }, false);
272
+ const temporary = `${path}.${randomUUID()}.tmp`;
273
+ try {
274
+ await writeFile(temporary, `${JSON.stringify(validated.settings, null, 2)}\n`, { flag: "wx" });
275
+ await rename(temporary, path);
276
+ }
277
+ finally {
278
+ await rm(temporary, { force: true });
279
+ }
280
+ return readPersonalSettings();
281
+ }
282
+ finally {
283
+ if (locked)
284
+ await rm(lock, { recursive: true, force: true });
285
+ }
286
+ }
287
+ export function allowedToolNames(settings) {
288
+ return settings.toolPreset === "full" || settings.toolPreset === "lazy"
289
+ ? [...TOOL_NAMES]
290
+ : settings.toolPreset === "lite"
291
+ ? [...LITE]
292
+ : [...new Set(settings.enabledTools)];
293
+ }
294
+ export const managementParameters = {
295
+ type: "object",
296
+ additionalProperties: false,
297
+ properties: {
298
+ operation: {
299
+ type: "string",
300
+ enum: ["list", "describe", "enable", "disable", "reset"],
301
+ default: "list",
302
+ },
303
+ names: { type: "array", items: { type: "string" }, maxItems: 25 },
304
+ },
305
+ };
306
+ export class PersonalSettingsManager {
307
+ available;
308
+ currentSettings;
309
+ currentFileExists = false;
310
+ currentNotices = [];
311
+ currentSource = "not-read";
312
+ selection = "";
313
+ enabled = new Set();
314
+ allowed = new Set();
315
+ constructor(available) {
316
+ this.available = available;
317
+ this.available.set("tools", {
318
+ description: "Inspect or change the loaded tools allowed by personal settings",
319
+ parameters: managementParameters,
320
+ });
321
+ }
322
+ async synchronize() {
323
+ const current = await readPersonalSettings();
324
+ this.currentSettings = current.settings;
325
+ this.currentFileExists = current.exists;
326
+ this.currentNotices = current.notices;
327
+ this.currentSource = current.source;
328
+ const key = JSON.stringify({
329
+ preset: current.settings.toolPreset,
330
+ tools: allowedToolNames(current.settings),
331
+ });
332
+ if (key !== this.selection) {
333
+ this.selection = key;
334
+ this.allowed = new Set(allowedToolNames(current.settings));
335
+ this.enabled = new Set((current.settings.toolPreset === "lazy"
336
+ ? ["tools"]
337
+ : [...this.allowed]).filter((name) => this.available.has(name)));
338
+ }
339
+ return current;
340
+ }
341
+ async authorize(action) {
342
+ await this.synchronize();
343
+ if (action === "prepare_input")
344
+ return undefined;
345
+ if (!TOOL_NAMES.includes(action))
346
+ return "UNKNOWN_ACTION";
347
+ if (!this.allowed.has(action))
348
+ return "TOOL_DISABLED_IN_SETTINGS: this tool is disabled by the user's personal selection";
349
+ if (!this.available.has(action))
350
+ return "TOOL_UNAVAILABLE: this capability is not implemented in this build";
351
+ if (!this.enabled.has(action))
352
+ return "TOOL_NOT_LOADED: enable an allowed tool through windows_computer_use_tools";
353
+ return undefined;
354
+ }
355
+ toolList() {
356
+ return TOOL_NAMES.map((name) => ({
357
+ name: `${TOOL_PREFIX}${name}`,
358
+ shortName: name,
359
+ available: this.available.has(name),
360
+ allowed: this.allowed.has(name),
361
+ enabled: this.enabled.has(name),
362
+ ...this.available.get(name),
363
+ }));
364
+ }
365
+ async handle(action, args) {
366
+ try {
367
+ const current = await this.synchronize();
368
+ if (action === "settings_get") {
369
+ if (Object.keys(args).length)
370
+ throw new Error("INVALID_ARGUMENTS: settings_get takes no options");
371
+ return { ok: true, action, ...current, tools: this.toolList() };
372
+ }
373
+ if (action === "settings_update") {
374
+ if (Object.keys(args).some((key) => key !== "changes"))
375
+ throw new Error("INVALID_ARGUMENTS: settings_update accepts changes only");
376
+ const updated = await updatePersonalSettings(args.changes);
377
+ this.selection = "";
378
+ await this.synchronize();
379
+ return { ok: true, action, ...updated, tools: this.toolList() };
380
+ }
381
+ if (!this.allowed.has("tools"))
382
+ throw new Error("TOOL_DISABLED_IN_SETTINGS: management is disabled in personal settings");
383
+ if (!this.enabled.has("tools"))
384
+ throw new Error("TOOL_NOT_LOADED: management is unloaded; restore the preset through /computer-settings");
385
+ if (Object.keys(args).some((key) => key !== "operation" && key !== "names"))
386
+ throw new Error("INVALID_ARGUMENTS: unknown management option");
387
+ const operation = args.operation ?? "list";
388
+ if (!["list", "describe", "enable", "disable", "reset"].includes(String(operation)) ||
389
+ typeof operation !== "string")
390
+ throw new Error("INVALID_ARGUMENTS: unknown management operation");
391
+ if (args.names !== undefined &&
392
+ (!Array.isArray(args.names) ||
393
+ args.names.length > 25 ||
394
+ args.names.some((name) => typeof name !== "string")))
395
+ throw new Error("INVALID_ARGUMENTS: names must be at most 25 strings");
396
+ const names = (args.names ?? []).map((name) => name.startsWith(TOOL_PREFIX) ? name.slice(TOOL_PREFIX.length) : name);
397
+ if (names.includes("*") &&
398
+ (names.length !== 1 || !["enable", "disable"].includes(operation)))
399
+ throw new Error("INVALID_ARGUMENTS: wildcard only supports enable/disable alone");
400
+ if (operation === "describe" && names.length !== 1)
401
+ throw new Error("INVALID_ARGUMENTS: describe requires one name");
402
+ if (operation === "reset" && names.length)
403
+ throw new Error("INVALID_ARGUMENTS: reset takes no names");
404
+ if (["enable", "disable"].includes(operation) && !names.length)
405
+ throw new Error("INVALID_ARGUMENTS: enable/disable requires names");
406
+ for (const name of names)
407
+ if (name !== "*" && !TOOL_NAMES.includes(name))
408
+ throw new Error(`UNKNOWN_TOOL: ${name}`);
409
+ const selected = names[0] === "*"
410
+ ? [...this.allowed].filter((name) => this.available.has(name))
411
+ : names;
412
+ if (operation === "enable")
413
+ for (const name of selected) {
414
+ if (!this.allowed.has(name))
415
+ throw new Error(`TOOL_DISABLED_IN_SETTINGS: ${name}`);
416
+ if (!this.available.has(name))
417
+ throw new Error(`TOOL_UNAVAILABLE: ${name}`);
418
+ }
419
+ if (operation === "enable")
420
+ for (const name of selected)
421
+ this.enabled.add(name);
422
+ if (operation === "disable")
423
+ for (const name of selected)
424
+ this.enabled.delete(name);
425
+ if (operation === "reset") {
426
+ this.selection = "";
427
+ await this.synchronize();
428
+ }
429
+ const tools = this.toolList();
430
+ return {
431
+ ok: true,
432
+ action: "tools",
433
+ operation,
434
+ preset: current.settings.toolPreset,
435
+ notices: current.notices,
436
+ tools: operation === "describe"
437
+ ? tools.filter((tool) => tool.shortName === names[0])
438
+ : tools,
439
+ };
440
+ }
441
+ catch (error) {
442
+ return {
443
+ ok: false,
444
+ action,
445
+ error: error instanceof Error ? error.message : String(error),
446
+ };
447
+ }
448
+ }
449
+ }
package/dist/stop.d.ts ADDED
@@ -0,0 +1,24 @@
1
+ import type { WorkerResult } from "./protocol.js";
2
+ import type { PersonalSettings } from "./settings.js";
3
+ export declare class StopListener {
4
+ private operationId;
5
+ private listenerProcessId;
6
+ private cancelEvent;
7
+ private readonly control;
8
+ private started;
9
+ private configuration;
10
+ private settingsKey;
11
+ constructor(onStop: (operationId: string) => void);
12
+ get hasStarted(): boolean;
13
+ get currentOperation(): string | undefined;
14
+ configure(settings: PersonalSettings): Promise<void>;
15
+ private start;
16
+ arm(operationId: string, signal?: AbortSignal): Promise<WorkerResult>;
17
+ acquire(operationId: string, signal?: AbortSignal): Promise<WorkerResult>;
18
+ suspendCapture(suspended: boolean): Promise<void>;
19
+ preview(settings: PersonalSettings, show: boolean): Promise<WorkerResult>;
20
+ status(): Promise<WorkerResult>;
21
+ signal(operationId: string): Promise<void>;
22
+ disarm(operationId: string): Promise<void>;
23
+ dispose(): Promise<void>;
24
+ }
package/dist/stop.js ADDED
@@ -0,0 +1,156 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { WorkerClient } from "./client.js";
3
+ import { compileNative } from "./native.js";
4
+ export class StopListener {
5
+ operationId;
6
+ listenerProcessId;
7
+ cancelEvent;
8
+ control;
9
+ started = false;
10
+ configuration;
11
+ settingsKey = "";
12
+ constructor(onStop) {
13
+ this.control = new WorkerClient({
14
+ launch: (signal) => compileNative(["stop.cs", "indicators.cs"], ["System.Drawing.dll", "System.Windows.Forms.dll"], signal),
15
+ onStop,
16
+ onExit: () => {
17
+ const operationId = this.operationId;
18
+ this.operationId = undefined;
19
+ this.cancelEvent = undefined;
20
+ this.listenerProcessId = undefined;
21
+ this.started = false;
22
+ if (operationId)
23
+ onStop(operationId);
24
+ },
25
+ });
26
+ }
27
+ get hasStarted() {
28
+ return this.started;
29
+ }
30
+ get currentOperation() {
31
+ return this.operationId;
32
+ }
33
+ async configure(settings) {
34
+ const previous = this.configuration;
35
+ this.configuration = settings;
36
+ const key = JSON.stringify([
37
+ settings.indicatorEnabled,
38
+ settings.cursorIndicatorEnabled,
39
+ settings.remoteIndicatorVisible,
40
+ settings.indicatorText,
41
+ settings.indicatorPosition,
42
+ settings.indicatorIdleMs,
43
+ settings.stopShortcut,
44
+ ]);
45
+ if (this.started && key !== this.settingsKey) {
46
+ const result = await this.control.execute("configure", { settings });
47
+ if (!result.ok)
48
+ throw new Error(String(result.error));
49
+ this.settingsKey = key;
50
+ }
51
+ if (previous && settings.previewToken !== previous.previewToken)
52
+ await this.preview(settings, true);
53
+ }
54
+ async start(signal) {
55
+ if (this.started)
56
+ return;
57
+ if (!this.configuration)
58
+ throw new Error("STOP_SETTINGS_UNAVAILABLE");
59
+ const result = await this.control.execute("configure", { settings: this.configuration }, signal);
60
+ if (!result.ok)
61
+ throw new Error(String(result.error));
62
+ this.started = true;
63
+ this.settingsKey = JSON.stringify([
64
+ this.configuration.indicatorEnabled,
65
+ this.configuration.cursorIndicatorEnabled,
66
+ this.configuration.remoteIndicatorVisible,
67
+ this.configuration.indicatorText,
68
+ this.configuration.indicatorPosition,
69
+ this.configuration.indicatorIdleMs,
70
+ this.configuration.stopShortcut,
71
+ ]);
72
+ }
73
+ async arm(operationId, signal) {
74
+ await this.start(signal);
75
+ if (this.operationId !== operationId) {
76
+ this.cancelEvent = `Local\\PiComputerUseStop-${process.pid}-${randomUUID()}`;
77
+ const result = await this.control.execute("arm", { operationId, cancelEvent: this.cancelEvent }, signal);
78
+ if (!result.ok)
79
+ return result;
80
+ this.listenerProcessId = Number(result.listenerProcessId);
81
+ this.operationId = operationId;
82
+ }
83
+ const status = await this.control.execute("status", {}, signal);
84
+ return {
85
+ ...status,
86
+ action: "prepare_input",
87
+ hookInstalled: true,
88
+ cancelEvent: this.cancelEvent,
89
+ listenerProcessId: this.listenerProcessId,
90
+ inputReady: true,
91
+ readyActions: [
92
+ "click",
93
+ "double_click",
94
+ "move",
95
+ "drag",
96
+ "scroll",
97
+ "type_text",
98
+ "keypress",
99
+ ],
100
+ note: "Pointer and keyboard actions implement cancellation polling and held-input cleanup.",
101
+ };
102
+ }
103
+ async acquire(operationId, signal) {
104
+ if (this.operationId !== operationId)
105
+ throw new Error("STOP_OPERATION_NOT_ARMED");
106
+ const result = await this.control.execute("take_control", { operationId }, signal);
107
+ if (result.ok &&
108
+ (result.exclusiveControl !== true ||
109
+ result.hookInstalled !== true ||
110
+ result.operationId !== operationId)) {
111
+ await this.signal(operationId);
112
+ return {
113
+ ok: false,
114
+ action: "take_control",
115
+ error: "EXCLUSIVE_CONTROL_UNAVAILABLE: helper did not confirm protection",
116
+ };
117
+ }
118
+ return result;
119
+ }
120
+ async suspendCapture(suspended) {
121
+ if (this.started)
122
+ await this.control.execute("capture", { suspended });
123
+ }
124
+ async preview(settings, show) {
125
+ await this.configure(settings);
126
+ await this.start();
127
+ return this.control.execute("preview", { show });
128
+ }
129
+ async status() {
130
+ if (!this.started)
131
+ return {
132
+ ok: true,
133
+ action: "status",
134
+ started: false,
135
+ hookInstalled: false,
136
+ presentation: { visible: false },
137
+ };
138
+ return { ...(await this.control.execute("status", {})), started: true };
139
+ }
140
+ async signal(operationId) {
141
+ if (this.operationId === operationId)
142
+ await this.control.execute("stop", { operationId });
143
+ }
144
+ async disarm(operationId) {
145
+ if (this.operationId !== operationId)
146
+ return;
147
+ const result = await this.control.execute("disarm", { operationId });
148
+ if (!result.ok || result.exclusiveControl !== false)
149
+ throw new Error("CONTROL_RELEASE_UNCONFIRMED: retain desktop ownership");
150
+ this.operationId = undefined;
151
+ this.cancelEvent = undefined;
152
+ }
153
+ async dispose() {
154
+ await this.control.dispose();
155
+ }
156
+ }