synabun 2.0.0 → 2.0.7
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 +97 -0
- package/NOTICE +5 -0
- package/README.md +2 -1
- package/THIRD-PARTY-LICENSES.md +59 -0
- package/benchmarks/fixtures/browser/README.md +1 -1
- package/benchmarks/fixtures/desktop/README.md +1 -1
- package/docs/whatsapp.md +153 -18
- package/hooks/claude-code/pre-compact.mjs +11 -0
- package/hooks/claude-code/prompt-submit.mjs +60 -184
- package/hooks/claude-code/session-start.mjs +73 -16
- package/hooks/claude-code/shared.mjs +26 -3
- package/hooks/claude-code/stop.mjs +5 -5
- package/lib/child-path.js +303 -0
- package/lib/claude-hooks.js +2 -1
- package/lib/data-home-migration.js +2 -0
- package/lib/desktop-app.js +265 -0
- package/lib/external-tools.js +7 -3
- package/lib/log-redact.js +102 -0
- package/lib/packaged-runtime.js +138 -11
- package/lib/start-launcher.js +88 -1
- package/mcp-server/dist/http.d.ts +3 -1
- package/mcp-server/dist/http.d.ts.map +1 -1
- package/mcp-server/dist/http.js +15 -4
- package/mcp-server/dist/http.js.map +1 -1
- package/mcp-server/dist/index.d.ts.map +1 -1
- package/mcp-server/dist/index.js +9 -6
- package/mcp-server/dist/index.js.map +1 -1
- package/mcp-server/dist/services/budget-judgments.d.ts +108 -0
- package/mcp-server/dist/services/budget-judgments.d.ts.map +1 -0
- package/mcp-server/dist/services/budget-judgments.js +153 -0
- package/mcp-server/dist/services/budget-judgments.js.map +1 -0
- package/mcp-server/dist/services/codex-config-heal.d.ts +5 -1
- package/mcp-server/dist/services/codex-config-heal.d.ts.map +1 -1
- package/mcp-server/dist/services/codex-config-heal.js +41 -4
- package/mcp-server/dist/services/codex-config-heal.js.map +1 -1
- package/mcp-server/dist/services/identity.d.ts +11 -0
- package/mcp-server/dist/services/identity.d.ts.map +1 -1
- package/mcp-server/dist/services/identity.js +24 -0
- package/mcp-server/dist/services/identity.js.map +1 -1
- package/mcp-server/dist/services/neural-interface.d.ts +13 -0
- package/mcp-server/dist/services/neural-interface.d.ts.map +1 -1
- package/mcp-server/dist/services/neural-interface.js +34 -2
- package/mcp-server/dist/services/neural-interface.js.map +1 -1
- package/mcp-server/dist/services/typesafe-config.d.ts +1 -1
- package/mcp-server/dist/services/typesafe-config.d.ts.map +1 -1
- package/mcp-server/dist/services/typesafe-config.js +4 -1
- package/mcp-server/dist/services/typesafe-config.js.map +1 -1
- package/mcp-server/dist/tools/agents.d.ts +10 -7
- package/mcp-server/dist/tools/agents.d.ts.map +1 -1
- package/mcp-server/dist/tools/agents.js +34 -13
- package/mcp-server/dist/tools/agents.js.map +1 -1
- package/mcp-server/dist/tools/whatsapp-send-file.d.ts +17 -0
- package/mcp-server/dist/tools/whatsapp-send-file.d.ts.map +1 -0
- package/mcp-server/dist/tools/whatsapp-send-file.js +44 -0
- package/mcp-server/dist/tools/whatsapp-send-file.js.map +1 -0
- package/mcp-server/dist/tui.js +2 -1
- package/mcp-server/dist/tui.js.map +1 -1
- package/mcp-server/dist/version.d.ts +5 -0
- package/mcp-server/dist/version.d.ts.map +1 -0
- package/mcp-server/dist/version.js +21 -0
- package/mcp-server/dist/version.js.map +1 -0
- package/mcp-server/package-lock.json +2 -2
- package/mcp-server/package.json +1 -3
- package/mcp-server/run.mjs +15 -9
- package/mcp-server/src/http.ts +18 -3
- package/mcp-server/src/index.ts +9 -6
- package/mcp-server/src/services/budget-judgments.ts +183 -0
- package/mcp-server/src/services/codex-config-heal.ts +35 -4
- package/mcp-server/src/services/identity.ts +27 -0
- package/mcp-server/src/services/neural-interface.ts +34 -2
- package/mcp-server/src/services/typesafe-config.ts +4 -1
- package/mcp-server/src/tools/agents.ts +33 -14
- package/mcp-server/src/tools/whatsapp-send-file.ts +51 -0
- package/mcp-server/src/tui.ts +2 -1
- package/mcp-server/src/version.ts +22 -0
- package/neural-interface/i18n/en.json +2126 -209
- package/neural-interface/i18n/pt-BR.json +2123 -207
- package/neural-interface/lib/account-usage.js +163 -0
- package/neural-interface/lib/assistant-api.js +316 -57
- package/neural-interface/lib/assistant-brains/claude.js +19 -2
- package/neural-interface/lib/assistant-brains/codex.js +3 -0
- package/neural-interface/lib/assistant-brains/opencode.js +17 -4
- package/neural-interface/lib/assistant-budget-planner.js +390 -0
- package/neural-interface/lib/assistant-budget-split.js +408 -0
- package/neural-interface/lib/assistant-budget.js +94 -95
- package/neural-interface/lib/assistant-catalog.js +179 -27
- package/neural-interface/lib/assistant-config.js +91 -14
- package/neural-interface/lib/assistant-dispatch.js +494 -138
- package/neural-interface/lib/assistant-hidden-models.js +393 -66
- package/neural-interface/lib/assistant-jev.js +26 -3
- package/neural-interface/lib/assistant-memory.js +24 -10
- package/neural-interface/lib/assistant-persona.js +73 -12
- package/neural-interface/lib/assistant-plan-permissions.js +8 -1
- package/neural-interface/lib/assistant-pricing.js +21 -6
- package/neural-interface/lib/assistant-route-accounts.js +14 -0
- package/neural-interface/lib/assistant-route-gate.js +19 -3
- package/neural-interface/lib/assistant-router.js +373 -42
- package/neural-interface/lib/assistant-runtime.js +488 -59
- package/neural-interface/lib/assistant-task-prompt.js +13 -3
- package/neural-interface/lib/assistant-test-density.js +142 -0
- package/neural-interface/lib/assistant-user-context-budgets.js +123 -0
- package/neural-interface/lib/assistant-user-context.js +908 -0
- package/neural-interface/lib/assistant-user-knowledge.js +328 -0
- package/neural-interface/lib/assistant-user-learn.js +404 -0
- package/neural-interface/lib/assistant-user-threads.js +226 -0
- package/neural-interface/lib/assistant-user-writer.js +183 -0
- package/neural-interface/lib/augmented-path.js +317 -53
- package/neural-interface/lib/browser-background-tab.js +134 -0
- package/neural-interface/lib/browser-screenshot.js +678 -61
- package/neural-interface/lib/claude-accounts.js +342 -31
- package/neural-interface/lib/claude-agent-bridge.js +123 -9
- package/neural-interface/lib/claude-executable.js +6 -1
- package/neural-interface/lib/claude-model-catalog.js +33 -5
- package/neural-interface/lib/claude-panel-auth.js +221 -0
- package/neural-interface/lib/claude-profile-api-key.js +62 -0
- package/neural-interface/lib/claude-temporary.js +46 -7
- package/neural-interface/lib/cli-install-api.js +137 -0
- package/neural-interface/lib/cli-install.js +797 -0
- package/neural-interface/lib/cli-path.js +130 -0
- package/neural-interface/lib/cli-resolver.js +744 -0
- package/neural-interface/lib/cli-update-planner.js +143 -7
- package/neural-interface/lib/cloudflared/install.js +533 -0
- package/neural-interface/lib/cloudflared/manager.js +545 -0
- package/neural-interface/lib/cloudflared/pins.js +115 -0
- package/neural-interface/lib/cloudflared/pins.json +63 -0
- package/neural-interface/lib/cloudflared/tunnel.js +265 -0
- package/neural-interface/lib/codex-account-auth.js +178 -0
- package/neural-interface/lib/codex-accounts.js +489 -0
- package/neural-interface/lib/codex-capabilities.js +2 -1
- package/neural-interface/lib/codex-compat-rpc.js +8 -2
- package/neural-interface/lib/codex-mcp-approvals.js +5 -1
- package/neural-interface/lib/codex-panel-auth.js +62 -0
- package/neural-interface/lib/codex-session-settings.js +1 -1
- package/neural-interface/lib/codex-settings-schema.js +120 -0
- package/neural-interface/lib/desktop/config.js +3 -0
- package/neural-interface/lib/editor-completion-endpoint.js +264 -0
- package/neural-interface/lib/editor-completion-opencode.js +436 -0
- package/neural-interface/lib/editor-completion.js +714 -0
- package/neural-interface/lib/editor-config.js +149 -0
- package/neural-interface/lib/editor-files-api.js +268 -0
- package/neural-interface/lib/editor-git-api.js +134 -0
- package/neural-interface/lib/file-explorer-api.js +1039 -0
- package/neural-interface/lib/file-explorer-system.js +355 -0
- package/neural-interface/lib/file-explorer-trash.ps1 +85 -0
- package/neural-interface/lib/file-explorer-watch.js +239 -0
- package/neural-interface/lib/git-bash/bzip2.js +339 -0
- package/neural-interface/lib/git-bash/claude-settings.js +73 -0
- package/neural-interface/lib/git-bash/extract-worker.js +34 -0
- package/neural-interface/lib/git-bash/install.js +529 -0
- package/neural-interface/lib/git-bash/licenses/COPYING +360 -0
- package/neural-interface/lib/git-bash/licenses/LGPL-2.1 +511 -0
- package/neural-interface/lib/git-bash/manager.js +268 -0
- package/neural-interface/lib/git-bash/pins.js +206 -0
- package/neural-interface/lib/git-bash/pins.json +34 -0
- package/neural-interface/lib/git-bash/resolver.js +194 -0
- package/neural-interface/lib/git-diff-api.js +103 -0
- package/neural-interface/lib/git-read.js +234 -0
- package/neural-interface/lib/greeting/api.js +89 -0
- package/neural-interface/lib/greeting/budgets.js +90 -0
- package/neural-interface/lib/greeting/engine.js +461 -0
- package/neural-interface/lib/greeting/facts.js +320 -0
- package/neural-interface/lib/greeting/sources.js +252 -0
- package/neural-interface/lib/greeting/writer.js +139 -0
- package/neural-interface/lib/http-guards.js +189 -13
- package/neural-interface/lib/install-details.js +145 -0
- package/neural-interface/lib/machine-names.js +88 -0
- package/neural-interface/lib/mcp-installer.js +14 -2
- package/neural-interface/lib/memory-api.js +31 -7
- package/neural-interface/lib/native-loop-providers.js +26 -8
- package/neural-interface/lib/native-loop-runtime.js +9 -4
- package/neural-interface/lib/os-path.js +319 -0
- package/neural-interface/lib/plugin-installer.js +4 -2
- package/neural-interface/lib/project-storage.js +5 -7
- package/neural-interface/lib/provider-panel-api-key-auth.js +92 -0
- package/neural-interface/lib/provider-panel-terminal-auth.js +95 -0
- package/neural-interface/lib/pty-host/core.js +2 -1
- package/neural-interface/lib/pty-host/manager.js +27 -2
- package/neural-interface/lib/remote-policy.js +20 -11
- package/neural-interface/lib/request-gate.js +589 -0
- package/neural-interface/lib/session-title-generator.js +4 -3
- package/neural-interface/lib/system-about.js +340 -0
- package/neural-interface/lib/system-activity.js +66 -0
- package/neural-interface/lib/terminal-env.js +11 -3
- package/neural-interface/lib/terminal-path.js +55 -0
- package/neural-interface/lib/ui-blob-api.js +108 -0
- package/neural-interface/lib/ui-blob-store.js +68 -0
- package/neural-interface/lib/virtual-socket.js +12 -1
- package/neural-interface/lib/whatsapp/baileys-adapter.js +165 -45
- package/neural-interface/lib/whatsapp/bridge.js +78 -34
- package/neural-interface/lib/whatsapp/commands.js +13 -14
- package/neural-interface/lib/whatsapp/config.js +134 -20
- package/neural-interface/lib/whatsapp/fake-baileys.js +99 -14
- package/neural-interface/lib/whatsapp/host-core.js +168 -35
- package/neural-interface/lib/whatsapp/inbound.js +5 -3
- package/neural-interface/lib/whatsapp/local-file.js +524 -0
- package/neural-interface/lib/whatsapp/manager.js +92 -32
- package/neural-interface/lib/whatsapp/paths.js +11 -1
- package/neural-interface/lib/whatsapp/protocol.js +65 -5
- package/neural-interface/lib/whatsapp/redact.js +63 -0
- package/neural-interface/lib/whatsapp/service.js +343 -91
- package/neural-interface/lib/whatsapp/transcription.js +463 -0
- package/neural-interface/package-lock.json +701 -2
- package/neural-interface/package.json +13 -2
- package/neural-interface/public/index.html +2 -0
- package/neural-interface/public/index2d.html +2 -0
- package/neural-interface/public/install-app.html +9 -6
- package/neural-interface/public/offline.html +19 -3
- package/neural-interface/public/onboarding.html +107 -2
- package/neural-interface/public/shared/account-i18n.js +7 -0
- package/neural-interface/public/shared/account-menu.js +539 -0
- package/neural-interface/public/shared/account-usage.js +19 -0
- package/neural-interface/public/shared/account-view.js +335 -0
- package/neural-interface/public/shared/agent-runtime-options.js +50 -14
- package/neural-interface/public/shared/assistant/asst-accounts.js +182 -184
- package/neural-interface/public/shared/assistant/asst-api.js +32 -2
- package/neural-interface/public/shared/assistant/asst-automation.js +286 -0
- package/neural-interface/public/shared/assistant/asst-brain-picker.js +430 -81
- package/neural-interface/public/shared/assistant/asst-budget.js +259 -303
- package/neural-interface/public/shared/assistant/asst-composer.js +11 -1
- package/neural-interface/public/shared/assistant/asst-mascot.js +140 -61
- package/neural-interface/public/shared/assistant/asst-menu.js +27 -11
- package/neural-interface/public/shared/assistant/asst-panel.js +198 -42
- package/neural-interface/public/shared/assistant/asst-render.js +197 -23
- package/neural-interface/public/shared/assistant/asst-route-models.js +108 -0
- package/neural-interface/public/shared/assistant/asst-route.js +95 -343
- package/neural-interface/public/shared/assistant/asst-routes-editor.js +534 -0
- package/neural-interface/public/shared/assistant/asst-sanitize.js +8 -1
- package/neural-interface/public/shared/assistant/asst-sidepanel.js +31 -9
- package/neural-interface/public/shared/assistant/asst-slash.js +1 -1
- package/neural-interface/public/shared/assistant/asst-state.js +186 -2
- package/neural-interface/public/shared/assistant/asst-styles.js +247 -11
- package/neural-interface/public/shared/blob-store.js +111 -22
- package/neural-interface/public/shared/cdx/cdx-commands.js +4 -1
- package/neural-interface/public/shared/cdx/cdx-features.js +22 -14
- package/neural-interface/public/shared/cdx/cdx-icons.js +2 -0
- package/neural-interface/public/shared/cdx/cdx-panel.js +80 -118
- package/neural-interface/public/shared/cdx/cdx-render.js +25 -89
- package/neural-interface/public/shared/cdx/cdx-settings.js +231 -0
- package/neural-interface/public/shared/cdx/cdx-styles.js +164 -107
- package/neural-interface/public/shared/cdx/cdx-tabs.js +348 -668
- package/neural-interface/public/shared/cdx/cdx-ws.js +2 -0
- package/neural-interface/public/shared/claude-account-view.js +3 -0
- package/neural-interface/public/shared/cli-install-view.js +392 -0
- package/neural-interface/public/shared/cli-status.js +282 -22
- package/neural-interface/public/shared/constants.js +6 -1
- package/neural-interface/public/shared/cp/cp-events.js +2 -1
- package/neural-interface/public/shared/cp/cp-rehydrate.js +2 -1
- package/neural-interface/public/shared/cp/cp-styles.js +61 -0
- package/neural-interface/public/shared/desktop-bridge.js +39 -0
- package/neural-interface/public/shared/desktop-titlebar-page.js +59 -0
- package/neural-interface/public/shared/desktop-titlebar-view.js +122 -0
- package/neural-interface/public/shared/desktop-titlebar.js +153 -0
- package/neural-interface/public/shared/desktop-update-view.js +462 -0
- package/neural-interface/public/shared/editor/editor-ai-logic.js +409 -0
- package/neural-interface/public/shared/editor/editor-ai.js +619 -0
- package/neural-interface/public/shared/editor/editor-commands.js +212 -0
- package/neural-interface/public/shared/editor/editor-compare.js +103 -0
- package/neural-interface/public/shared/editor/editor-completion-list.js +172 -0
- package/neural-interface/public/shared/editor/editor-doc.js +257 -0
- package/neural-interface/public/shared/editor/editor-git-logic.js +131 -0
- package/neural-interface/public/shared/editor/editor-git.js +95 -0
- package/neural-interface/public/shared/editor/editor-languages.js +156 -0
- package/neural-interface/public/shared/editor/editor-lint.js +229 -0
- package/neural-interface/public/shared/editor/editor-loader.js +51 -0
- package/neural-interface/public/shared/editor/editor-markdown.js +90 -0
- package/neural-interface/public/shared/editor/editor-menu.js +233 -0
- package/neural-interface/public/shared/editor/editor-minimap.js +371 -0
- package/neural-interface/public/shared/editor/editor-palette-logic.js +192 -0
- package/neural-interface/public/shared/editor/editor-palette.js +125 -0
- package/neural-interface/public/shared/editor/editor-panel-logic.js +288 -0
- package/neural-interface/public/shared/editor/editor-panel.js +2099 -0
- package/neural-interface/public/shared/editor/editor-prefs.js +224 -0
- package/neural-interface/public/shared/editor/editor-safety.js +500 -0
- package/neural-interface/public/shared/editor/editor-search.js +352 -0
- package/neural-interface/public/shared/editor/editor-statusbar.js +144 -0
- package/neural-interface/public/shared/editor/editor-tabs-logic.js +331 -0
- package/neural-interface/public/shared/editor/editor-tabs.js +233 -0
- package/neural-interface/public/shared/editor/editor-theme.js +244 -0
- package/neural-interface/public/shared/editor/editor-view.js +332 -0
- package/neural-interface/public/shared/explorer/fx-api.js +216 -0
- package/neural-interface/public/shared/explorer/fx-links.js +204 -0
- package/neural-interface/public/shared/explorer/fx-logic.js +537 -0
- package/neural-interface/public/shared/explorer/fx-main.js +865 -0
- package/neural-interface/public/shared/explorer/fx-names.js +87 -0
- package/neural-interface/public/shared/explorer/fx-ops.js +370 -0
- package/neural-interface/public/shared/explorer/fx-popups.js +490 -0
- package/neural-interface/public/shared/explorer/fx-tree.js +790 -0
- package/neural-interface/public/shared/file-links.js +339 -0
- package/neural-interface/public/shared/git-bash-view.js +145 -0
- package/neural-interface/public/shared/greeting/greeting-card.js +178 -0
- package/neural-interface/public/shared/greeting/greeting-view.js +148 -0
- package/neural-interface/public/shared/html-shell.js +161 -165
- package/neural-interface/public/shared/i18n.js +19 -1
- package/neural-interface/public/shared/install-details-view.js +86 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-childpanel.js +4 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-greeting.js +47 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-panel.js +131 -17
- package/neural-interface/public/shared/ocp-v2/ocp-v2-render.js +7 -1
- package/neural-interface/public/shared/ocp-v2/ocp-v2-send.js +22 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-styles.js +158 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-ws.js +3 -2
- package/neural-interface/public/shared/pwa-install-page.js +32 -2
- package/neural-interface/public/shared/pwa-install.css +3 -3
- package/neural-interface/public/shared/pwa-install.js +100 -1
- package/neural-interface/public/shared/settings/settings-about-pane.js +271 -0
- package/neural-interface/public/shared/settings/settings-about.js +254 -0
- package/neural-interface/public/shared/settings/settings-code-editor-pane.js +412 -0
- package/neural-interface/public/shared/settings/settings-code-editor.js +328 -0
- package/neural-interface/public/shared/settings/settings-greeting.js +133 -0
- package/neural-interface/public/shared/settings/settings-ia.js +33 -4
- package/neural-interface/public/shared/settings/settings-user-context.js +154 -0
- package/neural-interface/public/shared/showcase-privacy.js +489 -0
- package/neural-interface/public/shared/start-bridge.js +238 -19
- package/neural-interface/public/shared/storage.js +30 -3
- package/neural-interface/public/shared/styles.css +1710 -770
- package/neural-interface/public/shared/synabun-mascot.js +68 -58
- package/neural-interface/public/shared/tunnel-view.js +167 -0
- package/neural-interface/public/shared/ui-claude-panel.js +369 -229
- package/neural-interface/public/shared/ui-cli-banner.js +355 -0
- package/neural-interface/public/shared/ui-detail.js +15 -0
- package/neural-interface/public/shared/ui-explorer.js +22 -0
- package/neural-interface/public/shared/ui-file-explorer.js +481 -2496
- package/neural-interface/public/shared/ui-file-links.js +103 -0
- package/neural-interface/public/shared/ui-git-bash.js +124 -0
- package/neural-interface/public/shared/ui-help.js +26 -0
- package/neural-interface/public/shared/ui-invite.js +126 -116
- package/neural-interface/public/shared/ui-judgments.js +10 -1
- package/neural-interface/public/shared/ui-keybinds.js +4 -0
- package/neural-interface/public/shared/ui-loading.js +187 -19
- package/neural-interface/public/shared/ui-menubar.js +25 -33
- package/neural-interface/public/shared/ui-native-loop-router-state.js +11 -0
- package/neural-interface/public/shared/ui-native-loop-router.js +15 -2
- package/neural-interface/public/shared/ui-navbar.js +48 -22
- package/neural-interface/public/shared/ui-provider-auth.js +104 -0
- package/neural-interface/public/shared/ui-provider-terminal-auth.js +223 -0
- package/neural-interface/public/shared/ui-sessions.js +9 -3
- package/neural-interface/public/shared/ui-settings.js +509 -490
- package/neural-interface/public/shared/ui-sidepanel-runtime.js +2 -1
- package/neural-interface/public/shared/ui-sidepanel-sessions.js +3 -1
- package/neural-interface/public/shared/ui-sync.js +22 -0
- package/neural-interface/public/shared/ui-terminal.js +40 -5
- package/neural-interface/public/shared/ui-tool-updates.js +5 -0
- package/neural-interface/public/shared/ui-tunnel.js +130 -0
- package/neural-interface/public/shared/ui-tutorial-steps.js +6 -2
- package/neural-interface/public/shared/ui-update.js +594 -5
- package/neural-interface/public/shared/ui-whatsapp.js +21 -6
- package/neural-interface/public/shared/ui-whiteboard.js +12 -0
- package/neural-interface/public/shared/ui-worker-end.js +245 -0
- package/neural-interface/public/shared/ui-workspaces.js +41 -27
- package/neural-interface/public/shared/whatsapp/wa-view.js +51 -8
- package/neural-interface/public/shared/worker-end.js +253 -0
- package/neural-interface/public/sw.js +2 -2
- package/neural-interface/public/variant/2d/main.js +10 -1
- package/neural-interface/public/variant/3d/main.js +10 -1
- package/neural-interface/public/vendor/codemirror/chunks/apl-CSDKT72Z.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/asciiarmor-JYYPU5BM.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/asn1-NRWCZDA6.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/asterisk-5OU6D4ZQ.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/brainfuck-ID4N4SYC.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/chunk-4RZWGCS2.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/chunk-ACTJM3JR.js +11 -0
- package/neural-interface/public/vendor/codemirror/chunks/chunk-S6BMAXLK.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/chunk-TO2RDOHQ.js +3 -0
- package/neural-interface/public/vendor/codemirror/chunks/chunk-US2GMZZQ.js +13 -0
- package/neural-interface/public/vendor/codemirror/chunks/chunk-WYYO5ZAJ.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/chunk-XALZTDJC.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/chunk-YRNUTRDV.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/chunk-Z3QOKH62.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/clike-OWQBSOGL.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/clojure-KV75UQWC.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/cmake-GTEP5Y3C.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/cobol-WCZ7QEGJ.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/coffeescript-TGLBOT6B.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/commonlisp-WVVI432V.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/crystal-S6R6HLKQ.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/css-PDA2RINP.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/cypher-HL5QPL4C.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/d-HHJH2HAJ.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/diff-MN3A35BY.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/dist-25SQLBVS.js +3 -0
- package/neural-interface/public/vendor/codemirror/chunks/dist-27ZGGJJS.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/dist-4E52OJIF.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/dist-6FT73XDC.js +2 -0
- package/neural-interface/public/vendor/codemirror/chunks/dist-BE6YGRMW.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/dist-CJSGKW2O.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/dist-CRPH2WLI.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/dist-CTG7JEQN.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/dist-I3FEVELX.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/dist-IM5BBSWZ.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/dist-KERUKTIF.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/dist-LXTMWZAM.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/dist-MABASSET.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/dist-MJVBUZLL.js +3 -0
- package/neural-interface/public/vendor/codemirror/chunks/dist-MNB3WZVU.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/dist-QYWXOE3U.js +7 -0
- package/neural-interface/public/vendor/codemirror/chunks/dist-TEBOFWLZ.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/dist-UNXFROM3.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/dist-XL2SLQO6.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/dist-XO42Y3AP.js +6 -0
- package/neural-interface/public/vendor/codemirror/chunks/dist-Y2WWDWJK.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/dist-YFEZKWMU.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/dist-ZY7SWQO6.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/dockerfile-3Y4R4NCH.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/dtd-5343GJ4P.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/dylan-FI2CXSW2.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/ebnf-GNQ77YWA.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/ecl-MRPUAE2L.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/eiffel-DZROES27.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/elm-4KICRFKQ.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/erlang-3IHYC263.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/factor-C6D3E5UK.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/fcl-VYTWUJMI.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/forth-ZBO2EH5M.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/fortran-5NDCRWIK.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/gas-SKRMOXFP.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/gherkin-L45YXZLW.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/groovy-GLPTLN44.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/haskell-E7NF7P4Y.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/haxe-K2NVDOB6.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/http-Q6DRQIJB.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/idl-MZYNJVLN.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/javascript-6YRYK2HR.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/julia-HXEL2T5G.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/livescript-MV7CI2JZ.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/lua-E36NHOOD.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/mathematica-3XOZZCMC.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/mbox-ODYGFKS7.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/mirc-WB43AWF5.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/mllike-ZT6ET7ZH.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/modelica-LLMZPFGA.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/mscgen-2OPNNYFH.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/mumps-6P3HWJH6.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/nginx-TCGKQTRW.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/nsis-ATIGIXTJ.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/ntriples-5PLBI5OV.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/octave-QF2S3NL6.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/oz-JFIVFFH2.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/pascal-JG6SW76V.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/perl-4B3IWVVO.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/pig-ABEBH4MI.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/powershell-H32XE7TL.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/properties-NN644MMA.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/protobuf-YKWQK2J3.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/pug-RIYXEKFW.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/puppet-JZQ5WPEV.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/python-EBCGGHAC.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/q-5UMH2YQY.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/r-7XMGG453.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/rpm-IEV7TSLR.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/ruby-T4SPFPNM.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/sas-GEDX67ET.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/scheme-FTLOWKPC.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/shell-HUFRKFSJ.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/sieve-P5QUDVYO.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/smalltalk-2SORJNQI.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/solr-M22TGAR5.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/sparql-4M3KL752.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/spreadsheet-3K23LA4N.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/sql-VHRFMUBI.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/stex-QZDAQUY2.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/stylus-7ZDKAKBD.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/swift-WCLCLGMR.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/tcl-JLMU6WOK.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/textile-4CHDKZ4T.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/tiddlywiki-DKMCZZQ4.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/tiki-F4JIG36Q.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/toml-BHMHTIM2.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/troff-YQ2KV5NT.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/ttcn-ZHRCVFJY.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/ttcn-cfg-KJOVTY4X.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/turtle-535P5N6I.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/vb-76UMQHOT.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/vbscript-CGNDTK6Z.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/velocity-VSJDO5T5.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/verilog-4T6ETXIP.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/vhdl-T7FNAXUN.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/webidl-P6MJSJ6J.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/xquery-UOVILRGN.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/yacas-CV2APROL.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/z80-3KFNVM4M.js +1 -0
- package/neural-interface/public/vendor/codemirror/codemirror-6.43.14-75f05e67.min.js +2 -0
- package/neural-interface/public/vendor/codemirror/manifest.json +60 -0
- package/neural-interface/scripts/build-vendor.mjs +78 -2
- package/neural-interface/scripts/vendor-codemirror-entry.js +121 -0
- package/neural-interface/server.js +2085 -1629
- package/neural-interface/templates/rulesets/README.md +1 -1
- package/package.json +5 -8
- package/setup.js +53 -15
- package/benchmarks/browser-live-results.json +0 -79
- package/benchmarks/browser-v2-results.json +0 -258
- package/neural-interface/lib/codex-capabilities.d.ts +0 -41
- package/neural-interface/public/category-logos/claude.svg +0 -1
- package/neural-interface/public/category-logos/gemini-color.svg +0 -1
- package/neural-interface/public/category-logos/openai.svg +0 -1
- package/neural-interface/public/category-logos/synabun.png +0 -0
- package/neural-interface/public/claude.svg +0 -1
- package/neural-interface/public/gallery.svg +0 -6
- package/neural-interface/public/leonardoai.svg +0 -1
- package/neural-interface/public/mcp.svg +0 -4
- package/neural-interface/public/opencode-logo-dark-square.svg +0 -1
- package/neural-interface/public/pin-svgrepo-com.svg +0 -25
- package/neural-interface/public/sessionicon.svg +0 -8
- package/neural-interface/public/toolbar-album.svg +0 -8
- package/neural-interface/public/toolbar-question.svg +0 -6
- package/neural-interface/public/toolbar-sessions.svg +0 -6
- package/neural-interface/scripts/codex-protocol-fixture.mjs +0 -83
- package/skills/synabun/synabunicon.png +0 -0
|
@@ -8,12 +8,34 @@
|
|
|
8
8
|
// A viewport size is a CDP device-metrics override sent from a CDP session opened
|
|
9
9
|
// for this capture only. Chromium keeps one override per page, and detaching the
|
|
10
10
|
// session does not remove it, so it is always cleared explicitly when the capture
|
|
11
|
-
// ends — on errors and timeouts too — and the tab's innerWidth and
|
|
11
|
+
// ends — on errors and timeouts too — and the tab's innerWidth, innerHeight and
|
|
12
12
|
// devicePixelRatio are read again against the values from before the capture. A
|
|
13
13
|
// clear that fails, hangs or does not take is sent once more from a fresh CDP
|
|
14
|
-
// session; a tab still not confirmed is
|
|
15
|
-
// server.js
|
|
16
|
-
// its profile does not have.
|
|
14
|
+
// session; a tab still not confirmed is retired (viewportRestored: false):
|
|
15
|
+
// server.js takes it out of routing at once and closes it, so a MoreLogin tab
|
|
16
|
+
// never keeps a size, DPR or mobile flag its profile does not have.
|
|
17
|
+
//
|
|
18
|
+
// Measured in stock Chromium 147 with a real window (tests/browser-screenshot-
|
|
19
|
+
// window.browser.mjs), not yet on a MoreLogin browser: Page.captureScreenshot got
|
|
20
|
+
// no answer for the selected tab of a window that was minimized or whose
|
|
21
|
+
// application was hidden, for as long as it was waited on, while background tabs
|
|
22
|
+
// of that window were captured. The page cannot tell (visibilityState stays
|
|
23
|
+
// visible, requestAnimationFrame keeps firing), and for a capture with a clip
|
|
24
|
+
// Chromium has by then resized the tab's view to the clip (64x64 for the scale
|
|
25
|
+
// measurement) and puts the size back only when the capture answers. So every
|
|
26
|
+
// capture first sends one capture without a clip, which resizes nothing: no
|
|
27
|
+
// answer in time is reported as such, with the window state CDP reports, and the
|
|
28
|
+
// tab is untouched. A capture that stops answering later is cut short with the
|
|
29
|
+
// name of its phase, and the view is put back with Emulation.setVisibleSize. The
|
|
30
|
+
// page reads the clip's size only once the capture's CDP session is gone (its own
|
|
31
|
+
// emulation hides it until then; measured, about 2 ms after the detach answers, 2.2 ms seen),
|
|
32
|
+
// so the tab is confirmed after the detach, on two readings 150 ms apart.
|
|
33
|
+
//
|
|
34
|
+
// One capture runs per tab at a time, and one budget (the request deadline)
|
|
35
|
+
// covers the capture and putting the tab back: the answer never waits past it.
|
|
36
|
+
// A capture the browser never answered, on a CDP session that could not be
|
|
37
|
+
// confirmed detached, may still be pending: its tab is not given to the next
|
|
38
|
+
// capture until one of the two ends, and is retired if neither does.
|
|
17
39
|
|
|
18
40
|
import { closeSync, constants as fsConstants, existsSync, lstatSync, mkdirSync, openSync, readSync, realpathSync, renameSync, unlinkSync, writeSync } from 'node:fs';
|
|
19
41
|
import { basename, dirname, isAbsolute, join, resolve } from 'node:path';
|
|
@@ -234,44 +256,318 @@ export function writeScreenshotFile(path, bytes, { dataHome = getDataHome(), reg
|
|
|
234
256
|
// ── capture ─────────────────────────────────────────────────────────────────
|
|
235
257
|
|
|
236
258
|
const delay = (ms) => new Promise((done) => setTimeout(done, ms));
|
|
237
|
-
|
|
238
|
-
//
|
|
259
|
+
// `promise`, or a rejection after `ms`. `late(value)` gets a value that arrives
|
|
260
|
+
// after the wait was given up, so what it names (a CDP session) is not left behind.
|
|
261
|
+
const within = (promise, ms, what, late = null) => {
|
|
262
|
+
let timer;
|
|
263
|
+
let over = false;
|
|
264
|
+
const watched = Promise.resolve(promise);
|
|
265
|
+
if (late) watched.then((value) => { if (over) Promise.resolve().then(() => late(value)).catch(() => {}); }, () => {});
|
|
266
|
+
return Promise.race([watched, new Promise((_, fail) => { timer = setTimeout(() => { over = true; fail(new Error(`${what} timed out`)); }, ms); })]).finally(() => clearTimeout(timer));
|
|
267
|
+
};
|
|
268
|
+
// The longest one clear, one fresh CDP session or one viewport read may take.
|
|
269
|
+
// Every such wait is a slice of the capture's one budget (stepUntil), never more.
|
|
239
270
|
const RESTORE_STEP_MS = 2000;
|
|
271
|
+
// How long the browser gets to present one frame before a capture gives up.
|
|
272
|
+
const PRESENT_TIMEOUT_MS = 8000;
|
|
273
|
+
// The selected tab of a minimized window presents nothing; a background tab of it
|
|
274
|
+
// does, at once. This long to tell them apart.
|
|
275
|
+
const MINIMIZED_GRACE_MS = 1500;
|
|
276
|
+
// The budget of a capture whose caller names no deadline.
|
|
277
|
+
const DEFAULT_BUDGET_MS = 60_000;
|
|
278
|
+
// The part of the budget kept back for putting the tab's viewport back: a capture
|
|
279
|
+
// still working that long before the deadline is given up on (half of a budget
|
|
280
|
+
// under twice this).
|
|
281
|
+
const RECOVERY_SHARE_MS = 4000;
|
|
282
|
+
// Putting the viewport back never takes longer than this, however far the deadline is.
|
|
283
|
+
const RECOVERY_CAP_MS = 10_000;
|
|
284
|
+
// How long the answer waits to hear that a retired tab closed (never past the deadline).
|
|
285
|
+
const CLOSE_CONFIRM_MS = 3000;
|
|
286
|
+
// How long a tab stays kept from the next capture while an unanswered capture of it may
|
|
287
|
+
// still be pending (its CDP session was not confirmed detached); after that it is retired.
|
|
288
|
+
const PENDING_HOLD_MS = 60_000;
|
|
289
|
+
|
|
290
|
+
/** What is left of a wait that ends at `until`, at most `cap`; 1 ms once it is over, so the step fails at once. */
|
|
291
|
+
const stepUntil = (until, cap = RESTORE_STEP_MS) => Math.max(1, Math.min(cap, until - Date.now()));
|
|
292
|
+
const seconds = (ms) => `${Math.max(0.1, Math.round(ms / 100) / 10)} s`;
|
|
293
|
+
/** A CDP session not confirmed detached may still be attached in the browser: recorded, never dropped silently. */
|
|
294
|
+
const detachFailed = (what, error) => {
|
|
295
|
+
try { console.warn(`[browser] screenshot: ${what} was not confirmed detached and may still be attached: ${error?.message ?? error}`); } catch { /* nothing left to tell */ }
|
|
296
|
+
};
|
|
297
|
+
/**
|
|
298
|
+
* Detach `session`. `confirmed` settles true or false within the wait (never
|
|
299
|
+
* past `until`); `eventually` settles true when the browser does answer, however
|
|
300
|
+
* late, and false when it refuses. A detach refused or not answered in time is
|
|
301
|
+
* recorded.
|
|
302
|
+
*/
|
|
303
|
+
function letGo(session, until, what) {
|
|
304
|
+
const eventually = Promise.resolve().then(() => session.detach()).then(() => true, (error) => { detachFailed(what, error); return false; });
|
|
305
|
+
const confirmed = within(eventually, stepUntil(until), 'detaching').catch((error) => { detachFailed(what, error); return false; });
|
|
306
|
+
return { confirmed, eventually };
|
|
307
|
+
}
|
|
308
|
+
const detachWithin = (session, until, what) => letGo(session, until, what).confirmed;
|
|
309
|
+
/** A CDP session nobody will use (it arrived after its wait was given up): detached, and a failure recorded. */
|
|
310
|
+
const discard = (session, what) => Promise.resolve().then(() => session?.detach?.()).catch((error) => detachFailed(what, error));
|
|
311
|
+
const openSession = (newSession, until) => within(Promise.resolve().then(newSession), stepUntil(until), 'opening a CDP session', (session) => discard(session, 'a CDP session that arrived after its wait was given up'));
|
|
312
|
+
|
|
313
|
+
// What a capture is doing, for the error of one that is cut short.
|
|
314
|
+
const PHASES = {
|
|
315
|
+
queue: 'waiting for an earlier screenshot of this tab to finish',
|
|
316
|
+
session: 'opening a CDP session',
|
|
317
|
+
viewport: 'reading the tab\'s viewport',
|
|
318
|
+
present: 'waiting for the answer to its first capture (the one without a clip)',
|
|
319
|
+
layout: 'reading the page layout',
|
|
320
|
+
emulate: 'setting the viewport size',
|
|
321
|
+
calibrate: 'measuring the capture scale',
|
|
322
|
+
file: 'capturing the image for the file',
|
|
323
|
+
capture: 'capturing the image',
|
|
324
|
+
};
|
|
325
|
+
/**
|
|
326
|
+
* The window state CDP reported for a failure: the one read when the capture
|
|
327
|
+
* stopped, and the one read when it began whenever that is another (a state that
|
|
328
|
+
* could not be read is `unknown`, never the other reading).
|
|
329
|
+
*/
|
|
330
|
+
const windowWords = (atStop, atStart) => (atStop === atStart ? atStop || 'unknown' : `${atStop || 'unknown'} when it stopped, ${atStart || 'unknown'} when it began`);
|
|
331
|
+
/** What was measured when a capture stopped: its phase, how long it was in it and the window state CDP reported. */
|
|
332
|
+
const measured = (phase, waitedMs, windowState, windowAtStart = windowState) => `phase ${phase}, ${seconds(waitedMs)} in it; window state from CDP: ${windowWords(windowState || null, windowAtStart || null)}`;
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* The first capture, sent without a clip, got no answer in the time it was
|
|
336
|
+
* given; nothing that resizes the tab was sent. The message says what was
|
|
337
|
+
* measured (the command without an answer, the phase, the wait, the window state
|
|
338
|
+
* CDP reported when the capture began and when it stopped) and, for a minimized
|
|
339
|
+
* window, the one behaviour that was measured in Chromium 147; it names no other
|
|
340
|
+
* cause.
|
|
341
|
+
*/
|
|
342
|
+
export class ScreenshotStalledError extends Error {
|
|
343
|
+
constructor(windowState, waitedMs, windowAtStart = windowState) {
|
|
344
|
+
super(`No screenshot: the screenshot command (Page.captureScreenshot, sent without a clip) got no answer in the time it was given (${measured('present', waitedMs, windowState, windowAtStart)}). The tab was not resized. ${windowState === 'minimized' || windowAtStart === 'minimized'
|
|
345
|
+
? 'Measured in Chromium 147: a capture of the selected tab of a minimized window gets no answer until the window is restored, and captures of its other tabs do. Restore the window (it may stay behind other windows) and take the screenshot again.'
|
|
346
|
+
: 'CDP does not report a hidden application, and in Chromium 147 a capture of the selected tab of a hidden application\'s window gets no answer either; no other cause was measured. Check that the browser window is on screen and take the screenshot again.'}`);
|
|
347
|
+
this.status = 503;
|
|
348
|
+
this.code = 'SCREENSHOT_NOT_PRESENTING';
|
|
349
|
+
this.phase = 'present';
|
|
350
|
+
this.windowState = windowState || null;
|
|
351
|
+
this.windowAtStart = windowAtStart || null;
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/** An earlier screenshot of this tab was cut short and the tab is still being put back at its own size: nothing was sent. */
|
|
356
|
+
export class ScreenshotTabRecoveringError extends Error {
|
|
357
|
+
constructor(recovering) {
|
|
358
|
+
super(`No screenshot: this tab is being put back at its own size after a screenshot that stopped while ${PHASES[recovering.phase] || recovering.phase} ${seconds(Date.now() - recovering.since)} ago (phase recover). Nothing was sent to the tab; take the screenshot again in a few seconds.`);
|
|
359
|
+
this.status = 503;
|
|
360
|
+
this.code = 'SCREENSHOT_TAB_RECOVERING';
|
|
361
|
+
this.phase = 'recover';
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* An earlier screenshot of this tab got no answer and its CDP session could not
|
|
367
|
+
* be confirmed detached, so that capture may still be pending in the browser:
|
|
368
|
+
* nothing was sent, and no second capture is started next to it.
|
|
369
|
+
*/
|
|
370
|
+
export class ScreenshotPendingError extends Error {
|
|
371
|
+
constructor(pending) {
|
|
372
|
+
super(`No screenshot: an earlier screenshot of this tab got no answer while ${PHASES[pending.phase] || pending.phase} ${seconds(Date.now() - pending.since)} ago, and its CDP session could not be confirmed detached, so it may still be pending in the browser (phase pending). Nothing was sent to the tab; take the screenshot again in a few seconds. A tab still in this state ${seconds(pending.holdMs)} after that screenshot is retired.`);
|
|
373
|
+
this.status = 503;
|
|
374
|
+
this.code = 'SCREENSHOT_CAPTURE_PENDING';
|
|
375
|
+
this.phase = 'pending';
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/** The tab was retired after an earlier screenshot (its size could not be confirmed, or a capture of it may be pending for good): nothing was sent. */
|
|
380
|
+
export class ScreenshotTabRetiredError extends Error {
|
|
381
|
+
constructor(retired) {
|
|
382
|
+
super(retired.reason === 'pending'
|
|
383
|
+
? `No screenshot: this tab was retired ${seconds(Date.now() - retired.at)} ago: a screenshot that got no answer while ${PHASES[retired.phase] || retired.phase} may still be pending in the browser, and its CDP session was never confirmed detached (phase retired). Nothing was sent to the tab; the next browser call opens a fresh one.`
|
|
384
|
+
: `No screenshot: this tab was retired ${seconds(Date.now() - retired.at)} ago, after a screenshot that stopped while ${PHASES[retired.phase] || retired.phase} left it at a size SynaBun could not confirm (phase retired). Nothing was sent to the tab; the next browser call opens a fresh one.`);
|
|
385
|
+
this.status = 410;
|
|
386
|
+
this.code = 'SCREENSHOT_TAB_RETIRED';
|
|
387
|
+
this.phase = 'retired';
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// One capture per tab at a time: a second one would read, resize and restore the
|
|
392
|
+
// view under the first. A gate per page holds the queue (`tail` settles when every
|
|
393
|
+
// turn taken so far has been released), what a cut-short capture is still putting
|
|
394
|
+
// back (`recovering`), a capture that may still be pending in the browser
|
|
395
|
+
// (`pending`) and whether the tab was retired. It lives as long as the
|
|
396
|
+
// page object, so a waiter that gives up frees its own place and nothing else:
|
|
397
|
+
// whoever comes after it still waits for the capture that is active.
|
|
398
|
+
const gates = new WeakMap();
|
|
399
|
+
function gateOf(page) {
|
|
400
|
+
let gate = gates.get(page);
|
|
401
|
+
if (!gate) {
|
|
402
|
+
gate = { tail: Promise.resolve(), holder: null, recovering: null, pending: null, retired: null, watchers: new Set() };
|
|
403
|
+
gates.set(page, gate);
|
|
404
|
+
}
|
|
405
|
+
return gate;
|
|
406
|
+
}
|
|
407
|
+
function takeTurn(gate) {
|
|
408
|
+
const previous = gate.tail;
|
|
409
|
+
let release;
|
|
410
|
+
const mine = new Promise((done) => { release = done; });
|
|
411
|
+
gate.tail = previous.then(() => mine);
|
|
412
|
+
return { ready: previous, release };
|
|
413
|
+
}
|
|
414
|
+
/** Those waiting for the tab's turn look at its gate again now (retired, or a capture that may still be pending). */
|
|
415
|
+
function wakeWaiters(gate) {
|
|
416
|
+
for (const wake of gate.watchers) wake();
|
|
417
|
+
gate.watchers.clear();
|
|
418
|
+
}
|
|
419
|
+
/** From now on no capture touches the tab: those waiting for it are told at once, later ones refused. */
|
|
420
|
+
function retireGate(gate, phase, reason = 'size') {
|
|
421
|
+
if (gate.retired) return;
|
|
422
|
+
gate.retired = { phase, reason, at: Date.now() };
|
|
423
|
+
wakeWaiters(gate);
|
|
424
|
+
}
|
|
240
425
|
|
|
241
426
|
/** The tab's own viewport: innerWidth, innerHeight and devicePixelRatio as the page sees them. */
|
|
242
|
-
function readViewport(page) {
|
|
243
|
-
return within(page.evaluate(() => ({ width: window.innerWidth, height: window.innerHeight, dpr: window.devicePixelRatio })),
|
|
427
|
+
function readViewport(page, ms = RESTORE_STEP_MS) {
|
|
428
|
+
return within(page.evaluate(() => ({ width: window.innerWidth, height: window.innerHeight, dpr: window.devicePixelRatio })), ms, 'reading the viewport');
|
|
244
429
|
}
|
|
430
|
+
const sameView = (now, before) => now.width === before.width && now.height === before.height && now.dpr === before.dpr;
|
|
245
431
|
|
|
246
432
|
/**
|
|
247
|
-
*
|
|
248
|
-
*
|
|
249
|
-
*
|
|
250
|
-
*
|
|
251
|
-
*
|
|
252
|
-
*
|
|
433
|
+
* True when the tab reads the width, height and devicePixelRatio it had before
|
|
434
|
+
* the capture (`before`). One reading that matches is enough after a capture
|
|
435
|
+
* that answered; one that does not is taken once more SETTLE_MS later, for a
|
|
436
|
+
* change still on its way. With `twice` (after a capture that failed with a
|
|
437
|
+
* clip sent) two readings SETTLE_MS apart must both match, and a reading that
|
|
438
|
+
* differs after one that matched is a tab that did not stay as it was. An
|
|
439
|
+
* unreadable tab is not confirmed.
|
|
253
440
|
*/
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
if (
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
441
|
+
async function viewportIs(page, before, until = Infinity, twice = false) {
|
|
442
|
+
let matched = 0;
|
|
443
|
+
for (let attempt = 0; attempt < (twice ? 3 : 2); attempt += 1) {
|
|
444
|
+
if (attempt) {
|
|
445
|
+
if (Date.now() + SETTLE_MS >= until) return false;
|
|
446
|
+
await delay(SETTLE_MS);
|
|
447
|
+
}
|
|
448
|
+
let same = false;
|
|
449
|
+
try {
|
|
450
|
+
same = sameView(await readViewport(page, stepUntil(until)), before);
|
|
451
|
+
} catch { /* unreadable: not confirmed */ }
|
|
452
|
+
if (same) {
|
|
453
|
+
matched += 1;
|
|
454
|
+
if (!twice || matched === 2) return true;
|
|
455
|
+
} else if (matched) {
|
|
456
|
+
return false;
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
return false;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
// Chromium resizes a tab's view to the clip of a capture and puts the size back
|
|
463
|
+
// only when that capture answers. Emulation.setVisibleSize sets it directly, in
|
|
464
|
+
// DIP: CSS px times the page zoom. So the first size asked for is the CSS size;
|
|
465
|
+
// the second is corrected once in proportion to what the page then reads (the
|
|
466
|
+
// zoom), and after that only a pixel at a time, until width and height are
|
|
467
|
+
// exactly the tab's own. A view that does not follow that way is left alone:
|
|
468
|
+
// no size further from the tab's own is ever asked for twice.
|
|
469
|
+
async function resizeViewTo(page, session, before, until = Infinity) {
|
|
470
|
+
const sized = (now) => now.width === before.width && now.height === before.height;
|
|
471
|
+
const near = (got, want) => Math.abs(got - want) <= 2;
|
|
472
|
+
const left = await readViewport(page, stepUntil(until));
|
|
473
|
+
if (sized(left)) return true;
|
|
474
|
+
let last = left;
|
|
475
|
+
let width = before.width;
|
|
476
|
+
let height = before.height;
|
|
477
|
+
let scaled = false;
|
|
478
|
+
for (let attempt = 0; attempt < 4 && Date.now() < until; attempt += 1) {
|
|
479
|
+
await within(session.send('Emulation.setVisibleSize', { width, height }), stepUntil(until), 'resizing the view');
|
|
480
|
+
let now = last;
|
|
481
|
+
for (let read = 0; read < 4 && Date.now() < until; read += 1) {
|
|
482
|
+
await delay(read ? SETTLE_MS : 20);
|
|
483
|
+
now = await readViewport(page, stepUntil(until));
|
|
484
|
+
if (sized(now)) return true;
|
|
485
|
+
if (now.width !== last.width || now.height !== last.height) break;
|
|
263
486
|
}
|
|
487
|
+
// Not applied at all, or a reading no page zoom explains (25% to 500%): leave it.
|
|
488
|
+
if (!attempt && now.width === left.width && now.height === left.height) return false;
|
|
489
|
+
if (!(now.width > 0 && now.height > 0)) return false;
|
|
490
|
+
const zoom = Math.max(width / now.width, height / now.height, now.width / width, now.height / height);
|
|
491
|
+
if (zoom > 5.5) return false;
|
|
492
|
+
if (!near(now.width, before.width) || !near(now.height, before.height)) {
|
|
493
|
+
if (scaled) return false;
|
|
494
|
+
scaled = true;
|
|
495
|
+
}
|
|
496
|
+
const next = (asked, got, want) => (got === want ? asked : near(got, want) ? asked + Math.sign(want - got) : Math.round(asked * want / got));
|
|
497
|
+
width = next(width, now.width, before.width);
|
|
498
|
+
height = next(height, now.height, before.height);
|
|
499
|
+
last = now;
|
|
500
|
+
}
|
|
501
|
+
return false;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
/**
|
|
505
|
+
* After a capture with a clip that did not answer, once its CDP session is
|
|
506
|
+
* detached (a pending capture holds the view at its size, and the page reads
|
|
507
|
+
* that size only when the session is gone): put the tab's view back at `before`
|
|
508
|
+
* if Chromium left it at the clip's size. True only when the tab then reads its
|
|
509
|
+
* own width, height and devicePixelRatio on two readings SETTLE_MS apart (the
|
|
510
|
+
* size showed about 2 ms after the detach answered: at most 1.8 ms in headless
|
|
511
|
+
* Chromium, 20 of 20 detaches per run of browser-screenshot-stall.browser.mjs,
|
|
512
|
+
* and 2.2 ms seen in a real minimized window); every wait ends at `until`, and
|
|
513
|
+
* a budget too short for the second reading confirms nothing.
|
|
514
|
+
*/
|
|
515
|
+
export async function restoreViewSize(page, before, { newSession = () => page.context().newCDPSession(page), until = Infinity } = {}) {
|
|
516
|
+
let now;
|
|
517
|
+
try {
|
|
518
|
+
now = await readViewport(page, stepUntil(until));
|
|
519
|
+
if (sameView(now, before)) {
|
|
520
|
+
if (Date.now() + SETTLE_MS >= until) return false;
|
|
521
|
+
await delay(SETTLE_MS);
|
|
522
|
+
now = await readViewport(page, stepUntil(until));
|
|
523
|
+
if (sameView(now, before)) return true;
|
|
524
|
+
}
|
|
525
|
+
} catch {
|
|
264
526
|
return false;
|
|
265
|
-
}
|
|
527
|
+
}
|
|
528
|
+
// Nothing here changes a devicePixelRatio: one that is not the tab's own stays unconfirmed.
|
|
529
|
+
if (now.dpr !== before.dpr) return false;
|
|
530
|
+
let fresh = null;
|
|
531
|
+
try {
|
|
532
|
+
fresh = await openSession(newSession, until);
|
|
533
|
+
await resizeViewTo(page, fresh, before, until);
|
|
534
|
+
} catch { /* checked below */ } finally {
|
|
535
|
+
if (fresh) await detachWithin(fresh, until, 'the CDP session that put the view back');
|
|
536
|
+
}
|
|
537
|
+
// A size put back counts once it has stayed: two readings apart, not the one right after the resize.
|
|
538
|
+
return viewportIs(page, before, until, true);
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
/**
|
|
542
|
+
* Clear the device-metrics override and confirm the tab is back: innerWidth,
|
|
543
|
+
* innerHeight and devicePixelRatio equal to what they were before the capture
|
|
544
|
+
* (a reading that differs is taken once more SETTLE_MS later). A clear that
|
|
545
|
+
* fails, hangs or does not take is sent once more from a fresh CDP session.
|
|
546
|
+
* Chromium ignores a clear from a session that never set an override, so that
|
|
547
|
+
* session first sets an empty one (0 width, height and scale: nothing
|
|
548
|
+
* overridden). A view still at another size then is one a scaled clip resized
|
|
549
|
+
* (the capture never answered): `release` detaches the capture's own session,
|
|
550
|
+
* whose pending capture holds the view, and the fresh session sets the size
|
|
551
|
+
* back. True when confirmed; every wait ends at `until`. After a capture that
|
|
552
|
+
* failed with a clip sent, captureScreenshot does not stop at this: it confirms
|
|
553
|
+
* the tab again once the capture's session is detached (restoreViewSize).
|
|
554
|
+
*/
|
|
555
|
+
export async function restoreViewport(page, cdp, before, { newSession = () => page.context().newCDPSession(page), release = async () => {}, until = Infinity } = {}) {
|
|
556
|
+
const clear = (session) => within(session.send('Emulation.clearDeviceMetricsOverride'), stepUntil(until), 'clearing the viewport');
|
|
557
|
+
const back = () => viewportIs(page, before, until);
|
|
266
558
|
try { await clear(cdp); } catch { /* retried below */ }
|
|
267
559
|
if (await back()) return true;
|
|
268
560
|
let fresh = null;
|
|
269
561
|
try {
|
|
270
|
-
fresh = await
|
|
271
|
-
await within(fresh.send('Emulation.setDeviceMetricsOverride', { width: 0, height: 0, deviceScaleFactor: 0, mobile: false }),
|
|
562
|
+
fresh = await openSession(newSession, until);
|
|
563
|
+
await within(fresh.send('Emulation.setDeviceMetricsOverride', { width: 0, height: 0, deviceScaleFactor: 0, mobile: false }), stepUntil(until), 'resetting the viewport');
|
|
272
564
|
await clear(fresh);
|
|
565
|
+
if (!await back()) {
|
|
566
|
+
await release();
|
|
567
|
+
await resizeViewTo(page, fresh, before, until);
|
|
568
|
+
}
|
|
273
569
|
} catch { /* checked below */ } finally {
|
|
274
|
-
if (fresh) await
|
|
570
|
+
if (fresh) await detachWithin(fresh, until, 'the CDP session that cleared the viewport');
|
|
275
571
|
}
|
|
276
572
|
return back();
|
|
277
573
|
}
|
|
@@ -284,6 +580,37 @@ async function settleLayout(page) {
|
|
|
284
580
|
await delay(SETTLE_MS);
|
|
285
581
|
}
|
|
286
582
|
|
|
583
|
+
/** The window state CDP reports for the tab's window (normal, minimized, maximized, fullscreen), or null. */
|
|
584
|
+
const readWindowState = (cdp, ms) => within(Promise.resolve().then(() => cdp.send('Browser.getWindowForTarget')), ms, 'reading the window state')
|
|
585
|
+
.then((target) => target?.bounds?.windowState || null, () => null); // a browser without windows: unknown
|
|
586
|
+
|
|
587
|
+
// One capture without a clip, or word that it got no answer in time
|
|
588
|
+
// (ScreenshotStalledError). Unlike every later capture it resizes nothing: when
|
|
589
|
+
// it does not answer, the tab is exactly as it was. In a minimized window the
|
|
590
|
+
// answer comes at once or not at all, so it is not waited on for long.
|
|
591
|
+
// `windowRead` settles with the window state CDP reported when the capture began.
|
|
592
|
+
function presentedFrame(cdp, timeoutMs) {
|
|
593
|
+
const started = Date.now();
|
|
594
|
+
const windowRead = readWindowState(cdp, 1000);
|
|
595
|
+
const frame = new Promise((done, fail) => {
|
|
596
|
+
let timer = null;
|
|
597
|
+
let over = false;
|
|
598
|
+
const finish = (settle, value) => {
|
|
599
|
+
if (over) return;
|
|
600
|
+
over = true;
|
|
601
|
+
clearTimeout(timer);
|
|
602
|
+
settle(value);
|
|
603
|
+
};
|
|
604
|
+
cdp.send('Page.captureScreenshot', { format: 'jpeg', quality: 1, optimizeForSpeed: true }).then(() => finish(done), (error) => finish(fail, error));
|
|
605
|
+
windowRead.then((windowState) => {
|
|
606
|
+
if (over) return;
|
|
607
|
+
const allowed = windowState === 'minimized' ? Math.min(MINIMIZED_GRACE_MS, timeoutMs) : timeoutMs;
|
|
608
|
+
timer = setTimeout(() => finish(fail, new ScreenshotStalledError(windowState, Date.now() - started)), Math.max(0, allowed - (Date.now() - started)));
|
|
609
|
+
});
|
|
610
|
+
});
|
|
611
|
+
return { frame, windowRead };
|
|
612
|
+
}
|
|
613
|
+
|
|
287
614
|
// Device pixels per CSS pixel in this session's captures, measured: the width of
|
|
288
615
|
// a small corner shot. The page's devicePixelRatio can be a fingerprint value, and
|
|
289
616
|
// another client's DPR emulation does not apply to this session's captures.
|
|
@@ -297,46 +624,172 @@ async function captureScaleOf(cdp, visual) {
|
|
|
297
624
|
}
|
|
298
625
|
|
|
299
626
|
/**
|
|
300
|
-
* Capture `page`. `options` from parseScreenshotOptions.
|
|
301
|
-
*
|
|
302
|
-
*
|
|
303
|
-
*
|
|
304
|
-
* the
|
|
305
|
-
*
|
|
306
|
-
*
|
|
307
|
-
*
|
|
627
|
+
* Capture `page`. `options` from parseScreenshotOptions.
|
|
628
|
+
*
|
|
629
|
+
* One budget, ending at `deadline` (epoch ms; 60 s from now when not given),
|
|
630
|
+
* covers the capture and putting the tab back: the call has settled by then,
|
|
631
|
+
* whatever the browser does. A capture still working RECOVERY_SHARE_MS before
|
|
632
|
+
* it is given up on, with the phase it was in (`phase`, and in the message,
|
|
633
|
+
* with the window state CDP reported); what is left goes to the viewport.
|
|
634
|
+
* `runBounded(work)` may end the capture earlier (server.js: the request was
|
|
635
|
+
* aborted); `presentTimeoutMs` is how long the browser gets to present a frame.
|
|
636
|
+
*
|
|
637
|
+
* Captures of one tab run one at a time, and the turn is held until everything
|
|
638
|
+
* a capture started is over. A first capture (no clip) that gets no answer
|
|
639
|
+
* throws ScreenshotStalledError and nothing that resizes the tab was sent. After
|
|
640
|
+
* any other failure that sent an override or a clip, the tab is put back
|
|
641
|
+
* (restoreViewport, restoreViewSize) and confirmed at the width, height and
|
|
642
|
+
* devicePixelRatio it had, after a clip on two readings apart taken once the
|
|
643
|
+
* capture's CDP session is detached; while that goes on, another call for the
|
|
644
|
+
* tab is refused at once (ScreenshotTabRecoveringError). A tab not confirmed
|
|
645
|
+
* when the capture answers, the deadline included, is retired: `retire(reason)`
|
|
646
|
+
* is called at once (server.js takes it out of routing and closes it;
|
|
647
|
+
* `{ closed }` settles true when the close was confirmed), every later call for
|
|
648
|
+
* the page is refused (ScreenshotTabRetiredError), and the answer carries
|
|
649
|
+
* `viewportRestored: false`, `tab: 'retired'` and `tabClosed`. A failure's `tab`
|
|
650
|
+
* is otherwise 'untouched' or 'restored'.
|
|
651
|
+
*
|
|
652
|
+
* When the capture's CDP session cannot be confirmed detached, what it left
|
|
653
|
+
* unanswered decides. The override or a capture with a clip: the browser may
|
|
654
|
+
* still apply it, so the tab is not confirmed and is retired as above. A capture
|
|
655
|
+
* without a clip: nothing can resize the tab, so it is kept, but its turn is not
|
|
656
|
+
* released (`capturePending` on the error; other calls are refused at once with
|
|
657
|
+
* ScreenshotPendingError) until that capture is answered or the detach is; a tab
|
|
658
|
+
* still held after `pendingHoldMs` is retired (`retire('pending')`). Nothing
|
|
659
|
+
* unanswered: the tab is given back and the session recorded as possibly left
|
|
660
|
+
* attached.
|
|
661
|
+
*
|
|
662
|
+
* `onRecord` gets one record per call: { outcome: 'ok' | 'failed' | 'refused',
|
|
663
|
+
* phase, code, windowState, windowAtStart, tab, tabClosed, ms } and
|
|
664
|
+
* `capturePending: true` when the tab is held. `windowState` is what CDP
|
|
665
|
+
* reported when the capture began; for a failure, what it reported when the
|
|
666
|
+
* capture stopped (null when that could not be read, never the earlier reading).
|
|
667
|
+
*
|
|
668
|
+
* Returns the base64 image to show (downscaled to `maxWidth` unless 0, and
|
|
669
|
+
* under 8000 px), the full-resolution file bytes when saving, the sizes and,
|
|
670
|
+
* after an emulated size, `viewportRestored`.
|
|
308
671
|
*/
|
|
309
|
-
export async function captureScreenshot(page, options, {
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
672
|
+
export async function captureScreenshot(page, options, {
|
|
673
|
+
runBounded = (work) => work(),
|
|
674
|
+
deadline = null,
|
|
675
|
+
restore = restoreViewport,
|
|
676
|
+
restoreSize = restoreViewSize,
|
|
677
|
+
presentTimeoutMs = PRESENT_TIMEOUT_MS,
|
|
678
|
+
pendingHoldMs = PENDING_HOLD_MS,
|
|
679
|
+
retire = null,
|
|
680
|
+
onRecord = null,
|
|
681
|
+
} = {}) {
|
|
682
|
+
const started = Date.now();
|
|
683
|
+
const until = Number.isFinite(deadline) ? deadline : started + DEFAULT_BUDGET_MS;
|
|
684
|
+
const gate = gateOf(page);
|
|
685
|
+
const run = {
|
|
686
|
+
phase: 'queue', since: started, cdp: null, before: null,
|
|
687
|
+
overrideSent: false,
|
|
688
|
+
// A capture with a clip went out: Chromium resizes the view to the clip until it answers.
|
|
689
|
+
clipSent: false,
|
|
690
|
+
// Commands the browser has not answered yet: captures, and those that resize the tab (the override, a capture with a clip).
|
|
691
|
+
captures: 0, resizing: 0, quiet: null,
|
|
692
|
+
// Its unanswered capture may still be pending in the browser: the tab's turn is not released.
|
|
693
|
+
pending: false,
|
|
694
|
+
settled: false, finished: false,
|
|
695
|
+
// The window state CDP reported when the capture began, and when it stopped (read for failures only).
|
|
696
|
+
windowAtStart: null, windowAtStop: null, windowRead: null,
|
|
697
|
+
};
|
|
698
|
+
const record = (outcome, error = null, tab = 'untouched', tabClosed = null) => {
|
|
699
|
+
try {
|
|
700
|
+
onRecord?.({ outcome, phase: error?.phase ?? null, code: error?.code ?? null, windowState: outcome === 'failed' ? run.windowAtStop : run.windowAtStart, windowAtStart: run.windowAtStart, tab, tabClosed, ...(run.pending && { capturePending: true }), ms: Date.now() - started });
|
|
701
|
+
} catch { /* a record that cannot be written never fails a capture */ }
|
|
702
|
+
};
|
|
703
|
+
if (gate.retired || gate.pending || gate.recovering) {
|
|
704
|
+
const refusal = gate.retired ? new ScreenshotTabRetiredError(gate.retired) : gate.pending ? new ScreenshotPendingError(gate.pending) : new ScreenshotTabRecoveringError(gate.recovering);
|
|
705
|
+
record('refused', refusal);
|
|
706
|
+
throw refusal;
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
const turn = takeTurn(gate);
|
|
710
|
+
const enter = (phase) => { run.phase = phase; run.since = Date.now(); };
|
|
711
|
+
const cancelled = () => { if (run.settled) throw new Error('Screenshot cancelled'); };
|
|
712
|
+
const work = async () => {
|
|
713
|
+
try {
|
|
714
|
+
// The turn, or word that the tab was retired, or is held for a capture that may still be pending, while this call waited for it.
|
|
715
|
+
let wake;
|
|
716
|
+
const told = new Promise((done) => { wake = done; });
|
|
717
|
+
gate.watchers.add(wake);
|
|
718
|
+
try { await Promise.race([turn.ready, told]); } finally { gate.watchers.delete(wake); }
|
|
719
|
+
if (gate.retired) throw new ScreenshotTabRetiredError(gate.retired);
|
|
720
|
+
if (gate.pending) throw new ScreenshotPendingError(gate.pending);
|
|
721
|
+
cancelled();
|
|
722
|
+
gate.holder = run;
|
|
723
|
+
enter('session');
|
|
724
|
+
const session = await page.context().newCDPSession(page);
|
|
725
|
+
if (run.settled) {
|
|
726
|
+
// Opened after the capture was given up on: nothing was sent through it.
|
|
727
|
+
discard(session, 'a CDP session that arrived after its capture was given up on');
|
|
728
|
+
cancelled();
|
|
729
|
+
}
|
|
730
|
+
run.cdp = session;
|
|
731
|
+
// The capture's own commands: a capture, or a command that resizes the tab (the
|
|
732
|
+
// override, a capture with a clip), is counted until the browser answers it.
|
|
733
|
+
const cdp = {
|
|
734
|
+
send: (...command) => {
|
|
735
|
+
const [method, params] = command;
|
|
736
|
+
const answer = session.send(...command);
|
|
737
|
+
const capture = method === 'Page.captureScreenshot';
|
|
738
|
+
const resizes = method === 'Emulation.setDeviceMetricsOverride' || (capture && !!params?.clip);
|
|
739
|
+
if (capture || resizes) {
|
|
740
|
+
if (capture) run.captures += 1;
|
|
741
|
+
if (resizes) run.resizing += 1;
|
|
742
|
+
const over = () => {
|
|
743
|
+
if (capture) run.captures -= 1;
|
|
744
|
+
if (resizes) run.resizing -= 1;
|
|
745
|
+
if (!run.captures) run.quiet?.();
|
|
746
|
+
};
|
|
747
|
+
Promise.resolve(answer).then(over, over);
|
|
748
|
+
}
|
|
749
|
+
return answer;
|
|
750
|
+
},
|
|
751
|
+
};
|
|
752
|
+
enter('viewport');
|
|
753
|
+
// What the tab is put back to; unreadable, nothing is captured.
|
|
754
|
+
run.before = await readViewport(page, 5000);
|
|
755
|
+
cancelled();
|
|
756
|
+
enter('present');
|
|
757
|
+
const presented = presentedFrame(cdp, presentTimeoutMs);
|
|
758
|
+
run.windowRead = presented.windowRead.then((state) => { run.windowAtStart = state; });
|
|
759
|
+
await presented.frame;
|
|
318
760
|
let emulated = null;
|
|
319
761
|
if (options.width || options.height) {
|
|
762
|
+
cancelled();
|
|
763
|
+
enter('layout');
|
|
320
764
|
const before = await cdp.send('Page.getLayoutMetrics');
|
|
321
765
|
const layout = before.cssLayoutViewport || before.layoutViewport;
|
|
322
766
|
emulated = {
|
|
323
767
|
width: options.width || Math.round(layout.clientWidth),
|
|
324
768
|
height: options.height || Math.round(layout.clientHeight),
|
|
325
769
|
};
|
|
326
|
-
// What the restore is confirmed against; unreadable, nothing is emulated.
|
|
327
|
-
viewportBefore = await readViewport(page);
|
|
328
770
|
// Checked and sent in one tick: once cleanup has begun, no override goes out.
|
|
329
|
-
|
|
330
|
-
|
|
771
|
+
cancelled();
|
|
772
|
+
enter('emulate');
|
|
773
|
+
run.overrideSent = true;
|
|
331
774
|
await cdp.send('Emulation.setDeviceMetricsOverride', {
|
|
332
775
|
width: emulated.width, height: emulated.height, deviceScaleFactor: 1, mobile: emulated.width < 768,
|
|
333
776
|
});
|
|
777
|
+
cancelled();
|
|
334
778
|
await settleLayout(page);
|
|
335
779
|
}
|
|
780
|
+
// Given up on: nothing more is sent, a read included.
|
|
781
|
+
cancelled();
|
|
782
|
+
enter('layout');
|
|
336
783
|
const metrics = await cdp.send('Page.getLayoutMetrics');
|
|
337
784
|
const visual = metrics.cssVisualViewport || metrics.visualViewport;
|
|
338
785
|
const content = metrics.cssContentSize || metrics.contentSize;
|
|
339
|
-
|
|
786
|
+
let dpr = 1;
|
|
787
|
+
if (!emulated) {
|
|
788
|
+
cancelled();
|
|
789
|
+
enter('calibrate');
|
|
790
|
+
run.clipSent = true;
|
|
791
|
+
dpr = await captureScaleOf(cdp, visual);
|
|
792
|
+
}
|
|
340
793
|
let region;
|
|
341
794
|
let scale;
|
|
342
795
|
let truncated = false;
|
|
@@ -359,6 +812,8 @@ export async function captureScreenshot(page, options, { runBounded = (work) =>
|
|
|
359
812
|
const outHeight = height * dpr;
|
|
360
813
|
const shrink = Math.min(1, options.maxWidth > 0 ? options.maxWidth / outWidth : 1, MAX_INLINE_EDGE / outWidth, MAX_INLINE_EDGE / outHeight);
|
|
361
814
|
const shoot = async (format, quality, factor) => {
|
|
815
|
+
cancelled();
|
|
816
|
+
run.clipSent = true;
|
|
362
817
|
const { data } = await cdp.send('Page.captureScreenshot', {
|
|
363
818
|
format,
|
|
364
819
|
...(format === 'jpeg' && { quality }),
|
|
@@ -367,8 +822,10 @@ export async function captureScreenshot(page, options, { runBounded = (work) =>
|
|
|
367
822
|
});
|
|
368
823
|
return data;
|
|
369
824
|
};
|
|
825
|
+
enter('file');
|
|
370
826
|
const fileData = options.save ? await shoot(options.fileFormat, options.fileQuality, 1) : null;
|
|
371
827
|
const reuse = fileData && shrink === 1 && options.inlineFormat === options.fileFormat;
|
|
828
|
+
enter('capture');
|
|
372
829
|
const data = reuse ? fileData : await shoot(options.inlineFormat, options.quality, shrink);
|
|
373
830
|
return {
|
|
374
831
|
data,
|
|
@@ -382,18 +839,178 @@ export async function captureScreenshot(page, options, { runBounded = (work) =>
|
|
|
382
839
|
emulated,
|
|
383
840
|
fileData,
|
|
384
841
|
};
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
|
|
842
|
+
} finally {
|
|
843
|
+
run.finished = true;
|
|
844
|
+
}
|
|
845
|
+
};
|
|
846
|
+
|
|
847
|
+
let result = null;
|
|
848
|
+
let failure = null;
|
|
849
|
+
// Where the capture stood when it was over or given up on. From then on the
|
|
850
|
+
// work sends nothing more: each command that resizes is checked and sent in one tick.
|
|
851
|
+
let stopped = null;
|
|
852
|
+
const stop = () => {
|
|
853
|
+
if (stopped) return;
|
|
854
|
+
run.settled = true;
|
|
855
|
+
stopped = { phase: run.phase, waited: Date.now() - run.since, cut: !run.finished };
|
|
856
|
+
};
|
|
857
|
+
// true / false once the tab's viewport had to be confirmed, null when nothing that resizes went out.
|
|
858
|
+
let restored = null;
|
|
859
|
+
const resized = () => run.overrideSent || (!!failure && run.clipSent);
|
|
860
|
+
|
|
861
|
+
const capture = async () => {
|
|
862
|
+
const share = Math.min(RECOVERY_SHARE_MS, Math.max(0, until - started) / 2);
|
|
863
|
+
let timer;
|
|
864
|
+
try {
|
|
865
|
+
result = await Promise.race([
|
|
866
|
+
Promise.resolve().then(() => runBounded(work)),
|
|
867
|
+
new Promise((_, fail) => { timer = setTimeout(() => fail(Object.assign(new Error('no answer within the time the request allows'), { code: 'BROWSER_OPERATION_TIMEOUT', ownBudget: true })), Math.max(0, until - share - Date.now())); }),
|
|
868
|
+
]);
|
|
869
|
+
} catch (error) {
|
|
870
|
+
failure = error;
|
|
871
|
+
} finally {
|
|
872
|
+
clearTimeout(timer);
|
|
873
|
+
}
|
|
874
|
+
stop();
|
|
875
|
+
};
|
|
876
|
+
// Settles when a tab held for a capture that may still be pending is given back or retired.
|
|
877
|
+
let held = null;
|
|
878
|
+
const putBack = async () => {
|
|
879
|
+
const session = run.cdp;
|
|
880
|
+
if (!session) return; // nothing was sent
|
|
881
|
+
const stopAt = Math.min(until, Date.now() + RECOVERY_CAP_MS);
|
|
882
|
+
// A capture cut short that may have resized the tab: until it is put back, other calls for it are refused at once.
|
|
883
|
+
if (failure && resized()) gate.recovering = { run, phase: stopped.phase, since: Date.now() };
|
|
884
|
+
// For the record of a failure, whatever its phase: what CDP reports of the window
|
|
885
|
+
// now, next to what it reported when the capture began. Asked for before the
|
|
886
|
+
// session is let go and kept as soon as it answers; waited for only when the
|
|
887
|
+
// tab is back, so it takes nothing from the time the viewport gets.
|
|
888
|
+
const windowNow = failure ? readWindowState(session, stepUntil(stopAt, 1000)).then((state) => { run.windowAtStop = state; }) : null;
|
|
889
|
+
let letting = null;
|
|
890
|
+
// True once the capture's CDP session is confirmed detached.
|
|
891
|
+
const drop = () => { letting ||= letGo(session, stopAt, 'the capture\'s CDP session'); return letting.confirmed; };
|
|
892
|
+
let detached = false;
|
|
893
|
+
try {
|
|
894
|
+
// Also when the capture timed out and is still pending: the clear goes out on
|
|
895
|
+
// its own (a detach alone would leave the override on the page).
|
|
896
|
+
if (run.overrideSent) restored = await restore(page, session, run.before, { release: drop, until: stopAt }) === true;
|
|
897
|
+
detached = await drop();
|
|
898
|
+
// A clip that did not answer: the view is at the clip's size, and the page reads
|
|
899
|
+
// that size only once the capture's session is gone. So the tab is confirmed
|
|
900
|
+
// from here, on two readings apart, whatever was read while it was attached.
|
|
901
|
+
if (failure && run.clipSent && restored !== false) restored = await restoreSize(page, run.before, { until: stopAt }) === true;
|
|
902
|
+
} catch {
|
|
903
|
+
if (resized()) restored = false;
|
|
904
|
+
detached = await drop();
|
|
905
|
+
}
|
|
906
|
+
if (!detached && run.resizing) {
|
|
907
|
+
// The override or a clip still unanswered on a session that may still be
|
|
908
|
+
// attached: the browser can apply it at any time. Nothing read now confirms the tab.
|
|
909
|
+
restored = false;
|
|
910
|
+
} else if (!detached && run.captures) {
|
|
911
|
+
// A capture without a clip still unanswered on a session that may still be
|
|
912
|
+
// attached: the browser may still be working on it, and nothing in it resizes
|
|
913
|
+
// the tab. The tab is kept, and so is its turn: those waiting are told, later
|
|
914
|
+
// callers refused, until the capture is answered or the detach is. A tab still
|
|
915
|
+
// held after `pendingHoldMs` is retired: no screenshot of it would ever start again.
|
|
916
|
+
run.pending = true;
|
|
917
|
+
gate.pending = { run, phase: stopped.phase, since: Date.now(), holdMs: pendingHoldMs };
|
|
918
|
+
wakeWaiters(gate);
|
|
919
|
+
let holdTimer;
|
|
920
|
+
held = Promise.race([
|
|
921
|
+
new Promise((done) => { run.quiet = () => done('answered'); }),
|
|
922
|
+
letting.eventually.then((gone) => (gone ? 'detached' : new Promise(() => {}))),
|
|
923
|
+
new Promise((done) => { holdTimer = setTimeout(() => done('over'), pendingHoldMs); holdTimer.unref?.(); }),
|
|
924
|
+
]).then((how) => {
|
|
925
|
+
clearTimeout(holdTimer);
|
|
926
|
+
if (how !== 'over' || gate.retired) return;
|
|
927
|
+
retireGate(gate, stopped.phase, 'pending');
|
|
928
|
+
try { console.warn(`[browser] screenshot: a capture that got no answer while ${PHASES[stopped.phase]} was still unanswered, and its CDP session not confirmed detached, after ${seconds(pendingHoldMs)}: the tab is retired`); } catch { /* nothing left to tell */ }
|
|
929
|
+
try { retire?.('pending'); } catch { /* out of this module's hands */ }
|
|
930
|
+
});
|
|
931
|
+
}
|
|
932
|
+
// The window state the capture began with is an answer of a few ms; a browser that owes it is not waited on for long.
|
|
933
|
+
if (run.windowRead) await Promise.race([run.windowRead, delay(failure ? stepUntil(stopAt, 1000) : 100)]);
|
|
934
|
+
if (windowNow) await windowNow;
|
|
935
|
+
};
|
|
936
|
+
const everything = capture().then(putBack);
|
|
937
|
+
// The tab's turn ends when everything this capture started is over, whoever still waits for its answer.
|
|
938
|
+
everything.catch(() => {}).then(async () => {
|
|
939
|
+
if (gate.recovering?.run === run) gate.recovering = null;
|
|
940
|
+
if (held) {
|
|
941
|
+
await held;
|
|
942
|
+
if (gate.pending?.run === run) gate.pending = null;
|
|
943
|
+
}
|
|
944
|
+
if (gate.holder === run) gate.holder = null;
|
|
945
|
+
turn.release();
|
|
946
|
+
});
|
|
947
|
+
|
|
948
|
+
// The answer: when all of it is over, or at the deadline with whatever is not.
|
|
949
|
+
let timer;
|
|
950
|
+
const inTime = await Promise.race([
|
|
951
|
+
everything.then(() => true, () => true),
|
|
952
|
+
new Promise((done) => { timer = setTimeout(() => done(false), Math.max(0, until - Date.now())); }),
|
|
953
|
+
]);
|
|
954
|
+
clearTimeout(timer);
|
|
955
|
+
if (!inTime) {
|
|
956
|
+
if (!stopped && !failure) failure = Object.assign(new Error('no answer within the time the request allows'), { code: 'BROWSER_OPERATION_TIMEOUT', ownBudget: true });
|
|
957
|
+
stop();
|
|
958
|
+
// Not confirmed by now is not confirmed: the rest goes on without the caller.
|
|
959
|
+
// (A tab already confirmed stays so; only its session is still being let go.)
|
|
960
|
+
if (resized() && restored === null) restored = false;
|
|
961
|
+
} else if (resized() && restored === null) {
|
|
962
|
+
restored = false; // putting it back ended without a verdict: not confirmed
|
|
388
963
|
}
|
|
389
|
-
//
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
const
|
|
393
|
-
|
|
394
|
-
if (
|
|
395
|
-
|
|
396
|
-
|
|
964
|
+
// What this call answers with, whatever the work or the cleanup still running sets later.
|
|
965
|
+
const failed = failure;
|
|
966
|
+
const shot = result;
|
|
967
|
+
const unverified = restored === false;
|
|
968
|
+
let tabClosed = null;
|
|
969
|
+
if (unverified) {
|
|
970
|
+
retireGate(gate, stopped.phase);
|
|
971
|
+
let retiring = null;
|
|
972
|
+
try { retiring = retire ? retire('size') : null; } catch { /* reported as not closed */ }
|
|
973
|
+
tabClosed = false;
|
|
974
|
+
if (retiring?.closed) {
|
|
975
|
+
let closeTimer;
|
|
976
|
+
tabClosed = await Promise.race([
|
|
977
|
+
Promise.resolve(retiring.closed).then((closed) => closed === true, () => false),
|
|
978
|
+
new Promise((done) => { closeTimer = setTimeout(() => done(false), Math.max(0, Math.min(CLOSE_CONFIRM_MS, until - Date.now()))); }),
|
|
979
|
+
]);
|
|
980
|
+
clearTimeout(closeTimer);
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
const tab = unverified ? 'retired' : restored === true ? 'restored' : 'untouched';
|
|
984
|
+
|
|
985
|
+
if (failed) {
|
|
986
|
+
const { phase, waited } = stopped;
|
|
987
|
+
// What CDP reported of the window when the capture stopped (null when that was not read) and when it began.
|
|
988
|
+
const { windowAtStop, windowAtStart } = run;
|
|
989
|
+
let error = failed;
|
|
990
|
+
if (phase === 'present' && (stopped.cut || failed instanceof ScreenshotStalledError)) {
|
|
991
|
+
// The first capture got no answer, whoever stopped the wait: said with both window readings.
|
|
992
|
+
error = new ScreenshotStalledError(windowAtStop, waited, windowAtStart);
|
|
993
|
+
} else if (stopped.cut) {
|
|
994
|
+
const earlier = phase === 'queue' && gate.holder && gate.holder !== run ? `; the earlier one is ${PHASES[gate.holder.phase]}` : '';
|
|
995
|
+
error = new Error(`Screenshot timed out while ${PHASES[phase]} (${measured(phase, waited, windowAtStop, windowAtStart)}${earlier})${failed.ownBudget ? '' : `: ${failed.message}`}`);
|
|
996
|
+
Object.assign(error, { code: failed.code, status: failed.status, phase });
|
|
997
|
+
} else if (!(failed instanceof ScreenshotTabRetiredError || failed instanceof ScreenshotPendingError)) {
|
|
998
|
+
error = new Error(`Screenshot failed while ${PHASES[phase]} (${measured(phase, waited, windowAtStop, windowAtStart)}): ${failed?.message ?? failed}`);
|
|
999
|
+
Object.assign(error, { code: failed?.code, status: failed?.status, phase });
|
|
1000
|
+
}
|
|
1001
|
+
if (error !== failed) error.cause = failed;
|
|
1002
|
+
if (error.windowState === undefined) error.windowState = windowAtStop;
|
|
1003
|
+
if (error.windowAtStart === undefined) error.windowAtStart = windowAtStart;
|
|
1004
|
+
error.tab = tab;
|
|
1005
|
+
if (run.pending) {
|
|
1006
|
+
error.capturePending = true;
|
|
1007
|
+
error.message += ` Its CDP session could not be confirmed detached, so the capture may still be pending in the browser: until it is answered or the session detaches, other screenshots of this tab are refused (SCREENSHOT_CAPTURE_PENDING), and a tab still in that state after ${seconds(pendingHoldMs)} is retired.`;
|
|
1008
|
+
}
|
|
1009
|
+
if (unverified) Object.assign(error, { viewportRestored: false, tabClosed });
|
|
1010
|
+
record('failed', error, tab, tabClosed);
|
|
1011
|
+
throw error;
|
|
397
1012
|
}
|
|
398
|
-
|
|
1013
|
+
record('ok', null, tab, tabClosed);
|
|
1014
|
+
if (restored === null) return shot;
|
|
1015
|
+
return { ...shot, viewportRestored: restored, ...(unverified && { tab, tabClosed }) };
|
|
399
1016
|
}
|