@moretea-labs/forge 1.8.1 → 1.8.2
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/AGENTS.md +5 -2
- package/CHANGELOG.md +20 -0
- package/README.en.md +24 -3
- package/README.md +10 -6
- package/README.zh-CN.md +5 -3
- package/THIRD_PARTY_NOTICES.md +1 -2
- package/adapters/chatgpt/browser-delivery-runtime.ts +5 -3
- package/adapters/chatgpt/provider-delivery.ts +1 -1
- package/adapters/chatgpt/work-conversation-binding-store.ts +20 -1
- package/adapters/computer/chatgpt-extension-target.ts +295 -50
- package/adapters/computer/chrome-extension/background.js +227 -71
- package/adapters/computer/chrome-extension/content.js +181 -157
- package/adapters/computer/chrome-extension/manifest.json +3 -1
- package/adapters/computer/desktop-operator-provider.ts +18 -6
- package/adapters/computer/interaction-target-authority.ts +39 -9
- package/adapters/mcp/access-mode.ts +18 -39
- package/adapters/mcp/auth.ts +2 -6
- package/adapters/mcp/instructions.ts +2 -2
- package/adapters/mcp/multi-repository.ts +7 -28
- package/adapters/mcp/runtime-gateway/context-adapter.ts +79 -40
- package/adapters/mcp/runtime-gateway/context-pack-sidecar.ts +7 -15
- package/adapters/mcp/runtime-gateway/execution-tools.ts +7 -13
- package/adapters/mcp/runtime-gateway/plugin-adapter.ts +16 -3
- package/adapters/mcp/runtime-gateway/process-tools.ts +3 -21
- package/adapters/mcp/runtime-gateway/router.ts +3 -5
- package/adapters/mcp/runtime-gateway/routing-policy.ts +17 -10
- package/adapters/mcp/runtime-gateway/runtime-observation-adapter.ts +1 -1
- package/adapters/mcp/runtime-gateway/runtime-tool-definitions.ts +14 -9
- package/adapters/mcp/runtime-gateway/runtime-tools.ts +1 -5
- package/adapters/mcp/runtime-gateway/scheduler-adapter.ts +36 -1
- package/adapters/mcp/runtime-gateway/status-inbox-adapter.ts +58 -4
- package/adapters/mcp/runtime-gateway/typescript-navigation-sidecar.ts +7 -3
- package/adapters/mcp/runtime-gateway/work-adapter.ts +16 -16
- package/adapters/mcp/runtime-gateway/work-input-wire-migration.ts +0 -17
- package/adapters/mcp/runtime-gateway/work-semantic-operations.ts +69 -12
- package/adapters/mcp/runtime-gateway/workflow-supervisor-adapter.ts +26 -2
- package/adapters/mcp/server.ts +24 -3
- package/adapters/mcp/setup.ts +4 -10
- package/adapters/mcp/tool-mapping/access-tools.ts +0 -6
- package/adapters/mcp/tool-mapping/legacy-tool-service.ts +672 -2935
- package/adapters/mcp/tool-mapping/post-finalize-work-attribution.ts +1 -1
- package/adapters/mcp/tool-mapping/repository-tools.ts +52 -86
- package/adapters/mcp/toolset-names.ts +11 -42
- package/adapters/mcp/toolset.ts +16 -90
- package/{src/cli → adapters}/mcp/transports/AGENTS.md +5 -5
- package/{src/cli → adapters}/mcp/transports/CLAUDE.md +5 -5
- package/adapters/mcp/transports/http-observation.ts +18 -6
- package/adapters/mcp/transports/http-post-lifecycle.ts +19 -0
- package/adapters/mcp/transports/http.ts +54 -14
- package/adapters/mcp/types.ts +0 -6
- package/assets/hooks/lib/session-state.sh +0 -38
- package/assets/hooks/post-bash.sh +1 -1
- package/assets/hooks/post-tool-observer.sh +0 -5
- package/assets/hooks/prompt-guard.sh +0 -55
- package/assets/partials/04-project-structure.partial.md +1 -1
- package/assets/partials/07-footer.partial.md +1 -1
- package/assets/partials-agents/02-operating-mode.partial.md +1 -1
- package/assets/partials-agents/08-deep-docs.partial.md +1 -1
- package/assets/reference-configs/agentic-development-flow.md +1 -1
- package/assets/reference-configs/external-tooling.md +0 -76
- package/assets/reference-configs/release-deploy.md +1 -1
- package/assets/templates/helpers/architecture-queue.sh +1 -1
- package/assets/templates/helpers/check-agent-tooling.sh +1 -327
- package/assets/templates/helpers/check-task-workflow.sh +1 -1
- package/assets/templates/helpers/ensure-task-workflow.sh +0 -14
- package/assets/templates/helpers/migrate-project-template.sh +2 -2
- package/assets/workflow-contract.v1.json +0 -2
- package/docs/README.md +5 -2
- package/docs/architecture/CURRENT.md +114 -30
- package/docs/architecture/decisions/20260904-ios-device-migration-baseline.md +42 -0
- package/docs/architecture/decisions/20260918-stable-candidate-release-lifecycle.md +15 -2
- package/docs/architecture/decisions/20260924-thin-semantic-working-context.md +4 -2
- package/docs/architecture/decisions/20261001-supervisor-turn-authority.md +109 -4
- package/docs/architecture/decisions/20261009-exact-code-context-discovery.md +19 -0
- package/docs/architecture/decisions/20261009-generated-xcode-cache-retention.md +37 -0
- package/docs/architecture/decisions/20261011-autonomous-continuation-convergence.md +597 -0
- package/docs/architecture/decisions/20261017-edit-session-finalization-reconciliation.md +17 -0
- package/docs/architecture/decisions/20261019-retire-host-skills.md +16 -0
- package/docs/architecture/diagrams/agentic-dev-plugin-architecture.html +1 -1
- package/docs/architecture/domains/public-surface.md +3 -5
- package/docs/architecture/domains/verification.md +5 -5
- package/docs/architecture/modules/controller-runtime/transport-lifecycle.md +2 -2
- package/docs/architecture/requests/2026-09-04-computer-plugin-workflow-taxonomy.md +4 -4
- package/docs/architecture/reviews/20261015-code-search-runtime-acceptance.md +75 -0
- package/docs/forge-plugin-management.md +14 -2
- package/docs/operations/controller-browser-plugin.md +23 -0
- package/docs/operations/features.md +6 -7
- package/docs/operations/features.zh-CN.md +1 -1
- package/docs/operations/github-repository.md +4 -4
- package/docs/operations/github-semantic-release-projection.md +20 -0
- package/docs/operations/homebrew.md +2 -2
- package/docs/operations/mcp-tool-exposure.md +13 -54
- package/docs/operations/runtime-performance-diagnostics.md +8 -0
- package/docs/operations/standalone-disaster-recovery.md +1 -1
- package/docs/tutorials/01-install-and-start.zh-CN.md +1 -1
- package/docs/tutorials/03-first-repository-task.md +2 -0
- package/docs/wiki/Computer-and-Plugins.md +78 -0
- package/docs/wiki/Computer-and-Plugins.zh-CN.md +88 -0
- package/docs/wiki/Home.md +16 -2
- package/docs/wiki/Implementation.md +24 -12
- package/docs/wiki/Installation.md +28 -32
- package/docs/wiki/Integrations.md +4 -0
- package/docs/wiki/Quick-Start.md +35 -22
- package/docs/wiki/Releases-and-Upgrades.md +15 -25
- package/docs/wiki/_Sidebar.md +5 -1
- package/package.json +1 -12
- package/packages/kernel/controller/infrastructure/controller-session-store.ts +63 -38
- package/packages/kernel/identity/infrastructure/activity-projection-store.ts +10 -0
- package/packages/kernel/scheduler/application/eligibility.ts +4 -1
- package/packages/kernel/scheduler/infrastructure/schedule-store.ts +13 -10
- package/packages/kernel/work/application/work-service.ts +1 -0
- package/packages/kernel/work/domain/repository-completion-receipt.ts +1 -1
- package/packages/kernel/work/domain/state-machine.ts +9 -2
- package/packages/kernel/work/infrastructure/work-contract-store.ts +28 -1
- package/packages/plugin-runtime/browser/provider-registry.ts +12 -36
- package/packages/plugin-runtime/computer/conversation-target.ts +17 -7
- package/scripts/assemble-template.ts +11 -72
- package/scripts/check-mcp-compatibility.ts +55 -74
- package/scripts/check-package-identity.mjs +0 -5
- package/scripts/check-runtime-architecture.mjs +305 -28
- package/scripts/init-project.sh +0 -46
- package/scripts/install-ios-hid-toolchain.sh +60 -0
- package/scripts/install-standalone-recovery.ts +1 -1
- package/scripts/lib/project-init-lib.sh +3 -3
- package/scripts/migrate-windows-wsl-controller-home.ts +2 -0
- package/scripts/open-source-audit-allowlist.txt +0 -1
- package/scripts/run-governed-gate.ts +2 -0
- package/scripts/smoke-ios-physical-device.ts +38 -10
- package/scripts/smoke-mcp-http-runtime.ts +7 -8
- package/scripts/smoke-mcp-tool-surface.ts +27 -41
- package/scripts/smoke-workflow-supervisor-chrome-adapter.ts +6 -3
- package/scripts/sync-generated-authority.mjs +0 -1
- package/src/cli/agent-jobs/executor-health.ts +1 -1
- package/src/cli/agent-jobs/integration.ts +2 -2
- package/src/cli/chatgpt-browser/file-policy.ts +2 -2
- package/src/cli/commands/doctor.ts +1 -143
- package/src/cli/commands/global-runtime.ts +3 -196
- package/src/cli/commands/init-hook.ts +6 -23
- package/src/cli/commands/init.ts +3 -252
- package/src/cli/commands/mcp.ts +0 -5
- package/src/cli/commands/prompt-guard-decision.ts +0 -2
- package/src/cli/commands/recovery.ts +2 -14
- package/src/cli/commands/repository.ts +17 -1
- package/src/cli/commands/setup-execution.ts +41 -0
- package/src/cli/controller/completion-orchestrator.ts +1 -1
- package/src/cli/controller/composite-operations.ts +1 -1
- package/src/cli/controller/context/instruction-context.ts +1 -1
- package/src/cli/controller/context/known-paths.ts +2 -2
- package/src/cli/controller/context/query-planning.ts +11 -4
- package/src/cli/controller/context/rule-resolution.ts +2 -2
- package/src/cli/controller/context/source-materializer.ts +2 -2
- package/src/cli/controller/context/structural-provider.ts +84 -0
- package/src/cli/controller/context/structural.ts +6 -6
- package/src/cli/controller/context/types.ts +8 -9
- package/src/cli/controller/context-pack.ts +97 -76
- package/src/cli/controller/operational-plan.ts +1 -1
- package/src/cli/controller/systemd-user.ts +4 -10
- package/src/cli/editing/edit-session.ts +17 -5
- package/src/cli/github/github.ts +4 -4
- package/src/cli/github/semantic-projection.ts +174 -0
- package/src/cli/hook/prompt-intents.ts +0 -9
- package/src/cli/index.ts +1 -43
- package/src/cli/local-bridge/connector-freshness.ts +1 -12
- package/src/cli/local-bridge/console-automations.ts +1 -1
- package/src/cli/local-bridge/console-view-models.ts +1 -1
- package/src/cli/local-bridge/facade-api.ts +0 -2
- package/src/cli/repositories/command-classifier.ts +0 -9
- package/src/cli/repositories/command-scope.ts +2 -2
- package/src/cli/repositories/locks.ts +20 -0
- package/src/cli/repositories/registry.ts +2 -1
- package/src/cli/repositories/safe-patch.ts +2 -12
- package/src/cli/repositories/structured-git.ts +39 -3
- package/src/cli/repository/inspector.ts +185 -343
- package/src/effects/process-runner.ts +17 -1
- package/src/runtime/context/generic-lsp-provider.ts +1 -1
- package/src/runtime/context/semantic-navigation-contract.ts +7 -0
- package/src/runtime/context/semantic-navigation.ts +1 -1
- package/src/runtime/control-plane/cleanup-artifact-retention.ts +4 -4
- package/src/runtime/control-plane/execution/work-delivery-service.ts +150 -31
- package/src/runtime/control-plane/execution/work-handle-authority.ts +12 -4
- package/src/runtime/control-plane/execution/work-handle-store.ts +53 -1
- package/src/runtime/control-plane/execution/work-operation-service.ts +20 -10
- package/src/runtime/control-plane/execution/work-preparation-service.ts +108 -4
- package/src/runtime/control-plane/execution/work-process-evidence.ts +2 -2
- package/src/runtime/control-plane/execution/work-terminal-cleanup.ts +28 -13
- package/src/runtime/control-plane/execution/work-validation-reconciler.ts +5 -1
- package/src/runtime/control-plane/execution/work-verification-snapshot.ts +1 -1
- package/src/runtime/control-plane/facade/capability-registry.ts +2 -0
- package/src/runtime/control-plane/facade/work-continuation.ts +1 -1
- package/src/runtime/control-plane/generated-cache-retention.ts +114 -63
- package/src/runtime/control-plane/global-scheduler/maintenance.ts +20 -8
- package/src/runtime/control-plane/global-scheduler/reconciliation.ts +1 -11
- package/src/runtime/control-plane/governance/external-effects.ts +1 -1
- package/src/runtime/control-plane/launcher/chatgpt-work-continuation.ts +1 -0
- package/src/runtime/control-plane/runtime-cleanup.ts +12 -93
- package/src/runtime/evidence/sensitive-output.ts +22 -2
- package/src/runtime/execution/jobs/child-reference.ts +11 -0
- package/src/runtime/execution/jobs/receipt-recovery.ts +1 -11
- package/src/runtime/execution/process-runtime/command-facade.ts +3 -3
- package/src/runtime/execution/process-runtime/runtime.ts +20 -16
- package/src/runtime/execution/thin-harness/execution-router.ts +16 -16
- package/src/runtime/execution/thin-harness/fast-executor.ts +2 -2
- package/src/runtime/execution/thin-harness/search-worker.ts +2 -2
- package/src/runtime/execution/workers/executor.ts +1 -2
- package/src/runtime/maintenance/cleanup.ts +41 -5
- package/src/runtime/plugins/action-application.ts +12 -1
- package/src/runtime/plugins/app-store-connect-adapter.ts +252 -4
- package/src/runtime/plugins/browser-adapter.ts +381 -313
- package/src/runtime/plugins/browser-macos-bridge.ts +114 -112
- package/src/runtime/plugins/browser-manifest-surface.ts +2 -1
- package/src/runtime/plugins/browser-registration.ts +2 -0
- package/src/runtime/plugins/browser-runtime.ts +26 -35
- package/src/runtime/plugins/computer-chatgpt-browser-adapter-installer.ts +11 -2
- package/src/runtime/plugins/computer-registration.ts +76 -9
- package/src/runtime/plugins/github-adapter.ts +30 -3
- package/src/runtime/plugins/gmail-adapter.ts +166 -1
- package/src/runtime/plugins/google-shared.ts +10 -5
- package/src/runtime/plugins/ios/remote-xpc-hid.ts +70 -30
- package/src/runtime/plugins/ios-adapter.ts +45 -40
- package/src/runtime/plugins/ios-agent-device.ts +54 -4
- package/src/runtime/plugins/ios-device-ownership.ts +103 -0
- package/src/runtime/plugins/ios-physical-device.ts +497 -79
- package/src/runtime/plugins/local-system-adapter.ts +1 -99
- package/src/runtime/plugins/store.ts +80 -7
- package/src/runtime/plugins/types.ts +4 -0
- package/src/runtime/projections/git-status-sampler.ts +2 -19
- package/src/runtime/recovery/actions.ts +1 -1
- package/src/runtime/recovery/lifecycle-inventory.ts +1 -11
- package/src/runtime/recovery/probe.ts +2 -2
- package/src/runtime/release/release-coordinator.ts +20 -4
- package/src/runtime/root/controller-progression-composition.ts +67 -5
- package/src/runtime/root/package-connector-service.ts +123 -11
- package/src/runtime/root/package-runtime-release.ts +6 -18
- package/src/runtime/root/package-runtime-service.ts +7 -13
- package/src/runtime/root/release-execution-canary.ts +18 -5
- package/src/runtime/root/release-manifest.ts +0 -34
- package/src/runtime/root/release-materialize.ts +0 -78
- package/src/runtime/root/runtime.ts +1 -2
- package/src/runtime/root/scheduled-controller-composition.ts +2 -1
- package/src/runtime/root/service.ts +90 -11
- package/src/runtime/root/types.ts +0 -6
- package/src/runtime/root/workflow-supervisor-composition.ts +207 -19
- package/src/runtime/root/workflow-supervisor-runtime.ts +365 -10
- package/src/runtime/safe-tooling/google-credential-store.ts +1 -11
- package/src/runtime/safe-tooling/ios-development.ts +22 -8
- package/src/runtime/safe-tooling/job-summary.ts +1 -1
- package/src/runtime/safe-tooling/workspace-auth.ts +4 -1
- package/src/runtime/shared/atomic-text-file.ts +21 -0
- package/src/runtime/shared/json-files.ts +8 -3
- package/src/runtime/standalone-recovery/core.ts +189 -174
- package/src/runtime/standalone-recovery/entry.ts +62 -30
- package/src/runtime/standalone-recovery/operation-lock.ts +11 -1
- package/src/runtime/workflow/schedules/chatgpt-automation-migration.ts +1 -0
- package/src/runtime/workflow/schedules/engine.ts +36 -14
- package/src/runtime/workflow/schedules/pending-activation.ts +11 -4
- package/src/runtime/workflow/schedules/work-continuation.ts +51 -6
- package/supervisor/client.ts +31 -14
- package/supervisor/control-plane.ts +210 -115
- package/supervisor/forge-validators.ts +88 -21
- package/supervisor/native-browser-adapter.ts +393 -28
- package/supervisor/native-messaging/host.ts +21 -2
- package/supervisor/protocol.ts +74 -26
- package/supervisor/server.ts +16 -8
- package/supervisor/store.ts +476 -57
- package/supervisor/types.ts +19 -13
- package/SKILL.md +0 -427
- package/adapters/computer/chatgpt-target-router.ts +0 -103
- package/adapters/mcp/runtime-gateway/automation-receipt-adapter.ts +0 -87
- package/adapters/mcp/runtime-gateway/work-validation-reconciler.ts +0 -1
- package/adapters/mcp/toolset-selection.ts +0 -41
- package/agents/openai.yaml +0 -4
- package/assets/skill-hooks.json +0 -41
- package/assets/skill-version.json +0 -189
- package/assets/skills/claude-review/SKILL.md +0 -231
- package/assets/skills/codex-review/SKILL.md +0 -103
- package/assets/templates/helpers/check-skill-version.ts +0 -193
- package/scripts/check-skill-version.ts +0 -5
- package/scripts/ensure-codegraph.sh +0 -16
- package/scripts/run-skill-hook.ts +0 -244
- package/scripts/sync-codex-installed-copies.sh +0 -182
- package/src/cli/commands/tools.ts +0 -167
- package/src/cli/mcp/access-mode.ts +0 -2
- package/src/cli/mcp/access-tools.ts +0 -2
- package/src/cli/mcp/audit.ts +0 -2
- package/src/cli/mcp/auth.ts +0 -2
- package/src/cli/mcp/instructions.ts +0 -2
- package/src/cli/mcp/legacy-context.ts +0 -2
- package/src/cli/mcp/legacy-tool-service.ts +0 -2
- package/src/cli/mcp/multi-repository.ts +0 -2
- package/src/cli/mcp/oauth.ts +0 -2
- package/src/cli/mcp/openai-secure-tunnel.ts +0 -2
- package/src/cli/mcp/paths.ts +0 -2
- package/src/cli/mcp/policy.ts +0 -2
- package/src/cli/mcp/post-finalize-work-attribution.ts +0 -2
- package/src/cli/mcp/proxy-env.ts +0 -2
- package/src/cli/mcp/readiness-projection.ts +0 -2
- package/src/cli/mcp/redaction.ts +0 -2
- package/src/cli/mcp/repo.ts +0 -2
- package/src/cli/mcp/repository-tools.ts +0 -2
- package/src/cli/mcp/server.ts +0 -2
- package/src/cli/mcp/setup.ts +0 -2
- package/src/cli/mcp/tool-capability-cache.ts +0 -2
- package/src/cli/mcp/tool-contract.ts +0 -2
- package/src/cli/mcp/tools.ts +0 -2
- package/src/cli/mcp/toolset-names.ts +0 -2
- package/src/cli/mcp/toolset-selection.ts +0 -2
- package/src/cli/mcp/toolset.ts +0 -2
- package/src/cli/mcp/transports/http.ts +0 -2
- package/src/cli/mcp/transports/session-registry.ts +0 -2
- package/src/cli/mcp/transports/stdio.ts +0 -2
- package/src/cli/mcp/types.ts +0 -2
- package/src/cli/mcp/umbrella-tools.ts +0 -2
- package/src/cli/tools/codegraph.ts +0 -695
- package/src/core/adoption/helper-wrapper-plan.ts +0 -159
- package/src/runtime/context/codegraph-cache-boundary.ts +0 -255
- package/src/runtime/context/codegraph-read-provider.ts +0 -611
- package/src/runtime/context/codegraph-sidecar.cjs +0 -248
- package/src/runtime/control-plane/codegraph-cache-retention.ts +0 -164
- package/src/runtime/gateway/mcp/execution-tools.ts +0 -2
- package/src/runtime/gateway/mcp/legacy-ios-tool-adapter.ts +0 -2
- package/src/runtime/gateway/mcp/persisted-check-process.ts +0 -2
- package/src/runtime/gateway/mcp/process-tools.ts +0 -2
- package/src/runtime/gateway/mcp/router.ts +0 -2
- package/src/runtime/gateway/mcp/runtime-tool-definitions.ts +0 -2
- package/src/runtime/gateway/mcp/runtime-tools.ts +0 -2
- package/src/runtime/gateway/mcp/work-validation-reconciler.ts +0 -2
- package/src/runtime/plugins/browser-provider-registry.ts +0 -2
- package/src/runtime/plugins/computer-chatgpt-macos-target.ts +0 -891
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
(() => {
|
|
2
2
|
const core = globalThis.ForgeComputerChatgptCore;
|
|
3
3
|
if (!core) throw new Error('FORGE_COMPUTER_CHATGPT_CORE_MISSING');
|
|
4
|
+
// Declarative document_idle loading and exact-tab receiver recovery can race.
|
|
5
|
+
// Install one listener set per document or one Send could be handled twice.
|
|
6
|
+
if (globalThis.__forgeComputerChatgptContentInstalledV1 === true) return;
|
|
7
|
+
globalThis.__forgeComputerChatgptContentInstalledV1 = true;
|
|
4
8
|
const ASSISTANT = '[data-message-author-role="assistant"], [data-chatgpt-search-unit-key$=":assistant"], [data-content-search-unit-key$=":assistant"]';
|
|
5
9
|
const USER = '[data-message-author-role="user"], [data-chatgpt-search-unit-key$=":user"], [data-content-search-unit-key$=":user"]';
|
|
6
10
|
const COMPOSER = '#prompt-textarea, [data-testid="prompt-textarea"], [data-testid="composer-text-input"], textarea[name="prompt"], textarea[placeholder*="Message"], textarea[placeholder*="消息"], form [contenteditable="true"], div[role="textbox"][contenteditable="true"]';
|
|
@@ -52,7 +56,9 @@
|
|
|
52
56
|
const messageText = (node) => {
|
|
53
57
|
if (!node) return '';
|
|
54
58
|
const semanticContent = node.querySelector?.('[data-chatgpt-selection-message-id]');
|
|
55
|
-
|
|
59
|
+
// Message line boundaries carry the final Supervisor receipt. Normalizing
|
|
60
|
+
// them like UI labels destroys the protocol before the owner can parse it.
|
|
61
|
+
return String(semanticContent?.innerText ?? semanticContent?.textContent ?? node.innerText ?? node.textContent ?? '').trim();
|
|
56
62
|
};
|
|
57
63
|
const roleNodes = (selector) => {
|
|
58
64
|
const seen = new Set();
|
|
@@ -111,7 +117,8 @@
|
|
|
111
117
|
const status = Array.from(document.querySelectorAll('[role="alert"], [role="status"], [aria-live="assertive"], [aria-live="polite"]'))
|
|
112
118
|
.filter(visible);
|
|
113
119
|
const capacity = Array.from(conversationRoot().querySelectorAll('p, span, div'))
|
|
114
|
-
|
|
120
|
+
// Exclude message subtrees before layout-dependent visibility reads.
|
|
121
|
+
.filter((node) => !node.closest(TURN) && !node.closest('[data-message-author-role]') && visible(node))
|
|
115
122
|
.filter((node) => {
|
|
116
123
|
const text = core.normalizeText(node.innerText ?? node.textContent);
|
|
117
124
|
return text.length <= 250 && /(?:你已达到此对话的长度上限|此对话已达到长度上限|you(?:'ve| have) reached the (?:maximum length for this conversation|conversation length limit)|this conversation has reached its maximum length)/i.test(text);
|
|
@@ -120,12 +127,46 @@
|
|
|
120
127
|
.map((node) => core.normalizeText(node.innerText ?? node.textContent))
|
|
121
128
|
.filter(Boolean).slice(-8).join('\n').slice(-PROVIDER_FAILURE_CHARS);
|
|
122
129
|
};
|
|
123
|
-
|
|
124
|
-
|
|
130
|
+
// Project pages can retain hidden SSR editors beside the live hydrated one.
|
|
131
|
+
// Resolve one visible editor, preferring the active main, or fail closed:
|
|
132
|
+
// a hidden editor's draft is not proof that the task-owned composer is busy.
|
|
133
|
+
const composer = () => {
|
|
134
|
+
const visibleEditors = Array.from(document.querySelectorAll(COMPOSER))
|
|
135
|
+
.filter((node) => visible(node) && !node.closest?.('[aria-hidden="true"], [hidden], [inert]'));
|
|
136
|
+
const root = conversationRoot();
|
|
137
|
+
const inMain = root === document ? [] : visibleEditors.filter((node) => root.contains?.(node));
|
|
138
|
+
const candidates = inMain.length ? inMain : visibleEditors;
|
|
139
|
+
return candidates.length === 1 ? candidates[0] : undefined;
|
|
140
|
+
};
|
|
141
|
+
// Editor text is a provider payload, not a UI label: preserve its whitespace.
|
|
142
|
+
const exactComposerText = (value) => String(value ?? '').replace(/\r\n?/g, '\n');
|
|
143
|
+
const composerText = (editor = composer()) => {
|
|
144
|
+
const blocks = editor?.isContentEditable ? Array.from(editor.children ?? []) : [];
|
|
145
|
+
// ChatGPT's ProseMirror writes one P block per entered newline but
|
|
146
|
+
// HTMLElement.innerText synthesizes TWO newlines between those blocks.
|
|
147
|
+
// Reconstruct the actual editor payload from its direct paragraphs rather
|
|
148
|
+
// than comparing synthetic layout whitespace to an immutable effect.
|
|
149
|
+
const observed = blocks.length > 0 && blocks.every((block) => block.tagName === 'P')
|
|
150
|
+
? blocks.map((block) => {
|
|
151
|
+
// innerText may insert a *visual* newline around auto-linked URLs or
|
|
152
|
+
// inline styled descendants even inside one ProseMirror paragraph.
|
|
153
|
+
// One direct <p> is one actual editor line: read its text nodes.
|
|
154
|
+
const value = exactComposerText(block.textContent ?? block.innerText);
|
|
155
|
+
return !value.trim() ? '' : value;
|
|
156
|
+
}).join('\n')
|
|
157
|
+
: editor?.value ?? editor?.innerText ?? editor?.textContent;
|
|
158
|
+
const text = exactComposerText(observed);
|
|
159
|
+
// Only whitespace-only *empty* editors collapse; all nonblank text,
|
|
160
|
+
// including leading/trailing spaces and intentional blank lines, remains
|
|
161
|
+
// byte-for-byte subject only to CRLF normalization.
|
|
162
|
+
return text.trim() ? text : '';
|
|
163
|
+
};
|
|
125
164
|
const pageSnapshot = (options = {}) => {
|
|
126
165
|
const userNodes = roleNodes(USER);
|
|
127
166
|
const assistantNodes = roleNodes(ASSISTANT);
|
|
128
|
-
|
|
167
|
+
// Reading innerText on the entire page can force layout on every poll.
|
|
168
|
+
// Only collect it when a caller explicitly requests the full page.
|
|
169
|
+
const editor = composer();
|
|
129
170
|
return {
|
|
130
171
|
url: location.href,
|
|
131
172
|
title: document.title,
|
|
@@ -135,10 +176,15 @@
|
|
|
135
176
|
userMessages: userNodes.map(messageText).filter(Boolean),
|
|
136
177
|
assistantMessages: assistantNodes.map(messageText).filter(Boolean),
|
|
137
178
|
} : {}),
|
|
138
|
-
...(
|
|
179
|
+
...(editor ? { composerText: composerText(editor) } : {}),
|
|
180
|
+
composerSelectorVersion: 'visible-single-v2',
|
|
181
|
+
composerReplacementContract: 'exact-backed-up-project-draft-v1',
|
|
182
|
+
// Read-only hydration attestation for a NEW Project bootstrap tab.
|
|
183
|
+
// URL/DOM shell visibility alone cannot prove that the React model
|
|
184
|
+
// picker has mounted or that the provider editor is send-ready.
|
|
139
185
|
providerActivityText: providerActivityText(),
|
|
140
186
|
providerFailureText: providerFailureText(),
|
|
141
|
-
...(options.includePageText === true ? { pageText:
|
|
187
|
+
...(options.includePageText === true ? { pageText: String(document.body?.innerText ?? '').slice(-500000) } : {}),
|
|
142
188
|
latestTurnRole: latestTurnRole(),
|
|
143
189
|
isGenerating: isGenerating(),
|
|
144
190
|
};
|
|
@@ -165,98 +211,27 @@
|
|
|
165
211
|
selection?.removeAllRanges();
|
|
166
212
|
}
|
|
167
213
|
};
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
if (/extrahigh|xhigh|超高|极高/.test(label)) return 'xhigh';
|
|
174
|
-
if (/medium|中等/.test(label) || label === '中') return 'medium';
|
|
175
|
-
if (/high|高/.test(label)) return 'high';
|
|
176
|
-
return undefined;
|
|
177
|
-
};
|
|
178
|
-
const reasoningControl = () => {
|
|
179
|
-
const selectors = 'button[aria-haspopup="menu"], button[aria-haspopup="listbox"], [role="button"][aria-haspopup="menu"], button[data-testid*="model"], button[data-testid*="reasoning"]';
|
|
180
|
-
const candidates = Array.from(conversationRoot().querySelectorAll(selectors)).filter(visible);
|
|
181
|
-
return candidates.find((node) => {
|
|
182
|
-
const label = core.normalizeText((node.getAttribute?.('aria-label') ?? '') + ' ' + (node.innerText ?? node.textContent ?? ''));
|
|
183
|
-
const normalized = label.toLowerCase().replace(/[\s_-]+/g, '');
|
|
184
|
-
return /^gpt\d/.test(normalized) || normalized.includes('reasoning') || normalized.includes('thinking')
|
|
185
|
-
|| normalized.includes('推理') || normalized.includes('思考') || reasoningLevel(label) !== undefined;
|
|
186
|
-
});
|
|
187
|
-
};
|
|
188
|
-
const reasoningControlLabel = (node) => core.normalizeText((node?.getAttribute?.('aria-label') ?? '') + ' ' + (node?.innerText ?? node?.textContent ?? ''));
|
|
189
|
-
const selectedReasoningOption = (level) => Array.from(document.querySelectorAll('[role="menuitemradio"], [role="option"], [role="radio"]'))
|
|
190
|
-
.filter(visible)
|
|
191
|
-
.find((node) => reasoningLevel(reasoningControlLabel(node)) === level
|
|
192
|
-
&& (node.getAttribute('aria-checked') === 'true' || node.getAttribute('aria-selected') === 'true'));
|
|
193
|
-
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
194
|
-
const ensureReasoning = async (level) => {
|
|
195
|
-
if (level !== 'medium' && level !== 'high' && level !== 'xhigh') {
|
|
196
|
-
return { verified: false, reason: 'COMPUTER_CHATGPT_REASONING_INVALID' };
|
|
197
|
-
}
|
|
198
|
-
const control = reasoningControl();
|
|
199
|
-
if (!control) return { verified: false, reason: 'COMPUTER_CHATGPT_REASONING_CONTROL_UNAVAILABLE' };
|
|
200
|
-
if (reasoningLevel(reasoningControlLabel(control)) === level) return { verified: true, level };
|
|
201
|
-
control.click();
|
|
202
|
-
await sleep(80);
|
|
203
|
-
const menuOptions = Array.from(document.querySelectorAll('[role="menuitemradio"], [role="option"], [role="radio"]'))
|
|
204
|
-
.filter(visible)
|
|
205
|
-
.map((node) => ({ node, level: reasoningLevel(reasoningControlLabel(node)), disabled: node.hasAttribute('disabled') || node.getAttribute('aria-disabled') === 'true' }));
|
|
206
|
-
// xhigh means highest *available*, not a claim that every plan exposes
|
|
207
|
-
// Extra High. Prefer it, then High, then Medium; record the actual level.
|
|
208
|
-
const menuLevel = level === 'xhigh'
|
|
209
|
-
? ['xhigh', 'high', 'medium'].find((candidate) => menuOptions.some((item) => item.level === candidate && !item.disabled))
|
|
210
|
-
: level;
|
|
211
|
-
const option = menuOptions.find((item) => item.level === menuLevel);
|
|
212
|
-
if (option) {
|
|
213
|
-
if (option.disabled) return { verified: false, reason: 'COMPUTER_CHATGPT_REASONING_OPTION_DISABLED' };
|
|
214
|
-
option.node.click();
|
|
215
|
-
await sleep(80);
|
|
216
|
-
if (reasoningLevel(reasoningControlLabel(reasoningControl())) === menuLevel || selectedReasoningOption(menuLevel)) {
|
|
217
|
-
return { verified: true, level: menuLevel };
|
|
218
|
-
}
|
|
219
|
-
return { verified: false, reason: 'COMPUTER_CHATGPT_REASONING_NOT_VERIFIED' };
|
|
220
|
-
}
|
|
221
|
-
// Older ChatGPT layouts expose a five-position slider in the model menu.
|
|
222
|
-
// React must acknowledge each navigation by changing aria-valuenow; writing
|
|
223
|
-
// our own attribute would be a false verification.
|
|
224
|
-
const sliders = Array.from(document.querySelectorAll('[role="slider"][aria-valuenow]')).filter(visible);
|
|
225
|
-
if (sliders.length !== 1) return { verified: false, reason: 'COMPUTER_CHATGPT_REASONING_OPTION_UNAVAILABLE' };
|
|
226
|
-
const slider = sliders[0];
|
|
227
|
-
const max = Number(slider.getAttribute('aria-valuemax'));
|
|
228
|
-
const min = Number(slider.getAttribute('aria-valuemin') ?? '0');
|
|
229
|
-
const target = level === 'xhigh' ? max : level === 'high' ? 3 : 2;
|
|
230
|
-
const actualLevel = target === 4 ? 'xhigh' : target === 3 ? 'high' : target === 2 ? 'medium' : undefined;
|
|
231
|
-
let current = Number(slider.getAttribute('aria-valuenow'));
|
|
232
|
-
if (![min, max, current, target].every(Number.isInteger) || min < 0 || max > 4 || max < 2
|
|
233
|
-
|| current < min || current > max || target < min || target > max || !actualLevel) {
|
|
234
|
-
return { verified: false, reason: 'COMPUTER_CHATGPT_REASONING_STATE_UNAVAILABLE' };
|
|
235
|
-
}
|
|
236
|
-
for (let step = 0; step < Math.abs(target - current); step += 1) {
|
|
237
|
-
slider.focus?.();
|
|
238
|
-
slider.dispatchEvent(new KeyboardEvent('keydown', { key: target > current ? 'ArrowRight' : 'ArrowLeft', bubbles: true }));
|
|
239
|
-
await sleep(40);
|
|
240
|
-
const next = Number(slider.getAttribute('aria-valuenow'));
|
|
241
|
-
if (next !== current + (target > current ? 1 : -1)) {
|
|
242
|
-
return { verified: false, reason: 'COMPUTER_CHATGPT_REASONING_NOT_VERIFIED' };
|
|
243
|
-
}
|
|
244
|
-
current = next;
|
|
245
|
-
}
|
|
246
|
-
if (Number(slider.getAttribute('aria-valuenow')) !== target) {
|
|
247
|
-
return { verified: false, reason: 'COMPUTER_CHATGPT_REASONING_NOT_VERIFIED' };
|
|
248
|
-
}
|
|
249
|
-
const valueText = slider.getAttribute('aria-valuetext');
|
|
250
|
-
if (valueText && reasoningLevel(valueText) !== actualLevel) {
|
|
251
|
-
return { verified: false, reason: 'COMPUTER_CHATGPT_REASONING_NOT_VERIFIED' };
|
|
252
|
-
}
|
|
253
|
-
return { verified: true, level: actualLevel };
|
|
254
|
-
};
|
|
255
|
-
const dispatchPrompt = async (prompt, mode = 'send', reasoning, expectedConversationId, prepareOnly = false) => {
|
|
256
|
-
const exactConversationIsCurrent = () => !expectedConversationId
|
|
257
|
-
|| core.parseConversation(location.href)?.conversationId === expectedConversationId;
|
|
214
|
+
const dispatchPrompt = async (prompt, mode = 'send', expectedConversationId, prepareOnly = false, expectedProjectId) => {
|
|
215
|
+
const exactConversationIsCurrent = () => expectedConversationId
|
|
216
|
+
? core.parseConversation(location.href)?.conversationId === expectedConversationId
|
|
217
|
+
: expectedProjectId ? core.projectId(location.href) === expectedProjectId && !core.parseConversation(location.href)
|
|
218
|
+
: true;
|
|
258
219
|
if (!exactConversationIsCurrent()) return { dispatched: false, reason: 'target_identity_changed' };
|
|
259
220
|
if (typeof prompt !== 'string' || !prompt.trim()) return { dispatched: false, reason: 'prompt_required' };
|
|
221
|
+
// This bounded prepare-only envelope is produced by the canonical
|
|
222
|
+
// Supervisor only after an exact 0600 on-device backup and operator marker.
|
|
223
|
+
// It is never part of a sent effect and never authorizes another provider.
|
|
224
|
+
const replacement = /^<<<FORGE_DRAFT_REPLACE_V1:([a-f0-9]{24}):([a-f0-9]{32})>>>\n/.exec(prompt);
|
|
225
|
+
if (replacement && (!prepareOnly || !expectedProjectId || mode !== 'send')) {
|
|
226
|
+
return { dispatched: false, reason: 'composer_replacement_mode_invalid' };
|
|
227
|
+
}
|
|
228
|
+
// Prepare-only exact retirement; never submits or retries the stopped task.
|
|
229
|
+
const retired = /^<<<FORGE_RETIRED_DRAFT_V1:((?:fx|crpe)_[a-zA-Z0-9_-]{8,120}):([a-f0-9]{24})>>>\n/.exec(prompt);
|
|
230
|
+
if (retired && (!prepareOnly || !expectedProjectId || mode !== 'send' || replacement)) {
|
|
231
|
+
return { dispatched: false, reason: 'composer_replacement_mode_invalid' };
|
|
232
|
+
}
|
|
233
|
+
const actualPrompt = replacement ? prompt.slice(replacement[0].length)
|
|
234
|
+
: retired ? prompt.slice(retired[0].length) : prompt;
|
|
260
235
|
if (mode === 'recover') {
|
|
261
236
|
const stop = document.querySelector(STOP);
|
|
262
237
|
if (prepareOnly && stop instanceof HTMLElement) return { prepared: false, reason: 'provider_recovery_stop_pending' };
|
|
@@ -272,66 +247,114 @@
|
|
|
272
247
|
} else if (isGenerating()) return { dispatched: false, reason: 'provider_busy' };
|
|
273
248
|
const node = composer();
|
|
274
249
|
if (!node) return { dispatched: false, reason: 'composer_missing' };
|
|
275
|
-
|
|
276
|
-
const
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
const
|
|
280
|
-
const
|
|
281
|
-
const boundChip = () => {
|
|
282
|
-
const chips = Array.from(node.querySelectorAll('[plugin-mention-name], [app-mention-name]'));
|
|
283
|
-
return chips.length === 1
|
|
284
|
-
&& chips[0]?.getAttribute('app-mention-name') === 'forge'
|
|
285
|
-
&& chips[0]?.getAttribute('app-mention-path') === 'app://asdk_app_6ac09022b26081918f20ff868c773824'
|
|
286
|
-
&& composerText() === boundText;
|
|
287
|
-
};
|
|
250
|
+
// Project instructions select tools. No @forge UI mention is needed.
|
|
251
|
+
const supervisor = /^(?:@forge\s+)?<<<FORGE_WORKFLOW_EFFECT_V1:(?:fx|crpe)_[a-zA-Z0-9_-]{8,120}>>>/.test(actualPrompt);
|
|
252
|
+
if (supervisor && !expectedConversationId && !expectedProjectId) return { dispatched: false, reason: 'target_identity_unavailable' };
|
|
253
|
+
// Strip only the exact legacy prefix on already-durable effects.
|
|
254
|
+
const payload = supervisor ? actualPrompt.replace(/^@forge\s+(?=<<<FORGE_WORKFLOW_EFFECT_V1:(?:fx|crpe)_[a-zA-Z0-9_-]{8,120}>>>)/, '') : actualPrompt;
|
|
255
|
+
const expected = exactComposerText(payload);
|
|
288
256
|
const existing = composerText();
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
257
|
+
// A restored draft belongs to this effect only when it matches precisely.
|
|
258
|
+
// Never overwrite another task's draft or a human-authored message.
|
|
259
|
+
const ownedDrafts = supervisor
|
|
260
|
+
? [expected, expected + '\n', exactComposerText(payload + '\n@'), exactComposerText(payload + '\nforge'), exactComposerText('forge ' + payload)]
|
|
261
|
+
: [expected];
|
|
262
|
+
if (node.querySelector('[plugin-mention-name], [app-mention-name]')) return { dispatched: false, reason: 'composer_has_app_mention' };
|
|
263
|
+
const unowned = Boolean(existing && !ownedDrafts.includes(existing));
|
|
264
|
+
if (unowned && ((!replacement && !retired) || !supervisor)) return { dispatched: false, reason: 'composer_not_empty' };
|
|
265
|
+
if (retired && (!unowned || !existing.startsWith(`<<<FORGE_WORKFLOW_EFFECT_V1:${retired[1]}>>>`))) {
|
|
266
|
+
return { dispatched: false, reason: 'composer_not_empty' };
|
|
267
|
+
}
|
|
268
|
+
if (unowned) {
|
|
269
|
+
// Hash the *currently visible* single editor, not the page or another
|
|
270
|
+
// tab. If it changed since the backup, do not touch it.
|
|
271
|
+
if (!globalThis.crypto?.subtle || !globalThis.TextEncoder) return { dispatched: false, reason: 'composer_replacement_digest_unavailable' };
|
|
272
|
+
const bytes = new TextEncoder().encode('preflight-composer-v1:\0' + existing);
|
|
273
|
+
const digest = new Uint8Array(await crypto.subtle.digest('SHA-256', bytes));
|
|
274
|
+
const prefix = Array.from(digest).map((byte) => byte.toString(16).padStart(2, '0')).join('').slice(0, 24);
|
|
275
|
+
if (prefix !== (replacement ? replacement[1] : retired?.[2])) return { dispatched: false, reason: 'composer_not_empty' };
|
|
295
276
|
}
|
|
296
277
|
if (!exactConversationIsCurrent()) return { dispatched: false, reason: 'target_identity_changed' };
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
278
|
+
// Hashing and model-picker hydration yield to page/user edits. Revalidate
|
|
279
|
+
// the same single editor before overwriting any bytes observed above.
|
|
280
|
+
if (composer() !== node || composerText(node) !== existing) return { dispatched: false, reason: 'composer_not_empty' };
|
|
281
|
+
if (existing !== expected && existing !== expected + '\n') writeComposer(node, payload);
|
|
282
|
+
// React may acknowledge the exact draft before it enables Send. Observe
|
|
283
|
+
// this same target for at most two seconds, below the broker deadline;
|
|
284
|
+
// this is read-only readiness, never another write or Send attempt.
|
|
285
|
+
const ready = await new Promise((resolve) => {
|
|
286
|
+
let timer;
|
|
287
|
+
let settled = false;
|
|
288
|
+
const finish = (value) => {
|
|
289
|
+
if (settled) return;
|
|
290
|
+
settled = true;
|
|
291
|
+
observer.disconnect();
|
|
292
|
+
document.removeEventListener('input', check, true);
|
|
293
|
+
clearTimeout(timer);
|
|
294
|
+
resolve(value);
|
|
295
|
+
};
|
|
296
|
+
const check = (expired = false) => {
|
|
297
|
+
if (!exactConversationIsCurrent()) { finish({ reason: 'target_identity_changed' }); return; }
|
|
298
|
+
// ProseMirror may expose one editor-generated final newline. Do not
|
|
299
|
+
// widen matching beyond that break or tolerate another visible draft.
|
|
300
|
+
const text = composerText();
|
|
301
|
+
if (text !== expected && text !== expected + '\n') { finish({ reason: 'composer_write_unconfirmed' }); return; }
|
|
302
|
+
if (isGenerating()) { finish({ reason: 'provider_busy' }); return; }
|
|
303
|
+
const buttons = Array.from(document.querySelectorAll(SEND)).filter(visible);
|
|
304
|
+
const send = buttons.length === 1 ? buttons[0] : undefined;
|
|
305
|
+
if (send && !send.disabled && send.getAttribute('aria-disabled') !== 'true') { finish({ send }); return; }
|
|
306
|
+
if (expired === true) finish({ reason: send ? 'send_button_disabled' : 'send_button_missing' });
|
|
307
|
+
};
|
|
308
|
+
const observer = new MutationObserver(() => check());
|
|
309
|
+
observer.observe(document.documentElement, { childList: true, subtree: true, characterData: true, attributes: true });
|
|
310
|
+
document.addEventListener('input', check, true);
|
|
311
|
+
timer = setTimeout(() => check(true), 2_000);
|
|
312
|
+
check();
|
|
313
|
+
});
|
|
314
|
+
if (!ready.send) return { dispatched: false, reason: ready.reason };
|
|
315
|
+
// Await resumes in a new microtask: retain the exact pre-click fences even
|
|
316
|
+
// if an input/route update was queued alongside the readiness observation.
|
|
300
317
|
if (!exactConversationIsCurrent()) return { dispatched: false, reason: 'target_identity_changed' };
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
node.focus();
|
|
307
|
-
const selection = window.getSelection();
|
|
308
|
-
if (!selection) return { dispatched: false, reason: 'COMPUTER_CHATGPT_PLUGIN_SELECTION_UNAVAILABLE' };
|
|
309
|
-
const range = document.createRange(); range.selectNodeContents(node); range.collapse(false);
|
|
310
|
-
selection.removeAllRanges(); selection.addRange(range);
|
|
311
|
-
if (!document.execCommand('insertText', false, 'forge')) return { dispatched: false, reason: 'COMPUTER_CHATGPT_PLUGIN_SELECTION_UNAVAILABLE' };
|
|
312
|
-
await sleep(150);
|
|
313
|
-
const entries = Array.from(document.querySelectorAll('[data-mention-section-items] > button'));
|
|
314
|
-
const selected = entries.filter((entry) => {
|
|
315
|
-
const label = core.normalizeText(entry.innerText ?? entry.textContent);
|
|
316
|
-
return label === 'forge' || label.startsWith('forge Current Forge 1.8.1');
|
|
317
|
-
});
|
|
318
|
-
if (selected.length !== 1) {
|
|
319
|
-
writeComposer(node, payload);
|
|
320
|
-
return { dispatched: false, reason: 'COMPUTER_CHATGPT_FORGE_PLUGIN_UNAVAILABLE' };
|
|
321
|
-
}
|
|
322
|
-
if (!exactConversationIsCurrent()) return { dispatched: false, reason: 'target_identity_changed' };
|
|
323
|
-
selected[0].click();
|
|
324
|
-
await sleep(150);
|
|
325
|
-
}
|
|
326
|
-
if (!boundChip()) return { dispatched: false, reason: 'COMPUTER_CHATGPT_FORGE_PLUGIN_CHIP_UNVERIFIED' };
|
|
327
|
-
} else if (composerText() !== expected) return { dispatched: false, reason: 'composer_write_unconfirmed' };
|
|
328
|
-
const send = document.querySelector(SEND);
|
|
329
|
-
if (!send) return { dispatched: false, reason: 'send_button_missing' };
|
|
318
|
+
const text = composerText();
|
|
319
|
+
if (text !== expected && text !== expected + '\n') return { dispatched: false, reason: 'composer_write_unconfirmed' };
|
|
320
|
+
if (isGenerating()) return { dispatched: false, reason: 'provider_busy' };
|
|
321
|
+
const send = ready.send;
|
|
322
|
+
if (!send.isConnected || !visible(send)) return { dispatched: false, reason: 'send_button_missing' };
|
|
330
323
|
if (send.disabled || send.getAttribute('aria-disabled') === 'true') return { dispatched: false, reason: 'send_button_disabled' };
|
|
331
|
-
if (!exactConversationIsCurrent()) return { dispatched: false, reason: 'target_identity_changed' };
|
|
332
324
|
if (prepareOnly) return { prepared: true };
|
|
333
|
-
|
|
334
|
-
|
|
325
|
+
// Confirm the committed turn inside the content script, rather than
|
|
326
|
+
// requesting delayed snapshots from the extension background worker.
|
|
327
|
+
const beforeSend = roleNodes(USER).at(-1);
|
|
328
|
+
// Once click starts, an exception cannot prove that the provider did not
|
|
329
|
+
// receive the message. Keep post-click observation failures outcome-unknown.
|
|
330
|
+
try {
|
|
331
|
+
send.click();
|
|
332
|
+
const confirmed = await new Promise((resolve) => {
|
|
333
|
+
// Unobserved submission is unknown, never proof that Send did not run.
|
|
334
|
+
let timer;
|
|
335
|
+
let settled = false;
|
|
336
|
+
const finish = (value) => {
|
|
337
|
+
if (settled) return;
|
|
338
|
+
settled = true;
|
|
339
|
+
observer.disconnect();
|
|
340
|
+
clearTimeout(timer);
|
|
341
|
+
resolve(value);
|
|
342
|
+
};
|
|
343
|
+
const check = () => {
|
|
344
|
+
if (expectedConversationId && !exactConversationIsCurrent()) return;
|
|
345
|
+
const latest = roleNodes(USER).at(-1);
|
|
346
|
+
if (latest && latest !== beforeSend
|
|
347
|
+
&& exactComposerText(messageText(latest)) === expected) finish(true);
|
|
348
|
+
};
|
|
349
|
+
const observer = new MutationObserver(check);
|
|
350
|
+
observer.observe(document.documentElement, { childList: true, subtree: true, characterData: true });
|
|
351
|
+
timer = setTimeout(() => finish(false), 2_000);
|
|
352
|
+
check();
|
|
353
|
+
});
|
|
354
|
+
return { dispatched: true, confirmed };
|
|
355
|
+
} catch {
|
|
356
|
+
return { dispatched: true, confirmed: false };
|
|
357
|
+
}
|
|
335
358
|
};
|
|
336
359
|
chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
|
|
337
360
|
if (!message || message.type === 'forge-computer-chatgpt-scan') { notify(); sendResponse?.({ ok: true }); return false; }
|
|
@@ -340,21 +363,22 @@
|
|
|
340
363
|
return false;
|
|
341
364
|
}
|
|
342
365
|
if (message.type === 'forge-computer-chatgpt-snapshot') { sendResponse(pageSnapshot(message.options ?? {})); return false; }
|
|
343
|
-
// Read-only exact-tab
|
|
344
|
-
// never receive an xhigh-required send that it would silently ignore.
|
|
366
|
+
// Read-only exact-tab attestation for bootstrap/draft target guards.
|
|
345
367
|
if (message.type === 'forge-computer-chatgpt-capabilities') {
|
|
346
|
-
sendResponse({
|
|
368
|
+
sendResponse({ bootstrapTargetGuard: 'project-checked-before-send-v1',
|
|
369
|
+
composerReplacementContract: 'exact-backed-up-project-draft-v1' });
|
|
347
370
|
return false;
|
|
348
371
|
}
|
|
349
372
|
if (message.type === 'forge-computer-chatgpt-prepare') {
|
|
350
|
-
dispatchPrompt(String(message.prompt ?? ''), String(message.mode ?? 'send'), message.
|
|
351
|
-
.then((result) => sendResponse(result?.prepared === true
|
|
373
|
+
dispatchPrompt(String(message.prompt ?? ''), String(message.mode ?? 'send'), message.expectedConversationId, true, message.expectedProjectId)
|
|
374
|
+
.then((result) => sendResponse(result?.prepared === true
|
|
375
|
+
? { prepared: true }
|
|
352
376
|
: { prepared: false, reason: String(result?.reason ?? 'COMPUTER_CHATGPT_PREPARE_REJECTED') }),
|
|
353
377
|
(error) => sendResponse({ prepared: false, reason: String(error?.message ?? error) }));
|
|
354
378
|
return true;
|
|
355
379
|
}
|
|
356
380
|
if (message.type === 'forge-computer-chatgpt-dispatch') {
|
|
357
|
-
dispatchPrompt(String(message.prompt ?? ''), String(message.mode ?? 'send'), message.
|
|
381
|
+
dispatchPrompt(String(message.prompt ?? ''), String(message.mode ?? 'send'), message.expectedConversationId, false, message.expectedProjectId)
|
|
358
382
|
.then(sendResponse, (error) => sendResponse({ dispatched: false, reason: String(error?.message ?? error) }));
|
|
359
383
|
return true;
|
|
360
384
|
}
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"manifest_version": 3,
|
|
3
3
|
"name": "Forge Computer ChatGPT Provider",
|
|
4
|
-
"version": "0.3.
|
|
4
|
+
"version": "0.3.3",
|
|
5
5
|
"description": "Computer-owned stable ChatGPT target provider for Forge Runtime.",
|
|
6
6
|
"key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxFzo1eixjsWsZbN1pBuyzdKinJSAAuzTRavD0xFQFwrTIEQ7hxxueEBEhifpGq9nplNnmxmZvIL7PJycEAYT8mrbyXBLCPR1jQBuL4YR775phnlNVpF3dHX5OWDHLWRnHRgOO1FibQ54fM2rKylr66+x+J6/4C7a9dpiSMuxf3fOStXA6wJb0d7A4E22Q1+GGfWgs0NCyVI5k4aczK+J5Ao61ZXKBr8Qw/FCmwhCcDgQdIpURgoMHkyvQH3ryYWocucjRhMVsU8H65adIIKFHkEhPJCiVY64L6bu6kNR2fpf0yJ1GvI5ota6Hf4NAEi7Yt7PL7i3ISyv3pPQWW1nIwIDAQAB",
|
|
7
7
|
"permissions": [
|
|
8
8
|
"nativeMessaging",
|
|
9
|
+
"storage",
|
|
9
10
|
"tabs",
|
|
11
|
+
"scripting",
|
|
10
12
|
"alarms"
|
|
11
13
|
],
|
|
12
14
|
"host_permissions": [
|
|
@@ -290,22 +290,34 @@ export async function callDesktopOperatorComputerBrowserAutomation(
|
|
|
290
290
|
export function createDesktopOperatorComputerProvider(
|
|
291
291
|
options: DesktopOperatorComputerProviderOptions = {},
|
|
292
292
|
): DesktopOperatorComputerProvider {
|
|
293
|
-
let active: { key: string; binding: DesktopOperatorComputerBinding } | undefined;
|
|
294
|
-
const binding = (): DesktopOperatorComputerBinding => {
|
|
293
|
+
let active: { key: string; binding: DesktopOperatorComputerBinding; observations?: DesktopOperatorComputerBinding } | undefined;
|
|
294
|
+
const binding = (observation = false): DesktopOperatorComputerBinding => {
|
|
295
295
|
const endpoint = resolveDesktopOperatorComputerEndpoint(options);
|
|
296
296
|
const key = endpointBindingKey(endpoint);
|
|
297
|
-
if (active?.key
|
|
298
|
-
|
|
299
|
-
|
|
297
|
+
if (active?.key !== key) {
|
|
298
|
+
active?.binding.close();
|
|
299
|
+
active?.observations?.close();
|
|
300
|
+
active = { key, binding: createBinding(endpoint) };
|
|
301
|
+
}
|
|
302
|
+
// Desktop Operator executes RPCs serially within each socket connection.
|
|
303
|
+
// A slow Browser RPC must not head-of-line block native snapshots/capture.
|
|
304
|
+
// Keep all input, console and legacy Browser requests on their original
|
|
305
|
+
// ordered stream; target/session locks still serialize observation epochs.
|
|
306
|
+
if (observation) return active.observations ??= createBinding(endpoint);
|
|
300
307
|
return active.binding;
|
|
301
308
|
};
|
|
302
309
|
return {
|
|
303
310
|
providerId: DESKTOP_OPERATOR_PROVIDER_PLUGIN_ID,
|
|
304
311
|
capabilities: desktopOperatorComputerProviderCapabilities(options),
|
|
305
|
-
execute: async (request, timeoutMs) => await binding(
|
|
312
|
+
execute: async (request, timeoutMs) => await binding(
|
|
313
|
+
request.capability === COMPUTER_OBSERVE_CAPABILITY
|
|
314
|
+
|| request.capability === COMPUTER_ELEMENT_OBSERVE_CAPABILITY
|
|
315
|
+
|| request.capability === COMPUTER_CAPTURE_CAPABILITY,
|
|
316
|
+
).execute(request, timeoutMs),
|
|
306
317
|
executeBrowserCompatibility: async (request, timeoutMs) => await binding().executeBrowserCompatibility(request, timeoutMs),
|
|
307
318
|
dispose: () => {
|
|
308
319
|
active?.binding.close();
|
|
320
|
+
active?.observations?.close();
|
|
309
321
|
active = undefined;
|
|
310
322
|
},
|
|
311
323
|
};
|
|
@@ -218,6 +218,7 @@ interface ComputerSurfaceIndexReadyRecord {
|
|
|
218
218
|
schemaVersion: 1;
|
|
219
219
|
status: 'ready';
|
|
220
220
|
indexedAt: string;
|
|
221
|
+
terminalBindingIndexReady?: true;
|
|
221
222
|
}
|
|
222
223
|
|
|
223
224
|
const COMPUTER_SURFACE_INDEX_READY_KEY = 'surface-index-v1-ready';
|
|
@@ -231,6 +232,12 @@ function surfaceBindingIndexKey(binding: ComputerSurfaceProviderBinding | undefi
|
|
|
231
232
|
return `binding-${digest40(`${binding.providerId}:${binding.browserProduct ?? ''}:${binding.windowId}:${binding.tabId}`)}`;
|
|
232
233
|
}
|
|
233
234
|
|
|
235
|
+
// Migration-only negative evidence; never claims the live tab binding.
|
|
236
|
+
function terminalBindingIndexKey(binding: ComputerSurfaceProviderBinding | undefined): string | undefined {
|
|
237
|
+
const key = surfaceBindingIndexKey(binding);
|
|
238
|
+
return key ? `terminal-${key}` : undefined;
|
|
239
|
+
}
|
|
240
|
+
|
|
234
241
|
function surfaceStableIdentityIndexKey(identity: ComputerSurfaceStableIdentity): string | undefined {
|
|
235
242
|
const normalized = normalizeSurfaceStableIdentity(identity);
|
|
236
243
|
if (!normalized.resource) return undefined;
|
|
@@ -282,7 +289,10 @@ function writeSurfaceIndexes(
|
|
|
282
289
|
): void {
|
|
283
290
|
// Terminal imports retain stable/alias identity, never a physical tab claim.
|
|
284
291
|
const indexed = status === 'tombstoned' ? { ...target, providerBinding: undefined } : target;
|
|
285
|
-
|
|
292
|
+
const keys = surfaceIndexKeysForTarget(indexed);
|
|
293
|
+
const terminal = status === 'tombstoned' ? terminalBindingIndexKey(target.providerBinding) : undefined;
|
|
294
|
+
if (terminal) keys.push(terminal);
|
|
295
|
+
for (const key of keys) {
|
|
286
296
|
const current = transaction.read<ComputerSurfaceIndexRecord>(COMPUTER_TARGET_INDEX_NAMESPACE, COMPUTER_TARGET_SCOPE, key);
|
|
287
297
|
if (current?.value.targetId === target.targetId) continue;
|
|
288
298
|
if (current) {
|
|
@@ -311,7 +321,7 @@ function ensureSurfaceIndexes(transaction: ComputerTargetPersistenceTransaction)
|
|
|
311
321
|
COMPUTER_TARGET_SCOPE,
|
|
312
322
|
COMPUTER_SURFACE_INDEX_READY_KEY,
|
|
313
323
|
);
|
|
314
|
-
if (ready?.value.status === 'ready') return;
|
|
324
|
+
if (ready?.value.status === 'ready' && ready.value.terminalBindingIndexReady === true) return;
|
|
315
325
|
const surfaces = transaction.listAll<ComputerInteractionTargetEntry>({
|
|
316
326
|
namespace: COMPUTER_TARGET_NAMESPACE,
|
|
317
327
|
scope: COMPUTER_TARGET_SCOPE,
|
|
@@ -324,7 +334,7 @@ function ensureSurfaceIndexes(transaction: ComputerTargetPersistenceTransaction)
|
|
|
324
334
|
scope: COMPUTER_TARGET_SCOPE,
|
|
325
335
|
key: COMPUTER_SURFACE_INDEX_READY_KEY,
|
|
326
336
|
schemaVersion: 1,
|
|
327
|
-
value: { schemaVersion: 1, status: 'ready', indexedAt: now() } satisfies ComputerSurfaceIndexReadyRecord,
|
|
337
|
+
value: { schemaVersion: 1, status: 'ready', indexedAt: now(), terminalBindingIndexReady: true } satisfies ComputerSurfaceIndexReadyRecord,
|
|
328
338
|
action: ready ? 'computer_surface_index_ready_repair' : 'computer_surface_index_ready',
|
|
329
339
|
expectedRevision: ready?.revision ?? null,
|
|
330
340
|
});
|
|
@@ -347,9 +357,12 @@ function readIndexedSurfaceRecord(
|
|
|
347
357
|
const index = transaction.read<ComputerSurfaceIndexRecord>(COMPUTER_TARGET_INDEX_NAMESPACE, COMPUTER_TARGET_SCOPE, indexKey);
|
|
348
358
|
if (!index) return undefined;
|
|
349
359
|
const target = transaction.read<ComputerInteractionTargetEntry>(COMPUTER_TARGET_NAMESPACE, COMPUTER_TARGET_SCOPE, index.value.targetId);
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
360
|
+
const surface = target?.value.target.kind === 'surface' ? normalizedSurfaceTarget(target.value.target) : undefined;
|
|
361
|
+
const valid = indexKey.startsWith('terminal-')
|
|
362
|
+
? target?.value.status === 'tombstoned' && terminalBindingIndexKey(surface?.providerBinding) === indexKey
|
|
363
|
+
: (target?.value.status === 'active' || !indexKey.startsWith('binding-'))
|
|
364
|
+
&& Boolean(surface && surfaceIndexKeysForTarget(surface).includes(indexKey));
|
|
365
|
+
if (target && valid) return target;
|
|
353
366
|
transaction.delete({
|
|
354
367
|
namespace: COMPUTER_TARGET_INDEX_NAMESPACE,
|
|
355
368
|
scope: COMPUTER_TARGET_SCOPE,
|
|
@@ -363,8 +376,14 @@ function readIndexedSurfaceRecord(
|
|
|
363
376
|
function deleteSurfaceIndexes(
|
|
364
377
|
transaction: ComputerTargetPersistenceTransaction,
|
|
365
378
|
target: ComputerSurfaceTarget,
|
|
379
|
+
status: 'active' | 'tombstoned' = 'active',
|
|
366
380
|
): void {
|
|
367
|
-
|
|
381
|
+
const keys = surfaceIndexKeysForTarget(target);
|
|
382
|
+
if (status === 'tombstoned') {
|
|
383
|
+
const terminal = terminalBindingIndexKey(target.providerBinding);
|
|
384
|
+
if (terminal) keys.push(terminal);
|
|
385
|
+
}
|
|
386
|
+
for (const key of keys) {
|
|
368
387
|
const index = transaction.read<ComputerSurfaceIndexRecord>(COMPUTER_TARGET_INDEX_NAMESPACE, COMPUTER_TARGET_SCOPE, key);
|
|
369
388
|
if (!index || index.value.targetId !== target.targetId) continue;
|
|
370
389
|
transaction.delete({
|
|
@@ -607,6 +626,15 @@ export function createComputerInteractionTargetAuthority(
|
|
|
607
626
|
const inputIndexKeys = surfaceInputIndexKeys({ stableIdentity, visibility, compatibilityAliases, repositoryIds, providerBinding });
|
|
608
627
|
return persistence.transaction(controllerHome, (transaction) => {
|
|
609
628
|
ensureSurfaceIndexes(transaction);
|
|
629
|
+
// An old repository's import is observation, not permission to revive a
|
|
630
|
+
// terminal physical tab. Current authoritative writes remain unaffected.
|
|
631
|
+
if (rawInput.reactivate === false) {
|
|
632
|
+
const terminalKey = terminalBindingIndexKey(providerBinding);
|
|
633
|
+
const terminal = terminalKey ? readIndexedSurfaceRecord(transaction, terminalKey) : undefined;
|
|
634
|
+
if (terminal?.value.status === 'tombstoned' && terminal.value.target.kind === 'surface') {
|
|
635
|
+
return { target: structuredClone(normalizedSurfaceTarget(terminal.value.target)), status: 'tombstoned', created: false };
|
|
636
|
+
}
|
|
637
|
+
}
|
|
610
638
|
const indexed = new Map<string, ComputerTargetPersistenceRecord<ComputerInteractionTargetEntry>>();
|
|
611
639
|
for (const indexKey of inputIndexKeys) {
|
|
612
640
|
const candidate = readIndexedSurfaceRecord(transaction, indexKey);
|
|
@@ -674,7 +702,7 @@ export function createComputerInteractionTargetAuthority(
|
|
|
674
702
|
...(existing.value.status === 'tombstoned' ? { providerBinding } : providerBinding ? { providerBinding } : {}),
|
|
675
703
|
updatedAt: at,
|
|
676
704
|
};
|
|
677
|
-
deleteSurfaceIndexes(transaction, currentTarget);
|
|
705
|
+
deleteSurfaceIndexes(transaction, currentTarget, existing.value.status);
|
|
678
706
|
transaction.write({
|
|
679
707
|
namespace: COMPUTER_TARGET_NAMESPACE,
|
|
680
708
|
scope: COMPUTER_TARGET_SCOPE,
|
|
@@ -869,6 +897,7 @@ export function createComputerInteractionTargetAuthority(
|
|
|
869
897
|
action: 'computer_surface_target_tombstone',
|
|
870
898
|
expectedRevision: current.revision,
|
|
871
899
|
});
|
|
900
|
+
writeSurfaceIndexes(transaction, normalizedSurfaceTarget(current.value.target), 'tombstoned');
|
|
872
901
|
return true;
|
|
873
902
|
});
|
|
874
903
|
}
|
|
@@ -962,6 +991,7 @@ export function createComputerInteractionTargetAuthority(
|
|
|
962
991
|
action: 'computer_surface_target_tombstone',
|
|
963
992
|
expectedRevision: current.revision,
|
|
964
993
|
});
|
|
994
|
+
writeSurfaceIndexes(transaction, normalizedSurfaceTarget(current.value.target), 'tombstoned');
|
|
965
995
|
});
|
|
966
996
|
return structuredClone(target);
|
|
967
997
|
},
|
|
@@ -1008,7 +1038,7 @@ export function createComputerInteractionTargetAuthority(
|
|
|
1008
1038
|
const currentAtMs = Date.parse(current.value.tombstonedAt ?? current.updatedAt);
|
|
1009
1039
|
const stillExpired = !Number.isFinite(currentAtMs) || nowMs - currentAtMs >= ttlMs;
|
|
1010
1040
|
if (!stillExpired && !capacityEligible) return false;
|
|
1011
|
-
if (current.value.target.kind === 'surface') deleteSurfaceIndexes(transaction, normalizedSurfaceTarget(current.value.target));
|
|
1041
|
+
if (current.value.target.kind === 'surface') deleteSurfaceIndexes(transaction, normalizedSurfaceTarget(current.value.target), 'tombstoned');
|
|
1012
1042
|
return transaction.delete({
|
|
1013
1043
|
namespace: COMPUTER_TARGET_NAMESPACE,
|
|
1014
1044
|
scope: COMPUTER_TARGET_SCOPE,
|