@xingular/harness 1.0.0 → 1.1.1

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 (257) hide show
  1. package/CHANGELOG.md +33 -5
  2. package/README.md +70 -19
  3. package/dist/cli.js +10321 -5785
  4. package/dist/sdk/agents/builtin.d.ts +2 -0
  5. package/dist/sdk/agents/builtin.js +34 -0
  6. package/dist/sdk/agents/narrow.d.ts +13 -0
  7. package/dist/sdk/agents/narrow.js +29 -0
  8. package/dist/sdk/agents/profiles.d.ts +80 -0
  9. package/dist/sdk/agents/profiles.js +221 -0
  10. package/dist/sdk/annotations/bridge.d.ts +42 -0
  11. package/dist/sdk/annotations/bridge.js +264 -0
  12. package/dist/sdk/annotations/capture.d.ts +116 -0
  13. package/dist/sdk/annotations/capture.js +308 -0
  14. package/dist/sdk/annotations/delivery.d.ts +72 -0
  15. package/dist/sdk/annotations/delivery.js +130 -0
  16. package/dist/sdk/annotations/inspect.d.ts +10 -0
  17. package/dist/sdk/annotations/inspect.js +33 -0
  18. package/dist/sdk/annotations/isolated.d.ts +6 -0
  19. package/dist/sdk/annotations/isolated.js +51 -0
  20. package/dist/sdk/annotations/pageScript.d.ts +28 -0
  21. package/dist/sdk/annotations/pageScript.js +570 -0
  22. package/dist/sdk/annotations/part.d.ts +10 -0
  23. package/dist/sdk/annotations/part.js +144 -0
  24. package/dist/sdk/annotations/queue.d.ts +4 -0
  25. package/dist/sdk/annotations/queue.js +40 -0
  26. package/dist/sdk/annotations/serverMethods.d.ts +3 -0
  27. package/dist/sdk/annotations/serverMethods.js +62 -0
  28. package/dist/sdk/annotations/store.d.ts +160 -0
  29. package/dist/sdk/annotations/store.js +386 -0
  30. package/dist/sdk/browser/index.d.ts +14 -3
  31. package/dist/sdk/browser/index.js +11 -4
  32. package/dist/sdk/browser/serverMethods.d.ts +3 -0
  33. package/dist/sdk/browser/serverMethods.js +7 -0
  34. package/dist/sdk/browser/session.d.ts +1 -0
  35. package/dist/sdk/browser/session.js +45 -9
  36. package/dist/sdk/cli.js +19 -11
  37. package/dist/sdk/core/bus.js +1 -1
  38. package/dist/sdk/core/env.d.ts +1 -1
  39. package/dist/sdk/core/env.js +1 -1
  40. package/dist/sdk/doctor/serverMethods.d.ts +3 -0
  41. package/dist/sdk/doctor/serverMethods.js +8 -0
  42. package/dist/sdk/harness.d.ts +15 -0
  43. package/dist/sdk/harness.js +44 -5
  44. package/dist/sdk/history/recall.js +8 -8
  45. package/dist/sdk/history/serverMethods.d.ts +3 -0
  46. package/dist/sdk/history/serverMethods.js +8 -0
  47. package/dist/sdk/hub/cli.d.ts +1 -1
  48. package/dist/sdk/hub/cli.js +1 -1
  49. package/dist/sdk/hub/schema.d.ts +1 -1
  50. package/dist/sdk/hub/serverMethods.d.ts +3 -0
  51. package/dist/sdk/hub/serverMethods.js +11 -0
  52. package/dist/sdk/index.d.ts +10 -0
  53. package/dist/sdk/index.js +7 -0
  54. package/dist/sdk/mcp/connections.js +14 -7
  55. package/dist/sdk/mcp/serverMethods.d.ts +3 -0
  56. package/dist/sdk/mcp/serverMethods.js +22 -0
  57. package/dist/sdk/plugins/builtin.d.ts +1 -0
  58. package/dist/sdk/plugins/builtin.js +128 -33
  59. package/dist/sdk/plugins/builtinRegistry.d.ts +53 -0
  60. package/dist/sdk/plugins/builtinRegistry.js +49 -0
  61. package/dist/sdk/plugins/cli.d.ts +1 -1
  62. package/dist/sdk/plugins/cli.js +4 -3
  63. package/dist/sdk/prompt/compile.d.ts +5 -2
  64. package/dist/sdk/prompt/compile.js +25 -8
  65. package/dist/sdk/refine/cli.d.ts +1 -1
  66. package/dist/sdk/refine/cli.js +2 -2
  67. package/dist/sdk/refine/serverMethods.d.ts +3 -0
  68. package/dist/sdk/refine/serverMethods.js +20 -0
  69. package/dist/sdk/server/methods.js +9 -86
  70. package/dist/sdk/server/params.d.ts +4 -0
  71. package/dist/sdk/server/params.js +11 -0
  72. package/dist/sdk/sessions/contracts.d.ts +17 -0
  73. package/dist/sdk/sessions/contracts.js +42 -3
  74. package/dist/sdk/sessions/planMode.d.ts +5 -2
  75. package/dist/sdk/sessions/planMode.js +12 -3
  76. package/dist/sdk/sessions/reasoningSummaries.d.ts +2 -2
  77. package/dist/sdk/sessions/reasoningSummaries.js +3 -3
  78. package/dist/sdk/sessions/registry.d.ts +1 -0
  79. package/dist/sdk/sessions/registry.js +4 -1
  80. package/dist/sdk/sessions/runIndex.js +7 -7
  81. package/dist/sdk/sessions/runStore.js +3 -1
  82. package/dist/sdk/sessions/runner.js +2 -2
  83. package/dist/sdk/sessions/sessions.d.ts +17 -5
  84. package/dist/sdk/sessions/sessions.js +79 -12
  85. package/dist/sdk/sessions/sideQuestionServerMethods.d.ts +3 -0
  86. package/dist/sdk/sessions/sideQuestionServerMethods.js +19 -0
  87. package/dist/sdk/sessions/sideQuestions.d.ts +66 -0
  88. package/dist/sdk/sessions/sideQuestions.js +290 -0
  89. package/dist/sdk/sessions/subagentServerMethods.d.ts +3 -0
  90. package/dist/sdk/sessions/subagentServerMethods.js +41 -0
  91. package/dist/sdk/sessions/subagentService.d.ts +13 -2
  92. package/dist/sdk/sessions/subagentService.js +34 -16
  93. package/dist/sdk/sessions/subagents.d.ts +9 -1
  94. package/dist/sdk/sessions/subagents.js +19 -0
  95. package/dist/sdk/terminal/browser.d.ts +62 -0
  96. package/dist/sdk/terminal/browser.js +41 -0
  97. package/dist/sdk/terminal/browserControl.d.ts +29 -0
  98. package/dist/sdk/terminal/browserControl.js +186 -0
  99. package/dist/sdk/terminal/commandHelp.d.ts +17 -0
  100. package/dist/sdk/terminal/commandHelp.js +20 -0
  101. package/dist/sdk/terminal/controller.d.ts +7 -1
  102. package/dist/sdk/terminal/controller.js +34 -2
  103. package/dist/sdk/terminal/opentui/subagentsWidget.d.ts +28 -0
  104. package/dist/sdk/terminal/opentui/subagentsWidget.js +52 -0
  105. package/dist/sdk/terminal/piExtension.js +33 -48
  106. package/dist/sdk/terminal/socket.d.ts +6 -3
  107. package/dist/sdk/terminal/socket.js +40 -9
  108. package/dist/sdk/terminal/standalone.d.ts +2 -0
  109. package/dist/sdk/terminal/standalone.js +50 -10
  110. package/dist/sdk/terminal/subagentMonitor.d.ts +38 -0
  111. package/dist/sdk/terminal/subagentMonitor.js +121 -0
  112. package/dist/sdk/terminal/terminals.d.ts +42 -6
  113. package/dist/sdk/terminal/terminals.js +359 -18
  114. package/dist/sdk/terminal/trajectory.js +2 -2
  115. package/dist/sdk/terminal/tui/annotateCommand.d.ts +3 -0
  116. package/dist/sdk/terminal/tui/annotateCommand.js +22 -0
  117. package/dist/sdk/terminal/tui/commandPages.d.ts +5 -1
  118. package/dist/sdk/terminal/tui/commandPages.js +54 -6
  119. package/dist/sdk/terminal/tui/commands.d.ts +8 -4
  120. package/dist/sdk/terminal/tui/commands.js +181 -58
  121. package/dist/sdk/terminal/tui/dialogGeometry.d.ts +2 -2
  122. package/dist/sdk/terminal/tui/dialogGeometry.js +4 -2
  123. package/dist/sdk/terminal/tui/dialogs.d.ts +6 -1
  124. package/dist/sdk/terminal/tui/dialogs.js +9 -2
  125. package/dist/sdk/terminal/tui/modelPicker.js +1 -6
  126. package/dist/sdk/terminal/tui/page.d.ts +3 -0
  127. package/dist/sdk/terminal/tui/pageOverlay.js +17 -6
  128. package/dist/sdk/terminal/tui/pages/accountsPage.d.ts +26 -4
  129. package/dist/sdk/terminal/tui/pages/accountsPage.js +264 -36
  130. package/dist/sdk/terminal/tui/pages/agentsPage.d.ts +74 -0
  131. package/dist/sdk/terminal/tui/pages/agentsPage.js +190 -0
  132. package/dist/sdk/terminal/tui/pages/annotatePage.d.ts +94 -0
  133. package/dist/sdk/terminal/tui/pages/annotatePage.js +341 -0
  134. package/dist/sdk/terminal/tui/pages/browserPage.d.ts +87 -0
  135. package/dist/sdk/terminal/tui/pages/browserPage.js +738 -0
  136. package/dist/sdk/terminal/tui/pages/doctorPage.d.ts +2 -0
  137. package/dist/sdk/terminal/tui/pages/doctorPage.js +103 -15
  138. package/dist/sdk/terminal/tui/pages/notesPage.d.ts +75 -0
  139. package/dist/sdk/terminal/tui/pages/notesPage.js +201 -0
  140. package/dist/sdk/terminal/tui/pages/settingsPage.d.ts +8 -0
  141. package/dist/sdk/terminal/tui/pages/settingsPage.js +16 -1
  142. package/dist/sdk/terminal/tui/pages/subagentsPage.d.ts +23 -0
  143. package/dist/sdk/terminal/tui/pages/subagentsPage.js +70 -0
  144. package/dist/sdk/terminal/tui/pages/trajectoryPage.d.ts +12 -5
  145. package/dist/sdk/terminal/tui/pages/trajectoryPage.js +156 -83
  146. package/dist/sdk/terminal/tui/pages/worktreesPage.d.ts +72 -0
  147. package/dist/sdk/terminal/tui/pages/worktreesPage.js +203 -0
  148. package/dist/sdk/terminal/tui/planMode.d.ts +1 -1
  149. package/dist/sdk/terminal/tui/planMode.js +1 -1
  150. package/dist/sdk/terminal/tui/pluginCommands.d.ts +14 -0
  151. package/dist/sdk/terminal/tui/pluginCommands.js +27 -0
  152. package/dist/sdk/terminal/tui/questions.d.ts +2 -0
  153. package/dist/sdk/terminal/tui/runtime.js +4 -2
  154. package/dist/sdk/terminal/tui/settingsModel.d.ts +2 -0
  155. package/dist/sdk/terminal/tui/settingsModel.js +32 -2
  156. package/dist/sdk/terminal/tui/sideQuestion.d.ts +26 -26
  157. package/dist/sdk/terminal/tui/sideQuestion.js +131 -98
  158. package/dist/sdk/terminal/tui/ui.d.ts +2 -0
  159. package/dist/sdk/terminal/tui/ui.js +1 -0
  160. package/dist/sdk/tools/askUser.js +26 -8
  161. package/dist/sdk/tools/checklist.js +2 -2
  162. package/dist/sdk/tools/harnessTools.d.ts +2 -0
  163. package/dist/sdk/tools/harnessTools.js +1 -1
  164. package/dist/sdk/tools/spawnSubagent.js +13 -4
  165. package/dist/sdk/web/part.d.ts +10 -0
  166. package/dist/sdk/web/part.js +6 -0
  167. package/dist/sdk/web/serverMethods.d.ts +3 -0
  168. package/dist/sdk/web/serverMethods.js +7 -0
  169. package/dist/sdk/wiki/serverMethods.d.ts +3 -0
  170. package/dist/sdk/wiki/serverMethods.js +26 -0
  171. package/dist/sdk/worktrees/cli.d.ts +11 -0
  172. package/dist/sdk/worktrees/cli.js +146 -0
  173. package/dist/sdk/worktrees/copy.d.ts +9 -0
  174. package/dist/sdk/worktrees/copy.js +111 -0
  175. package/dist/sdk/worktrees/git.d.ts +16 -0
  176. package/dist/sdk/worktrees/git.js +81 -0
  177. package/dist/sdk/worktrees/serverMethods.d.ts +3 -0
  178. package/dist/sdk/worktrees/serverMethods.js +30 -0
  179. package/dist/sdk/worktrees/service.d.ts +73 -0
  180. package/dist/sdk/worktrees/service.js +483 -0
  181. package/dist/sdk/worktrees/setup.d.ts +23 -0
  182. package/dist/sdk/worktrees/setup.js +65 -0
  183. package/dist/sdk/worktrees/types.d.ts +91 -0
  184. package/dist/sdk/worktrees/types.js +1 -0
  185. package/dist/sdk/world/cli.d.ts +2 -2
  186. package/dist/sdk/world/cli.js +6 -6
  187. package/dist/sdk/world/world.d.ts +3 -1
  188. package/dist/sdk/world/world.js +8 -9
  189. package/dist/terminal-opentui.js +14540 -7374
  190. package/dist/terminal-pi.js +285 -666
  191. package/docs/README.md +22 -2
  192. package/docs/architecture/00-overview.md +28 -16
  193. package/docs/architecture/02-sessions-and-runs.md +72 -15
  194. package/docs/architecture/03-variants.md +10 -2
  195. package/docs/architecture/04-prompt.md +26 -6
  196. package/docs/architecture/06-plugins-and-capabilities.md +39 -3
  197. package/docs/architecture/07-tools-and-gate.md +21 -6
  198. package/docs/architecture/09-accounts-and-connectors.md +8 -0
  199. package/docs/architecture/10-server-protocol.md +111 -2
  200. package/docs/architecture/11-terminal.md +185 -5
  201. package/docs/architecture/12-storage.md +15 -1
  202. package/docs/architecture/13-world-tools.md +28 -6
  203. package/docs/architecture/14-surfaces-and-parity.md +89 -0
  204. package/docs/architecture/README.md +15 -4
  205. package/docs/architecture/proposals/PROP-0006-run-checklist.md +4 -2
  206. package/docs/architecture/proposals/PROP-0013-resource-browser.md +58 -0
  207. package/docs/architecture/proposals/PROP-0014-fullscreen-browser-tabs.md +58 -0
  208. package/docs/architecture/proposals/PROP-0015-one-capability-surface.md +88 -0
  209. package/docs/architecture/proposals/PROP-0016-managed-worktrees.md +156 -0
  210. package/docs/architecture/proposals/PROP-0017-page-annotations.md +127 -0
  211. package/docs/architecture/proposals/PROP-0018-annotation-queue-and-app-targets.md +179 -0
  212. package/docs/architecture/proposals/PROP-0019-contextual-side-questions.md +122 -0
  213. package/docs/architecture/proposals/PROP-0020-builtin-feature-plugins.md +105 -0
  214. package/docs/architecture/proposals/PROP-0021-command-namespaces-and-duplicates.md +99 -0
  215. package/docs/architecture/proposals/PROP-0022-plan-only-questions.md +102 -0
  216. package/docs/architecture/proposals/PROP-0023-xagent-cli-name.md +61 -0
  217. package/docs/architecture/proposals/PROP-0024-question-sketch-previews.md +130 -0
  218. package/docs/architecture/proposals/PROP-0025-contextual-question-wireframes.md +49 -0
  219. package/docs/architecture/proposals/PROP-0026-connected-sketch-display.md +94 -0
  220. package/docs/architecture/proposals/PROP-0027-xingular-agent-product-identity.md +60 -0
  221. package/docs/architecture/proposals/PROP-0028-normalize-question-wireframes.md +55 -0
  222. package/docs/architecture/proposals/PROP-0029-agent-profiles.md +124 -0
  223. package/docs/architecture/proposals/PROP-0030-subagent-title.md +86 -0
  224. package/docs/architecture/proposals/README.md +25 -0
  225. package/docs/decisions/ADR-0009-xharness-command.md +6 -5
  226. package/docs/decisions/ADR-0014-public-release-baseline.md +3 -2
  227. package/docs/decisions/ADR-0015-resource-browser.md +28 -0
  228. package/docs/decisions/ADR-0016-fullscreen-browser-tabs.md +31 -0
  229. package/docs/decisions/ADR-0017-one-capability-surface.md +34 -0
  230. package/docs/decisions/ADR-0018-builtin-feature-plugins.md +35 -0
  231. package/docs/decisions/ADR-0019-managed-git-worktrees.md +31 -0
  232. package/docs/decisions/ADR-0020-page-annotations.md +37 -0
  233. package/docs/decisions/ADR-0021-required-command-namespaces.md +40 -0
  234. package/docs/decisions/ADR-0022-plan-only-question-tools.md +34 -0
  235. package/docs/decisions/ADR-0023-contextual-side-question-runs.md +35 -0
  236. package/docs/decisions/ADR-0024-xagent-command.md +40 -0
  237. package/docs/decisions/ADR-0025-question-choice-sketches.md +49 -0
  238. package/docs/decisions/ADR-0026-contextual-question-wireframes.md +58 -0
  239. package/docs/decisions/ADR-0027-connected-sketch-display.md +60 -0
  240. package/docs/decisions/ADR-0028-xingular-agent-product-identity.md +56 -0
  241. package/docs/decisions/ADR-0029-normalized-question-wireframe-borders.md +33 -0
  242. package/docs/decisions/ADR-0030-agent-profiles.md +43 -0
  243. package/docs/doctor.md +10 -4
  244. package/docs/features/README.md +26 -0
  245. package/docs/features/interfaces/README.md +1 -1
  246. package/docs/future-work.md +48 -0
  247. package/docs/parity.md +78 -0
  248. package/docs/releases/1.0.0.md +2 -2
  249. package/docs/releases/4.0.0.md +46 -0
  250. package/docs/releases/README.md +20 -15
  251. package/docs/study/00-index.md +5 -0
  252. package/docs/study/autonomous-engineering-sources.md +277 -0
  253. package/docs/study/autonomy-sources.md +147 -0
  254. package/docs/study/proactive-autonomy.md +408 -0
  255. package/docs/study/tui-resource-browser.md +78 -0
  256. package/docs/terminal-tui.md +325 -93
  257. package/package.json +3 -3
@@ -0,0 +1,570 @@
1
+ // The page helper both front ends run inside a web page to capture and reveal note targets: the
2
+ // Harness's own headed browser and a host's web view (PROP-0017). It is plain DOM code with a pull
3
+ // model: the front end calls it with one operation (`identity`, `begin`, `capture`, `next`, `toast`,
4
+ // `cancel`, `resolve`, `reveal`) and reads the JSON it returns. `begin` with kind `inspect` turns on
5
+ // the inspector (PROP-0018): hover outlines, a click or a text drag opens a comment popover, and
6
+ // each submitted comment waits for `next`. The page never calls out. Everything it
7
+ // returns is page-controlled data, never authentication; the caller validates it before saving.
8
+ // No note bodies or host capabilities enter the page.
9
+ /** The helper's source. `__ARGS__` is replaced with one JSON operation; the expression evaluates to a JSON string. */
10
+ export const PAGE_SCRIPT = String.raw `(() => {
11
+ const args = __ARGS__
12
+ try {
13
+ const key = Symbol.for('xingular.browser.annotations.page.v2')
14
+ if (!globalThis[key]) {
15
+ globalThis[key] = (() => {
16
+ // This fallback also works on non-secure HTTP pages. Neither token is a secret.
17
+ const documentId = globalThis.crypto?.randomUUID?.() ?? 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, char => {
18
+ const random = Math.floor(Math.random() * 16)
19
+ return (char === 'x' ? random : (random & 3) | 8).toString(16)
20
+ })
21
+ const excluded = 'script,style,noscript,template,input,textarea,select,option,iframe,object,embed,[contenteditable],[role="textbox"],[hidden],[inert],[aria-hidden="true"],[data-sensitive],[data-private],[data-annotation-overlay],[data-harness-annotation-overlay],[autocomplete*="password"],[autocomplete*="cc-"],[autocomplete*="one-time-code"]'
22
+ const overlays = new WeakSet()
23
+ const cp = value => Array.from(value)
24
+ const norm = value => String(value ?? '').replace(/\s+/gu, ' ').trim()
25
+ const bounded = (value, size) => cp(norm(value)).slice(0, size).join('')
26
+ const validString = (value, size) => typeof value === 'string' && cp(value).length <= size
27
+ let pending = null
28
+ let picked = null
29
+ let listeners = []
30
+ let mark = null
31
+ let overlay = null
32
+ let frame = 0
33
+ let observer = null
34
+ let mutations = null
35
+
36
+ const allowed = element => {
37
+ if (!element || element.getRootNode() !== document || element.closest(excluded)) return false
38
+ for (let current = element; current; current = current.parentElement) {
39
+ if (overlays.has(current) || current.shadowRoot) return false
40
+ const style = getComputedStyle(current)
41
+ if (style.display === 'none' || style.visibility === 'hidden' || style.visibility === 'collapse' || Number(style.opacity) === 0) return false
42
+ }
43
+ return element.getClientRects().length > 0
44
+ }
45
+ const block = element => {
46
+ while (element && element !== document.body) {
47
+ if (!['inline', 'contents', 'inline-block'].includes(getComputedStyle(element).display)) return element
48
+ element = element.parentElement
49
+ }
50
+ return document.body
51
+ }
52
+ // Positions are normalized Unicode code points. Mapping offsets remain DOM UTF-16.
53
+ const stream = (root = document.body) => {
54
+ const chars = []
55
+ const map = []
56
+ if (!root) return { chars, map }
57
+ const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT)
58
+ let node
59
+ let previousBlock
60
+ let visited = 0
61
+ const append = (char, start, end) => {
62
+ if (/\s/u.test(char)) {
63
+ if (!chars.length) return
64
+ if (chars.at(-1) === ' ') { map.at(-1).end = end; return }
65
+ char = ' '
66
+ }
67
+ chars.push(char)
68
+ map.push({ start, end })
69
+ }
70
+ while ((node = walker.nextNode())) {
71
+ if (++visited > 20000 || chars.length + node.data.length > 250000) throw new Error('This page is too large to anchor safely. Use a page note instead.')
72
+ if (!allowed(node.parentElement)) continue
73
+ const currentBlock = block(node.parentElement)
74
+ if (previousBlock && currentBlock !== previousBlock && chars.length) {
75
+ append(' ', map.at(-1).end, [node, 0])
76
+ }
77
+ previousBlock = currentBlock
78
+ let offset = 0
79
+ for (const char of node.data) {
80
+ append(char, [node, offset], [node, offset + char.length])
81
+ offset += char.length
82
+ }
83
+ }
84
+ if (chars.at(-1) === ' ') { chars.pop(); map.pop() }
85
+ return { chars, map }
86
+ }
87
+ const text = element => stream(element).chars.join('')
88
+ const role = element => norm(element.getAttribute('role')) || ({ BUTTON: 'button', A: element.hasAttribute('href') ? 'link' : '', IMG: 'img', SUMMARY: 'button' }[element.tagName] ?? '')
89
+ const name = element => {
90
+ const labelled = norm(element.getAttribute('aria-labelledby')).split(' ').filter(Boolean).map(id => {
91
+ const label = document.getElementById(id)
92
+ return allowed(label) ? text(label) : ''
93
+ }).join(' ')
94
+ return bounded(labelled || element.getAttribute('aria-label') || (element.tagName === 'IMG' ? element.getAttribute('alt') : '') || text(element), 200)
95
+ }
96
+ const describe = element => {
97
+ let selector = element.tagName.toLowerCase()
98
+ if (element.id) selector = '#' + CSS.escape(element.id)
99
+ else if (element.getAttribute('data-testid')) selector = '[data-testid="' + CSS.escape(element.getAttribute('data-testid')) + '"]'
100
+ return {
101
+ kind: 'element', tag: element.tagName.toLowerCase(), role: bounded(role(element), 80),
102
+ name: name(element), text: bounded(text(element), 500),
103
+ context: bounded(allowed(element.parentElement) ? text(element.parentElement) : '', 200),
104
+ selector: cp(selector).length <= 500 ? selector : element.tagName.toLowerCase()
105
+ }
106
+ }
107
+ const rangeFor = (data, start, end) => {
108
+ const range = document.createRange()
109
+ range.setStart(...data.map[start].start)
110
+ range.setEnd(...data.map[end - 1].end)
111
+ return range
112
+ }
113
+ const selectionTarget = () => {
114
+ const selection = getSelection()
115
+ if (!selection || selection.rangeCount !== 1 || selection.isCollapsed) return null
116
+ const range = selection.getRangeAt(0)
117
+ if (range.startContainer.getRootNode() !== document || range.endContainer.getRootNode() !== document) return null
118
+ const data = stream()
119
+ const same = (point, node, offset) => point[0] === node && point[1] === offset
120
+ const indices = data.map.flatMap((entry, index) =>
121
+ range.comparePoint(...entry.end) >= 0 && range.comparePoint(...entry.start) <= 0 &&
122
+ !same(entry.end, range.startContainer, range.startOffset) && !same(entry.start, range.endContainer, range.endOffset) ? [index] : [])
123
+ if (!indices.length) return null
124
+ let start = indices[0]
125
+ let end = indices.at(-1) + 1
126
+ while (data.chars[start] === ' ') start++
127
+ while (data.chars[end - 1] === ' ') end--
128
+ if (end <= start || end - start > 4000) return null
129
+ // Refuse ranges crossing excluded subtrees rather than silently capturing partial secrets.
130
+ const walker = document.createTreeWalker(range.commonAncestorContainer, NodeFilter.SHOW_ELEMENT)
131
+ let element
132
+ if (range.commonAncestorContainer.nodeType === Node.TEXT_NODE && !allowed(range.commonAncestorContainer.parentElement)) return null
133
+ while ((element = walker.nextNode())) {
134
+ if (!allowed(element) && range.intersectsNode(element)) return null
135
+ }
136
+ return { kind: 'text', exact: data.chars.slice(start, end).join(''), prefix: data.chars.slice(Math.max(0, start - 128), start).join(''), suffix: data.chars.slice(end, end + 128).join(''), start, end }
137
+ }
138
+ const resolve = target => {
139
+ if (!target || typeof target !== 'object') throw new Error('Invalid target')
140
+ if (target.kind === 'page') return { state: 'matched' }
141
+ if (target.kind === 'text') {
142
+ if (!validString(target.exact, 4000) || !target.exact || norm(target.exact) !== target.exact ||
143
+ !validString(target.prefix, 128) || !validString(target.suffix, 128) ||
144
+ !Number.isSafeInteger(target.start) || !Number.isSafeInteger(target.end) || target.start < 0 || target.end <= target.start) throw new Error('Invalid text target')
145
+ const data = stream()
146
+ const quote = cp(target.exact)
147
+ const prefix = cp(target.prefix)
148
+ const suffix = cp(target.suffix)
149
+ const matches = []
150
+ // Do not prefer the stored position: a duplicate there is still ambiguous.
151
+ const content = data.chars.join('')
152
+ let utf16Index = content.indexOf(target.exact)
153
+ let previousUtf16 = 0
154
+ let index = 0
155
+ while (utf16Index >= 0) {
156
+ index += cp(content.slice(previousUtf16, utf16Index)).length
157
+ previousUtf16 = utf16Index
158
+ utf16Index = content.indexOf(target.exact, utf16Index + 1)
159
+ const end = index + quote.length
160
+ if (prefix.length && data.chars.slice(Math.max(0, index - prefix.length), index).join('') !== target.prefix) continue
161
+ if (suffix.length && data.chars.slice(end, end + suffix.length).join('') !== target.suffix) continue
162
+ matches.push([index, end])
163
+ if (matches.length > 1) return { state: 'ambiguous', reason: 'Quote and context match more than once' }
164
+ }
165
+ if (!matches.length) return { state: 'unresolved', reason: 'Quote or context changed or is unavailable' }
166
+ return { state: 'matched', range: rangeFor(data, ...matches[0]) }
167
+ }
168
+ if (target.kind === 'element') {
169
+ if (!validString(target.tag, 100) || !/^[a-z][a-z0-9-]*$/.test(target.tag) ||
170
+ !validString(target.role, 200) || !validString(target.name, 200) || !validString(target.text, 500) ||
171
+ !validString(target.context, 200) || !validString(target.selector, 500)) throw new Error('Invalid element target')
172
+ if (!target.name && !target.text) return { state: 'unresolved', reason: 'No semantic evidence' }
173
+ const elements = document.getElementsByTagName(target.tag)
174
+ if (elements.length > 2000) return { state: 'unresolved', reason: 'Too many element candidates to resolve safely' }
175
+ let candidates = Array.from(elements).filter(allowed).filter(element => {
176
+ const candidate = describe(element)
177
+ return candidate.role === target.role && candidate.name === target.name && candidate.text === target.text
178
+ })
179
+ if (candidates.length > 1 && target.context) candidates = candidates.filter(element => describe(element).context === target.context)
180
+ if (candidates.length > 1) return { state: 'ambiguous', reason: 'Element semantics are duplicated' }
181
+ if (!candidates.length) return { state: 'unresolved', reason: 'Element semantics changed or are unavailable' }
182
+ return { state: 'matched', element: candidates[0] }
183
+ }
184
+ throw new Error('Unknown target kind')
185
+ }
186
+ const listen = (node, type, handler, options = true) => {
187
+ node.addEventListener(type, handler, options)
188
+ listeners.push(() => node.removeEventListener(type, handler, options))
189
+ }
190
+ const clear = () => {
191
+ listeners.forEach(remove => remove())
192
+ listeners = []
193
+ observer?.disconnect()
194
+ observer = null
195
+ mutations?.disconnect()
196
+ mutations = null
197
+ if (frame) clearTimeout(frame)
198
+ frame = 0
199
+ overlay?.remove()
200
+ overlay = null
201
+ mark = null
202
+ }
203
+ const draw = () => {
204
+ frame = 0
205
+ if (!mark) return
206
+ const match = resolve(mark)
207
+ overlay?.remove()
208
+ overlay = null
209
+ if (match.state !== 'matched' || (!match.range && !match.element)) return
210
+ const rects = match.range ? Array.from(match.range.getClientRects()) : [match.element.getBoundingClientRect()]
211
+ // Clip marks to nested scrollports, not just the top-level viewport.
212
+ const anchor = match.element ?? match.range.commonAncestorContainer
213
+ let ancestor = anchor.nodeType === Node.ELEMENT_NODE ? anchor : anchor.parentElement
214
+ let clip = { left: 0, top: 0, right: innerWidth, bottom: innerHeight }
215
+ if (match.element) ancestor = ancestor.parentElement
216
+ for (; ancestor; ancestor = ancestor.parentElement) {
217
+ const style = getComputedStyle(ancestor)
218
+ const rect = ancestor.getBoundingClientRect()
219
+ if (/(auto|scroll|hidden|clip)/.test(style.overflowX)) {
220
+ clip.left = Math.max(clip.left, rect.left)
221
+ clip.right = Math.min(clip.right, rect.right)
222
+ }
223
+ if (/(auto|scroll|hidden|clip)/.test(style.overflowY)) {
224
+ clip.top = Math.max(clip.top, rect.top)
225
+ clip.bottom = Math.min(clip.bottom, rect.bottom)
226
+ }
227
+ }
228
+ overlay = document.createElement('div')
229
+ overlays.add(overlay)
230
+ overlay.setAttribute('data-harness-annotation-overlay', '')
231
+ overlay.setAttribute('aria-hidden', 'true')
232
+ overlay.style.cssText = 'all:initial!important;position:fixed!important;inset:0!important;pointer-events:none!important;z-index:2147483647!important;overflow:hidden!important;'
233
+ for (const original of rects) {
234
+ const rect = { left: Math.max(original.left, clip.left), top: Math.max(original.top, clip.top) }
235
+ rect.width = Math.min(original.right, clip.right) - rect.left
236
+ rect.height = Math.min(original.bottom, clip.bottom) - rect.top
237
+ if (rect.width <= 0 || rect.height <= 0) continue
238
+ const box = document.createElement('div')
239
+ box.style.cssText = 'all:initial!important;position:absolute!important;pointer-events:none!important;box-sizing:border-box!important;left:' + rect.left + 'px!important;top:' + rect.top + 'px!important;width:' + rect.width + 'px!important;height:' + rect.height + 'px!important;border:2px solid Highlight!important;background:color-mix(in srgb, Highlight 12%, transparent)!important;'
240
+ overlay.append(box)
241
+ }
242
+ document.documentElement.append(overlay)
243
+ }
244
+ const schedule = () => { if (!frame) frame = setTimeout(draw, 16) }
245
+ const envelope = target => ({ url: location.href, title: bounded(document.title, 200), documentId, target })
246
+
247
+ // ---- Inspect mode (PROP-0018): hover outlines a component, a click opens a comment popover.
248
+ // Everything drawn lives in a closed shadow root, so page CSS and scripts cannot reach it;
249
+ // a page can still call this helper, so hosts treat each result as untrusted.
250
+ let inspect = null
251
+ const INSPECT_CSS = ':host{all:initial!important}' +
252
+ '.layer{position:fixed;inset:0;pointer-events:none;z-index:2147483647;font:12px/1.4 -apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;color-scheme:light dark}' +
253
+ '.box{position:fixed;box-sizing:border-box;border:2px solid #3b82f6;background:rgba(59,130,246,.12);border-radius:2px;display:none}' +
254
+ '.box.frozen{border-color:#f97316;background:rgba(249,115,22,.12)}' +
255
+ '.chip{position:fixed;display:none;padding:2px 6px;border-radius:4px;background:#1e293b;color:#f8fafc;font-size:11px;white-space:nowrap;max-width:360px;overflow:hidden;text-overflow:ellipsis}' +
256
+ '.chip b{font-weight:600;color:#93c5fd}' +
257
+ '.pop{position:fixed;display:none;width:320px;box-sizing:border-box;padding:10px;border-radius:8px;pointer-events:auto;background:Canvas;color:CanvasText;border:1px solid rgba(127,127,127,.35);box-shadow:0 8px 28px rgba(0,0,0,.28)}' +
258
+ '.what{font-size:11px;opacity:.7;margin:0 0 6px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}' +
259
+ 'textarea{all:unset;display:block;box-sizing:border-box;width:100%;min-height:68px;max-height:200px;padding:6px 8px;border-radius:6px;border:1px solid rgba(127,127,127,.45);font:inherit;font-size:13px;white-space:pre-wrap;overflow:auto;background:Field;color:FieldText}' +
260
+ 'textarea:focus{outline:2px solid #3b82f6;outline-offset:-1px}' +
261
+ '.row{display:flex;gap:6px;margin-top:8px;justify-content:flex-end;flex-wrap:wrap}' +
262
+ 'button{all:unset;cursor:pointer;padding:4px 9px;border-radius:6px;font-size:12px;border:1px solid rgba(127,127,127,.45)}' +
263
+ 'button:hover{background:rgba(127,127,127,.15)}button:focus-visible{outline:2px solid #3b82f6}' +
264
+ 'button.primary{background:#2563eb;border-color:#2563eb;color:#fff}button.primary:hover{background:#1d4ed8}' +
265
+ 'button[disabled]{opacity:.45;cursor:default}' +
266
+ '.hint{margin-top:6px;font-size:10px;opacity:.55;text-align:right}' +
267
+ '.toast{position:fixed;right:16px;bottom:16px;padding:6px 10px;border-radius:6px;background:#1e293b;color:#f8fafc;font-size:12px;display:none}'
268
+ const label = element => {
269
+ const tag = element.tagName.toLowerCase()
270
+ const id = element.id ? '#' + element.id : ''
271
+ const classes = typeof element.className === 'string' ? element.className.trim().split(/\s+/).filter(Boolean).slice(0, 2).map(name => '.' + name).join('') : ''
272
+ const rect = element.getBoundingClientRect()
273
+ return { tag, rest: (id || classes) + ' ' + Math.round(rect.width) + '×' + Math.round(rect.height) }
274
+ }
275
+ const place = (node, rect, width, height, gap) => {
276
+ let top = rect.bottom + gap
277
+ if (top + height > innerHeight - 4 && rect.top - gap - height >= 4) top = rect.top - gap - height
278
+ top = Math.max(4, Math.min(top, innerHeight - height - 4))
279
+ const left = Math.max(4, Math.min(rect.left, innerWidth - width - 4))
280
+ node.style.top = top + 'px'
281
+ node.style.left = left + 'px'
282
+ }
283
+ const stopInspect = () => {
284
+ if (!inspect) return
285
+ inspect.listeners.forEach(remove => remove())
286
+ inspect.host.remove()
287
+ inspect = null
288
+ }
289
+ const startInspect = captureId => {
290
+ stopInspect()
291
+ const host = document.createElement('div')
292
+ overlays.add(host)
293
+ host.setAttribute('data-harness-annotation-overlay', '')
294
+ host.style.cssText = 'all:initial!important;position:fixed!important;inset:0!important;pointer-events:none!important;z-index:2147483647!important;'
295
+ const shadow = host.attachShadow({ mode: 'closed' })
296
+ shadow.innerHTML = '<style>' + INSPECT_CSS + '</style><div class="layer"><div class="box"></div><div class="chip"></div>' +
297
+ '<div class="pop" role="dialog" aria-label="Annotate"><p class="what"></p><textarea placeholder="What should change?" aria-label="Comment"></textarea>' +
298
+ '<div class="row"><button data-action="save">Create annotation</button><button data-action="queue">Ask agent to fix</button><button class="primary" data-action="send">Proceed now</button></div>' +
299
+ '<div class="hint">Esc to cancel · ⌘↵ proceed now</div></div><div class="toast" role="status"></div></div>'
300
+ const $ = selector => shadow.querySelector(selector)
301
+ const state = { captureId, host, shadow, listeners: [], hovered: null, frozen: null, frame: 0, results: [], box: $('.box'), chip: $('.chip'), pop: $('.pop'), what: $('.what'), input: $('textarea'), toast: $('.toast'), toastTimer: 0 }
302
+ inspect = state
303
+ const on = (node, type, handler, options = true) => {
304
+ node.addEventListener(type, handler, options)
305
+ state.listeners.push(() => node.removeEventListener(type, handler, options))
306
+ }
307
+ const rectOf = picked => picked.range ? picked.range.getBoundingClientRect() : picked.element.getBoundingClientRect()
308
+ const paint = () => {
309
+ state.frame = 0
310
+ const current = state.frozen ?? (state.hovered ? { element: state.hovered } : null)
311
+ if (!current || (current.element && !current.element.isConnected)) {
312
+ state.box.style.display = state.chip.style.display = 'none'
313
+ return
314
+ }
315
+ const rect = rectOf(current)
316
+ Object.assign(state.box.style, { display: 'block', left: rect.left + 'px', top: rect.top + 'px', width: rect.width + 'px', height: rect.height + 'px' })
317
+ state.box.classList.toggle('frozen', Boolean(state.frozen))
318
+ if (state.frozen) {
319
+ state.chip.style.display = 'none'
320
+ place(state.pop, rect, 320, state.pop.offsetHeight || 170, 8)
321
+ } else {
322
+ const text = label(current.element)
323
+ state.chip.innerHTML = ''
324
+ const tag = document.createElement('b')
325
+ tag.textContent = text.tag
326
+ state.chip.append(tag, document.createTextNode(text.rest))
327
+ state.chip.style.display = 'block'
328
+ place(state.chip, { left: rect.left, top: rect.top - 22, bottom: rect.top - 22 + 0 }, Math.min(360, state.chip.offsetWidth || 120), 20, 0)
329
+ if (rect.top < 26) place(state.chip, { left: rect.left, top: rect.bottom, bottom: rect.bottom }, Math.min(360, state.chip.offsetWidth || 120), 20, 2)
330
+ }
331
+ }
332
+ const repaint = () => { if (!state.frame) state.frame = requestAnimationFrame(paint) }
333
+ const closePopover = () => {
334
+ state.frozen = null
335
+ state.pop.style.display = 'none'
336
+ state.input.value = ''
337
+ repaint()
338
+ }
339
+ const openPopover = picked => {
340
+ state.frozen = picked
341
+ const target = picked.target
342
+ state.what.textContent = target.kind === 'text' ? '“' + bounded(target.exact, 80) + '”' : '<' + target.tag + '> ' + (target.name || target.text || target.selector)
343
+ state.pop.style.display = 'block'
344
+ state.chip.style.display = 'none'
345
+ paint()
346
+ state.input.focus()
347
+ setTimeout(() => { if (state.frozen) state.input.focus() }, 0)
348
+ }
349
+ const submit = action => {
350
+ if (!state.frozen) return
351
+ const body = state.input.value.trim()
352
+ if (!body) { state.input.focus(); return }
353
+ state.results.push({ ...envelope(state.frozen.target), body: cp(body).slice(0, 10000).join(''), action })
354
+ if (state.results.length > 20) state.results.shift()
355
+ getSelection()?.removeAllRanges()
356
+ closePopover()
357
+ }
358
+ // The popover's own events stay inside it: the page sees nothing typed or clicked there.
359
+ for (const type of ['keydown', 'keyup', 'keypress', 'input', 'beforeinput', 'paste', 'copy', 'cut', 'pointerdown', 'mousedown', 'pointerup', 'mouseup', 'click', 'dblclick', 'wheel', 'focusin', 'focusout']) {
360
+ on(state.pop, type, event => event.stopPropagation(), false)
361
+ }
362
+ on(state.pop, 'click', event => {
363
+ const button = event.composedPath().find(node => node instanceof HTMLElement && node.dataset?.action)
364
+ if (button) submit(button.dataset.action)
365
+ }, false)
366
+ on(state.input, 'keydown', event => {
367
+ if (event.key === 'Escape') { event.preventDefault(); closePopover() }
368
+ if (event.key === 'Enter' && (event.metaKey || event.ctrlKey)) { event.preventDefault(); submit(event.shiftKey ? 'queue' : 'send') }
369
+ }, false)
370
+ const inPopover = event => event.composedPath().includes(host)
371
+ const pickable = element => element instanceof Element && allowed(element) && !element.tagName.includes('-')
372
+ on(window, 'pointermove', event => {
373
+ if (state.frozen || inPopover(event)) return
374
+ const element = event.composedPath()[0]
375
+ const next = pickable(element) ? element : null
376
+ if (next !== state.hovered) { state.hovered = next; repaint() }
377
+ }, { capture: true, passive: true })
378
+ let down = null
379
+ // The page never sees presses on what is being inspected; the browser may still select text.
380
+ for (const type of ['pointerdown', 'mousedown']) {
381
+ on(window, type, event => {
382
+ if (!event.isTrusted || inPopover(event)) return
383
+ event.stopImmediatePropagation()
384
+ if (event.type === 'pointerdown' && event.button === 0) down = { x: event.clientX, y: event.clientY, element: event.composedPath()[0] }
385
+ }, { capture: true, passive: false })
386
+ }
387
+ for (const type of ['pointerup', 'mouseup', 'click', 'dblclick', 'auxclick', 'contextmenu', 'submit']) {
388
+ on(window, type, event => {
389
+ if (inPopover(event)) return
390
+ event.preventDefault()
391
+ event.stopImmediatePropagation()
392
+ if (!event.isTrusted || event.button !== 0 || !down || state.frozen) return
393
+ const start = down
394
+ const moved = Math.hypot(event.clientX - start.x, event.clientY - start.y) > 4
395
+ if (event.type === 'pointerup') {
396
+ // A drag selects text; the popover opens on the selection.
397
+ if (!moved) return
398
+ down = null
399
+ let target = null
400
+ try { target = selectionTarget() } catch { target = null }
401
+ if (target) openPopover({ target, range: getSelection().getRangeAt(0).cloneRange() })
402
+ return
403
+ }
404
+ if (event.type !== 'click' || moved) return
405
+ down = null
406
+ const element = event.composedPath()[0]
407
+ if (element !== start.element || !pickable(element)) return
408
+ openPopover({ target: describe(element), element })
409
+ }, { capture: true, passive: false })
410
+ }
411
+ on(window, 'keydown', event => {
412
+ if (event.key === 'Escape' && !inPopover(event)) {
413
+ if (state.frozen) closePopover()
414
+ else { state.hovered = null; repaint() }
415
+ }
416
+ }, true)
417
+ on(window, 'scroll', repaint, { capture: true, passive: true })
418
+ on(window, 'resize', repaint)
419
+ document.documentElement.append(host)
420
+ return state
421
+ }
422
+ const showToast = text => {
423
+ if (!inspect) return
424
+ const state = inspect
425
+ state.toast.textContent = bounded(text, 120)
426
+ state.toast.style.display = 'block'
427
+ clearTimeout(state.toastTimer)
428
+ state.toastTimer = setTimeout(() => { state.toast.style.display = 'none' }, 2200)
429
+ }
430
+ return operation => {
431
+ switch (operation.operation) {
432
+ case 'identity': return { url: location.href, title: bounded(document.title, 200), documentId }
433
+ case 'begin': {
434
+ if (!['text', 'element', 'inspect'].includes(operation.kind) || !validString(operation.captureId, 200) || !operation.captureId) throw new Error('Invalid capture request')
435
+ if (operation.kind === 'inspect') {
436
+ // Stays on across in-page navigation, until cancel or a new document.
437
+ clear()
438
+ pending = null
439
+ picked = null
440
+ startInspect(operation.captureId)
441
+ return null
442
+ }
443
+ stopInspect()
444
+ clear()
445
+ picked = null
446
+ pending = { kind: operation.kind, captureId: operation.captureId }
447
+ const captureUrl = location.href
448
+ const expired = setTimeout(() => { clear(); pending = null; picked = null }, 300000)
449
+ const navigation = setInterval(() => {
450
+ if (location.href !== captureUrl) { clear(); pending = null; picked = null }
451
+ }, 250)
452
+ listeners.push(() => clearTimeout(expired), () => clearInterval(navigation))
453
+ listen(window, 'keydown', event => {
454
+ if (event.key === 'Escape') { clear(); pending = null; picked = null }
455
+ })
456
+ if (pending.kind === 'text') {
457
+ listen(document, 'selectionchange', () => {
458
+ try { const target = selectionTarget(); if (target) picked = envelope(target) } catch { picked = null }
459
+ })
460
+ }
461
+ if (pending.kind === 'element') {
462
+ let chosen = null
463
+ const choose = event => {
464
+ if (!event.isTrusted || event.button !== 0 || !pending || pending.kind !== 'element') return
465
+ // Never traverse frames/shadow roots, including retargeted closed shadow events.
466
+ const element = event.target
467
+ if (!(element instanceof Element) || !allowed(element) || event.composedPath()[0] !== element) return
468
+ if (element.tagName.includes('-')) return
469
+ event.preventDefault()
470
+ event.stopImmediatePropagation()
471
+ if (event.type === 'pointerdown') chosen = element
472
+ if (event.type === 'click' && chosen === element) {
473
+ picked = envelope(describe(element))
474
+ // Keep suppression through this click; capture/cancel removes the listeners.
475
+ }
476
+ }
477
+ for (const type of ['pointerdown', 'mousedown', 'pointerup', 'mouseup', 'click', 'dblclick', 'auxclick']) listen(window, type, choose, { capture: true, passive: false })
478
+ }
479
+ return null
480
+ }
481
+ case 'capture': {
482
+ if (!pending || operation.captureId !== pending.captureId) return null
483
+ const result = picked ?? (pending.kind === 'text' ? selectionTarget() : null)
484
+ if (!result) return null
485
+ const captured = picked ?? envelope(result)
486
+ clear()
487
+ // The trusted host previews, then rechecks this same snapshot on Save.
488
+ picked = captured
489
+ return captured
490
+ }
491
+ case 'next': {
492
+ // Inspect mode: whether it is still on, and the next comment the person submitted.
493
+ const active = Boolean(inspect && inspect.captureId === operation.captureId)
494
+ return { url: location.href, title: bounded(document.title, 200), documentId, active, result: active ? inspect.results.shift() ?? null : null }
495
+ }
496
+ case 'toast':
497
+ if (!validString(operation.text, 400)) throw new Error('Invalid toast')
498
+ showToast(operation.text)
499
+ return null
500
+ case 'cancel':
501
+ if (inspect && (!operation.captureId || operation.captureId === inspect.captureId)) { stopInspect(); return null }
502
+ if (operation.captureId && operation.captureId !== pending?.captureId) return null
503
+ clear()
504
+ pending = null
505
+ picked = null
506
+ return null
507
+ case 'resolve':
508
+ case 'reveal': {
509
+ const match = resolve(operation.target)
510
+ if (operation.operation === 'reveal') {
511
+ clear()
512
+ pending = null
513
+ picked = null
514
+ if (match.state === 'matched' && operation.target.kind !== 'page') {
515
+ const element = match.element ?? match.range.startContainer.parentElement
516
+ element.scrollIntoView({ block: 'center', inline: 'nearest', behavior: 'instant' })
517
+ mark = operation.target
518
+ listen(window, 'scroll', schedule)
519
+ listen(window, 'resize', schedule)
520
+ if (window.visualViewport) {
521
+ listen(window.visualViewport, 'scroll', schedule)
522
+ listen(window.visualViewport, 'resize', schedule)
523
+ }
524
+ if (globalThis.ResizeObserver) {
525
+ observer = new ResizeObserver(schedule)
526
+ observer.observe(document.documentElement)
527
+ observer.observe(element)
528
+ }
529
+ draw()
530
+ mutations = new MutationObserver(records => {
531
+ if (records.some(record => {
532
+ if (overlays.has(record.target) || overlays.has(record.target.parentElement)) return false
533
+ if (record.type !== 'childList') return true
534
+ return [...record.addedNodes, ...record.removedNodes].some(node => !overlays.has(node))
535
+ })) schedule()
536
+ })
537
+ mutations.observe(document.documentElement, { subtree: true, childList: true, characterData: true, attributes: true })
538
+ }
539
+ }
540
+ return { state: match.state, ...(match.reason ? { reason: match.reason } : {}) }
541
+ }
542
+ default: throw new Error('Unknown annotation operation')
543
+ }
544
+ }
545
+ })()
546
+ }
547
+ return JSON.stringify({ ok: true, result: globalThis[key](args) })
548
+ } catch (error) {
549
+ return JSON.stringify({ ok: false, error: String(error?.message ?? error) })
550
+ }
551
+ })()`;
552
+ /** The expression to evaluate in the page for one operation. */
553
+ export function pageCall(operation) {
554
+ return PAGE_SCRIPT.replace('__ARGS__', () => JSON.stringify(operation));
555
+ }
556
+ /** Reads the helper's reply, throwing its error. */
557
+ export function pageReply(reply) {
558
+ if (typeof reply !== 'string')
559
+ throw new Error('The page did not answer.');
560
+ let parsed;
561
+ try {
562
+ parsed = JSON.parse(reply);
563
+ }
564
+ catch {
565
+ throw new Error('The page answered with something that is not JSON.');
566
+ }
567
+ if (!parsed.ok)
568
+ throw new Error(parsed.error || 'The page helper failed.');
569
+ return parsed.result;
570
+ }
@@ -0,0 +1,10 @@
1
+ import type { PluginDescriptor } from "../plugins/types.js";
2
+ import type { WorldPart } from "../world/world.js";
3
+ export declare const PAGE_NOTES_LIST = "page_notes_list";
4
+ export declare const PAGE_NOTES_READ = "page_notes_read";
5
+ export declare const PAGE_NOTES_MARK_FIXED = "page_notes_mark_fixed";
6
+ export declare const ANNOTATIONS_PLUGIN: PluginDescriptor;
7
+ export declare function annotationsPart(options: {
8
+ root: () => string;
9
+ changed?: (sessionId: string | null, ids: string[]) => void;
10
+ }): WorldPart;