runline 0.22.0 → 0.24.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 (27) hide show
  1. package/dist/plugins/shiftOcr/src/index.js +13 -0
  2. package/dist/plugins/shiftPages/src/pages.js +72 -15
  3. package/dist/plugins/shiftPages/src/shared.js +5 -0
  4. package/dist/plugins/shiftTranscription/src/index.js +1 -1
  5. package/dist/plugins/shiftTranscription/src/transcription.js +21 -5
  6. package/dist/plugins/shiftWork/src/issues.js +119 -33
  7. package/dist/plugins/steel/src/browser.js +11 -79
  8. package/dist/plugins/steel/src/index.js +2 -0
  9. package/dist/plugins/steel/src/page/bundle.generated.js +11 -0
  10. package/dist/plugins/steel/src/page/cdp.js +232 -0
  11. package/dist/plugins/steel/src/page/driver.js +286 -0
  12. package/dist/plugins/steel/src/page/entry.js +91 -0
  13. package/dist/plugins/steel/src/page/install.js +3 -0
  14. package/dist/plugins/steel/src/page/page-snapshot.js +330 -0
  15. package/dist/plugins/steel/src/page/tools.js +649 -0
  16. package/dist/plugins/steel/src/page/vendor/aria-snapshot/ariaSnapshot.js +411 -0
  17. package/dist/plugins/steel/src/page/vendor/aria-snapshot/ariaSnapshotDistiller.js +207 -0
  18. package/dist/plugins/steel/src/page/vendor/aria-snapshot/ariaTypes.js +62 -0
  19. package/dist/plugins/steel/src/page/vendor/aria-snapshot/cssTokenizer.js +949 -0
  20. package/dist/plugins/steel/src/page/vendor/aria-snapshot/domUtils.js +157 -0
  21. package/dist/plugins/steel/src/page/vendor/aria-snapshot/roleUtils.js +1193 -0
  22. package/dist/plugins/steel/src/page/vendor/aria-snapshot/stringUtils.js +194 -0
  23. package/dist/plugins/steel/src/page/vendor/aria-snapshot/yaml.js +80 -0
  24. package/dist/plugins/steel/src/page/wait-for.js +30 -0
  25. package/dist/plugins/steel/src/sessions.js +2 -7
  26. package/dist/plugins/steel/src/shared.js +12 -0
  27. package/package.json +1 -1
@@ -0,0 +1,286 @@
1
+ /**
2
+ * Trusted browser input over raw CDP.
3
+ *
4
+ * CDP domains are addressed directly rather than through a browser
5
+ * automation library, because the plugin must work in hosts that have
6
+ * none installed. The events are the real ones — `Input.dispatchMouseEvent`
7
+ * and friends produce `isTrusted` events, so pages that reject synthetic
8
+ * input behave as they do for a person.
9
+ *
10
+ * Targets arrive as a `locatorToken` the page bridge stamped onto the
11
+ * element. Resolving that token to coordinates happens here, immediately
12
+ * before dispatch, so a layout shift between preparation and action is
13
+ * caught rather than clicked through.
14
+ */
15
+ import { CdpError } from "./cdp.js";
16
+ import { TARGET_ATTRIBUTE } from "./page-snapshot.js";
17
+ const MODIFIER_BITS = {
18
+ Alt: 1,
19
+ Control: 2,
20
+ Meta: 4,
21
+ Shift: 8,
22
+ };
23
+ /**
24
+ * CDP wants a bitmask; the tools speak in names. Unknown names are
25
+ * refused rather than folded in as `undefined`, which would turn the
26
+ * whole mask into NaN and silently strip every modifier.
27
+ */
28
+ function modifierMask(modifiers = []) {
29
+ let mask = 0;
30
+ for (const name of modifiers) {
31
+ const bit = MODIFIER_BITS[name];
32
+ if (bit === undefined) {
33
+ throw new CdpError("invalid_arguments", `Unknown modifier ${name}; expected Alt, Control, Meta, or Shift`, false);
34
+ }
35
+ mask |= bit;
36
+ }
37
+ return mask;
38
+ }
39
+ /** A page expression selecting the element the bridge stamped for us. */
40
+ function elementExpression(locatorToken) {
41
+ return `document.querySelector(${JSON.stringify(`[${TARGET_ATTRIBUTE}=${JSON.stringify(locatorToken)}]`)})`;
42
+ }
43
+ export class CdpDriver {
44
+ page;
45
+ openDialog = null;
46
+ detachDialogs;
47
+ constructor(page) {
48
+ this.page = page;
49
+ // Dialogs block the renderer: every subsequent evaluate would hang
50
+ // until one is handled, so the tools need to see it as a result
51
+ // rather than as a timeout.
52
+ this.detachDialogs = page.onEvent((method, params) => {
53
+ if (method === "Page.javascriptDialogOpening") {
54
+ this.openDialog = {
55
+ type: String(params.type ?? "dialog"),
56
+ message: String(params.message ?? ""),
57
+ defaultValue: String(params.defaultPrompt ?? ""),
58
+ };
59
+ }
60
+ if (method === "Page.javascriptDialogClosed")
61
+ this.openDialog = null;
62
+ });
63
+ }
64
+ dialog() {
65
+ return this.openDialog;
66
+ }
67
+ dispose() {
68
+ this.detachDialogs();
69
+ }
70
+ async handleDialog(options) {
71
+ if (!this.openDialog) {
72
+ throw new CdpError("no_dialog", "No dialog is currently open", false);
73
+ }
74
+ await this.page.send("Page.handleJavaScriptDialog", {
75
+ accept: options.accept,
76
+ ...(options.promptText === undefined
77
+ ? {}
78
+ : { promptText: options.promptText }),
79
+ });
80
+ this.openDialog = null;
81
+ }
82
+ /**
83
+ * Resolve a prepared token to viewport coordinates. Runs in the page so
84
+ * the element's own scroll container is honoured, and fails loudly when
85
+ * the element has gone or moved out of view.
86
+ */
87
+ async point(target) {
88
+ const box = await this.page.evaluate(`(() => {
89
+ const el = ${elementExpression(target.locatorToken)};
90
+ if (!el) return { error: 'detached' };
91
+ el.scrollIntoView({ block: 'center', inline: 'center' });
92
+ const r = el.getBoundingClientRect();
93
+ if (r.width <= 0 || r.height <= 0) return { error: 'not_visible' };
94
+ return { x: r.x + r.width / 2, y: r.y + r.height / 2 };
95
+ })()`);
96
+ if ("error" in box) {
97
+ throw box.error === "detached"
98
+ ? new CdpError("stale_ref", "The target element left the page between preparing and acting on it. Capture a fresh snapshot and retry.")
99
+ : new CdpError("element_not_visible", "The target element is not visible at action time");
100
+ }
101
+ return box;
102
+ }
103
+ async mouse(type, point, options = {}) {
104
+ await this.page.send("Input.dispatchMouseEvent", {
105
+ type,
106
+ x: point.x,
107
+ y: point.y,
108
+ button: options.button ?? "left",
109
+ clickCount: options.clickCount ?? 0,
110
+ modifiers: options.modifiers ?? 0,
111
+ });
112
+ }
113
+ async click(target, options) {
114
+ const point = await this.point(target);
115
+ const modifiers = modifierMask(options.modifiers);
116
+ const button = options.button ?? "left";
117
+ await this.mouse("mouseMoved", point, { modifiers });
118
+ const clicks = options.doubleClick ? 2 : 1;
119
+ for (let index = 1; index <= clicks; index += 1) {
120
+ await this.mouse("mousePressed", point, {
121
+ button,
122
+ clickCount: index,
123
+ modifiers,
124
+ });
125
+ await this.mouse("mouseReleased", point, {
126
+ button,
127
+ clickCount: index,
128
+ modifiers,
129
+ });
130
+ }
131
+ }
132
+ async hover(target) {
133
+ await this.mouse("mouseMoved", await this.point(target));
134
+ }
135
+ async drag(start, end) {
136
+ const from = await this.point(start);
137
+ const to = await this.point(end);
138
+ await this.mouse("mouseMoved", from);
139
+ await this.mouse("mousePressed", from, { clickCount: 1 });
140
+ // Intermediate moves: drag handlers commonly ignore a single jump.
141
+ for (let step = 1; step <= 4; step += 1) {
142
+ await this.mouse("mouseMoved", {
143
+ x: from.x + ((to.x - from.x) * step) / 4,
144
+ y: from.y + ((to.y - from.y) * step) / 4,
145
+ });
146
+ }
147
+ await this.mouse("mouseReleased", to, { clickCount: 1 });
148
+ }
149
+ async scroll(deltaY, target) {
150
+ const point = target
151
+ ? await this.point(target)
152
+ : await this.viewportCentre();
153
+ await this.page.send("Input.dispatchMouseEvent", {
154
+ type: "mouseWheel",
155
+ x: point.x,
156
+ y: point.y,
157
+ deltaX: 0,
158
+ deltaY,
159
+ });
160
+ }
161
+ async viewportCentre() {
162
+ return await this.page.evaluate("({ x: window.innerWidth / 2, y: window.innerHeight / 2 })");
163
+ }
164
+ async type(target, text, options) {
165
+ // The bridge already focused the element and set the selection for
166
+ // replace/append, so typing here just replays keys into it.
167
+ if (options.slowly) {
168
+ for (const char of text)
169
+ await this.insertKey(char);
170
+ return;
171
+ }
172
+ if (text.length === 0) {
173
+ // Replace-mode with empty text means "clear": the selection is
174
+ // set, so a delete is what removes it.
175
+ await this.press("Delete");
176
+ return;
177
+ }
178
+ await this.page.send("Input.insertText", { text });
179
+ }
180
+ async insertKey(char) {
181
+ await this.page.send("Input.dispatchKeyEvent", {
182
+ type: "keyDown",
183
+ text: char,
184
+ unmodifiedText: char,
185
+ });
186
+ await this.page.send("Input.dispatchKeyEvent", {
187
+ type: "keyUp",
188
+ text: char,
189
+ });
190
+ }
191
+ async press(key) {
192
+ const parts = key.split("+");
193
+ const main = parts[parts.length - 1] ?? key;
194
+ const modifiers = modifierMask(parts.slice(0, -1));
195
+ const descriptor = keyDescriptor(main);
196
+ await this.page.send("Input.dispatchKeyEvent", {
197
+ type: descriptor.text ? "keyDown" : "rawKeyDown",
198
+ modifiers,
199
+ ...descriptor,
200
+ });
201
+ await this.page.send("Input.dispatchKeyEvent", {
202
+ type: "keyUp",
203
+ modifiers,
204
+ ...descriptor,
205
+ });
206
+ }
207
+ async select(target, values) {
208
+ const ok = await this.page.evaluate(`(() => {
209
+ const el = ${elementExpression(target.locatorToken)};
210
+ if (!el) return false;
211
+ const wanted = new Set(${JSON.stringify(values)});
212
+ for (const option of el.options) option.selected = wanted.has(option.value);
213
+ el.dispatchEvent(new Event('input', { bubbles: true }));
214
+ el.dispatchEvent(new Event('change', { bubbles: true }));
215
+ return true;
216
+ })()`);
217
+ if (!ok) {
218
+ throw new CdpError("stale_ref", "The select element left the page between preparing and acting on it. Capture a fresh snapshot and retry.");
219
+ }
220
+ }
221
+ async screenshot(options) {
222
+ const params = {
223
+ format: "jpeg",
224
+ quality: 80,
225
+ };
226
+ if (options.fullPage)
227
+ params.captureBeyondViewport = true;
228
+ const result = (await this.page.send("Page.captureScreenshot", params));
229
+ return { data: result.data, mimeType: "image/jpeg" };
230
+ }
231
+ /** Let the page react to the action before it is snapshotted. */
232
+ async settle() {
233
+ await new Promise((resolve) => setTimeout(resolve, 120));
234
+ if (this.openDialog)
235
+ return;
236
+ await this.page
237
+ .evaluate(`new Promise((r) => requestAnimationFrame(() => requestAnimationFrame(r)))`)
238
+ .catch(() => {
239
+ // A navigation or dialog can tear down the world mid-settle;
240
+ // the caller's snapshot reports the real state either way.
241
+ });
242
+ }
243
+ }
244
+ const NAMED_KEYS = {
245
+ Enter: { code: "Enter", keyCode: 13 },
246
+ Tab: { code: "Tab", keyCode: 9 },
247
+ Escape: { code: "Escape", keyCode: 27 },
248
+ Backspace: { code: "Backspace", keyCode: 8 },
249
+ Delete: { code: "Delete", keyCode: 46 },
250
+ ArrowUp: { code: "ArrowUp", keyCode: 38 },
251
+ ArrowDown: { code: "ArrowDown", keyCode: 40 },
252
+ ArrowLeft: { code: "ArrowLeft", keyCode: 37 },
253
+ ArrowRight: { code: "ArrowRight", keyCode: 39 },
254
+ Home: { code: "Home", keyCode: 36 },
255
+ End: { code: "End", keyCode: 35 },
256
+ PageUp: { code: "PageUp", keyCode: 33 },
257
+ PageDown: { code: "PageDown", keyCode: 34 },
258
+ Space: { code: "Space", keyCode: 32 },
259
+ };
260
+ /**
261
+ * CDP needs `key`/`code`/`windowsVirtualKeyCode` for non-printable keys;
262
+ * printable ones also need `text` or they arrive as bare keydowns.
263
+ */
264
+ function keyDescriptor(key) {
265
+ const match = NAMED_KEYS[key];
266
+ if (match) {
267
+ return {
268
+ key: key === "Space" ? " " : key,
269
+ code: match.code,
270
+ windowsVirtualKeyCode: match.keyCode,
271
+ ...(key === "Enter" ? { text: "\r" } : {}),
272
+ ...(key === "Space" ? { text: " " } : {}),
273
+ };
274
+ }
275
+ if (key.length === 1) {
276
+ const upper = key.toUpperCase();
277
+ return {
278
+ key,
279
+ code: /[a-z]/i.test(key) ? `Key${upper}` : `Digit${key}`,
280
+ windowsVirtualKeyCode: upper.charCodeAt(0),
281
+ text: key,
282
+ unmodifiedText: key,
283
+ };
284
+ }
285
+ return { key };
286
+ }
@@ -0,0 +1,91 @@
1
+ /**
2
+ * The in-page half of the Steel page tools.
3
+ *
4
+ * This module is bundled (see `scripts/build-steel-page-bundle.ts`) into
5
+ * one self-contained IIFE and evaluated inside the Steel session's page.
6
+ * Everything here is pure DOM code — it never sees the Steel API key, the
7
+ * CDP socket, or any plugin state.
8
+ *
9
+ * Why a resident global rather than a fresh evaluate per call: element
10
+ * refs (`e12`) are only stable while the aria-snapshot module's counter
11
+ * and each element's cached `_ariaRef` survive between calls. A new
12
+ * bundle per call would renumber every element and silently invalidate
13
+ * refs the agent is holding. So the bundle installs itself once per
14
+ * execution context and the driver re-injects only when the world is
15
+ * gone — which is exactly navigation, where fresh refs are correct.
16
+ */
17
+ import { PageSnapshotService, BrowserToolError, } from "./page-snapshot.js";
18
+ import { waitForPage } from "./wait-for.js";
19
+ /** Bumped when the in-page contract changes, so a stale world is replaced. */
20
+ export const PAGE_BRIDGE_VERSION = 1;
21
+ export const PAGE_BRIDGE_GLOBAL = "__steelPageBridge";
22
+ export function createPageBridge(document) {
23
+ let service = new PageSnapshotService(document);
24
+ let maxChars;
25
+ return {
26
+ version: PAGE_BRIDGE_VERSION,
27
+ async handle(request) {
28
+ try {
29
+ return { ok: true, value: await dispatch(request) };
30
+ }
31
+ catch (error) {
32
+ if (error instanceof BrowserToolError) {
33
+ return {
34
+ ok: false,
35
+ error: {
36
+ code: error.code,
37
+ message: error.message,
38
+ retryable: error.retryable,
39
+ },
40
+ };
41
+ }
42
+ return {
43
+ ok: false,
44
+ error: {
45
+ code: "page_error",
46
+ message: error instanceof Error ? error.message : String(error),
47
+ retryable: true,
48
+ },
49
+ };
50
+ }
51
+ },
52
+ };
53
+ async function dispatch(request) {
54
+ switch (request.action) {
55
+ case "snapshot": {
56
+ // maxChars is a construction option upstream; honour a per-call
57
+ // override by rebuilding rather than reaching into the service.
58
+ if (request.maxChars !== undefined && request.maxChars !== maxChars) {
59
+ maxChars = request.maxChars;
60
+ service = new PageSnapshotService(document, { maxChars });
61
+ }
62
+ return service.capture({
63
+ ...(request.depth !== undefined ? { depth: request.depth } : {}),
64
+ ...(request.boxes !== undefined ? { boxes: request.boxes } : {}),
65
+ });
66
+ }
67
+ case "prepare_target":
68
+ return await service.prepareTarget(request.target);
69
+ case "prepare_editable":
70
+ return await service.prepareEditableTarget(request.target, request.mode);
71
+ case "editable_value":
72
+ return service.readEditableValue(request.target);
73
+ case "resolve_options":
74
+ return { selected: service.resolveOptions(request.target, request.values) };
75
+ case "wait_for":
76
+ return await waitForPage(document, request);
77
+ }
78
+ }
79
+ }
80
+ /**
81
+ * Install idempotently. Re-evaluating the bundle in a world that already
82
+ * has a current bridge must not reset the ref counter.
83
+ */
84
+ export function installPageBridge() {
85
+ const existing = globalThis[PAGE_BRIDGE_GLOBAL];
86
+ if (existing?.version === PAGE_BRIDGE_VERSION)
87
+ return existing;
88
+ const bridge = createPageBridge(document);
89
+ globalThis[PAGE_BRIDGE_GLOBAL] = bridge;
90
+ return bridge;
91
+ }
@@ -0,0 +1,3 @@
1
+ /** Bundle entrypoint: install the bridge into the page's global world. */
2
+ import { installPageBridge } from "./entry.js";
3
+ installPageBridge();