@xingular/harness 1.0.0 → 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 +46 -5
- package/README.md +75 -21
- package/dist/cli.js +17421 -8711
- 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.d.ts +2 -0
- package/dist/sdk/agents/builtin.js +48 -0
- package/dist/sdk/agents/narrow.d.ts +13 -0
- package/dist/sdk/agents/narrow.js +29 -0
- package/dist/sdk/agents/planResearch.d.ts +5 -0
- package/dist/sdk/agents/planResearch.js +19 -0
- package/dist/sdk/agents/profiles.d.ts +81 -0
- package/dist/sdk/agents/profiles.js +226 -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 +871 -0
- package/dist/sdk/annotations/part.d.ts +10 -0
- package/dist/sdk/annotations/part.js +155 -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 +167 -0
- package/dist/sdk/annotations/store.js +394 -0
- 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/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/browser/tools.d.ts +1 -4
- package/dist/sdk/browser/tools.js +14 -91
- package/dist/sdk/cli.js +24 -15
- 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/bus.js +1 -1
- package/dist/sdk/core/commandBudget.d.ts +23 -0
- package/dist/sdk/core/commandBudget.js +57 -0
- package/dist/sdk/core/env.d.ts +1 -1
- package/dist/sdk/core/env.js +1 -1
- 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/serverMethods.d.ts +3 -0
- package/dist/sdk/doctor/serverMethods.js +8 -0
- package/dist/sdk/doctor/types.d.ts +2 -2
- package/dist/sdk/harness.d.ts +22 -0
- package/dist/sdk/harness.js +84 -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 +16 -1
- package/dist/sdk/index.js +10 -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/pi/runtime.d.ts +2 -1
- package/dist/sdk/pi/runtime.js +2 -2
- package/dist/sdk/plugins/builtin.d.ts +1 -0
- package/dist/sdk/plugins/builtin.js +145 -33
- package/dist/sdk/plugins/builtinRegistry.d.ts +60 -0
- package/dist/sdk/plugins/builtinRegistry.js +71 -0
- package/dist/sdk/plugins/catalogue.js +2 -1
- package/dist/sdk/plugins/cli.d.ts +1 -1
- package/dist/sdk/plugins/cli.js +4 -3
- 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 +19 -2
- package/dist/sdk/prompt/compile.js +79 -26
- package/dist/sdk/prompt/standard.d.ts +17 -0
- package/dist/sdk/prompt/standard.js +21 -0
- 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 +27 -86
- package/dist/sdk/server/params.d.ts +4 -0
- package/dist/sdk/server/params.js +11 -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 +185 -0
- package/dist/sdk/sessions/contracts.js +101 -4
- 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 +18 -4
- package/dist/sdk/sessions/planMode.js +82 -16
- package/dist/sdk/sessions/reasoningSummaries.d.ts +2 -2
- package/dist/sdk/sessions/reasoningSummaries.js +3 -3
- package/dist/sdk/sessions/registry.d.ts +2 -1
- package/dist/sdk/sessions/registry.js +6 -3
- package/dist/sdk/sessions/runIndex.js +7 -7
- package/dist/sdk/sessions/runStore.d.ts +14 -0
- package/dist/sdk/sessions/runStore.js +38 -1
- package/dist/sdk/sessions/runner.d.ts +40 -2
- package/dist/sdk/sessions/runner.js +163 -15
- package/dist/sdk/sessions/sessions.d.ts +40 -8
- package/dist/sdk/sessions/sessions.js +279 -27
- 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 +22 -2
- package/dist/sdk/sessions/subagentService.js +93 -16
- package/dist/sdk/sessions/subagents.d.ts +9 -1
- package/dist/sdk/sessions/subagents.js +19 -0
- 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/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 +187 -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 +15 -1
- package/dist/sdk/terminal/controller.js +67 -8
- package/dist/sdk/terminal/opentui/subagentsWidget.d.ts +28 -0
- package/dist/sdk/terminal/opentui/subagentsWidget.js +52 -0
- 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 +169 -84
- package/dist/sdk/terminal/socket.d.ts +6 -3
- package/dist/sdk/terminal/socket.js +40 -9
- package/dist/sdk/terminal/standalone.d.ts +9 -0
- package/dist/sdk/terminal/standalone.js +118 -19
- package/dist/sdk/terminal/subagentMonitor.d.ts +47 -0
- package/dist/sdk/terminal/subagentMonitor.js +152 -0
- package/dist/sdk/terminal/terminals.d.ts +60 -7
- package/dist/sdk/terminal/terminals.js +598 -47
- 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 +7 -1
- package/dist/sdk/terminal/tui/commandPages.js +57 -6
- package/dist/sdk/terminal/tui/commands.d.ts +10 -4
- package/dist/sdk/terminal/tui/commands.js +269 -65
- 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/list.d.ts +14 -1
- package/dist/sdk/terminal/tui/list.js +96 -14
- 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 +27 -7
- 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/pluginsPage.d.ts +0 -5
- package/dist/sdk/terminal/tui/pages/pluginsPage.js +9 -3
- 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 +24 -0
- package/dist/sdk/terminal/tui/pages/subagentsPage.js +152 -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/usagePage.d.ts +43 -0
- package/dist/sdk/terminal/tui/pages/usagePage.js +370 -0
- 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/piAdapter.d.ts +1 -2
- package/dist/sdk/terminal/tui/piAdapter.js +3 -5
- 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 +16 -0
- package/dist/sdk/terminal/tui/pluginCommands.js +31 -0
- package/dist/sdk/terminal/tui/questions.d.ts +2 -0
- package/dist/sdk/terminal/tui/runtime.js +27 -5
- package/dist/sdk/terminal/tui/sessionPicker.d.ts +40 -0
- package/dist/sdk/terminal/tui/sessionPicker.js +141 -0
- 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 +3 -2
- 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/harnessTools.d.ts +2 -0
- package/dist/sdk/tools/harnessTools.js +1 -1
- package/dist/sdk/tools/spawnSubagent.js +14 -4
- 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/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 +16456 -5966
- package/dist/terminal-pi.js +1579 -573
- package/docs/README.md +22 -2
- package/docs/architecture/00-overview.md +28 -16
- package/docs/architecture/02-sessions-and-runs.md +184 -25
- package/docs/architecture/03-variants.md +10 -2
- package/docs/architecture/04-prompt.md +65 -7
- package/docs/architecture/06-plugins-and-capabilities.md +57 -4
- package/docs/architecture/07-tools-and-gate.md +112 -17
- package/docs/architecture/08-memory-and-history.md +4 -2
- package/docs/architecture/09-accounts-and-connectors.md +39 -1
- package/docs/architecture/10-server-protocol.md +168 -4
- package/docs/architecture/11-terminal.md +251 -8
- package/docs/architecture/12-storage.md +45 -3
- package/docs/architecture/13-world-tools.md +49 -6
- package/docs/architecture/14-surfaces-and-parity.md +136 -0
- package/docs/architecture/README.md +17 -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 +131 -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/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 +39 -0
- package/docs/decisions/ADR-0001-owned-terminal.md +1 -1
- 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/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/doctor.md +10 -4
- 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/features/README.md +26 -0
- package/docs/features/interfaces/README.md +1 -1
- package/docs/future-work.md +48 -0
- package/docs/handoff-2026-10-25-app-parity-and-solo-tabs.md +202 -0
- package/docs/parity.md +134 -0
- package/docs/releases/1.0.0.md +2 -2
- 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 +47 -0
- package/docs/releases/README.md +37 -27
- package/docs/study/00-index.md +8 -0
- package/docs/study/STUDY-0003-long-horizon-runs.md +210 -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 +386 -113
- package/package.json +6 -5
|
@@ -0,0 +1,871 @@
|
|
|
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
|
+
// ---- 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
|
+
}
|
|
235
|
+
// Positions are normalized Unicode code points. Mapping offsets remain DOM UTF-16.
|
|
236
|
+
const stream = (root = document.body) => {
|
|
237
|
+
const chars = []
|
|
238
|
+
const map = []
|
|
239
|
+
if (!root) return { chars, map }
|
|
240
|
+
const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT)
|
|
241
|
+
let node
|
|
242
|
+
let previousBlock
|
|
243
|
+
let visited = 0
|
|
244
|
+
const append = (char, start, end) => {
|
|
245
|
+
if (/\s/u.test(char)) {
|
|
246
|
+
if (!chars.length) return
|
|
247
|
+
if (chars.at(-1) === ' ') { map.at(-1).end = end; return }
|
|
248
|
+
char = ' '
|
|
249
|
+
}
|
|
250
|
+
chars.push(char)
|
|
251
|
+
map.push({ start, end })
|
|
252
|
+
}
|
|
253
|
+
while ((node = walker.nextNode())) {
|
|
254
|
+
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.')
|
|
255
|
+
if (!allowed(node.parentElement)) continue
|
|
256
|
+
const currentBlock = block(node.parentElement)
|
|
257
|
+
if (previousBlock && currentBlock !== previousBlock && chars.length) {
|
|
258
|
+
append(' ', map.at(-1).end, [node, 0])
|
|
259
|
+
}
|
|
260
|
+
previousBlock = currentBlock
|
|
261
|
+
let offset = 0
|
|
262
|
+
for (const char of node.data) {
|
|
263
|
+
append(char, [node, offset], [node, offset + char.length])
|
|
264
|
+
offset += char.length
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
if (chars.at(-1) === ' ') { chars.pop(); map.pop() }
|
|
268
|
+
return { chars, map }
|
|
269
|
+
}
|
|
270
|
+
const text = element => stream(element).chars.join('')
|
|
271
|
+
const role = element => norm(element.getAttribute('role')) || ({ BUTTON: 'button', A: element.hasAttribute('href') ? 'link' : '', IMG: 'img', SUMMARY: 'button' }[element.tagName] ?? '')
|
|
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
|
|
275
|
+
const labelled = norm(element.getAttribute('aria-labelledby')).split(' ').filter(Boolean).map(id => {
|
|
276
|
+
const label = scope.getElementById?.(id) ?? null
|
|
277
|
+
return composed ? (allowedComposed(label) ? textComposed(label) : '') : (allowed(label) ? text(label) : '')
|
|
278
|
+
}).join(' ')
|
|
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
|
|
312
|
+
}
|
|
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) : '')
|
|
319
|
+
return {
|
|
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' } : {})
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
const rangeFor = (data, start, end) => {
|
|
327
|
+
const range = document.createRange()
|
|
328
|
+
range.setStart(...data.map[start].start)
|
|
329
|
+
range.setEnd(...data.map[end - 1].end)
|
|
330
|
+
return range
|
|
331
|
+
}
|
|
332
|
+
const selectionTarget = () => {
|
|
333
|
+
const selection = getSelection()
|
|
334
|
+
if (!selection || selection.rangeCount !== 1 || selection.isCollapsed) return null
|
|
335
|
+
const range = selection.getRangeAt(0)
|
|
336
|
+
if (range.startContainer.getRootNode() !== document || range.endContainer.getRootNode() !== document) return null
|
|
337
|
+
const data = stream()
|
|
338
|
+
const same = (point, node, offset) => point[0] === node && point[1] === offset
|
|
339
|
+
const indices = data.map.flatMap((entry, index) =>
|
|
340
|
+
range.comparePoint(...entry.end) >= 0 && range.comparePoint(...entry.start) <= 0 &&
|
|
341
|
+
!same(entry.end, range.startContainer, range.startOffset) && !same(entry.start, range.endContainer, range.endOffset) ? [index] : [])
|
|
342
|
+
if (!indices.length) return null
|
|
343
|
+
let start = indices[0]
|
|
344
|
+
let end = indices.at(-1) + 1
|
|
345
|
+
while (data.chars[start] === ' ') start++
|
|
346
|
+
while (data.chars[end - 1] === ' ') end--
|
|
347
|
+
if (end <= start || end - start > 4000) return null
|
|
348
|
+
// Refuse ranges crossing excluded subtrees rather than silently capturing partial secrets.
|
|
349
|
+
const walker = document.createTreeWalker(range.commonAncestorContainer, NodeFilter.SHOW_ELEMENT)
|
|
350
|
+
let element
|
|
351
|
+
if (range.commonAncestorContainer.nodeType === Node.TEXT_NODE && !allowed(range.commonAncestorContainer.parentElement)) return null
|
|
352
|
+
while ((element = walker.nextNode())) {
|
|
353
|
+
if (!allowed(element) && range.intersectsNode(element)) return null
|
|
354
|
+
}
|
|
355
|
+
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 }
|
|
356
|
+
}
|
|
357
|
+
const resolve = target => {
|
|
358
|
+
if (!target || typeof target !== 'object') throw new Error('Invalid target')
|
|
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'
|
|
362
|
+
if (target.kind === 'text') {
|
|
363
|
+
if (!validString(target.exact, 4000) || !target.exact || norm(target.exact) !== target.exact ||
|
|
364
|
+
!validString(target.prefix, 128) || !validString(target.suffix, 128) ||
|
|
365
|
+
!Number.isSafeInteger(target.start) || !Number.isSafeInteger(target.end) || target.start < 0 || target.end <= target.start) throw new Error('Invalid text target')
|
|
366
|
+
const data = composed ? streamComposed() : stream()
|
|
367
|
+
const quote = cp(target.exact)
|
|
368
|
+
const prefix = cp(target.prefix)
|
|
369
|
+
const suffix = cp(target.suffix)
|
|
370
|
+
const matches = []
|
|
371
|
+
// Do not prefer the stored position: a duplicate there is still ambiguous.
|
|
372
|
+
const content = data.chars.join('')
|
|
373
|
+
let utf16Index = content.indexOf(target.exact)
|
|
374
|
+
let previousUtf16 = 0
|
|
375
|
+
let index = 0
|
|
376
|
+
while (utf16Index >= 0) {
|
|
377
|
+
index += cp(content.slice(previousUtf16, utf16Index)).length
|
|
378
|
+
previousUtf16 = utf16Index
|
|
379
|
+
utf16Index = content.indexOf(target.exact, utf16Index + 1)
|
|
380
|
+
const end = index + quote.length
|
|
381
|
+
if (prefix.length && data.chars.slice(Math.max(0, index - prefix.length), index).join('') !== target.prefix) continue
|
|
382
|
+
if (suffix.length && data.chars.slice(end, end + suffix.length).join('') !== target.suffix) continue
|
|
383
|
+
matches.push([index, end])
|
|
384
|
+
if (matches.length > 1) return { state: 'ambiguous', reason: 'Quote and context match more than once' }
|
|
385
|
+
}
|
|
386
|
+
if (!matches.length) return { state: 'unresolved', reason: 'Quote or context changed or is unavailable' }
|
|
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' }
|
|
390
|
+
}
|
|
391
|
+
if (target.kind === 'element') {
|
|
392
|
+
if (!validString(target.tag, 100) || !/^[a-z][a-z0-9-]*$/.test(target.tag) ||
|
|
393
|
+
!validString(target.role, 200) || !validString(target.name, 200) || !validString(target.text, 500) ||
|
|
394
|
+
!validString(target.context, 200) || !validString(target.selector, 500)) throw new Error('Invalid element target')
|
|
395
|
+
const semantic = Boolean(target.name.trim() || target.text.trim())
|
|
396
|
+
const elements = composed ? byTagComposed(target.tag) : document.getElementsByTagName(target.tag)
|
|
397
|
+
if (elements.length > 2000) return { state: 'unresolved', reason: 'Too many element candidates to resolve safely' }
|
|
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)
|
|
412
|
+
return candidate.role === target.role && candidate.name === target.name && candidate.text === target.text
|
|
413
|
+
})
|
|
414
|
+
if (candidates.length > 1 && target.context) candidates = candidates.filter(element => describe(element, composed).context === target.context)
|
|
415
|
+
if (candidates.length > 1) return { state: 'ambiguous', reason: 'Element semantics are duplicated' }
|
|
416
|
+
if (!candidates.length) return { state: 'unresolved', reason: 'Element semantics changed or are unavailable' }
|
|
417
|
+
return { state: 'matched', element: candidates[0] }
|
|
418
|
+
}
|
|
419
|
+
throw new Error('Unknown target kind')
|
|
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()))
|
|
453
|
+
const listen = (node, type, handler, options = true) => {
|
|
454
|
+
node.addEventListener(type, handler, options)
|
|
455
|
+
listeners.push(() => node.removeEventListener(type, handler, options))
|
|
456
|
+
}
|
|
457
|
+
const clear = () => {
|
|
458
|
+
listeners.forEach(remove => remove())
|
|
459
|
+
listeners = []
|
|
460
|
+
observer?.disconnect()
|
|
461
|
+
observer = null
|
|
462
|
+
mutations?.disconnect()
|
|
463
|
+
mutations = null
|
|
464
|
+
if (frame) clearTimeout(frame)
|
|
465
|
+
frame = 0
|
|
466
|
+
overlay?.remove()
|
|
467
|
+
overlay = null
|
|
468
|
+
mark = null
|
|
469
|
+
}
|
|
470
|
+
const draw = () => {
|
|
471
|
+
frame = 0
|
|
472
|
+
if (!mark) return
|
|
473
|
+
const match = resolve(mark)
|
|
474
|
+
overlay?.remove()
|
|
475
|
+
overlay = null
|
|
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)
|
|
481
|
+
let clip = { left: 0, top: 0, right: innerWidth, bottom: innerHeight }
|
|
482
|
+
if (match.element) ancestor = composedParent(ancestor)
|
|
483
|
+
for (; ancestor; ancestor = composedParent(ancestor)) {
|
|
484
|
+
const style = getComputedStyle(ancestor)
|
|
485
|
+
const rect = ancestor.getBoundingClientRect()
|
|
486
|
+
if (/(auto|scroll|hidden|clip)/.test(style.overflowX)) {
|
|
487
|
+
clip.left = Math.max(clip.left, rect.left)
|
|
488
|
+
clip.right = Math.min(clip.right, rect.right)
|
|
489
|
+
}
|
|
490
|
+
if (/(auto|scroll|hidden|clip)/.test(style.overflowY)) {
|
|
491
|
+
clip.top = Math.max(clip.top, rect.top)
|
|
492
|
+
clip.bottom = Math.min(clip.bottom, rect.bottom)
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
overlay = document.createElement('div')
|
|
496
|
+
overlays.add(overlay)
|
|
497
|
+
overlay.setAttribute('data-harness-annotation-overlay', '')
|
|
498
|
+
overlay.setAttribute('aria-hidden', 'true')
|
|
499
|
+
overlay.style.cssText = 'all:initial!important;position:fixed!important;inset:0!important;pointer-events:none!important;z-index:2147483647!important;overflow:hidden!important;'
|
|
500
|
+
for (const original of rects) {
|
|
501
|
+
const rect = { left: Math.max(original.left, clip.left), top: Math.max(original.top, clip.top) }
|
|
502
|
+
rect.width = Math.min(original.right, clip.right) - rect.left
|
|
503
|
+
rect.height = Math.min(original.bottom, clip.bottom) - rect.top
|
|
504
|
+
if (rect.width <= 0 || rect.height <= 0) continue
|
|
505
|
+
const box = document.createElement('div')
|
|
506
|
+
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;'
|
|
507
|
+
overlay.append(box)
|
|
508
|
+
}
|
|
509
|
+
document.documentElement.append(overlay)
|
|
510
|
+
}
|
|
511
|
+
const schedule = () => { if (!frame) frame = setTimeout(draw, 16) }
|
|
512
|
+
const envelope = target => ({ url: location.href, title: bounded(document.title, 200), documentId, target })
|
|
513
|
+
|
|
514
|
+
// ---- Inspect mode (PROP-0018): hover outlines a component, a click opens a comment popover.
|
|
515
|
+
// Everything drawn lives in a closed shadow root, so page CSS and scripts cannot reach it;
|
|
516
|
+
// a page can still call this helper, so hosts treat each result as untrusted.
|
|
517
|
+
let inspect = null
|
|
518
|
+
const INSPECT_CSS = ':host{all:initial!important}' +
|
|
519
|
+
'.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}' +
|
|
520
|
+
'.box{position:fixed;box-sizing:border-box;border:2px solid #3b82f6;background:rgba(59,130,246,.12);border-radius:2px;display:none}' +
|
|
521
|
+
'.box.frozen{border-color:#f97316;background:rgba(249,115,22,.12)}' +
|
|
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}' +
|
|
523
|
+
'.chip b{font-weight:600;color:#93c5fd}' +
|
|
524
|
+
'.chip.refused{background:#334155;color:#e2e8f0;font-style:italic}' +
|
|
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)}' +
|
|
526
|
+
'.what{font-size:11px;opacity:.7;margin:0 0 6px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}' +
|
|
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}' +
|
|
528
|
+
'textarea:focus{outline:2px solid #3b82f6;outline-offset:-1px}' +
|
|
529
|
+
'.row{display:flex;gap:6px;margin-top:8px;justify-content:flex-end;flex-wrap:wrap}' +
|
|
530
|
+
'button{all:unset;cursor:pointer;padding:4px 9px;border-radius:6px;font-size:12px;border:1px solid rgba(127,127,127,.45)}' +
|
|
531
|
+
'button:hover{background:rgba(127,127,127,.15)}button:focus-visible{outline:2px solid #3b82f6}' +
|
|
532
|
+
'button.primary{background:#2563eb;border-color:#2563eb;color:#fff}button.primary:hover{background:#1d4ed8}' +
|
|
533
|
+
'button[disabled]{opacity:.45;cursor:default}' +
|
|
534
|
+
'.hint{margin-top:6px;font-size:10px;opacity:.55;text-align:right}' +
|
|
535
|
+
'.toast{position:fixed;right:16px;bottom:16px;padding:6px 10px;border-radius:6px;background:#1e293b;color:#f8fafc;font-size:12px;display:none}'
|
|
536
|
+
const label = element => {
|
|
537
|
+
const tag = element.tagName.toLowerCase()
|
|
538
|
+
const id = element.id ? '#' + element.id : ''
|
|
539
|
+
const classes = typeof element.className === 'string' ? element.className.trim().split(/\s+/).filter(Boolean).slice(0, 2).map(name => '.' + name).join('') : ''
|
|
540
|
+
const rect = element.getBoundingClientRect()
|
|
541
|
+
return { tag, rest: (id || classes) + ' ' + Math.round(rect.width) + '×' + Math.round(rect.height) }
|
|
542
|
+
}
|
|
543
|
+
const place = (node, rect, width, height, gap) => {
|
|
544
|
+
let top = rect.bottom + gap
|
|
545
|
+
if (top + height > innerHeight - 4 && rect.top - gap - height >= 4) top = rect.top - gap - height
|
|
546
|
+
top = Math.max(4, Math.min(top, innerHeight - height - 4))
|
|
547
|
+
const left = Math.max(4, Math.min(rect.left, innerWidth - width - 4))
|
|
548
|
+
node.style.top = top + 'px'
|
|
549
|
+
node.style.left = left + 'px'
|
|
550
|
+
}
|
|
551
|
+
const stopInspect = () => {
|
|
552
|
+
if (!inspect) return
|
|
553
|
+
inspect.listeners.forEach(remove => remove())
|
|
554
|
+
inspect.host.remove()
|
|
555
|
+
inspect = null
|
|
556
|
+
}
|
|
557
|
+
const startInspect = captureId => {
|
|
558
|
+
stopInspect()
|
|
559
|
+
const host = document.createElement('div')
|
|
560
|
+
overlays.add(host)
|
|
561
|
+
host.setAttribute('data-harness-annotation-overlay', '')
|
|
562
|
+
host.style.cssText = 'all:initial!important;position:fixed!important;inset:0!important;pointer-events:none!important;z-index:2147483647!important;'
|
|
563
|
+
const shadow = host.attachShadow({ mode: 'closed' })
|
|
564
|
+
shadow.innerHTML = '<style>' + INSPECT_CSS + '</style><div class="layer"><div class="box"></div><div class="chip"></div>' +
|
|
565
|
+
'<div class="pop" role="dialog" aria-label="Annotate"><p class="what"></p><textarea placeholder="What should change?" aria-label="Comment"></textarea>' +
|
|
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>' +
|
|
567
|
+
'<div class="hint">Esc to cancel · ⌘↵ proceed now</div></div><div class="toast" role="status"></div></div>'
|
|
568
|
+
const $ = selector => shadow.querySelector(selector)
|
|
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 }
|
|
570
|
+
inspect = state
|
|
571
|
+
const on = (node, type, handler, options = true) => {
|
|
572
|
+
node.addEventListener(type, handler, options)
|
|
573
|
+
state.listeners.push(() => node.removeEventListener(type, handler, options))
|
|
574
|
+
}
|
|
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
|
+
}
|
|
585
|
+
const paint = () => {
|
|
586
|
+
state.frame = 0
|
|
587
|
+
const current = state.frozen ?? (state.hovered ? { element: state.hovered } : null)
|
|
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) {
|
|
600
|
+
state.box.style.display = state.chip.style.display = 'none'
|
|
601
|
+
return
|
|
602
|
+
}
|
|
603
|
+
const rect = rectOf(current)
|
|
604
|
+
Object.assign(state.box.style, { display: 'block', left: rect.left + 'px', top: rect.top + 'px', width: rect.width + 'px', height: rect.height + 'px' })
|
|
605
|
+
state.box.classList.toggle('frozen', Boolean(state.frozen))
|
|
606
|
+
if (state.frozen) {
|
|
607
|
+
state.chip.style.display = 'none'
|
|
608
|
+
place(state.pop, rect, 320, state.pop.offsetHeight || 170, 8)
|
|
609
|
+
} else {
|
|
610
|
+
const text = label(current.element)
|
|
611
|
+
state.chip.innerHTML = ''
|
|
612
|
+
const tag = document.createElement('b')
|
|
613
|
+
tag.textContent = text.tag
|
|
614
|
+
state.chip.append(tag, document.createTextNode(text.rest))
|
|
615
|
+
state.chip.style.display = 'block'
|
|
616
|
+
place(state.chip, { left: rect.left, top: rect.top - 22, bottom: rect.top - 22 + 0 }, Math.min(360, state.chip.offsetWidth || 120), 20, 0)
|
|
617
|
+
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)
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
const repaint = () => { if (!state.frame) state.frame = requestAnimationFrame(paint) }
|
|
621
|
+
const closePopover = () => {
|
|
622
|
+
state.frozen = null
|
|
623
|
+
state.pop.style.display = 'none'
|
|
624
|
+
state.input.value = ''
|
|
625
|
+
repaint()
|
|
626
|
+
}
|
|
627
|
+
const openPopover = picked => {
|
|
628
|
+
state.frozen = picked
|
|
629
|
+
state.refusal = null
|
|
630
|
+
watch(picked.element ?? picked.segments?.[0]?.startContainer ?? picked.range?.startContainer)
|
|
631
|
+
const target = picked.target
|
|
632
|
+
state.what.textContent = target.kind === 'text' ? '“' + bounded(target.exact, 80) + '”' : '<' + target.tag + '> ' + (target.name || target.text || target.selector)
|
|
633
|
+
state.pop.style.display = 'block'
|
|
634
|
+
state.chip.style.display = 'none'
|
|
635
|
+
paint()
|
|
636
|
+
state.input.focus()
|
|
637
|
+
setTimeout(() => { if (state.frozen) state.input.focus() }, 0)
|
|
638
|
+
}
|
|
639
|
+
const submit = action => {
|
|
640
|
+
if (!state.frozen) return
|
|
641
|
+
const body = state.input.value.trim()
|
|
642
|
+
if (!body) { state.input.focus(); return }
|
|
643
|
+
state.results.push({ ...envelope(state.frozen.target), body: cp(body).slice(0, 10000).join(''), action })
|
|
644
|
+
if (state.results.length > 20) state.results.shift()
|
|
645
|
+
getSelection()?.removeAllRanges()
|
|
646
|
+
closePopover()
|
|
647
|
+
}
|
|
648
|
+
// The popover's own events stay inside it: the page sees nothing typed or clicked there.
|
|
649
|
+
for (const type of ['keydown', 'keyup', 'keypress', 'input', 'beforeinput', 'paste', 'copy', 'cut', 'pointerdown', 'mousedown', 'pointerup', 'mouseup', 'click', 'dblclick', 'wheel', 'focusin', 'focusout']) {
|
|
650
|
+
on(state.pop, type, event => event.stopPropagation(), false)
|
|
651
|
+
}
|
|
652
|
+
on(state.pop, 'click', event => {
|
|
653
|
+
const button = event.composedPath().find(node => node instanceof HTMLElement && node.dataset?.action)
|
|
654
|
+
if (button) submit(button.dataset.action)
|
|
655
|
+
}, false)
|
|
656
|
+
on(state.input, 'keydown', event => {
|
|
657
|
+
if (event.key === 'Escape') { event.preventDefault(); closePopover() }
|
|
658
|
+
if (event.key === 'Enter' && (event.metaKey || event.ctrlKey)) { event.preventDefault(); submit(event.shiftKey ? 'queue' : 'send') }
|
|
659
|
+
}, false)
|
|
660
|
+
const inPopover = event => event.composedPath().includes(host)
|
|
661
|
+
on(window, 'pointermove', event => {
|
|
662
|
+
if (state.frozen || inPopover(event)) return
|
|
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
|
+
}
|
|
671
|
+
}, { capture: true, passive: true })
|
|
672
|
+
let down = null
|
|
673
|
+
// The page never sees presses on what is being inspected; the browser may still select text.
|
|
674
|
+
for (const type of ['pointerdown', 'mousedown']) {
|
|
675
|
+
on(window, type, event => {
|
|
676
|
+
if (!event.isTrusted || inPopover(event)) return
|
|
677
|
+
event.stopImmediatePropagation()
|
|
678
|
+
if (event.type === 'pointerdown' && event.button === 0) down = { x: event.clientX, y: event.clientY, element: pick(event).element }
|
|
679
|
+
}, { capture: true, passive: false })
|
|
680
|
+
}
|
|
681
|
+
for (const type of ['pointerup', 'mouseup', 'click', 'dblclick', 'auxclick', 'contextmenu', 'submit']) {
|
|
682
|
+
on(window, type, event => {
|
|
683
|
+
if (inPopover(event)) return
|
|
684
|
+
event.preventDefault()
|
|
685
|
+
event.stopImmediatePropagation()
|
|
686
|
+
if (!event.isTrusted || event.button !== 0 || !down || state.frozen) return
|
|
687
|
+
const start = down
|
|
688
|
+
const moved = Math.hypot(event.clientX - start.x, event.clientY - start.y) > 4
|
|
689
|
+
if (event.type === 'pointerup') {
|
|
690
|
+
// A drag selects text; the popover opens on the selection.
|
|
691
|
+
if (!moved) return
|
|
692
|
+
down = null
|
|
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.')
|
|
697
|
+
return
|
|
698
|
+
}
|
|
699
|
+
if (event.type !== 'click' || moved) return
|
|
700
|
+
down = null
|
|
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 })
|
|
705
|
+
}, { capture: true, passive: false })
|
|
706
|
+
}
|
|
707
|
+
on(window, 'keydown', event => {
|
|
708
|
+
if (event.key === 'Escape' && !inPopover(event)) {
|
|
709
|
+
if (state.frozen) closePopover()
|
|
710
|
+
else { state.hovered = null; repaint() }
|
|
711
|
+
}
|
|
712
|
+
}, true)
|
|
713
|
+
on(window, 'scroll', repaint, { capture: true, passive: true })
|
|
714
|
+
on(window, 'resize', repaint)
|
|
715
|
+
document.documentElement.append(host)
|
|
716
|
+
return state
|
|
717
|
+
}
|
|
718
|
+
const showToast = text => {
|
|
719
|
+
if (!inspect) return
|
|
720
|
+
const state = inspect
|
|
721
|
+
state.toast.textContent = bounded(text, 120)
|
|
722
|
+
state.toast.style.display = 'block'
|
|
723
|
+
clearTimeout(state.toastTimer)
|
|
724
|
+
state.toastTimer = setTimeout(() => { state.toast.style.display = 'none' }, 2200)
|
|
725
|
+
}
|
|
726
|
+
return operation => {
|
|
727
|
+
switch (operation.operation) {
|
|
728
|
+
case 'identity': return { url: location.href, title: bounded(document.title, 200), documentId }
|
|
729
|
+
case 'begin': {
|
|
730
|
+
if (!['text', 'element', 'inspect'].includes(operation.kind) || !validString(operation.captureId, 200) || !operation.captureId) throw new Error('Invalid capture request')
|
|
731
|
+
if (operation.kind === 'inspect') {
|
|
732
|
+
// Stays on across in-page navigation, until cancel or a new document.
|
|
733
|
+
clear()
|
|
734
|
+
pending = null
|
|
735
|
+
picked = null
|
|
736
|
+
startInspect(operation.captureId)
|
|
737
|
+
return null
|
|
738
|
+
}
|
|
739
|
+
stopInspect()
|
|
740
|
+
clear()
|
|
741
|
+
picked = null
|
|
742
|
+
pending = { kind: operation.kind, captureId: operation.captureId }
|
|
743
|
+
const captureUrl = location.href
|
|
744
|
+
const expired = setTimeout(() => { clear(); pending = null; picked = null }, 300000)
|
|
745
|
+
const navigation = setInterval(() => {
|
|
746
|
+
if (location.href !== captureUrl) { clear(); pending = null; picked = null }
|
|
747
|
+
}, 250)
|
|
748
|
+
listeners.push(() => clearTimeout(expired), () => clearInterval(navigation))
|
|
749
|
+
listen(window, 'keydown', event => {
|
|
750
|
+
if (event.key === 'Escape') { clear(); pending = null; picked = null }
|
|
751
|
+
})
|
|
752
|
+
if (pending.kind === 'text') {
|
|
753
|
+
listen(document, 'selectionchange', () => {
|
|
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 }
|
|
756
|
+
})
|
|
757
|
+
}
|
|
758
|
+
if (pending.kind === 'element') {
|
|
759
|
+
let chosen = null
|
|
760
|
+
const choose = event => {
|
|
761
|
+
if (!event.isTrusted || event.button !== 0 || !pending || pending.kind !== 'element') return
|
|
762
|
+
// Open shadow roots are followed; frames and closed shadow roots never are.
|
|
763
|
+
const element = pick(event).element
|
|
764
|
+
if (!element) return
|
|
765
|
+
event.preventDefault()
|
|
766
|
+
event.stopImmediatePropagation()
|
|
767
|
+
if (event.type === 'pointerdown') chosen = element
|
|
768
|
+
if (event.type === 'click' && chosen === element) {
|
|
769
|
+
picked = envelope(describe(element, true))
|
|
770
|
+
// Keep suppression through this click; capture/cancel removes the listeners.
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
for (const type of ['pointerdown', 'mousedown', 'pointerup', 'mouseup', 'click', 'dblclick', 'auxclick']) listen(window, type, choose, { capture: true, passive: false })
|
|
774
|
+
}
|
|
775
|
+
return null
|
|
776
|
+
}
|
|
777
|
+
case 'capture': {
|
|
778
|
+
if (!pending || operation.captureId !== pending.captureId) return null
|
|
779
|
+
const result = picked ?? (pending.kind === 'text' ? currentSelection()?.target ?? null : null)
|
|
780
|
+
if (!result) return null
|
|
781
|
+
const captured = picked ?? envelope(result)
|
|
782
|
+
clear()
|
|
783
|
+
// The trusted host previews, then rechecks this same snapshot on Save.
|
|
784
|
+
picked = captured
|
|
785
|
+
return captured
|
|
786
|
+
}
|
|
787
|
+
case 'next': {
|
|
788
|
+
// Inspect mode: whether it is still on, and the next comment the person submitted.
|
|
789
|
+
const active = Boolean(inspect && inspect.captureId === operation.captureId)
|
|
790
|
+
return { url: location.href, title: bounded(document.title, 200), documentId, active, result: active ? inspect.results.shift() ?? null : null }
|
|
791
|
+
}
|
|
792
|
+
case 'toast':
|
|
793
|
+
if (!validString(operation.text, 400)) throw new Error('Invalid toast')
|
|
794
|
+
showToast(operation.text)
|
|
795
|
+
return null
|
|
796
|
+
case 'cancel':
|
|
797
|
+
if (inspect && (!operation.captureId || operation.captureId === inspect.captureId)) { stopInspect(); return null }
|
|
798
|
+
if (operation.captureId && operation.captureId !== pending?.captureId) return null
|
|
799
|
+
clear()
|
|
800
|
+
pending = null
|
|
801
|
+
picked = null
|
|
802
|
+
return null
|
|
803
|
+
case 'resolve':
|
|
804
|
+
case 'reveal': {
|
|
805
|
+
const match = resolve(operation.target)
|
|
806
|
+
if (operation.operation === 'reveal') {
|
|
807
|
+
clear()
|
|
808
|
+
pending = null
|
|
809
|
+
picked = null
|
|
810
|
+
if (match.state === 'matched' && operation.target.kind !== 'page') {
|
|
811
|
+
const start = (match.range ?? match.segments?.[0])?.startContainer
|
|
812
|
+
const element = match.element ?? start.parentElement ?? start.parentNode.host
|
|
813
|
+
element.scrollIntoView({ block: 'center', inline: 'nearest', behavior: 'instant' })
|
|
814
|
+
mark = operation.target
|
|
815
|
+
const roots = rootsOf(element)
|
|
816
|
+
listen(window, 'scroll', schedule)
|
|
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 })
|
|
820
|
+
if (window.visualViewport) {
|
|
821
|
+
listen(window.visualViewport, 'scroll', schedule)
|
|
822
|
+
listen(window.visualViewport, 'resize', schedule)
|
|
823
|
+
}
|
|
824
|
+
if (globalThis.ResizeObserver) {
|
|
825
|
+
observer = new ResizeObserver(schedule)
|
|
826
|
+
observer.observe(document.documentElement)
|
|
827
|
+
observer.observe(element)
|
|
828
|
+
}
|
|
829
|
+
draw()
|
|
830
|
+
mutations = new MutationObserver(records => {
|
|
831
|
+
if (records.some(record => {
|
|
832
|
+
if (overlays.has(record.target) || overlays.has(record.target.parentElement)) return false
|
|
833
|
+
if (record.type !== 'childList') return true
|
|
834
|
+
return [...record.addedNodes, ...record.removedNodes].some(node => !overlays.has(node))
|
|
835
|
+
})) schedule()
|
|
836
|
+
})
|
|
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 })
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
return { state: match.state, ...(match.reason ? { reason: match.reason } : {}) }
|
|
842
|
+
}
|
|
843
|
+
default: throw new Error('Unknown annotation operation')
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
})()
|
|
847
|
+
}
|
|
848
|
+
return JSON.stringify({ ok: true, result: globalThis[key](args) })
|
|
849
|
+
} catch (error) {
|
|
850
|
+
return JSON.stringify({ ok: false, error: String(error?.message ?? error) })
|
|
851
|
+
}
|
|
852
|
+
})()`;
|
|
853
|
+
/** The expression to evaluate in the page for one operation. */
|
|
854
|
+
export function pageCall(operation) {
|
|
855
|
+
return PAGE_SCRIPT.replace('__ARGS__', () => JSON.stringify(operation));
|
|
856
|
+
}
|
|
857
|
+
/** Reads the helper's reply, throwing its error. */
|
|
858
|
+
export function pageReply(reply) {
|
|
859
|
+
if (typeof reply !== 'string')
|
|
860
|
+
throw new Error('The page did not answer.');
|
|
861
|
+
let parsed;
|
|
862
|
+
try {
|
|
863
|
+
parsed = JSON.parse(reply);
|
|
864
|
+
}
|
|
865
|
+
catch {
|
|
866
|
+
throw new Error('The page answered with something that is not JSON.');
|
|
867
|
+
}
|
|
868
|
+
if (!parsed.ok)
|
|
869
|
+
throw new Error(parsed.error || 'The page helper failed.');
|
|
870
|
+
return parsed.result;
|
|
871
|
+
}
|