@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.
- package/CHANGELOG.md +33 -5
- package/README.md +70 -19
- package/dist/cli.js +10321 -5785
- package/dist/sdk/agents/builtin.d.ts +2 -0
- package/dist/sdk/agents/builtin.js +34 -0
- package/dist/sdk/agents/narrow.d.ts +13 -0
- package/dist/sdk/agents/narrow.js +29 -0
- package/dist/sdk/agents/profiles.d.ts +80 -0
- package/dist/sdk/agents/profiles.js +221 -0
- package/dist/sdk/annotations/bridge.d.ts +42 -0
- package/dist/sdk/annotations/bridge.js +264 -0
- package/dist/sdk/annotations/capture.d.ts +116 -0
- package/dist/sdk/annotations/capture.js +308 -0
- package/dist/sdk/annotations/delivery.d.ts +72 -0
- package/dist/sdk/annotations/delivery.js +130 -0
- package/dist/sdk/annotations/inspect.d.ts +10 -0
- package/dist/sdk/annotations/inspect.js +33 -0
- package/dist/sdk/annotations/isolated.d.ts +6 -0
- package/dist/sdk/annotations/isolated.js +51 -0
- package/dist/sdk/annotations/pageScript.d.ts +28 -0
- package/dist/sdk/annotations/pageScript.js +570 -0
- package/dist/sdk/annotations/part.d.ts +10 -0
- package/dist/sdk/annotations/part.js +144 -0
- package/dist/sdk/annotations/queue.d.ts +4 -0
- package/dist/sdk/annotations/queue.js +40 -0
- package/dist/sdk/annotations/serverMethods.d.ts +3 -0
- package/dist/sdk/annotations/serverMethods.js +62 -0
- package/dist/sdk/annotations/store.d.ts +160 -0
- package/dist/sdk/annotations/store.js +386 -0
- package/dist/sdk/browser/index.d.ts +14 -3
- package/dist/sdk/browser/index.js +11 -4
- package/dist/sdk/browser/serverMethods.d.ts +3 -0
- package/dist/sdk/browser/serverMethods.js +7 -0
- package/dist/sdk/browser/session.d.ts +1 -0
- package/dist/sdk/browser/session.js +45 -9
- package/dist/sdk/cli.js +19 -11
- package/dist/sdk/core/bus.js +1 -1
- package/dist/sdk/core/env.d.ts +1 -1
- package/dist/sdk/core/env.js +1 -1
- package/dist/sdk/doctor/serverMethods.d.ts +3 -0
- package/dist/sdk/doctor/serverMethods.js +8 -0
- package/dist/sdk/harness.d.ts +15 -0
- package/dist/sdk/harness.js +44 -5
- package/dist/sdk/history/recall.js +8 -8
- package/dist/sdk/history/serverMethods.d.ts +3 -0
- package/dist/sdk/history/serverMethods.js +8 -0
- package/dist/sdk/hub/cli.d.ts +1 -1
- package/dist/sdk/hub/cli.js +1 -1
- package/dist/sdk/hub/schema.d.ts +1 -1
- package/dist/sdk/hub/serverMethods.d.ts +3 -0
- package/dist/sdk/hub/serverMethods.js +11 -0
- package/dist/sdk/index.d.ts +10 -0
- package/dist/sdk/index.js +7 -0
- package/dist/sdk/mcp/connections.js +14 -7
- package/dist/sdk/mcp/serverMethods.d.ts +3 -0
- package/dist/sdk/mcp/serverMethods.js +22 -0
- package/dist/sdk/plugins/builtin.d.ts +1 -0
- package/dist/sdk/plugins/builtin.js +128 -33
- package/dist/sdk/plugins/builtinRegistry.d.ts +53 -0
- package/dist/sdk/plugins/builtinRegistry.js +49 -0
- package/dist/sdk/plugins/cli.d.ts +1 -1
- package/dist/sdk/plugins/cli.js +4 -3
- package/dist/sdk/prompt/compile.d.ts +5 -2
- package/dist/sdk/prompt/compile.js +25 -8
- package/dist/sdk/refine/cli.d.ts +1 -1
- package/dist/sdk/refine/cli.js +2 -2
- package/dist/sdk/refine/serverMethods.d.ts +3 -0
- package/dist/sdk/refine/serverMethods.js +20 -0
- package/dist/sdk/server/methods.js +9 -86
- package/dist/sdk/server/params.d.ts +4 -0
- package/dist/sdk/server/params.js +11 -0
- package/dist/sdk/sessions/contracts.d.ts +17 -0
- package/dist/sdk/sessions/contracts.js +42 -3
- package/dist/sdk/sessions/planMode.d.ts +5 -2
- package/dist/sdk/sessions/planMode.js +12 -3
- package/dist/sdk/sessions/reasoningSummaries.d.ts +2 -2
- package/dist/sdk/sessions/reasoningSummaries.js +3 -3
- package/dist/sdk/sessions/registry.d.ts +1 -0
- package/dist/sdk/sessions/registry.js +4 -1
- package/dist/sdk/sessions/runIndex.js +7 -7
- package/dist/sdk/sessions/runStore.js +3 -1
- package/dist/sdk/sessions/runner.js +2 -2
- package/dist/sdk/sessions/sessions.d.ts +17 -5
- package/dist/sdk/sessions/sessions.js +79 -12
- package/dist/sdk/sessions/sideQuestionServerMethods.d.ts +3 -0
- package/dist/sdk/sessions/sideQuestionServerMethods.js +19 -0
- package/dist/sdk/sessions/sideQuestions.d.ts +66 -0
- package/dist/sdk/sessions/sideQuestions.js +290 -0
- package/dist/sdk/sessions/subagentServerMethods.d.ts +3 -0
- package/dist/sdk/sessions/subagentServerMethods.js +41 -0
- package/dist/sdk/sessions/subagentService.d.ts +13 -2
- package/dist/sdk/sessions/subagentService.js +34 -16
- package/dist/sdk/sessions/subagents.d.ts +9 -1
- package/dist/sdk/sessions/subagents.js +19 -0
- package/dist/sdk/terminal/browser.d.ts +62 -0
- package/dist/sdk/terminal/browser.js +41 -0
- package/dist/sdk/terminal/browserControl.d.ts +29 -0
- package/dist/sdk/terminal/browserControl.js +186 -0
- package/dist/sdk/terminal/commandHelp.d.ts +17 -0
- package/dist/sdk/terminal/commandHelp.js +20 -0
- package/dist/sdk/terminal/controller.d.ts +7 -1
- package/dist/sdk/terminal/controller.js +34 -2
- package/dist/sdk/terminal/opentui/subagentsWidget.d.ts +28 -0
- package/dist/sdk/terminal/opentui/subagentsWidget.js +52 -0
- package/dist/sdk/terminal/piExtension.js +33 -48
- package/dist/sdk/terminal/socket.d.ts +6 -3
- package/dist/sdk/terminal/socket.js +40 -9
- package/dist/sdk/terminal/standalone.d.ts +2 -0
- package/dist/sdk/terminal/standalone.js +50 -10
- package/dist/sdk/terminal/subagentMonitor.d.ts +38 -0
- package/dist/sdk/terminal/subagentMonitor.js +121 -0
- package/dist/sdk/terminal/terminals.d.ts +42 -6
- package/dist/sdk/terminal/terminals.js +359 -18
- package/dist/sdk/terminal/trajectory.js +2 -2
- package/dist/sdk/terminal/tui/annotateCommand.d.ts +3 -0
- package/dist/sdk/terminal/tui/annotateCommand.js +22 -0
- package/dist/sdk/terminal/tui/commandPages.d.ts +5 -1
- package/dist/sdk/terminal/tui/commandPages.js +54 -6
- package/dist/sdk/terminal/tui/commands.d.ts +8 -4
- package/dist/sdk/terminal/tui/commands.js +181 -58
- package/dist/sdk/terminal/tui/dialogGeometry.d.ts +2 -2
- package/dist/sdk/terminal/tui/dialogGeometry.js +4 -2
- package/dist/sdk/terminal/tui/dialogs.d.ts +6 -1
- package/dist/sdk/terminal/tui/dialogs.js +9 -2
- package/dist/sdk/terminal/tui/modelPicker.js +1 -6
- package/dist/sdk/terminal/tui/page.d.ts +3 -0
- package/dist/sdk/terminal/tui/pageOverlay.js +17 -6
- package/dist/sdk/terminal/tui/pages/accountsPage.d.ts +26 -4
- package/dist/sdk/terminal/tui/pages/accountsPage.js +264 -36
- package/dist/sdk/terminal/tui/pages/agentsPage.d.ts +74 -0
- package/dist/sdk/terminal/tui/pages/agentsPage.js +190 -0
- package/dist/sdk/terminal/tui/pages/annotatePage.d.ts +94 -0
- package/dist/sdk/terminal/tui/pages/annotatePage.js +341 -0
- package/dist/sdk/terminal/tui/pages/browserPage.d.ts +87 -0
- package/dist/sdk/terminal/tui/pages/browserPage.js +738 -0
- package/dist/sdk/terminal/tui/pages/doctorPage.d.ts +2 -0
- package/dist/sdk/terminal/tui/pages/doctorPage.js +103 -15
- package/dist/sdk/terminal/tui/pages/notesPage.d.ts +75 -0
- package/dist/sdk/terminal/tui/pages/notesPage.js +201 -0
- package/dist/sdk/terminal/tui/pages/settingsPage.d.ts +8 -0
- package/dist/sdk/terminal/tui/pages/settingsPage.js +16 -1
- package/dist/sdk/terminal/tui/pages/subagentsPage.d.ts +23 -0
- package/dist/sdk/terminal/tui/pages/subagentsPage.js +70 -0
- package/dist/sdk/terminal/tui/pages/trajectoryPage.d.ts +12 -5
- package/dist/sdk/terminal/tui/pages/trajectoryPage.js +156 -83
- package/dist/sdk/terminal/tui/pages/worktreesPage.d.ts +72 -0
- package/dist/sdk/terminal/tui/pages/worktreesPage.js +203 -0
- package/dist/sdk/terminal/tui/planMode.d.ts +1 -1
- package/dist/sdk/terminal/tui/planMode.js +1 -1
- package/dist/sdk/terminal/tui/pluginCommands.d.ts +14 -0
- package/dist/sdk/terminal/tui/pluginCommands.js +27 -0
- package/dist/sdk/terminal/tui/questions.d.ts +2 -0
- package/dist/sdk/terminal/tui/runtime.js +4 -2
- package/dist/sdk/terminal/tui/settingsModel.d.ts +2 -0
- package/dist/sdk/terminal/tui/settingsModel.js +32 -2
- package/dist/sdk/terminal/tui/sideQuestion.d.ts +26 -26
- package/dist/sdk/terminal/tui/sideQuestion.js +131 -98
- package/dist/sdk/terminal/tui/ui.d.ts +2 -0
- package/dist/sdk/terminal/tui/ui.js +1 -0
- package/dist/sdk/tools/askUser.js +26 -8
- package/dist/sdk/tools/checklist.js +2 -2
- package/dist/sdk/tools/harnessTools.d.ts +2 -0
- package/dist/sdk/tools/harnessTools.js +1 -1
- package/dist/sdk/tools/spawnSubagent.js +13 -4
- package/dist/sdk/web/part.d.ts +10 -0
- package/dist/sdk/web/part.js +6 -0
- package/dist/sdk/web/serverMethods.d.ts +3 -0
- package/dist/sdk/web/serverMethods.js +7 -0
- package/dist/sdk/wiki/serverMethods.d.ts +3 -0
- package/dist/sdk/wiki/serverMethods.js +26 -0
- package/dist/sdk/worktrees/cli.d.ts +11 -0
- package/dist/sdk/worktrees/cli.js +146 -0
- package/dist/sdk/worktrees/copy.d.ts +9 -0
- package/dist/sdk/worktrees/copy.js +111 -0
- package/dist/sdk/worktrees/git.d.ts +16 -0
- package/dist/sdk/worktrees/git.js +81 -0
- package/dist/sdk/worktrees/serverMethods.d.ts +3 -0
- package/dist/sdk/worktrees/serverMethods.js +30 -0
- package/dist/sdk/worktrees/service.d.ts +73 -0
- package/dist/sdk/worktrees/service.js +483 -0
- package/dist/sdk/worktrees/setup.d.ts +23 -0
- package/dist/sdk/worktrees/setup.js +65 -0
- package/dist/sdk/worktrees/types.d.ts +91 -0
- package/dist/sdk/worktrees/types.js +1 -0
- package/dist/sdk/world/cli.d.ts +2 -2
- package/dist/sdk/world/cli.js +6 -6
- package/dist/sdk/world/world.d.ts +3 -1
- package/dist/sdk/world/world.js +8 -9
- package/dist/terminal-opentui.js +14540 -7374
- package/dist/terminal-pi.js +285 -666
- package/docs/README.md +22 -2
- package/docs/architecture/00-overview.md +28 -16
- package/docs/architecture/02-sessions-and-runs.md +72 -15
- package/docs/architecture/03-variants.md +10 -2
- package/docs/architecture/04-prompt.md +26 -6
- package/docs/architecture/06-plugins-and-capabilities.md +39 -3
- package/docs/architecture/07-tools-and-gate.md +21 -6
- package/docs/architecture/09-accounts-and-connectors.md +8 -0
- package/docs/architecture/10-server-protocol.md +111 -2
- package/docs/architecture/11-terminal.md +185 -5
- package/docs/architecture/12-storage.md +15 -1
- package/docs/architecture/13-world-tools.md +28 -6
- package/docs/architecture/14-surfaces-and-parity.md +89 -0
- package/docs/architecture/README.md +15 -4
- package/docs/architecture/proposals/PROP-0006-run-checklist.md +4 -2
- package/docs/architecture/proposals/PROP-0013-resource-browser.md +58 -0
- package/docs/architecture/proposals/PROP-0014-fullscreen-browser-tabs.md +58 -0
- package/docs/architecture/proposals/PROP-0015-one-capability-surface.md +88 -0
- package/docs/architecture/proposals/PROP-0016-managed-worktrees.md +156 -0
- package/docs/architecture/proposals/PROP-0017-page-annotations.md +127 -0
- package/docs/architecture/proposals/PROP-0018-annotation-queue-and-app-targets.md +179 -0
- package/docs/architecture/proposals/PROP-0019-contextual-side-questions.md +122 -0
- package/docs/architecture/proposals/PROP-0020-builtin-feature-plugins.md +105 -0
- package/docs/architecture/proposals/PROP-0021-command-namespaces-and-duplicates.md +99 -0
- package/docs/architecture/proposals/PROP-0022-plan-only-questions.md +102 -0
- package/docs/architecture/proposals/PROP-0023-xagent-cli-name.md +61 -0
- package/docs/architecture/proposals/PROP-0024-question-sketch-previews.md +130 -0
- package/docs/architecture/proposals/PROP-0025-contextual-question-wireframes.md +49 -0
- package/docs/architecture/proposals/PROP-0026-connected-sketch-display.md +94 -0
- package/docs/architecture/proposals/PROP-0027-xingular-agent-product-identity.md +60 -0
- package/docs/architecture/proposals/PROP-0028-normalize-question-wireframes.md +55 -0
- package/docs/architecture/proposals/PROP-0029-agent-profiles.md +124 -0
- package/docs/architecture/proposals/PROP-0030-subagent-title.md +86 -0
- package/docs/architecture/proposals/README.md +25 -0
- package/docs/decisions/ADR-0009-xharness-command.md +6 -5
- package/docs/decisions/ADR-0014-public-release-baseline.md +3 -2
- package/docs/decisions/ADR-0015-resource-browser.md +28 -0
- package/docs/decisions/ADR-0016-fullscreen-browser-tabs.md +31 -0
- package/docs/decisions/ADR-0017-one-capability-surface.md +34 -0
- package/docs/decisions/ADR-0018-builtin-feature-plugins.md +35 -0
- package/docs/decisions/ADR-0019-managed-git-worktrees.md +31 -0
- package/docs/decisions/ADR-0020-page-annotations.md +37 -0
- package/docs/decisions/ADR-0021-required-command-namespaces.md +40 -0
- package/docs/decisions/ADR-0022-plan-only-question-tools.md +34 -0
- package/docs/decisions/ADR-0023-contextual-side-question-runs.md +35 -0
- package/docs/decisions/ADR-0024-xagent-command.md +40 -0
- package/docs/decisions/ADR-0025-question-choice-sketches.md +49 -0
- package/docs/decisions/ADR-0026-contextual-question-wireframes.md +58 -0
- package/docs/decisions/ADR-0027-connected-sketch-display.md +60 -0
- package/docs/decisions/ADR-0028-xingular-agent-product-identity.md +56 -0
- package/docs/decisions/ADR-0029-normalized-question-wireframe-borders.md +33 -0
- package/docs/decisions/ADR-0030-agent-profiles.md +43 -0
- package/docs/doctor.md +10 -4
- package/docs/features/README.md +26 -0
- package/docs/features/interfaces/README.md +1 -1
- package/docs/future-work.md +48 -0
- package/docs/parity.md +78 -0
- package/docs/releases/1.0.0.md +2 -2
- package/docs/releases/4.0.0.md +46 -0
- package/docs/releases/README.md +20 -15
- package/docs/study/00-index.md +5 -0
- package/docs/study/autonomous-engineering-sources.md +277 -0
- package/docs/study/autonomy-sources.md +147 -0
- package/docs/study/proactive-autonomy.md +408 -0
- package/docs/study/tui-resource-browser.md +78 -0
- package/docs/terminal-tui.md +325 -93
- 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;
|