@xingular/harness 1.1.1 → 1.2.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.
- package/CHANGELOG.md +13 -0
- package/README.md +28 -25
- package/dist/cli.js +6675 -2501
- package/dist/sdk/accounts/accounts.d.ts +27 -3
- package/dist/sdk/accounts/accounts.js +78 -24
- package/dist/sdk/accounts/secrets.d.ts +12 -0
- package/dist/sdk/accounts/secrets.js +114 -7
- package/dist/sdk/accounts/store.d.ts +26 -1
- package/dist/sdk/accounts/store.js +1 -1
- package/dist/sdk/agents/builtin.js +15 -1
- package/dist/sdk/agents/planResearch.d.ts +5 -0
- package/dist/sdk/agents/planResearch.js +19 -0
- package/dist/sdk/agents/profiles.d.ts +2 -1
- package/dist/sdk/agents/profiles.js +9 -4
- package/dist/sdk/annotations/pageScript.js +349 -48
- package/dist/sdk/annotations/part.js +34 -23
- package/dist/sdk/annotations/store.d.ts +7 -0
- package/dist/sdk/annotations/store.js +13 -5
- package/dist/sdk/browser/backend.d.ts +16 -0
- package/dist/sdk/browser/backend.js +17 -0
- package/dist/sdk/browser/contracts.d.ts +19 -0
- package/dist/sdk/browser/contracts.js +116 -0
- package/dist/sdk/browser/tools.d.ts +1 -4
- package/dist/sdk/browser/tools.js +14 -91
- package/dist/sdk/cli.js +5 -4
- package/dist/sdk/commands/list.d.ts +3 -0
- package/dist/sdk/commands/list.js +75 -0
- package/dist/sdk/commands/registry.d.ts +250 -0
- package/dist/sdk/commands/registry.js +47 -0
- package/dist/sdk/commands/routes.d.ts +16 -0
- package/dist/sdk/commands/routes.js +33 -0
- package/dist/sdk/core/commandBudget.d.ts +23 -0
- package/dist/sdk/core/commandBudget.js +57 -0
- package/dist/sdk/core/index.js +17 -1
- package/dist/sdk/core/interactiveProfile.d.ts +46 -0
- package/dist/sdk/core/interactiveProfile.js +143 -0
- package/dist/sdk/core/workspace.d.ts +13 -0
- package/dist/sdk/core/workspace.js +88 -0
- package/dist/sdk/core/workspaceBashTool.d.ts +4 -0
- package/dist/sdk/core/workspaceBashTool.js +36 -0
- package/dist/sdk/core/workspaceSandbox.d.ts +31 -0
- package/dist/sdk/core/workspaceSandbox.js +320 -0
- package/dist/sdk/core/workspaceWriteOperations.d.ts +22 -0
- package/dist/sdk/core/workspaceWriteOperations.js +206 -0
- package/dist/sdk/doctor/types.d.ts +2 -2
- package/dist/sdk/harness.d.ts +7 -0
- package/dist/sdk/harness.js +42 -2
- package/dist/sdk/index.d.ts +6 -1
- package/dist/sdk/index.js +3 -0
- package/dist/sdk/pi/runtime.d.ts +2 -1
- package/dist/sdk/pi/runtime.js +2 -2
- package/dist/sdk/plugins/builtin.js +29 -12
- package/dist/sdk/plugins/builtinRegistry.d.ts +10 -3
- package/dist/sdk/plugins/builtinRegistry.js +30 -8
- package/dist/sdk/plugins/catalogue.js +2 -1
- package/dist/sdk/plugins/commandPrefix.d.ts +8 -0
- package/dist/sdk/plugins/commandPrefix.js +21 -0
- package/dist/sdk/plugins/firstParty.d.ts +1 -1
- package/dist/sdk/plugins/firstParty.js +8 -2
- package/dist/sdk/plugins/inventory.js +4 -3
- package/dist/sdk/plugins/manifest.d.ts +2 -0
- package/dist/sdk/plugins/manifest.js +2 -1
- package/dist/sdk/plugins/surfaces.d.ts +5 -0
- package/dist/sdk/plugins/surfaces.js +11 -0
- package/dist/sdk/plugins/types.d.ts +15 -0
- package/dist/sdk/prompt/compile.d.ts +14 -0
- package/dist/sdk/prompt/compile.js +65 -29
- package/dist/sdk/prompt/standard.d.ts +17 -0
- package/dist/sdk/prompt/standard.js +21 -0
- package/dist/sdk/server/methods.js +18 -0
- package/dist/sdk/server/remote.js +36 -11
- package/dist/sdk/sessions/compactionCheckpoint.d.ts +11 -0
- package/dist/sdk/sessions/compactionCheckpoint.js +79 -0
- package/dist/sdk/sessions/continuation.d.ts +3 -0
- package/dist/sdk/sessions/continuation.js +5 -1
- package/dist/sdk/sessions/contracts.d.ts +168 -0
- package/dist/sdk/sessions/contracts.js +59 -1
- package/dist/sdk/sessions/control.d.ts +72 -0
- package/dist/sdk/sessions/control.js +158 -0
- package/dist/sdk/sessions/initialBash.d.ts +7 -0
- package/dist/sdk/sessions/initialBash.js +7 -0
- package/dist/sdk/sessions/planMode.d.ts +14 -3
- package/dist/sdk/sessions/planMode.js +73 -16
- package/dist/sdk/sessions/registry.d.ts +1 -1
- package/dist/sdk/sessions/registry.js +2 -2
- package/dist/sdk/sessions/runStore.d.ts +14 -0
- package/dist/sdk/sessions/runStore.js +35 -0
- package/dist/sdk/sessions/runner.d.ts +40 -2
- package/dist/sdk/sessions/runner.js +161 -13
- package/dist/sdk/sessions/sessions.d.ts +23 -3
- package/dist/sdk/sessions/sessions.js +202 -17
- package/dist/sdk/sessions/subagentService.d.ts +9 -0
- package/dist/sdk/sessions/subagentService.js +63 -4
- package/dist/sdk/sessions/workSlice.d.ts +103 -0
- package/dist/sdk/sessions/workSlice.js +297 -0
- package/dist/sdk/sessions/workspaceBoundary.d.ts +15 -0
- package/dist/sdk/sessions/workspaceBoundary.js +46 -0
- package/dist/sdk/terminal/browserControl.js +2 -1
- package/dist/sdk/terminal/controller.d.ts +11 -3
- package/dist/sdk/terminal/controller.js +40 -13
- package/dist/sdk/terminal/opentui/theme.d.ts +43 -0
- package/dist/sdk/terminal/opentui/theme.js +87 -0
- package/dist/sdk/terminal/piExtension.d.ts +6 -1
- package/dist/sdk/terminal/piExtension.js +154 -54
- package/dist/sdk/terminal/standalone.d.ts +7 -0
- package/dist/sdk/terminal/standalone.js +72 -13
- package/dist/sdk/terminal/subagentMonitor.d.ts +11 -2
- package/dist/sdk/terminal/subagentMonitor.js +32 -1
- package/dist/sdk/terminal/terminals.d.ts +19 -2
- package/dist/sdk/terminal/terminals.js +245 -35
- package/dist/sdk/terminal/tui/annotateCommand.d.ts +1 -1
- package/dist/sdk/terminal/tui/annotateCommand.js +3 -3
- package/dist/sdk/terminal/tui/commandPages.d.ts +3 -1
- package/dist/sdk/terminal/tui/commandPages.js +3 -0
- package/dist/sdk/terminal/tui/commands.d.ts +2 -0
- package/dist/sdk/terminal/tui/commands.js +205 -124
- package/dist/sdk/terminal/tui/list.d.ts +14 -1
- package/dist/sdk/terminal/tui/list.js +96 -14
- package/dist/sdk/terminal/tui/pageOverlay.js +13 -4
- package/dist/sdk/terminal/tui/pages/annotatePage.d.ts +1 -1
- package/dist/sdk/terminal/tui/pages/notesPage.d.ts +5 -5
- package/dist/sdk/terminal/tui/pages/notesPage.js +18 -18
- package/dist/sdk/terminal/tui/pages/pluginsPage.d.ts +0 -5
- package/dist/sdk/terminal/tui/pages/pluginsPage.js +9 -3
- package/dist/sdk/terminal/tui/pages/subagentsPage.d.ts +1 -0
- package/dist/sdk/terminal/tui/pages/subagentsPage.js +98 -16
- package/dist/sdk/terminal/tui/pages/usagePage.d.ts +43 -0
- package/dist/sdk/terminal/tui/pages/usagePage.js +370 -0
- package/dist/sdk/terminal/tui/piAdapter.d.ts +1 -2
- package/dist/sdk/terminal/tui/piAdapter.js +3 -5
- package/dist/sdk/terminal/tui/pluginCommands.d.ts +2 -0
- package/dist/sdk/terminal/tui/pluginCommands.js +13 -9
- package/dist/sdk/terminal/tui/runtime.js +23 -3
- package/dist/sdk/terminal/tui/sessionPicker.d.ts +40 -0
- package/dist/sdk/terminal/tui/sessionPicker.js +141 -0
- package/dist/sdk/tools/checklist.js +1 -0
- package/dist/sdk/tools/checkpoint.d.ts +27 -0
- package/dist/sdk/tools/checkpoint.js +154 -0
- package/dist/sdk/tools/getCapabilities.d.ts +31 -0
- package/dist/sdk/tools/getCapabilities.js +34 -0
- package/dist/sdk/tools/spawnSubagent.js +4 -3
- package/dist/sdk/usage/analytics.d.ts +20 -0
- package/dist/sdk/usage/analytics.js +151 -0
- package/dist/sdk/usage/ledger.d.ts +38 -0
- package/dist/sdk/usage/ledger.js +142 -0
- package/dist/sdk/usage/limits/adapters.d.ts +11 -0
- package/dist/sdk/usage/limits/adapters.js +349 -0
- package/dist/sdk/usage/limits/capture.d.ts +8 -0
- package/dist/sdk/usage/limits/capture.js +47 -0
- package/dist/sdk/usage/limits/http.d.ts +12 -0
- package/dist/sdk/usage/limits/http.js +51 -0
- package/dist/sdk/usage/limits/normalize.d.ts +15 -0
- package/dist/sdk/usage/limits/normalize.js +211 -0
- package/dist/sdk/usage/limits/service.d.ts +30 -0
- package/dist/sdk/usage/limits/service.js +176 -0
- package/dist/sdk/usage/limits/types.d.ts +65 -0
- package/dist/sdk/usage/limits/types.js +1 -0
- package/dist/sdk/usage/serverMethods.d.ts +3 -0
- package/dist/sdk/usage/serverMethods.js +22 -0
- package/dist/sdk/usage/service.d.ts +32 -0
- package/dist/sdk/usage/service.js +66 -0
- package/dist/sdk/usage/settings.d.ts +7 -0
- package/dist/sdk/usage/settings.js +28 -0
- package/dist/sdk/usage/types.d.ts +60 -0
- package/dist/sdk/usage/types.js +4 -0
- package/dist/terminal-opentui.js +5207 -1883
- package/dist/terminal-pi.js +1658 -271
- package/docs/architecture/02-sessions-and-runs.md +128 -26
- package/docs/architecture/04-prompt.md +50 -12
- package/docs/architecture/06-plugins-and-capabilities.md +24 -7
- package/docs/architecture/07-tools-and-gate.md +104 -24
- package/docs/architecture/08-memory-and-history.md +4 -2
- package/docs/architecture/09-accounts-and-connectors.md +31 -1
- package/docs/architecture/10-server-protocol.md +58 -3
- package/docs/architecture/11-terminal.md +105 -42
- package/docs/architecture/12-storage.md +30 -2
- package/docs/architecture/13-world-tools.md +22 -1
- package/docs/architecture/14-surfaces-and-parity.md +54 -7
- package/docs/architecture/README.md +3 -1
- package/docs/architecture/proposals/PROP-0017-page-annotations.md +6 -2
- package/docs/architecture/proposals/PROP-0018-annotation-queue-and-app-targets.md +1 -1
- package/docs/architecture/proposals/PROP-0031-plugin-command-owner-namespaces.md +54 -0
- package/docs/architecture/proposals/PROP-0032-planning-quality.md +196 -0
- package/docs/architecture/proposals/PROP-0033-compaction-plan-checkpoints.md +75 -0
- package/docs/architecture/proposals/PROP-0034-conversation-fast-mode.md +120 -0
- package/docs/architecture/proposals/PROP-0035-retain-subagent-status.md +47 -0
- package/docs/architecture/proposals/PROP-0036-first-class-harness-commands.md +59 -0
- package/docs/architecture/proposals/PROP-0037-workspace-write-boundary.md +63 -0
- package/docs/architecture/proposals/PROP-0038-annotate-open-shadow-roots.md +73 -0
- package/docs/architecture/proposals/PROP-0039-workspace-boundary-hardening.md +139 -0
- package/docs/architecture/proposals/PROP-0040-one-launch-path.md +87 -0
- package/docs/architecture/proposals/PROP-0041-bounded-work-slices.md +247 -0
- package/docs/architecture/proposals/PROP-0042-usage-and-provider-limits.md +127 -0
- package/docs/architecture/proposals/PROP-0043-read-only-plan-bash.md +74 -0
- package/docs/architecture/proposals/README.md +14 -0
- package/docs/decisions/ADR-0001-owned-terminal.md +1 -1
- package/docs/decisions/ADR-0021-required-command-namespaces.md +1 -1
- package/docs/decisions/ADR-0031-plugin-command-owner-namespaces.md +37 -0
- package/docs/decisions/ADR-0032-compaction-run-checkpoints.md +33 -0
- package/docs/decisions/ADR-0033-plan-mode-research-delegation.md +47 -0
- package/docs/decisions/ADR-0034-first-class-harness-terminal-commands.md +25 -0
- package/docs/decisions/ADR-0035-one-launch-path.md +40 -0
- package/docs/decisions/ADR-0036-workspace-write-operation-boundary.md +54 -0
- package/docs/decisions/ADR-0037-usage-ledger-and-provider-limits.md +44 -0
- package/docs/decisions/ADR-0038-plan-bash-os-read-only.md +29 -0
- package/docs/experiments/EXP-0003-performance-audit/data/session-runtime-replication.json +420 -0
- package/docs/experiments/EXP-0003-performance-audit/data/session-runtime.json +420 -0
- package/docs/experiments/EXP-0003-performance-audit/data/terminal-compact.json +376 -0
- package/docs/experiments/EXP-0003-performance-audit/data/terminal-history.json +422 -0
- package/docs/experiments/EXP-0003-performance-audit/data/terminal-question.json +95 -0
- package/docs/experiments/EXP-0003-performance-audit/data/terminal-wide.json +1036 -0
- package/docs/experiments/EXP-0003-performance-audit/plan.md +109 -0
- package/docs/experiments/EXP-0003-performance-audit/results.md +151 -0
- package/docs/handoff-2026-10-25-app-parity-and-solo-tabs.md +202 -0
- package/docs/parity.md +80 -24
- package/docs/releases/1.1.1.md +28 -0
- package/docs/releases/1.2.0.md +49 -0
- package/docs/releases/4.0.0.md +10 -9
- package/docs/releases/README.md +35 -30
- package/docs/study/00-index.md +3 -0
- package/docs/study/STUDY-0003-long-horizon-runs.md +210 -0
- package/docs/terminal-tui.md +100 -59
- package/package.json +4 -3
|
@@ -49,6 +49,189 @@ export const PAGE_SCRIPT = String.raw `(() => {
|
|
|
49
49
|
}
|
|
50
50
|
return document.body
|
|
51
51
|
}
|
|
52
|
+
// ---- The composed tree: open shadow roots and slotted content, in rendered order. Targets
|
|
53
|
+
// captured with it carry tree:'composed'; older targets keep the document-only rules above
|
|
54
|
+
// and below, so a saved note never changes what it points at. Closed shadow roots stay
|
|
55
|
+
// invisible to script, and frames are never entered.
|
|
56
|
+
const refusedContent = 'input,textarea,select,option,iframe,object,embed,[contenteditable],[role="textbox"],[data-sensitive],[data-private],[autocomplete*="password"],[autocomplete*="cc-"],[autocomplete*="one-time-code"]'
|
|
57
|
+
const tooLarge = () => new Error('This page is too large to anchor safely. Use a page note instead.')
|
|
58
|
+
const composedParent = node => node.assignedSlot ?? node.parentElement ?? (node.parentNode instanceof ShadowRoot ? node.parentNode.host : null)
|
|
59
|
+
const flatChildren = node => {
|
|
60
|
+
if (node instanceof Element && node.shadowRoot) return Array.from(node.shadowRoot.childNodes)
|
|
61
|
+
if (node instanceof HTMLSlotElement) {
|
|
62
|
+
const assigned = node.assignedNodes()
|
|
63
|
+
return assigned.length ? assigned : Array.from(node.childNodes)
|
|
64
|
+
}
|
|
65
|
+
return Array.from(node.childNodes)
|
|
66
|
+
}
|
|
67
|
+
const shown = element => {
|
|
68
|
+
if (overlays.has(element) || element.matches(excluded)) return false
|
|
69
|
+
const style = getComputedStyle(element)
|
|
70
|
+
if (style.display === 'none' || style.visibility === 'hidden' || style.visibility === 'collapse' || Number(style.opacity) === 0) return false
|
|
71
|
+
return style.display === 'contents' || element.getClientRects().length > 0
|
|
72
|
+
}
|
|
73
|
+
const allowedComposed = element => {
|
|
74
|
+
if (!(element instanceof Element) || !element.isConnected || !element.getClientRects().length) return false
|
|
75
|
+
for (let current = element; current; current = composedParent(current)) if (!shown(current)) return false
|
|
76
|
+
return true
|
|
77
|
+
}
|
|
78
|
+
const rootsOf = node => {
|
|
79
|
+
const roots = []
|
|
80
|
+
for (let root = node?.getRootNode?.(); root instanceof ShadowRoot; root = root.host.getRootNode()) roots.push(root)
|
|
81
|
+
return roots
|
|
82
|
+
}
|
|
83
|
+
const openRoots = () => {
|
|
84
|
+
const roots = []
|
|
85
|
+
const pending = [document]
|
|
86
|
+
let seen = 0
|
|
87
|
+
while (pending.length) {
|
|
88
|
+
for (const element of pending.pop().querySelectorAll('*')) {
|
|
89
|
+
if (++seen > 50000) throw tooLarge()
|
|
90
|
+
if (element.shadowRoot) { roots.push(element.shadowRoot); pending.push(element.shadowRoot) }
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return roots
|
|
94
|
+
}
|
|
95
|
+
const byTagComposed = tag => {
|
|
96
|
+
const found = []
|
|
97
|
+
const pending = [document]
|
|
98
|
+
let seen = 0
|
|
99
|
+
while (pending.length) {
|
|
100
|
+
for (const element of pending.pop().querySelectorAll('*')) {
|
|
101
|
+
if (++seen > 50000) throw tooLarge()
|
|
102
|
+
if (element.localName === tag) found.push(element)
|
|
103
|
+
if (element.shadowRoot) pending.push(element.shadowRoot)
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return found
|
|
107
|
+
}
|
|
108
|
+
const blockComposed = node => {
|
|
109
|
+
let element = composedParent(node)
|
|
110
|
+
while (element && element !== document.body) {
|
|
111
|
+
if (!['inline', 'contents', 'inline-block'].includes(getComputedStyle(element).display)) return element
|
|
112
|
+
element = composedParent(element)
|
|
113
|
+
}
|
|
114
|
+
return document.body
|
|
115
|
+
}
|
|
116
|
+
// The same normalized stream as below, walked over the composed tree. It also records where
|
|
117
|
+
// it passed visible fields or private content, so a selection across one is refused, and
|
|
118
|
+
// where each node starts and ends, so selection boundaries in any tree map to positions.
|
|
119
|
+
const streamComposed = (root = document.body) => {
|
|
120
|
+
const chars = []
|
|
121
|
+
const map = []
|
|
122
|
+
const refused = []
|
|
123
|
+
const spans = new Map()
|
|
124
|
+
if (!root) return { chars, map, refused, spans }
|
|
125
|
+
let texts = 0
|
|
126
|
+
let elements = 0
|
|
127
|
+
let previousBlock
|
|
128
|
+
const append = (char, start, end) => {
|
|
129
|
+
if (/\s/u.test(char)) {
|
|
130
|
+
if (!chars.length) return false
|
|
131
|
+
if (chars.at(-1) === ' ') { map.at(-1).end = end; return false }
|
|
132
|
+
char = ' '
|
|
133
|
+
}
|
|
134
|
+
chars.push(char)
|
|
135
|
+
map.push({ start, end })
|
|
136
|
+
return true
|
|
137
|
+
}
|
|
138
|
+
const closing = Symbol('end of node')
|
|
139
|
+
const stack = [root]
|
|
140
|
+
while (stack.length) {
|
|
141
|
+
const node = stack.pop()
|
|
142
|
+
if (node[closing]) { node[closing].end = chars.length; continue }
|
|
143
|
+
if (node.nodeType === Node.TEXT_NODE) {
|
|
144
|
+
if (++texts > 20000 || chars.length + node.data.length > 250000) throw tooLarge()
|
|
145
|
+
// A component with a closed shadow root shows only the light text it slots; the rest is not rendered.
|
|
146
|
+
const holder = node.parentElement
|
|
147
|
+
if (holder && holder.localName.includes('-') && !holder.shadowRoot) {
|
|
148
|
+
const probe = document.createRange()
|
|
149
|
+
probe.selectNodeContents(node)
|
|
150
|
+
if (!probe.getClientRects().length) continue
|
|
151
|
+
}
|
|
152
|
+
const currentBlock = blockComposed(node)
|
|
153
|
+
if (previousBlock && currentBlock !== previousBlock && chars.length) append(' ', map.at(-1).end, [node, 0])
|
|
154
|
+
previousBlock = currentBlock
|
|
155
|
+
const span = { at: chars.length, starts: [], end: chars.length }
|
|
156
|
+
let offset = 0
|
|
157
|
+
for (const char of node.data) {
|
|
158
|
+
if (append(char, [node, offset], [node, offset + char.length])) span.starts.push(offset)
|
|
159
|
+
offset += char.length
|
|
160
|
+
}
|
|
161
|
+
span.end = chars.length
|
|
162
|
+
spans.set(node, span)
|
|
163
|
+
continue
|
|
164
|
+
}
|
|
165
|
+
if (node.nodeType !== Node.ELEMENT_NODE) continue
|
|
166
|
+
if (++elements > 50000) throw tooLarge()
|
|
167
|
+
if (node !== root && !shown(node)) {
|
|
168
|
+
if (node.matches(refusedContent) && node.getClientRects().length > 0) refused.push(chars.length)
|
|
169
|
+
continue
|
|
170
|
+
}
|
|
171
|
+
const span = { at: chars.length, starts: [], end: chars.length }
|
|
172
|
+
spans.set(node, span)
|
|
173
|
+
stack.push({ [closing]: span })
|
|
174
|
+
const children = flatChildren(node)
|
|
175
|
+
for (let index = children.length - 1; index >= 0; index--) stack.push(children[index])
|
|
176
|
+
}
|
|
177
|
+
if (chars.at(-1) === ' ') { chars.pop(); map.pop() }
|
|
178
|
+
return { chars, map, refused, spans }
|
|
179
|
+
}
|
|
180
|
+
// A boundary point (in any tree) as a stream position: the first character at or after it.
|
|
181
|
+
const positionAt = (data, container, offset) => {
|
|
182
|
+
if (container.nodeType === Node.TEXT_NODE) {
|
|
183
|
+
const span = data.spans.get(container)
|
|
184
|
+
return span ? span.at + span.starts.filter(start => start < offset).length : null
|
|
185
|
+
}
|
|
186
|
+
const children = container.childNodes
|
|
187
|
+
for (let index = offset; index < children.length; index++) {
|
|
188
|
+
const span = data.spans.get(children[index])
|
|
189
|
+
if (span) return span.at
|
|
190
|
+
}
|
|
191
|
+
const own = data.spans.get(container instanceof ShadowRoot ? container.host : container)
|
|
192
|
+
return own ? own.end : null
|
|
193
|
+
}
|
|
194
|
+
const textComposed = element => streamComposed(element).chars.join('')
|
|
195
|
+
// One Range per text node: a Range cannot span shadow trees.
|
|
196
|
+
const segmentsFor = (data, start, end) => {
|
|
197
|
+
const segments = []
|
|
198
|
+
let run = null
|
|
199
|
+
for (let index = start; index < end; index++) {
|
|
200
|
+
const entry = data.map[index]
|
|
201
|
+
if (entry.start[0] !== entry.end[0]) { run = null; continue }
|
|
202
|
+
if (run && run.node === entry.start[0]) { run.range.setEnd(...entry.end); continue }
|
|
203
|
+
const range = document.createRange()
|
|
204
|
+
range.setStart(...entry.start)
|
|
205
|
+
range.setEnd(...entry.end)
|
|
206
|
+
run = { node: entry.start[0], range }
|
|
207
|
+
segments.push(range)
|
|
208
|
+
}
|
|
209
|
+
return segments
|
|
210
|
+
}
|
|
211
|
+
const composedSelectionTarget = () => {
|
|
212
|
+
const selection = getSelection()
|
|
213
|
+
// rangeCount, isCollapsed and getRangeAt see a selection in a shadow root only from the document:
|
|
214
|
+
// WebKit reports no range at all for one that crosses into a component.
|
|
215
|
+
if (!selection || typeof selection.getComposedRanges !== 'function') return null
|
|
216
|
+
const roots = openRoots()
|
|
217
|
+
let ranges
|
|
218
|
+
try { ranges = selection.getComposedRanges({ shadowRoots: roots }) } catch { ranges = selection.getComposedRanges(...roots) }
|
|
219
|
+
const range = ranges?.[0]
|
|
220
|
+
if (!range || range.collapsed) return null
|
|
221
|
+
const data = streamComposed()
|
|
222
|
+
let start = positionAt(data, range.startContainer, range.startOffset)
|
|
223
|
+
let end = positionAt(data, range.endContainer, range.endOffset)
|
|
224
|
+
if (start === null || end === null) return null
|
|
225
|
+
while (data.chars[start] === ' ') start++
|
|
226
|
+
while (end > start && data.chars[end - 1] === ' ') end--
|
|
227
|
+
if (end <= start || end - start > 4000) return null
|
|
228
|
+
// Refuse ranges crossing fields or private content rather than silently capturing partial secrets.
|
|
229
|
+
if (data.refused.some(position => position > start && position < end)) return null
|
|
230
|
+
return {
|
|
231
|
+
target: { 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, tree: 'composed' },
|
|
232
|
+
segments: segmentsFor(data, start, end)
|
|
233
|
+
}
|
|
234
|
+
}
|
|
52
235
|
// Positions are normalized Unicode code points. Mapping offsets remain DOM UTF-16.
|
|
53
236
|
const stream = (root = document.body) => {
|
|
54
237
|
const chars = []
|
|
@@ -86,22 +269,58 @@ export const PAGE_SCRIPT = String.raw `(() => {
|
|
|
86
269
|
}
|
|
87
270
|
const text = element => stream(element).chars.join('')
|
|
88
271
|
const role = element => norm(element.getAttribute('role')) || ({ BUTTON: 'button', A: element.hasAttribute('href') ? 'link' : '', IMG: 'img', SUMMARY: 'button' }[element.tagName] ?? '')
|
|
89
|
-
|
|
272
|
+
// composed: read the composed tree (open shadow roots), where ids are scoped to their root.
|
|
273
|
+
const name = (element, composed = false) => {
|
|
274
|
+
const scope = composed ? element.getRootNode() : document
|
|
90
275
|
const labelled = norm(element.getAttribute('aria-labelledby')).split(' ').filter(Boolean).map(id => {
|
|
91
|
-
const label =
|
|
92
|
-
return allowed(label) ? text(label) : ''
|
|
276
|
+
const label = scope.getElementById?.(id) ?? null
|
|
277
|
+
return composed ? (allowedComposed(label) ? textComposed(label) : '') : (allowed(label) ? text(label) : '')
|
|
93
278
|
}).join(' ')
|
|
94
|
-
return bounded(labelled || element.getAttribute('aria-label') || (element.tagName === 'IMG' ? element.getAttribute('alt') : '') || text(element), 200)
|
|
279
|
+
return bounded(labelled || element.getAttribute('aria-label') || (element.tagName === 'IMG' ? element.getAttribute('alt') : '') || (composed ? textComposed(element) : text(element)), 200)
|
|
280
|
+
}
|
|
281
|
+
const specificSelector = (selector, tag) => selector.trim().toLowerCase() !== tag.toLowerCase() && ['#', '.', '[', ':', '>', '+', '~'].some(marker => selector.includes(marker))
|
|
282
|
+
const selectorPart = element => {
|
|
283
|
+
const tag = element.localName
|
|
284
|
+
const classes = Array.from(element.classList).slice(0, 3).map(name => '.' + CSS.escape(name)).join('')
|
|
285
|
+
const testId = element.getAttribute('data-testid')
|
|
286
|
+
const identity = element.id ? '#' + CSS.escape(element.id) : tag + (testId ? '[data-testid="' + CSS.escape(testId) + '"]' : '')
|
|
287
|
+
return identity + classes
|
|
288
|
+
}
|
|
289
|
+
const selectorFor = (element, semantic) => {
|
|
290
|
+
const root = element.getRootNode()
|
|
291
|
+
const unique = selector => {
|
|
292
|
+
try {
|
|
293
|
+
const matches = root.querySelectorAll(selector)
|
|
294
|
+
return matches.length === 1 && matches[0] === element
|
|
295
|
+
} catch { return false }
|
|
296
|
+
}
|
|
297
|
+
const direct = selectorPart(element)
|
|
298
|
+
if (cp(direct).length <= 500 && unique(direct) && (semantic || specificSelector(direct, element.localName))) return direct
|
|
299
|
+
const path = []
|
|
300
|
+
for (let current = element; current instanceof Element && current.getRootNode() === root; current = current.parentElement) {
|
|
301
|
+
let part = selectorPart(current)
|
|
302
|
+
const siblings = current.parentElement ? Array.from(current.parentElement.children) : Array.from(root.children ?? [])
|
|
303
|
+
const sameTag = siblings.filter(sibling => sibling.localName === current.localName)
|
|
304
|
+
const index = sameTag.indexOf(current)
|
|
305
|
+
if (!specificSelector(part, current.localName) || sameTag.length > 1) part += ':nth-of-type(' + (index + 1) + ')'
|
|
306
|
+
path.unshift(part)
|
|
307
|
+
const candidate = path.join(' > ')
|
|
308
|
+
if (cp(candidate).length <= 500 && specificSelector(candidate, element.localName) && unique(candidate)) return candidate
|
|
309
|
+
}
|
|
310
|
+
const candidate = path.join(' > ')
|
|
311
|
+
return cp(candidate).length <= 500 ? candidate : cp(direct).length <= 500 ? direct : element.localName
|
|
95
312
|
}
|
|
96
|
-
const describe = element => {
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
313
|
+
const describe = (element, composed = false) => {
|
|
314
|
+
const elementName = name(element, composed)
|
|
315
|
+
const elementText = bounded(composed ? textComposed(element) : text(element), 500)
|
|
316
|
+
// At the top of a shadow root the context is the component that holds it.
|
|
317
|
+
const parent = composed ? element.parentElement ?? (element.parentNode instanceof ShadowRoot ? element.parentNode.host : null) : element.parentElement
|
|
318
|
+
const context = composed ? (allowedComposed(parent) ? textComposed(parent) : '') : (allowed(parent) ? text(parent) : '')
|
|
100
319
|
return {
|
|
101
|
-
kind: 'element', tag: element.
|
|
102
|
-
name:
|
|
103
|
-
|
|
104
|
-
|
|
320
|
+
kind: 'element', tag: element.localName, role: bounded(role(element), 80),
|
|
321
|
+
name: elementName, text: elementText, context: bounded(context, 200),
|
|
322
|
+
selector: selectorFor(element, Boolean(elementName || elementText)),
|
|
323
|
+
...(composed ? { tree: 'composed' } : {})
|
|
105
324
|
}
|
|
106
325
|
}
|
|
107
326
|
const rangeFor = (data, start, end) => {
|
|
@@ -138,11 +357,13 @@ export const PAGE_SCRIPT = String.raw `(() => {
|
|
|
138
357
|
const resolve = target => {
|
|
139
358
|
if (!target || typeof target !== 'object') throw new Error('Invalid target')
|
|
140
359
|
if (target.kind === 'page') return { state: 'matched' }
|
|
360
|
+
if ('tree' in target && target.tree !== 'composed') throw new Error('Invalid target tree')
|
|
361
|
+
const composed = target.tree === 'composed'
|
|
141
362
|
if (target.kind === 'text') {
|
|
142
363
|
if (!validString(target.exact, 4000) || !target.exact || norm(target.exact) !== target.exact ||
|
|
143
364
|
!validString(target.prefix, 128) || !validString(target.suffix, 128) ||
|
|
144
365
|
!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()
|
|
366
|
+
const data = composed ? streamComposed() : stream()
|
|
146
367
|
const quote = cp(target.exact)
|
|
147
368
|
const prefix = cp(target.prefix)
|
|
148
369
|
const suffix = cp(target.suffix)
|
|
@@ -163,26 +384,72 @@ export const PAGE_SCRIPT = String.raw `(() => {
|
|
|
163
384
|
if (matches.length > 1) return { state: 'ambiguous', reason: 'Quote and context match more than once' }
|
|
164
385
|
}
|
|
165
386
|
if (!matches.length) return { state: 'unresolved', reason: 'Quote or context changed or is unavailable' }
|
|
166
|
-
return { state: 'matched', range: rangeFor(data, ...matches[0]) }
|
|
387
|
+
if (!composed) return { state: 'matched', range: rangeFor(data, ...matches[0]) }
|
|
388
|
+
const segments = segmentsFor(data, ...matches[0])
|
|
389
|
+
return segments.length ? { state: 'matched', segments } : { state: 'unresolved', reason: 'Quote or context changed or is unavailable' }
|
|
167
390
|
}
|
|
168
391
|
if (target.kind === 'element') {
|
|
169
392
|
if (!validString(target.tag, 100) || !/^[a-z][a-z0-9-]*$/.test(target.tag) ||
|
|
170
393
|
!validString(target.role, 200) || !validString(target.name, 200) || !validString(target.text, 500) ||
|
|
171
394
|
!validString(target.context, 200) || !validString(target.selector, 500)) throw new Error('Invalid element target')
|
|
172
|
-
|
|
173
|
-
const elements = document.getElementsByTagName(target.tag)
|
|
395
|
+
const semantic = Boolean(target.name.trim() || target.text.trim())
|
|
396
|
+
const elements = composed ? byTagComposed(target.tag) : document.getElementsByTagName(target.tag)
|
|
174
397
|
if (elements.length > 2000) return { state: 'unresolved', reason: 'Too many element candidates to resolve safely' }
|
|
175
|
-
|
|
176
|
-
|
|
398
|
+
if (specificSelector(target.selector, target.tag)) {
|
|
399
|
+
let anchored = []
|
|
400
|
+
try {
|
|
401
|
+
anchored = Array.from(elements).filter(composed ? allowedComposed : allowed).filter(element => element.matches(target.selector)).filter(element => {
|
|
402
|
+
const candidate = describe(element, composed)
|
|
403
|
+
return candidate.role === target.role && candidate.name === target.name && candidate.text === target.text && (!target.context || candidate.context === target.context)
|
|
404
|
+
})
|
|
405
|
+
} catch { anchored = [] }
|
|
406
|
+
if (anchored.length === 1) return { state: 'matched', element: anchored[0] }
|
|
407
|
+
if (anchored.length > 1 && !semantic) return { state: 'ambiguous', reason: 'The CSS selector matches more than one element' }
|
|
408
|
+
}
|
|
409
|
+
if (!semantic) return { state: 'unresolved', reason: 'No semantic evidence or unique CSS selector' }
|
|
410
|
+
let candidates = Array.from(elements).filter(composed ? allowedComposed : allowed).filter(element => {
|
|
411
|
+
const candidate = describe(element, composed)
|
|
177
412
|
return candidate.role === target.role && candidate.name === target.name && candidate.text === target.text
|
|
178
413
|
})
|
|
179
|
-
if (candidates.length > 1 && target.context) candidates = candidates.filter(element => describe(element).context === target.context)
|
|
414
|
+
if (candidates.length > 1 && target.context) candidates = candidates.filter(element => describe(element, composed).context === target.context)
|
|
180
415
|
if (candidates.length > 1) return { state: 'ambiguous', reason: 'Element semantics are duplicated' }
|
|
181
416
|
if (!candidates.length) return { state: 'unresolved', reason: 'Element semantics changed or are unavailable' }
|
|
182
417
|
return { state: 'matched', element: candidates[0] }
|
|
183
418
|
}
|
|
184
419
|
throw new Error('Unknown target kind')
|
|
185
420
|
}
|
|
421
|
+
// What a pointer event points at: the nearest annotatable element on its composed path, or
|
|
422
|
+
// why there is none. Icons and other decorative parts give way to the control around them.
|
|
423
|
+
const pick = event => {
|
|
424
|
+
for (const node of event.composedPath()) {
|
|
425
|
+
if (node === document.body || node === document.documentElement || node === document || node === window) break
|
|
426
|
+
if (!(node instanceof Element)) continue
|
|
427
|
+
if (node.matches('iframe,object,embed')) return { element: null, refusal: 'Embedded frames can\u2019t be annotated.' }
|
|
428
|
+
if (node.matches(refusedContent)) return { element: null, refusal: 'Form fields and private content can\u2019t be annotated.' }
|
|
429
|
+
if (!allowedComposed(node)) continue
|
|
430
|
+
// A component that shows nothing script can read (a closed shadow root) gives nothing to anchor to.
|
|
431
|
+
if (node.localName.includes('-') && !node.shadowRoot && !norm(node.getAttribute('aria-label')) && !textComposed(node)) {
|
|
432
|
+
return { element: null, refusal: 'This component\u2019s contents are private and can\u2019t be annotated.' }
|
|
433
|
+
}
|
|
434
|
+
return { element: node, refusal: null }
|
|
435
|
+
}
|
|
436
|
+
return { element: null, refusal: null }
|
|
437
|
+
}
|
|
438
|
+
// The current selection as a target: through the composed tree when the browser can say
|
|
439
|
+
// where a selection inside a shadow root is, else by the document-only rules.
|
|
440
|
+
const currentSelection = () => {
|
|
441
|
+
if (typeof getSelection()?.getComposedRanges === 'function') return composedSelectionTarget()
|
|
442
|
+
const target = selectionTarget()
|
|
443
|
+
return target ? { target, range: getSelection().getRangeAt(0).cloneRange() } : null
|
|
444
|
+
}
|
|
445
|
+
const unionRect = rects => {
|
|
446
|
+
const list = rects.filter(rect => rect.width || rect.height)
|
|
447
|
+
if (!list.length) return new DOMRect(0, 0, 0, 0)
|
|
448
|
+
const left = Math.min(...list.map(rect => rect.left))
|
|
449
|
+
const top = Math.min(...list.map(rect => rect.top))
|
|
450
|
+
return new DOMRect(left, top, Math.max(...list.map(rect => rect.right)) - left, Math.max(...list.map(rect => rect.bottom)) - top)
|
|
451
|
+
}
|
|
452
|
+
const segmentRects = segments => segments.flatMap(range => Array.from(range.getClientRects()))
|
|
186
453
|
const listen = (node, type, handler, options = true) => {
|
|
187
454
|
node.addEventListener(type, handler, options)
|
|
188
455
|
listeners.push(() => node.removeEventListener(type, handler, options))
|
|
@@ -206,14 +473,14 @@ export const PAGE_SCRIPT = String.raw `(() => {
|
|
|
206
473
|
const match = resolve(mark)
|
|
207
474
|
overlay?.remove()
|
|
208
475
|
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.
|
|
213
|
-
let ancestor = anchor.nodeType === Node.ELEMENT_NODE ? anchor : anchor
|
|
476
|
+
if (match.state !== 'matched' || (!match.range && !match.element && !match.segments?.length)) return
|
|
477
|
+
const rects = match.range ? Array.from(match.range.getClientRects()) : match.segments ? segmentRects(match.segments) : [match.element.getBoundingClientRect()]
|
|
478
|
+
// Clip marks to nested scrollports, not just the top-level viewport, including those inside components.
|
|
479
|
+
const anchor = match.element ?? match.range?.commonAncestorContainer ?? match.segments[0].startContainer
|
|
480
|
+
let ancestor = anchor.nodeType === Node.ELEMENT_NODE ? anchor : composedParent(anchor)
|
|
214
481
|
let clip = { left: 0, top: 0, right: innerWidth, bottom: innerHeight }
|
|
215
|
-
if (match.element) ancestor = ancestor
|
|
216
|
-
for (; ancestor; ancestor = ancestor
|
|
482
|
+
if (match.element) ancestor = composedParent(ancestor)
|
|
483
|
+
for (; ancestor; ancestor = composedParent(ancestor)) {
|
|
217
484
|
const style = getComputedStyle(ancestor)
|
|
218
485
|
const rect = ancestor.getBoundingClientRect()
|
|
219
486
|
if (/(auto|scroll|hidden|clip)/.test(style.overflowX)) {
|
|
@@ -254,6 +521,7 @@ export const PAGE_SCRIPT = String.raw `(() => {
|
|
|
254
521
|
'.box.frozen{border-color:#f97316;background:rgba(249,115,22,.12)}' +
|
|
255
522
|
'.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
523
|
'.chip b{font-weight:600;color:#93c5fd}' +
|
|
524
|
+
'.chip.refused{background:#334155;color:#e2e8f0;font-style:italic}' +
|
|
257
525
|
'.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
526
|
'.what{font-size:11px;opacity:.7;margin:0 0 6px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}' +
|
|
259
527
|
'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}' +
|
|
@@ -298,17 +566,37 @@ export const PAGE_SCRIPT = String.raw `(() => {
|
|
|
298
566
|
'<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
567
|
'<div class="hint">Esc to cancel · ⌘↵ proceed now</div></div><div class="toast" role="status"></div></div>'
|
|
300
568
|
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 }
|
|
569
|
+
const state = { captureId, host, shadow, listeners: [], hovered: null, refusal: null, pointer: null, frozen: null, frame: 0, results: [], box: $('.box'), chip: $('.chip'), pop: $('.pop'), what: $('.what'), input: $('textarea'), toast: $('.toast'), toastTimer: 0 }
|
|
302
570
|
inspect = state
|
|
303
571
|
const on = (node, type, handler, options = true) => {
|
|
304
572
|
node.addEventListener(type, handler, options)
|
|
305
573
|
state.listeners.push(() => node.removeEventListener(type, handler, options))
|
|
306
574
|
}
|
|
307
|
-
const rectOf = picked => picked.range ? picked.range.getBoundingClientRect() : picked.element.getBoundingClientRect()
|
|
575
|
+
const rectOf = picked => picked.range ? picked.range.getBoundingClientRect() : picked.segments ? unionRect(segmentRects(picked.segments)) : picked.element.getBoundingClientRect()
|
|
576
|
+
// Scrolls inside a component do not reach the window, so listen on each shadow root met.
|
|
577
|
+
const watched = new WeakSet()
|
|
578
|
+
const watch = node => {
|
|
579
|
+
for (const root of rootsOf(node)) {
|
|
580
|
+
if (watched.has(root)) continue
|
|
581
|
+
watched.add(root)
|
|
582
|
+
on(root, 'scroll', () => repaint(), { capture: true, passive: true })
|
|
583
|
+
}
|
|
584
|
+
}
|
|
308
585
|
const paint = () => {
|
|
309
586
|
state.frame = 0
|
|
310
587
|
const current = state.frozen ?? (state.hovered ? { element: state.hovered } : null)
|
|
311
|
-
|
|
588
|
+
state.chip.classList.remove('refused')
|
|
589
|
+
if (!current && state.refusal && state.pointer) {
|
|
590
|
+
// Say why nothing is outlined instead of staying silent.
|
|
591
|
+
state.box.style.display = 'none'
|
|
592
|
+
state.chip.textContent = state.refusal
|
|
593
|
+
state.chip.classList.add('refused')
|
|
594
|
+
state.chip.style.display = 'block'
|
|
595
|
+
place(state.chip, { left: state.pointer.x + 12, top: state.pointer.y + 14, bottom: state.pointer.y + 14 }, Math.min(360, state.chip.offsetWidth || 240), 20, 0)
|
|
596
|
+
return
|
|
597
|
+
}
|
|
598
|
+
const detached = current && (current.element ? !current.element.isConnected : current.segments ? !current.segments[0]?.startContainer.isConnected : false)
|
|
599
|
+
if (!current || detached) {
|
|
312
600
|
state.box.style.display = state.chip.style.display = 'none'
|
|
313
601
|
return
|
|
314
602
|
}
|
|
@@ -338,6 +626,8 @@ export const PAGE_SCRIPT = String.raw `(() => {
|
|
|
338
626
|
}
|
|
339
627
|
const openPopover = picked => {
|
|
340
628
|
state.frozen = picked
|
|
629
|
+
state.refusal = null
|
|
630
|
+
watch(picked.element ?? picked.segments?.[0]?.startContainer ?? picked.range?.startContainer)
|
|
341
631
|
const target = picked.target
|
|
342
632
|
state.what.textContent = target.kind === 'text' ? '“' + bounded(target.exact, 80) + '”' : '<' + target.tag + '> ' + (target.name || target.text || target.selector)
|
|
343
633
|
state.pop.style.display = 'block'
|
|
@@ -368,12 +658,16 @@ export const PAGE_SCRIPT = String.raw `(() => {
|
|
|
368
658
|
if (event.key === 'Enter' && (event.metaKey || event.ctrlKey)) { event.preventDefault(); submit(event.shiftKey ? 'queue' : 'send') }
|
|
369
659
|
}, false)
|
|
370
660
|
const inPopover = event => event.composedPath().includes(host)
|
|
371
|
-
const pickable = element => element instanceof Element && allowed(element) && !element.tagName.includes('-')
|
|
372
661
|
on(window, 'pointermove', event => {
|
|
373
662
|
if (state.frozen || inPopover(event)) return
|
|
374
|
-
const
|
|
375
|
-
|
|
376
|
-
if (
|
|
663
|
+
const found = pick(event)
|
|
664
|
+
state.pointer = { x: event.clientX, y: event.clientY }
|
|
665
|
+
if (found.element) watch(found.element)
|
|
666
|
+
if (found.element !== state.hovered || found.refusal !== state.refusal || found.refusal) {
|
|
667
|
+
state.hovered = found.element
|
|
668
|
+
state.refusal = found.refusal
|
|
669
|
+
repaint()
|
|
670
|
+
}
|
|
377
671
|
}, { capture: true, passive: true })
|
|
378
672
|
let down = null
|
|
379
673
|
// The page never sees presses on what is being inspected; the browser may still select text.
|
|
@@ -381,7 +675,7 @@ export const PAGE_SCRIPT = String.raw `(() => {
|
|
|
381
675
|
on(window, type, event => {
|
|
382
676
|
if (!event.isTrusted || inPopover(event)) return
|
|
383
677
|
event.stopImmediatePropagation()
|
|
384
|
-
if (event.type === 'pointerdown' && event.button === 0) down = { x: event.clientX, y: event.clientY, element: event.
|
|
678
|
+
if (event.type === 'pointerdown' && event.button === 0) down = { x: event.clientX, y: event.clientY, element: pick(event).element }
|
|
385
679
|
}, { capture: true, passive: false })
|
|
386
680
|
}
|
|
387
681
|
for (const type of ['pointerup', 'mouseup', 'click', 'dblclick', 'auxclick', 'contextmenu', 'submit']) {
|
|
@@ -396,16 +690,18 @@ export const PAGE_SCRIPT = String.raw `(() => {
|
|
|
396
690
|
// A drag selects text; the popover opens on the selection.
|
|
397
691
|
if (!moved) return
|
|
398
692
|
down = null
|
|
399
|
-
let
|
|
400
|
-
try {
|
|
401
|
-
if (
|
|
693
|
+
let selected = null
|
|
694
|
+
try { selected = currentSelection() } catch { selected = null }
|
|
695
|
+
if (selected) openPopover(selected)
|
|
696
|
+
else if (!getSelection()?.isCollapsed) showToast('This selection can\u2019t be annotated: it is too long or crosses a form field or private content.')
|
|
402
697
|
return
|
|
403
698
|
}
|
|
404
699
|
if (event.type !== 'click' || moved) return
|
|
405
700
|
down = null
|
|
406
|
-
const
|
|
407
|
-
if (
|
|
408
|
-
|
|
701
|
+
const found = pick(event)
|
|
702
|
+
if (found.refusal) { showToast(found.refusal); return }
|
|
703
|
+
if (!found.element || found.element !== start.element) return
|
|
704
|
+
openPopover({ target: describe(found.element, true), element: found.element })
|
|
409
705
|
}, { capture: true, passive: false })
|
|
410
706
|
}
|
|
411
707
|
on(window, 'keydown', event => {
|
|
@@ -455,22 +751,22 @@ export const PAGE_SCRIPT = String.raw `(() => {
|
|
|
455
751
|
})
|
|
456
752
|
if (pending.kind === 'text') {
|
|
457
753
|
listen(document, 'selectionchange', () => {
|
|
458
|
-
|
|
754
|
+
// A selection that grows over something refused must not leave the last acceptable one behind.
|
|
755
|
+
try { const selected = currentSelection(); picked = selected ? envelope(selected.target) : null } catch { picked = null }
|
|
459
756
|
})
|
|
460
757
|
}
|
|
461
758
|
if (pending.kind === 'element') {
|
|
462
759
|
let chosen = null
|
|
463
760
|
const choose = event => {
|
|
464
761
|
if (!event.isTrusted || event.button !== 0 || !pending || pending.kind !== 'element') return
|
|
465
|
-
//
|
|
466
|
-
const element = event.
|
|
467
|
-
if (!
|
|
468
|
-
if (element.tagName.includes('-')) return
|
|
762
|
+
// Open shadow roots are followed; frames and closed shadow roots never are.
|
|
763
|
+
const element = pick(event).element
|
|
764
|
+
if (!element) return
|
|
469
765
|
event.preventDefault()
|
|
470
766
|
event.stopImmediatePropagation()
|
|
471
767
|
if (event.type === 'pointerdown') chosen = element
|
|
472
768
|
if (event.type === 'click' && chosen === element) {
|
|
473
|
-
picked = envelope(describe(element))
|
|
769
|
+
picked = envelope(describe(element, true))
|
|
474
770
|
// Keep suppression through this click; capture/cancel removes the listeners.
|
|
475
771
|
}
|
|
476
772
|
}
|
|
@@ -480,7 +776,7 @@ export const PAGE_SCRIPT = String.raw `(() => {
|
|
|
480
776
|
}
|
|
481
777
|
case 'capture': {
|
|
482
778
|
if (!pending || operation.captureId !== pending.captureId) return null
|
|
483
|
-
const result = picked ?? (pending.kind === 'text' ?
|
|
779
|
+
const result = picked ?? (pending.kind === 'text' ? currentSelection()?.target ?? null : null)
|
|
484
780
|
if (!result) return null
|
|
485
781
|
const captured = picked ?? envelope(result)
|
|
486
782
|
clear()
|
|
@@ -512,11 +808,15 @@ export const PAGE_SCRIPT = String.raw `(() => {
|
|
|
512
808
|
pending = null
|
|
513
809
|
picked = null
|
|
514
810
|
if (match.state === 'matched' && operation.target.kind !== 'page') {
|
|
515
|
-
const
|
|
811
|
+
const start = (match.range ?? match.segments?.[0])?.startContainer
|
|
812
|
+
const element = match.element ?? start.parentElement ?? start.parentNode.host
|
|
516
813
|
element.scrollIntoView({ block: 'center', inline: 'nearest', behavior: 'instant' })
|
|
517
814
|
mark = operation.target
|
|
815
|
+
const roots = rootsOf(element)
|
|
518
816
|
listen(window, 'scroll', schedule)
|
|
519
817
|
listen(window, 'resize', schedule)
|
|
818
|
+
// Scrolls inside a component do not reach the window.
|
|
819
|
+
for (const root of roots) listen(root, 'scroll', schedule, { capture: true, passive: true })
|
|
520
820
|
if (window.visualViewport) {
|
|
521
821
|
listen(window.visualViewport, 'scroll', schedule)
|
|
522
822
|
listen(window.visualViewport, 'resize', schedule)
|
|
@@ -535,6 +835,7 @@ export const PAGE_SCRIPT = String.raw `(() => {
|
|
|
535
835
|
})) schedule()
|
|
536
836
|
})
|
|
537
837
|
mutations.observe(document.documentElement, { subtree: true, childList: true, characterData: true, attributes: true })
|
|
838
|
+
for (const root of roots) mutations.observe(root, { subtree: true, childList: true, characterData: true, attributes: true })
|
|
538
839
|
}
|
|
539
840
|
}
|
|
540
841
|
return { state: match.state, ...(match.reason ? { reason: match.reason } : {}) }
|