handhold-sdk 1.0.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 (50) hide show
  1. package/README.md +427 -0
  2. package/dist/d44f1fe8001e32f8e74d.svg +3056 -0
  3. package/dist/fonts/Phosphor.ttf +0 -0
  4. package/dist/fonts/Phosphor.woff +0 -0
  5. package/dist/fonts/Phosphor.woff2 +0 -0
  6. package/dist/handhold.min.js +1 -0
  7. package/package.json +114 -0
  8. package/src/HandHold.js +1599 -0
  9. package/src/agent/VENDOR.md +203 -0
  10. package/src/agent/agent/AgentLoop.js +366 -0
  11. package/src/agent/agent/prompts.js +163 -0
  12. package/src/agent/agent/tools.js +148 -0
  13. package/src/agent/controller.js +235 -0
  14. package/src/agent/dom/actions.js +456 -0
  15. package/src/agent/dom/domTree.js +1761 -0
  16. package/src/agent/dom/redact.js +98 -0
  17. package/src/agent/dom/serializer.js +332 -0
  18. package/src/agent/dom/utils.js +99 -0
  19. package/src/agent/index.js +169 -0
  20. package/src/agent/llm/connector.js +143 -0
  21. package/src/agent/package.json +3 -0
  22. package/src/agent/policy/PolicyGuard.js +172 -0
  23. package/src/agent/site/manifest.js +145 -0
  24. package/src/agent/ui/ChatWidget.js +219 -0
  25. package/src/agent-mode/AgentMode.js +429 -0
  26. package/src/api/APIClient.js +258 -0
  27. package/src/core/Config.js +207 -0
  28. package/src/core/UiState.js +83 -0
  29. package/src/core/defaults.js +20 -0
  30. package/src/events.js +59 -0
  31. package/src/index.js +77 -0
  32. package/src/intent/ActionVerbMap.js +324 -0
  33. package/src/intent/IntentExtractor.js +317 -0
  34. package/src/observers/ElementScanner.js +284 -0
  35. package/src/observers/NavigationObserver.js +182 -0
  36. package/src/observers/PageObserver.js +293 -0
  37. package/src/session/SessionManager.js +402 -0
  38. package/src/session/SessionStorage.js +148 -0
  39. package/src/ui/HelpWidget.js +1189 -0
  40. package/src/ui/UICoach.js +1725 -0
  41. package/src/ui/components/Button.css +137 -0
  42. package/src/ui/components/Button.js +102 -0
  43. package/src/ui/widget.css +599 -0
  44. package/src/utils/Logger.js +132 -0
  45. package/src/utils/MarkdownRenderer.js +39 -0
  46. package/src/utils/domUtils.js +350 -0
  47. package/src/utils/eventBus.js +102 -0
  48. package/src/utils/index.js +8 -0
  49. package/src/utils/stringUtils.js +202 -0
  50. package/types/index.d.ts +538 -0
@@ -0,0 +1,456 @@
1
+ /**
2
+ * actions.js — ported from alibaba/page-agent packages/page-controller/src/actions.ts (MIT).
3
+ * Audit before porting (2026-07-05): clean — dispatchEvent/innerText/execCommand only,
4
+ * no eval, no innerHTML, no network. This file keeps upstream logic intact because it
5
+ * encodes real-world compatibility work:
6
+ * - full W3C pointer+mouse event order so React/Vue/antd handlers fire
7
+ * - native prototype value setter so React controlled inputs update
8
+ * - contenteditable Plan A (synthetic InputEvents) with verified fallback to
9
+ * Plan B (execCommand) for Quill/Slate-style editors
10
+ * Changes vs upstream: mask CustomEvents → optional hooks (see utils.js).
11
+ */
12
+ import {
13
+ clickPointer,
14
+ disablePassThrough,
15
+ enablePassThrough,
16
+ getNativeValueSetter,
17
+ isHTMLElement,
18
+ isInputElement,
19
+ isSelectElement,
20
+ isTextAreaElement,
21
+ movePointerToElement,
22
+ waitFor,
23
+ } from './utils.js'
24
+
25
+ /** Get the HTMLElement by index from a selectorMap. */
26
+ export function getElementByIndex(selectorMap, index) {
27
+ const interactiveNode = selectorMap.get(index)
28
+ if (!interactiveNode) {
29
+ throw new Error(`No interactive element found at index ${index}`)
30
+ }
31
+ const element = interactiveNode.ref
32
+ if (!element) {
33
+ throw new Error(`Element at index ${index} does not have a reference`)
34
+ }
35
+ if (!isHTMLElement(element)) {
36
+ throw new Error(`Element at index ${index} is not an HTMLElement`)
37
+ }
38
+ return element
39
+ }
40
+
41
+ let lastClickedElement = null
42
+
43
+ function blurLastClickedElement() {
44
+ if (lastClickedElement) {
45
+ lastClickedElement.dispatchEvent(new PointerEvent('pointerout', { bubbles: true }))
46
+ lastClickedElement.dispatchEvent(new PointerEvent('pointerleave', { bubbles: false }))
47
+ lastClickedElement.dispatchEvent(new MouseEvent('mouseout', { bubbles: true }))
48
+ lastClickedElement.dispatchEvent(new MouseEvent('mouseleave', { bubbles: false }))
49
+ lastClickedElement.blur()
50
+ lastClickedElement = null
51
+ }
52
+ }
53
+
54
+ /**
55
+ * Simulate a full click following W3C Pointer Events + UI Events spec order:
56
+ * pointerover/enter → mouseover/enter → pointerdown → mousedown → [focus] →
57
+ * pointerup → mouseup → click
58
+ */
59
+ export async function clickElement(element) {
60
+ blurLastClickedElement()
61
+ lastClickedElement = element
62
+
63
+ await scrollIntoViewIfNeeded(element)
64
+ const frame = element.ownerDocument.defaultView?.frameElement
65
+ if (frame) await scrollIntoViewIfNeeded(frame)
66
+
67
+ const rect = element.getBoundingClientRect()
68
+ const x = rect.left + rect.width / 2
69
+ const y = rect.top + rect.height / 2
70
+
71
+ await movePointerToElement(element, x, y)
72
+ await clickPointer()
73
+
74
+ await waitFor(0.1)
75
+
76
+ // Hit-test to find the deepest element at click coordinates, matching
77
+ // real browser behavior where events target the innermost element.
78
+ const doc = element.ownerDocument
79
+ await enablePassThrough()
80
+ const hitTarget = doc.elementFromPoint ? doc.elementFromPoint(x, y) : null
81
+ await disablePassThrough()
82
+ const target = isHTMLElement(hitTarget) && element.contains(hitTarget) ? hitTarget : element
83
+
84
+ const pointerOpts = { bubbles: true, cancelable: true, clientX: x, clientY: y, pointerType: 'mouse' }
85
+ const mouseOpts = { bubbles: true, cancelable: true, clientX: x, clientY: y, button: 0 }
86
+
87
+ // Hover — pointer events first, then mouse events (spec order)
88
+ target.dispatchEvent(new PointerEvent('pointerover', pointerOpts))
89
+ target.dispatchEvent(new PointerEvent('pointerenter', { ...pointerOpts, bubbles: false }))
90
+ target.dispatchEvent(new MouseEvent('mouseover', mouseOpts))
91
+ target.dispatchEvent(new MouseEvent('mouseenter', { ...mouseOpts, bubbles: false }))
92
+
93
+ // Press
94
+ target.dispatchEvent(new PointerEvent('pointerdown', pointerOpts))
95
+ target.dispatchEvent(new MouseEvent('mousedown', mouseOpts))
96
+
97
+ // Focus the original element (nearest focusable ancestor), matching browsers.
98
+ element.focus({ preventScroll: true })
99
+
100
+ // Release
101
+ target.dispatchEvent(new PointerEvent('pointerup', pointerOpts))
102
+ target.dispatchEvent(new MouseEvent('mouseup', mouseOpts))
103
+
104
+ // Click — activation behavior (navigation, form submit, etc.) triggers
105
+ // via bubbling from target up to the interactive ancestor.
106
+ target.click()
107
+
108
+ await waitFor(0.2)
109
+ }
110
+
111
+ export async function inputTextElement(element, text) {
112
+ const isContentEditable = element.isContentEditable
113
+ if (!isInputElement(element) && !isTextAreaElement(element) && !isContentEditable) {
114
+ throw new Error('Element is not an input, textarea, or contenteditable')
115
+ }
116
+
117
+ await clickElement(element)
118
+
119
+ if (isContentEditable) {
120
+ // Plan A: synthetic InputEvents (React contenteditable, Quill).
121
+ if (
122
+ element.dispatchEvent(
123
+ new InputEvent('beforeinput', { bubbles: true, cancelable: true, inputType: 'deleteContent' })
124
+ )
125
+ ) {
126
+ element.innerText = ''
127
+ element.dispatchEvent(new InputEvent('input', { bubbles: true, inputType: 'deleteContent' }))
128
+ }
129
+
130
+ if (
131
+ element.dispatchEvent(
132
+ new InputEvent('beforeinput', {
133
+ bubbles: true,
134
+ cancelable: true,
135
+ inputType: 'insertText',
136
+ data: text,
137
+ })
138
+ )
139
+ ) {
140
+ element.innerText = text
141
+ element.dispatchEvent(
142
+ new InputEvent('input', { bubbles: true, inputType: 'insertText', data: text })
143
+ )
144
+ }
145
+
146
+ // Verify Plan A actually inserted the text.
147
+ const planASucceeded = element.innerText.trim() === text.trim()
148
+
149
+ if (!planASucceeded) {
150
+ // Plan B: execCommand fallback (deprecated but widely supported;
151
+ // integrates with the browser undo stack, handled natively by
152
+ // most rich-text editors: Quill, Slate.js, react-contenteditable).
153
+ element.focus()
154
+ const doc = element.ownerDocument
155
+ const selection = (doc.defaultView || window).getSelection()
156
+ const range = doc.createRange()
157
+ range.selectNodeContents(element)
158
+ selection?.removeAllRanges()
159
+ selection?.addRange(range)
160
+
161
+ doc.execCommand('delete', false)
162
+ doc.execCommand('insertText', false, text)
163
+ }
164
+
165
+ element.dispatchEvent(new Event('change', { bubbles: true }))
166
+ element.blur()
167
+ } else {
168
+ // Native prototype setter so React controlled components see the change.
169
+ getNativeValueSetter(element).call(element, text)
170
+ }
171
+
172
+ if (!isContentEditable) {
173
+ element.dispatchEvent(new Event('input', { bubbles: true }))
174
+ }
175
+
176
+ await waitFor(0.1)
177
+ blurLastClickedElement()
178
+ }
179
+
180
+ export async function selectOptionElement(selectElement, optionText) {
181
+ if (!isSelectElement(selectElement)) {
182
+ throw new Error('Element is not a select element')
183
+ }
184
+
185
+ const options = Array.from(selectElement.options)
186
+ const option = options.find((opt) => opt.textContent?.trim() === optionText.trim())
187
+
188
+ if (!option) {
189
+ throw new Error(`Option with text "${optionText}" not found in select element`)
190
+ }
191
+
192
+ selectElement.value = option.value
193
+ selectElement.dispatchEvent(new Event('change', { bubbles: true }))
194
+
195
+ await waitFor(0.1)
196
+ }
197
+
198
+ export async function scrollIntoViewIfNeeded(element) {
199
+ if (typeof element.scrollIntoViewIfNeeded === 'function') {
200
+ element.scrollIntoViewIfNeeded()
201
+ } else if (typeof element.scrollIntoView === 'function') {
202
+ element.scrollIntoView({ behavior: 'auto', block: 'center', inline: 'nearest' })
203
+ }
204
+ }
205
+
206
+ export async function scrollVertically(scroll_amount, element) {
207
+ // Element-specific scrolling if element is provided
208
+ if (element) {
209
+ const targetElement = element
210
+ let currentElement = targetElement
211
+ let scrollSuccess = false
212
+ let scrolledElement = null
213
+ let scrollDelta = 0
214
+ let attempts = 0
215
+ const dy = scroll_amount
216
+
217
+ while (currentElement && attempts < 10) {
218
+ const computedStyle = window.getComputedStyle(currentElement)
219
+ const hasScrollableY =
220
+ /(auto|scroll|overlay)/.test(computedStyle.overflowY) ||
221
+ (computedStyle.scrollbarWidth && computedStyle.scrollbarWidth !== 'auto') ||
222
+ (computedStyle.scrollbarGutter && computedStyle.scrollbarGutter !== 'auto')
223
+ const canScrollVertically = currentElement.scrollHeight > currentElement.clientHeight
224
+
225
+ if (hasScrollableY && canScrollVertically) {
226
+ const beforeScroll = currentElement.scrollTop
227
+ const maxScroll = currentElement.scrollHeight - currentElement.clientHeight
228
+
229
+ let scrollAmount = dy / 3
230
+ if (scrollAmount > 0) {
231
+ scrollAmount = Math.min(scrollAmount, maxScroll - beforeScroll)
232
+ } else {
233
+ scrollAmount = Math.max(scrollAmount, -beforeScroll)
234
+ }
235
+
236
+ currentElement.scrollTop = beforeScroll + scrollAmount
237
+
238
+ const afterScroll = currentElement.scrollTop
239
+ const actualScrollDelta = afterScroll - beforeScroll
240
+
241
+ if (Math.abs(actualScrollDelta) > 0.5) {
242
+ scrollSuccess = true
243
+ scrolledElement = currentElement
244
+ scrollDelta = actualScrollDelta
245
+ break
246
+ }
247
+ }
248
+
249
+ if (currentElement === document.body || currentElement === document.documentElement) {
250
+ break
251
+ }
252
+ currentElement = currentElement.parentElement
253
+ attempts++
254
+ }
255
+
256
+ if (scrollSuccess) {
257
+ return `Scrolled container (${scrolledElement?.tagName}) by ${scrollDelta}px`
258
+ } else {
259
+ return `No scrollable container found for element (${targetElement.tagName})`
260
+ }
261
+ }
262
+
263
+ // Page-level scrolling (default or fallback)
264
+ const dy = scroll_amount
265
+ const bigEnough = (el) => el.clientHeight >= window.innerHeight * 0.5
266
+ const canScroll = (el) =>
267
+ Boolean(
268
+ el &&
269
+ /(auto|scroll|overlay)/.test(getComputedStyle(el).overflowY) &&
270
+ el.scrollHeight > el.clientHeight &&
271
+ bigEnough(el)
272
+ )
273
+
274
+ let el = document.activeElement
275
+ while (el && !canScroll(el) && el !== document.body) el = el.parentElement
276
+
277
+ el = canScroll(el)
278
+ ? el
279
+ : Array.from(document.querySelectorAll('*')).find(canScroll) ||
280
+ document.scrollingElement ||
281
+ document.documentElement
282
+
283
+ if (el === document.scrollingElement || el === document.documentElement || el === document.body) {
284
+ const scrollBefore = window.scrollY
285
+ const scrollMax = document.documentElement.scrollHeight - window.innerHeight
286
+
287
+ window.scrollBy(0, dy)
288
+
289
+ const scrollAfter = window.scrollY
290
+ const scrolled = scrollAfter - scrollBefore
291
+
292
+ if (Math.abs(scrolled) < 1) {
293
+ return dy > 0
294
+ ? `⚠️ Already at the bottom of the page, cannot scroll down further.`
295
+ : `⚠️ Already at the top of the page, cannot scroll up further.`
296
+ }
297
+
298
+ const reachedBottom = dy > 0 && scrollAfter >= scrollMax - 1
299
+ const reachedTop = dy < 0 && scrollAfter <= 1
300
+
301
+ if (reachedBottom) return `✅ Scrolled page by ${scrolled}px. Reached the bottom of the page.`
302
+ if (reachedTop) return `✅ Scrolled page by ${scrolled}px. Reached the top of the page.`
303
+ return `✅ Scrolled page by ${scrolled}px.`
304
+ } else {
305
+ const warningMsg = `The document is not scrollable. Falling back to container scroll.`
306
+
307
+ const scrollBefore = el.scrollTop
308
+
309
+ el.scrollBy({ top: dy, behavior: 'smooth' })
310
+ await waitFor(0.1)
311
+
312
+ const scrollMax = el.scrollHeight - el.clientHeight
313
+ const scrollAfter = el.scrollTop
314
+ const scrolled = scrollAfter - scrollBefore
315
+
316
+ if (Math.abs(scrolled) < 1) {
317
+ return dy > 0
318
+ ? `⚠️ ${warningMsg} Already at the bottom of container (${el.tagName}), cannot scroll down further.`
319
+ : `⚠️ ${warningMsg} Already at the top of container (${el.tagName}), cannot scroll up further.`
320
+ }
321
+
322
+ const reachedBottom = dy > 0 && scrollAfter >= scrollMax - 1
323
+ const reachedTop = dy < 0 && scrollAfter <= 1
324
+
325
+ if (reachedBottom)
326
+ return `✅ ${warningMsg} Scrolled container (${el.tagName}) by ${scrolled}px. Reached the bottom.`
327
+ if (reachedTop)
328
+ return `✅ ${warningMsg} Scrolled container (${el.tagName}) by ${scrolled}px. Reached the top.`
329
+ return `✅ ${warningMsg} Scrolled container (${el.tagName}) by ${scrolled}px.`
330
+ }
331
+ }
332
+
333
+ export async function scrollHorizontally(scroll_amount, element) {
334
+ if (element) {
335
+ const targetElement = element
336
+ let currentElement = targetElement
337
+ let scrollSuccess = false
338
+ let scrolledElement = null
339
+ let scrollDelta = 0
340
+ let attempts = 0
341
+ const dx = scroll_amount
342
+
343
+ while (currentElement && attempts < 10) {
344
+ const computedStyle = window.getComputedStyle(currentElement)
345
+ const hasScrollableX =
346
+ /(auto|scroll|overlay)/.test(computedStyle.overflowX) ||
347
+ (computedStyle.scrollbarWidth && computedStyle.scrollbarWidth !== 'auto') ||
348
+ (computedStyle.scrollbarGutter && computedStyle.scrollbarGutter !== 'auto')
349
+ const canScrollHorizontally = currentElement.scrollWidth > currentElement.clientWidth
350
+
351
+ if (hasScrollableX && canScrollHorizontally) {
352
+ const beforeScroll = currentElement.scrollLeft
353
+ const maxScroll = currentElement.scrollWidth - currentElement.clientWidth
354
+
355
+ let scrollAmount = dx / 3
356
+ if (scrollAmount > 0) {
357
+ scrollAmount = Math.min(scrollAmount, maxScroll - beforeScroll)
358
+ } else {
359
+ scrollAmount = Math.max(scrollAmount, -beforeScroll)
360
+ }
361
+
362
+ currentElement.scrollLeft = beforeScroll + scrollAmount
363
+
364
+ const afterScroll = currentElement.scrollLeft
365
+ const actualScrollDelta = afterScroll - beforeScroll
366
+
367
+ if (Math.abs(actualScrollDelta) > 0.5) {
368
+ scrollSuccess = true
369
+ scrolledElement = currentElement
370
+ scrollDelta = actualScrollDelta
371
+ break
372
+ }
373
+ }
374
+
375
+ if (currentElement === document.body || currentElement === document.documentElement) {
376
+ break
377
+ }
378
+ currentElement = currentElement.parentElement
379
+ attempts++
380
+ }
381
+
382
+ if (scrollSuccess) {
383
+ return `Scrolled container (${scrolledElement?.tagName}) horizontally by ${scrollDelta}px`
384
+ } else {
385
+ return `No horizontally scrollable container found for element (${targetElement.tagName})`
386
+ }
387
+ }
388
+
389
+ const dx = scroll_amount
390
+ const bigEnough = (el) => el.clientWidth >= window.innerWidth * 0.5
391
+ const canScroll = (el) =>
392
+ Boolean(
393
+ el &&
394
+ /(auto|scroll|overlay)/.test(getComputedStyle(el).overflowX) &&
395
+ el.scrollWidth > el.clientWidth &&
396
+ bigEnough(el)
397
+ )
398
+
399
+ let el = document.activeElement
400
+ while (el && !canScroll(el) && el !== document.body) el = el.parentElement
401
+
402
+ el = canScroll(el)
403
+ ? el
404
+ : Array.from(document.querySelectorAll('*')).find(canScroll) ||
405
+ document.scrollingElement ||
406
+ document.documentElement
407
+
408
+ if (el === document.scrollingElement || el === document.documentElement || el === document.body) {
409
+ const scrollBefore = window.scrollX
410
+ const scrollMax = document.documentElement.scrollWidth - window.innerWidth
411
+
412
+ window.scrollBy(dx, 0)
413
+
414
+ const scrollAfter = window.scrollX
415
+ const scrolled = scrollAfter - scrollBefore
416
+
417
+ if (Math.abs(scrolled) < 1) {
418
+ return dx > 0
419
+ ? `⚠️ Already at the right edge of the page, cannot scroll right further.`
420
+ : `⚠️ Already at the left edge of the page, cannot scroll left further.`
421
+ }
422
+
423
+ const reachedRight = dx > 0 && scrollAfter >= scrollMax - 1
424
+ const reachedLeft = dx < 0 && scrollAfter <= 1
425
+
426
+ if (reachedRight) return `✅ Scrolled page by ${scrolled}px. Reached the right edge of the page.`
427
+ if (reachedLeft) return `✅ Scrolled page by ${scrolled}px. Reached the left edge of the page.`
428
+ return `✅ Scrolled page horizontally by ${scrolled}px.`
429
+ } else {
430
+ const warningMsg = `The document is not scrollable. Falling back to container scroll.`
431
+
432
+ const scrollBefore = el.scrollLeft
433
+
434
+ el.scrollBy({ left: dx, behavior: 'smooth' })
435
+ await waitFor(0.1)
436
+
437
+ const scrollMax = el.scrollWidth - el.clientWidth
438
+ const scrollAfter = el.scrollLeft
439
+ const scrolled = scrollAfter - scrollBefore
440
+
441
+ if (Math.abs(scrolled) < 1) {
442
+ return dx > 0
443
+ ? `⚠️ ${warningMsg} Already at the right edge of container (${el.tagName}), cannot scroll right further.`
444
+ : `⚠️ ${warningMsg} Already at the left edge of container (${el.tagName}), cannot scroll left further.`
445
+ }
446
+
447
+ const reachedRight = dx > 0 && scrollAfter >= scrollMax - 1
448
+ const reachedLeft = dx < 0 && scrollAfter <= 1
449
+
450
+ if (reachedRight)
451
+ return `✅ ${warningMsg} Scrolled container (${el.tagName}) by ${scrolled}px. Reached the right edge.`
452
+ if (reachedLeft)
453
+ return `✅ ${warningMsg} Scrolled container (${el.tagName}) by ${scrolled}px. Reached the left edge.`
454
+ return `✅ ${warningMsg} Scrolled container (${el.tagName}) horizontally by ${scrolled}px.`
455
+ }
456
+ }