openplanr 2.2640.8 → 2.2641.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +14 -0
- package/dist/dashboard/.vite/manifest.json +1 -1
- package/dist/dashboard/assets/{index-COZ36pFn.js → index-D7R6tnlX.js} +2 -2
- package/dist/dashboard/assets/{index-COZ36pFn.js.map → index-D7R6tnlX.js.map} +1 -1
- package/dist/dashboard/dashboard-manifest.json +8 -8
- package/dist/dashboard/index.html +1 -1
- package/lib/host-packages/adapter-registry.json +5 -5
- package/lib/host-packages/capability-map.json +1 -1
- package/lib/host-packages/claude/.claude-plugin/marketplace.json +2 -2
- package/lib/host-packages/claude/openplanr/.claude-plugin/plugin.json +1 -1
- package/lib/host-packages/claude/openplanr/.openplanr-content.json +167 -35
- package/lib/host-packages/claude/openplanr/README.md +22 -1
- package/lib/host-packages/claude/openplanr/runtime/design/scripts/design-artifact-shell.mjs +3 -11
- package/lib/host-packages/claude/openplanr/runtime/design/scripts/design-chunked-workspace-client.mjs +555 -0
- package/lib/host-packages/claude/openplanr/runtime/design/scripts/{design-pako-esm.mjs → design-compression.mjs} +2 -1521
- package/lib/host-packages/claude/openplanr/runtime/design/scripts/design-document.mjs +133 -12
- package/lib/host-packages/claude/openplanr/runtime/design/scripts/design-encrypted-workspace-client.mjs +647 -0
- package/lib/host-packages/claude/openplanr/runtime/design/scripts/design-entities.mjs +17 -67
- package/lib/host-packages/claude/openplanr/runtime/design/scripts/design-handoff.mjs +14 -16
- package/lib/host-packages/claude/openplanr/runtime/design/scripts/{design-planr-home.mjs → design-loopback-server.mjs} +15 -36
- package/lib/host-packages/claude/openplanr/runtime/design/scripts/design-owner-custody.mjs +121 -0
- package/lib/host-packages/claude/openplanr/runtime/design/scripts/design-resource-pack.mjs +698 -0
- package/lib/host-packages/claude/openplanr/runtime/design/scripts/design-review.mjs +798 -796
- package/lib/host-packages/claude/openplanr/runtime/design/scripts/design-runtime-home.mjs +39 -0
- package/lib/host-packages/claude/openplanr/runtime/design/scripts/design-runtime-support-5cb7531bede8.mjs +24 -0
- package/lib/host-packages/claude/openplanr/runtime/design/scripts/design-share.mjs +138 -765
- package/lib/host-packages/claude/openplanr/runtime/design/scripts/{design-escape.mjs → design-shared-artifact-support-dependencies-design-support-protocol-contracts-3d9b1a86.mjs} +158 -156
- package/lib/host-packages/{openai/openplanr/runtime/design/scripts/design-artifact-sources.mjs → claude/openplanr/runtime/design/scripts/design-shared-artifact-support-protocol-contracts-ea2cd15e.mjs} +1 -1
- package/lib/host-packages/{openai/openplanr/runtime/design/scripts/design-bounded-json-data.mjs → claude/openplanr/runtime/design/scripts/design-shared-protocol-contracts-31a760fc.mjs} +241 -454
- package/lib/host-packages/claude/openplanr/runtime/design/scripts/design-shared-protocol-contracts-75a938cc.mjs +222 -0
- package/lib/host-packages/claude/openplanr/runtime/design/scripts/design-upload-spool.mjs +198 -0
- package/lib/host-packages/claude/openplanr/runtime/design/scripts/design.mjs +12 -5
- package/lib/host-packages/claude/openplanr/runtime/design/scripts/runtime/packages/artifact/lib/artifact/ui/studio-shell.css +1 -1
- package/lib/host-packages/claude/openplanr/runtime/design/scripts/runtime/packages/artifact/templates/artifact-review-stage.js.sources/01-module-runtime.js +18 -0
- package/lib/host-packages/claude/openplanr/runtime/design/scripts/runtime/packages/artifact/templates/artifact-review-stage.js.sources/02-artifact-review-ui.js +809 -0
- package/lib/host-packages/claude/openplanr/runtime/design/scripts/runtime/packages/artifact/templates/artifact-review-stage.js.sources/03-protocol-contracts.js +5193 -0
- package/lib/host-packages/claude/openplanr/runtime/design/scripts/runtime/packages/artifact/templates/artifact-review-stage.js.sources/04-compression.js +4175 -0
- package/lib/host-packages/claude/openplanr/runtime/design/scripts/runtime/packages/artifact/templates/artifact-review-stage.js.sources/05-artifact-sharing.js +65 -0
- package/lib/host-packages/claude/openplanr/runtime/design/scripts/runtime/packages/artifact/templates/artifact-review-stage.js.sources/06-artifact-review-ui.js +1755 -0
- package/lib/host-packages/claude/openplanr/runtime/design/scripts/runtime/packages/artifact/templates/artifact-review-stage.js.sources/07-artifact-sharing.js +62 -0
- package/lib/host-packages/claude/openplanr/runtime/design/scripts/runtime/packages/artifact/templates/artifact-review-stage.js.sources/08-artifact-review-ui.js +2600 -0
- package/lib/host-packages/claude/openplanr/runtime/design/scripts/runtime/packages/artifact/templates/artifact-review-stage.js.sources.json +123 -0
- package/lib/host-packages/claude/openplanr/runtime/design/scripts/runtime/packages/design/package.json +3 -3
- package/lib/host-packages/claude/openplanr/runtime/design/scripts/runtime/packages/design/templates/studio/studio.css +3 -3
- package/lib/host-packages/claude/openplanr/runtime/design/scripts/runtime/packages/design/templates/studio/studio.js.sources/01-module-runtime.js +34 -0
- package/lib/host-packages/claude/openplanr/runtime/design/scripts/runtime/packages/design/templates/studio/studio.js.sources/02-react.js +992 -0
- package/lib/host-packages/claude/openplanr/runtime/design/scripts/runtime/packages/design/templates/studio/studio.js.sources/03-react-dom-client-fiber-foundations.js +2513 -0
- package/lib/host-packages/claude/openplanr/runtime/design/scripts/runtime/packages/design/templates/studio/studio.js.sources/04-react-dom-client-reconciliation-hooks.js +1919 -0
- package/lib/host-packages/claude/openplanr/runtime/design/scripts/runtime/packages/design/templates/studio/studio.js.sources/05-react-dom-client-render-phases.js +1759 -0
- package/lib/host-packages/claude/openplanr/runtime/design/scripts/runtime/packages/design/templates/studio/studio.js.sources/06-react-dom-client-commit-effects.js +1551 -0
- package/lib/host-packages/claude/openplanr/runtime/design/scripts/runtime/packages/design/templates/studio/studio.js.sources/07-react-dom-client-work-loop-scheduling.js +1121 -0
- package/lib/host-packages/claude/openplanr/runtime/design/scripts/runtime/packages/design/templates/studio/studio.js.sources/08-react-dom-client-dom-events-properties-hydration.js +2183 -0
- package/lib/host-packages/claude/openplanr/runtime/design/scripts/runtime/packages/design/templates/studio/studio.js.sources/09-react-dom-client-public-roots.js +705 -0
- package/lib/host-packages/claude/openplanr/runtime/design/scripts/runtime/packages/design/templates/studio/studio.js.sources/10-react.js +27 -0
- package/lib/host-packages/claude/openplanr/runtime/design/scripts/runtime/packages/design/templates/studio/studio.js.sources/11-artifact-review-ui.js +13 -0
- package/lib/host-packages/claude/openplanr/runtime/design/scripts/runtime/packages/design/templates/studio/studio.js.sources/12-design-review-experience.js +3561 -0
- package/lib/host-packages/claude/openplanr/runtime/design/scripts/runtime/packages/design/templates/studio/studio.js.sources/13-studio-shell.js +184 -0
- package/lib/host-packages/claude/openplanr/runtime/design/scripts/runtime/packages/design/templates/studio/studio.js.sources/14-interface-primitives.js +5045 -0
- package/lib/host-packages/claude/openplanr/runtime/design/scripts/runtime/packages/design/templates/studio/studio.js.sources/15-interface-primitives.js +1698 -0
- package/lib/host-packages/claude/openplanr/runtime/design/scripts/runtime/packages/design/templates/studio/studio.js.sources/16-studio-shell.js +455 -0
- package/lib/host-packages/claude/openplanr/runtime/design/scripts/runtime/packages/design/templates/studio/studio.js.sources/17-design-studio.js +1844 -0
- package/lib/host-packages/claude/openplanr/runtime/design/scripts/runtime/packages/design/templates/studio/studio.js.sources.json +257 -0
- package/lib/host-packages/claude/openplanr/runtime/design/scripts/runtime/packages/protocol/package.json +1 -1
- package/lib/host-packages/claude/openplanr/runtime/plan/scripts/design-protocol-contracts.mjs +4966 -0
- package/lib/host-packages/claude/openplanr/runtime/plan/scripts/design.mjs +240 -5192
- package/lib/host-packages/claude/openplanr/runtime/plan/scripts/runtime/packages/protocol/package.json +1 -1
- package/lib/host-packages/claude/openplanr/skills/delegate/scripts/backend-diagnostics.mjs +6 -2
- package/lib/host-packages/cursor/openplanr/.openplanr-content.json +166 -34
- package/lib/host-packages/cursor/openplanr/manifest.json +1 -1
- package/lib/host-packages/cursor/openplanr/rules/planr-delegate/scripts/backend-diagnostics.mjs +6 -2
- package/lib/host-packages/cursor/openplanr/runtime/design/scripts/design-artifact-shell.mjs +3 -11
- package/lib/host-packages/cursor/openplanr/runtime/design/scripts/design-chunked-workspace-client.mjs +555 -0
- package/lib/host-packages/{openai/openplanr/runtime/design/scripts/design-pako-esm.mjs → cursor/openplanr/runtime/design/scripts/design-compression.mjs} +2 -1521
- package/lib/host-packages/cursor/openplanr/runtime/design/scripts/design-document.mjs +133 -12
- package/lib/host-packages/cursor/openplanr/runtime/design/scripts/design-encrypted-workspace-client.mjs +647 -0
- package/lib/host-packages/cursor/openplanr/runtime/design/scripts/design-entities.mjs +17 -67
- package/lib/host-packages/cursor/openplanr/runtime/design/scripts/design-handoff.mjs +14 -16
- package/lib/host-packages/{openai/openplanr/runtime/design/scripts/design-planr-home.mjs → cursor/openplanr/runtime/design/scripts/design-loopback-server.mjs} +15 -36
- package/lib/host-packages/cursor/openplanr/runtime/design/scripts/design-owner-custody.mjs +121 -0
- package/lib/host-packages/cursor/openplanr/runtime/design/scripts/design-resource-pack.mjs +698 -0
- package/lib/host-packages/cursor/openplanr/runtime/design/scripts/design-review.mjs +798 -796
- package/lib/host-packages/cursor/openplanr/runtime/design/scripts/design-runtime-home.mjs +39 -0
- package/lib/host-packages/cursor/openplanr/runtime/design/scripts/design-runtime-support-5cb7531bede8.mjs +24 -0
- package/lib/host-packages/cursor/openplanr/runtime/design/scripts/design-share.mjs +138 -765
- package/lib/host-packages/{openai/openplanr/runtime/design/scripts/design-escape.mjs → cursor/openplanr/runtime/design/scripts/design-shared-artifact-support-dependencies-design-support-protocol-contracts-3d9b1a86.mjs} +158 -156
- package/lib/host-packages/{claude/openplanr/runtime/design/scripts/design-artifact-sources.mjs → cursor/openplanr/runtime/design/scripts/design-shared-artifact-support-protocol-contracts-ea2cd15e.mjs} +1 -1
- package/lib/host-packages/cursor/openplanr/runtime/design/scripts/{design-bounded-json-data.mjs → design-shared-protocol-contracts-31a760fc.mjs} +241 -454
- package/lib/host-packages/cursor/openplanr/runtime/design/scripts/design-shared-protocol-contracts-75a938cc.mjs +222 -0
- package/lib/host-packages/cursor/openplanr/runtime/design/scripts/design-upload-spool.mjs +198 -0
- package/lib/host-packages/cursor/openplanr/runtime/design/scripts/design.mjs +12 -5
- package/lib/host-packages/cursor/openplanr/runtime/design/scripts/runtime/packages/artifact/lib/artifact/ui/studio-shell.css +1 -1
- package/lib/host-packages/cursor/openplanr/runtime/design/scripts/runtime/packages/artifact/templates/artifact-review-stage.js.sources/01-module-runtime.js +18 -0
- package/lib/host-packages/cursor/openplanr/runtime/design/scripts/runtime/packages/artifact/templates/artifact-review-stage.js.sources/02-artifact-review-ui.js +809 -0
- package/lib/host-packages/cursor/openplanr/runtime/design/scripts/runtime/packages/artifact/templates/artifact-review-stage.js.sources/03-protocol-contracts.js +5193 -0
- package/lib/host-packages/cursor/openplanr/runtime/design/scripts/runtime/packages/artifact/templates/artifact-review-stage.js.sources/04-compression.js +4175 -0
- package/lib/host-packages/cursor/openplanr/runtime/design/scripts/runtime/packages/artifact/templates/artifact-review-stage.js.sources/05-artifact-sharing.js +65 -0
- package/lib/host-packages/cursor/openplanr/runtime/design/scripts/runtime/packages/artifact/templates/artifact-review-stage.js.sources/06-artifact-review-ui.js +1755 -0
- package/lib/host-packages/cursor/openplanr/runtime/design/scripts/runtime/packages/artifact/templates/artifact-review-stage.js.sources/07-artifact-sharing.js +62 -0
- package/lib/host-packages/cursor/openplanr/runtime/design/scripts/runtime/packages/artifact/templates/artifact-review-stage.js.sources/08-artifact-review-ui.js +2600 -0
- package/lib/host-packages/cursor/openplanr/runtime/design/scripts/runtime/packages/artifact/templates/artifact-review-stage.js.sources.json +123 -0
- package/lib/host-packages/cursor/openplanr/runtime/design/scripts/runtime/packages/design/package.json +3 -3
- package/lib/host-packages/cursor/openplanr/runtime/design/scripts/runtime/packages/design/templates/studio/studio.css +3 -3
- package/lib/host-packages/cursor/openplanr/runtime/design/scripts/runtime/packages/design/templates/studio/studio.js.sources/01-module-runtime.js +34 -0
- package/lib/host-packages/cursor/openplanr/runtime/design/scripts/runtime/packages/design/templates/studio/studio.js.sources/02-react.js +992 -0
- package/lib/host-packages/cursor/openplanr/runtime/design/scripts/runtime/packages/design/templates/studio/studio.js.sources/03-react-dom-client-fiber-foundations.js +2513 -0
- package/lib/host-packages/cursor/openplanr/runtime/design/scripts/runtime/packages/design/templates/studio/studio.js.sources/04-react-dom-client-reconciliation-hooks.js +1919 -0
- package/lib/host-packages/cursor/openplanr/runtime/design/scripts/runtime/packages/design/templates/studio/studio.js.sources/05-react-dom-client-render-phases.js +1759 -0
- package/lib/host-packages/cursor/openplanr/runtime/design/scripts/runtime/packages/design/templates/studio/studio.js.sources/06-react-dom-client-commit-effects.js +1551 -0
- package/lib/host-packages/cursor/openplanr/runtime/design/scripts/runtime/packages/design/templates/studio/studio.js.sources/07-react-dom-client-work-loop-scheduling.js +1121 -0
- package/lib/host-packages/cursor/openplanr/runtime/design/scripts/runtime/packages/design/templates/studio/studio.js.sources/08-react-dom-client-dom-events-properties-hydration.js +2183 -0
- package/lib/host-packages/cursor/openplanr/runtime/design/scripts/runtime/packages/design/templates/studio/studio.js.sources/09-react-dom-client-public-roots.js +705 -0
- package/lib/host-packages/cursor/openplanr/runtime/design/scripts/runtime/packages/design/templates/studio/studio.js.sources/10-react.js +27 -0
- package/lib/host-packages/cursor/openplanr/runtime/design/scripts/runtime/packages/design/templates/studio/studio.js.sources/11-artifact-review-ui.js +13 -0
- package/lib/host-packages/cursor/openplanr/runtime/design/scripts/runtime/packages/design/templates/studio/studio.js.sources/12-design-review-experience.js +3561 -0
- package/lib/host-packages/cursor/openplanr/runtime/design/scripts/runtime/packages/design/templates/studio/studio.js.sources/13-studio-shell.js +184 -0
- package/lib/host-packages/cursor/openplanr/runtime/design/scripts/runtime/packages/design/templates/studio/studio.js.sources/14-interface-primitives.js +5045 -0
- package/lib/host-packages/cursor/openplanr/runtime/design/scripts/runtime/packages/design/templates/studio/studio.js.sources/15-interface-primitives.js +1698 -0
- package/lib/host-packages/cursor/openplanr/runtime/design/scripts/runtime/packages/design/templates/studio/studio.js.sources/16-studio-shell.js +455 -0
- package/lib/host-packages/cursor/openplanr/runtime/design/scripts/runtime/packages/design/templates/studio/studio.js.sources/17-design-studio.js +1844 -0
- package/lib/host-packages/cursor/openplanr/runtime/design/scripts/runtime/packages/design/templates/studio/studio.js.sources.json +257 -0
- package/lib/host-packages/cursor/openplanr/runtime/design/scripts/runtime/packages/protocol/package.json +1 -1
- package/lib/host-packages/cursor/openplanr/runtime/plan/scripts/design-protocol-contracts.mjs +4966 -0
- package/lib/host-packages/cursor/openplanr/runtime/plan/scripts/design.mjs +240 -5192
- package/lib/host-packages/cursor/openplanr/runtime/plan/scripts/runtime/packages/protocol/package.json +1 -1
- package/lib/host-packages/openai/.claude-plugin/marketplace.json +2 -2
- package/lib/host-packages/openai/openplanr/.codex-plugin/plugin.json +1 -1
- package/lib/host-packages/openai/openplanr/.openplanr-content.json +166 -34
- package/lib/host-packages/openai/openplanr/runtime/design/scripts/design-artifact-shell.mjs +3 -11
- package/lib/host-packages/openai/openplanr/runtime/design/scripts/design-chunked-workspace-client.mjs +555 -0
- package/lib/host-packages/{cursor/openplanr/runtime/design/scripts/design-pako-esm.mjs → openai/openplanr/runtime/design/scripts/design-compression.mjs} +2 -1521
- package/lib/host-packages/openai/openplanr/runtime/design/scripts/design-document.mjs +133 -12
- package/lib/host-packages/openai/openplanr/runtime/design/scripts/design-encrypted-workspace-client.mjs +647 -0
- package/lib/host-packages/openai/openplanr/runtime/design/scripts/design-entities.mjs +17 -67
- package/lib/host-packages/openai/openplanr/runtime/design/scripts/design-handoff.mjs +14 -16
- package/lib/host-packages/{cursor/openplanr/runtime/design/scripts/design-planr-home.mjs → openai/openplanr/runtime/design/scripts/design-loopback-server.mjs} +15 -36
- package/lib/host-packages/openai/openplanr/runtime/design/scripts/design-owner-custody.mjs +121 -0
- package/lib/host-packages/openai/openplanr/runtime/design/scripts/design-resource-pack.mjs +698 -0
- package/lib/host-packages/openai/openplanr/runtime/design/scripts/design-review.mjs +798 -796
- package/lib/host-packages/openai/openplanr/runtime/design/scripts/design-runtime-home.mjs +39 -0
- package/lib/host-packages/openai/openplanr/runtime/design/scripts/design-runtime-support-5cb7531bede8.mjs +24 -0
- package/lib/host-packages/openai/openplanr/runtime/design/scripts/design-share.mjs +138 -765
- package/lib/host-packages/{cursor/openplanr/runtime/design/scripts/design-escape.mjs → openai/openplanr/runtime/design/scripts/design-shared-artifact-support-dependencies-design-support-protocol-contracts-3d9b1a86.mjs} +158 -156
- package/lib/host-packages/{cursor/openplanr/runtime/design/scripts/design-artifact-sources.mjs → openai/openplanr/runtime/design/scripts/design-shared-artifact-support-protocol-contracts-ea2cd15e.mjs} +1 -1
- package/lib/host-packages/{claude/openplanr/runtime/design/scripts/design-bounded-json-data.mjs → openai/openplanr/runtime/design/scripts/design-shared-protocol-contracts-31a760fc.mjs} +241 -454
- package/lib/host-packages/openai/openplanr/runtime/design/scripts/design-shared-protocol-contracts-75a938cc.mjs +222 -0
- package/lib/host-packages/openai/openplanr/runtime/design/scripts/design-upload-spool.mjs +198 -0
- package/lib/host-packages/openai/openplanr/runtime/design/scripts/design.mjs +12 -5
- package/lib/host-packages/openai/openplanr/runtime/design/scripts/runtime/packages/artifact/lib/artifact/ui/studio-shell.css +1 -1
- package/lib/host-packages/openai/openplanr/runtime/design/scripts/runtime/packages/artifact/templates/artifact-review-stage.js.sources/01-module-runtime.js +18 -0
- package/lib/host-packages/openai/openplanr/runtime/design/scripts/runtime/packages/artifact/templates/artifact-review-stage.js.sources/02-artifact-review-ui.js +809 -0
- package/lib/host-packages/openai/openplanr/runtime/design/scripts/runtime/packages/artifact/templates/artifact-review-stage.js.sources/03-protocol-contracts.js +5193 -0
- package/lib/host-packages/openai/openplanr/runtime/design/scripts/runtime/packages/artifact/templates/artifact-review-stage.js.sources/04-compression.js +4175 -0
- package/lib/host-packages/openai/openplanr/runtime/design/scripts/runtime/packages/artifact/templates/artifact-review-stage.js.sources/05-artifact-sharing.js +65 -0
- package/lib/host-packages/openai/openplanr/runtime/design/scripts/runtime/packages/artifact/templates/artifact-review-stage.js.sources/06-artifact-review-ui.js +1755 -0
- package/lib/host-packages/openai/openplanr/runtime/design/scripts/runtime/packages/artifact/templates/artifact-review-stage.js.sources/07-artifact-sharing.js +62 -0
- package/lib/host-packages/openai/openplanr/runtime/design/scripts/runtime/packages/artifact/templates/artifact-review-stage.js.sources/08-artifact-review-ui.js +2600 -0
- package/lib/host-packages/openai/openplanr/runtime/design/scripts/runtime/packages/artifact/templates/artifact-review-stage.js.sources.json +123 -0
- package/lib/host-packages/openai/openplanr/runtime/design/scripts/runtime/packages/design/package.json +3 -3
- package/lib/host-packages/openai/openplanr/runtime/design/scripts/runtime/packages/design/templates/studio/studio.css +3 -3
- package/lib/host-packages/openai/openplanr/runtime/design/scripts/runtime/packages/design/templates/studio/studio.js.sources/01-module-runtime.js +34 -0
- package/lib/host-packages/openai/openplanr/runtime/design/scripts/runtime/packages/design/templates/studio/studio.js.sources/02-react.js +992 -0
- package/lib/host-packages/openai/openplanr/runtime/design/scripts/runtime/packages/design/templates/studio/studio.js.sources/03-react-dom-client-fiber-foundations.js +2513 -0
- package/lib/host-packages/openai/openplanr/runtime/design/scripts/runtime/packages/design/templates/studio/studio.js.sources/04-react-dom-client-reconciliation-hooks.js +1919 -0
- package/lib/host-packages/openai/openplanr/runtime/design/scripts/runtime/packages/design/templates/studio/studio.js.sources/05-react-dom-client-render-phases.js +1759 -0
- package/lib/host-packages/openai/openplanr/runtime/design/scripts/runtime/packages/design/templates/studio/studio.js.sources/06-react-dom-client-commit-effects.js +1551 -0
- package/lib/host-packages/openai/openplanr/runtime/design/scripts/runtime/packages/design/templates/studio/studio.js.sources/07-react-dom-client-work-loop-scheduling.js +1121 -0
- package/lib/host-packages/openai/openplanr/runtime/design/scripts/runtime/packages/design/templates/studio/studio.js.sources/08-react-dom-client-dom-events-properties-hydration.js +2183 -0
- package/lib/host-packages/openai/openplanr/runtime/design/scripts/runtime/packages/design/templates/studio/studio.js.sources/09-react-dom-client-public-roots.js +705 -0
- package/lib/host-packages/openai/openplanr/runtime/design/scripts/runtime/packages/design/templates/studio/studio.js.sources/10-react.js +27 -0
- package/lib/host-packages/openai/openplanr/runtime/design/scripts/runtime/packages/design/templates/studio/studio.js.sources/11-artifact-review-ui.js +13 -0
- package/lib/host-packages/openai/openplanr/runtime/design/scripts/runtime/packages/design/templates/studio/studio.js.sources/12-design-review-experience.js +3561 -0
- package/lib/host-packages/openai/openplanr/runtime/design/scripts/runtime/packages/design/templates/studio/studio.js.sources/13-studio-shell.js +184 -0
- package/lib/host-packages/openai/openplanr/runtime/design/scripts/runtime/packages/design/templates/studio/studio.js.sources/14-interface-primitives.js +5045 -0
- package/lib/host-packages/openai/openplanr/runtime/design/scripts/runtime/packages/design/templates/studio/studio.js.sources/15-interface-primitives.js +1698 -0
- package/lib/host-packages/openai/openplanr/runtime/design/scripts/runtime/packages/design/templates/studio/studio.js.sources/16-studio-shell.js +455 -0
- package/lib/host-packages/openai/openplanr/runtime/design/scripts/runtime/packages/design/templates/studio/studio.js.sources/17-design-studio.js +1844 -0
- package/lib/host-packages/openai/openplanr/runtime/design/scripts/runtime/packages/design/templates/studio/studio.js.sources.json +257 -0
- package/lib/host-packages/openai/openplanr/runtime/design/scripts/runtime/packages/protocol/package.json +1 -1
- package/lib/host-packages/openai/openplanr/runtime/plan/scripts/design-protocol-contracts.mjs +4966 -0
- package/lib/host-packages/openai/openplanr/runtime/plan/scripts/design.mjs +240 -5192
- package/lib/host-packages/openai/openplanr/runtime/plan/scripts/runtime/packages/protocol/package.json +1 -1
- package/lib/host-packages/openai/openplanr/skills/delegate/scripts/backend-diagnostics.mjs +6 -2
- package/package.json +2 -2
- package/lib/host-packages/claude/openplanr/runtime/design/scripts/design-generated-b0798298053515fd.mjs +0 -18
- package/lib/host-packages/claude/openplanr/runtime/design/scripts/runtime/packages/artifact/templates/artifact-review-stage.js +0 -5
- package/lib/host-packages/claude/openplanr/runtime/design/scripts/runtime/packages/design/templates/studio/studio.js +0 -25597
- package/lib/host-packages/cursor/openplanr/runtime/design/scripts/design-generated-b0798298053515fd.mjs +0 -18
- package/lib/host-packages/cursor/openplanr/runtime/design/scripts/runtime/packages/artifact/templates/artifact-review-stage.js +0 -5
- package/lib/host-packages/cursor/openplanr/runtime/design/scripts/runtime/packages/design/templates/studio/studio.js +0 -25597
- package/lib/host-packages/openai/openplanr/runtime/design/scripts/design-generated-b0798298053515fd.mjs +0 -18
- package/lib/host-packages/openai/openplanr/runtime/design/scripts/runtime/packages/artifact/templates/artifact-review-stage.js +0 -5
- package/lib/host-packages/openai/openplanr/runtime/design/scripts/runtime/packages/design/templates/studio/studio.js +0 -25597
|
@@ -0,0 +1,1755 @@
|
|
|
1
|
+
|
|
2
|
+
|
|
3
|
+
// lib/artifact/ui/review-outbox.mjs
|
|
4
|
+
function createReviewOutbox({
|
|
5
|
+
scope,
|
|
6
|
+
indexedDB = globalThis.indexedDB,
|
|
7
|
+
databaseName = "openplanr-review-outbox-v1",
|
|
8
|
+
leaseMs = 3e4,
|
|
9
|
+
now = Date.now,
|
|
10
|
+
validatePayload = (payload) => payload !== null && typeof payload === "object"
|
|
11
|
+
}) {
|
|
12
|
+
for (const value of [scope.workspaceId, scope.revisionId, scope.actorId])
|
|
13
|
+
if (typeof value !== "string" || !value || value.length > 512)
|
|
14
|
+
throw new TypeError("Review custody requires workspace, revision and actor identities.");
|
|
15
|
+
if (!Number.isFinite(leaseMs) || leaseMs < 300)
|
|
16
|
+
throw new TypeError("A review lease must be at least 300ms.");
|
|
17
|
+
const scopeKey = JSON.stringify([scope.workspaceId, scope.revisionId, scope.actorId]);
|
|
18
|
+
const key = (id7) => {
|
|
19
|
+
if (typeof id7 !== "string" || !id7 || id7.length > 512)
|
|
20
|
+
throw new TypeError("A review operation needs a stable identity.");
|
|
21
|
+
return JSON.stringify([scopeKey, id7]);
|
|
22
|
+
};
|
|
23
|
+
let closed6 = false, draining = null;
|
|
24
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
25
|
+
const heartbeats = /* @__PURE__ */ new Set();
|
|
26
|
+
const channel = typeof globalThis.BroadcastChannel === "function" ? new globalThis.BroadcastChannel(databaseName) : null;
|
|
27
|
+
const notify = () => {
|
|
28
|
+
listeners.forEach((listener) => {
|
|
29
|
+
listener();
|
|
30
|
+
});
|
|
31
|
+
channel?.postMessage(scopeKey);
|
|
32
|
+
};
|
|
33
|
+
if (channel)
|
|
34
|
+
channel.onmessage = (event) => {
|
|
35
|
+
if (event.data === scopeKey)
|
|
36
|
+
listeners.forEach((listener) => {
|
|
37
|
+
listener();
|
|
38
|
+
});
|
|
39
|
+
};
|
|
40
|
+
const database = new Promise((resolve, reject) => {
|
|
41
|
+
if (!indexedDB) {
|
|
42
|
+
reject(
|
|
43
|
+
new Error(
|
|
44
|
+
"Durable review storage is unavailable. Enable browser storage before sending feedback."
|
|
45
|
+
)
|
|
46
|
+
);
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
const request = indexedDB.open(databaseName, 1);
|
|
50
|
+
request.onupgradeneeded = () => {
|
|
51
|
+
const store = request.result.createObjectStore("operations", { keyPath: "key" });
|
|
52
|
+
store.createIndex("scope", "scopeKey", { unique: false });
|
|
53
|
+
};
|
|
54
|
+
request.onerror = () => reject(request.error ?? new Error("Durable review storage could not open."));
|
|
55
|
+
request.onblocked = () => reject(new Error("Close older review tabs to open durable review storage."));
|
|
56
|
+
request.onsuccess = () => {
|
|
57
|
+
const db = request.result;
|
|
58
|
+
db.onversionchange = () => db.close();
|
|
59
|
+
if (closed6) db.close();
|
|
60
|
+
resolve(db);
|
|
61
|
+
};
|
|
62
|
+
});
|
|
63
|
+
void database.catch(() => {
|
|
64
|
+
});
|
|
65
|
+
async function transaction(mode, work) {
|
|
66
|
+
if (closed6) throw new Error("Review custody has closed.");
|
|
67
|
+
const db = await database;
|
|
68
|
+
return new Promise((resolve, reject) => {
|
|
69
|
+
const tx = db.transaction("operations", mode), store = tx.objectStore("operations");
|
|
70
|
+
let value;
|
|
71
|
+
tx.oncomplete = () => resolve(value);
|
|
72
|
+
tx.onerror = () => reject(tx.error ?? new Error("Review storage could not commit."));
|
|
73
|
+
tx.onabort = () => reject(tx.error ?? new Error("Review storage did not commit."));
|
|
74
|
+
const guard = (callback) => () => {
|
|
75
|
+
try {
|
|
76
|
+
callback();
|
|
77
|
+
} catch (error) {
|
|
78
|
+
try {
|
|
79
|
+
tx.abort();
|
|
80
|
+
} catch {
|
|
81
|
+
}
|
|
82
|
+
reject(error);
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
guard(
|
|
86
|
+
() => work(
|
|
87
|
+
store,
|
|
88
|
+
(next) => {
|
|
89
|
+
value = next;
|
|
90
|
+
},
|
|
91
|
+
guard
|
|
92
|
+
)
|
|
93
|
+
)();
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
const publicEntry = ({
|
|
97
|
+
key: _key,
|
|
98
|
+
scopeKey: _scope,
|
|
99
|
+
leaseOwner: _owner,
|
|
100
|
+
leaseUntil: _until,
|
|
101
|
+
custodyInvalid: _invalid,
|
|
102
|
+
invalidRecord: _record,
|
|
103
|
+
...entry
|
|
104
|
+
}) => entry;
|
|
105
|
+
function payloadValid(payload) {
|
|
106
|
+
try {
|
|
107
|
+
return validatePayload(payload);
|
|
108
|
+
} catch {
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
function keyedIdentity(primaryKey) {
|
|
113
|
+
if (typeof primaryKey !== "string") return null;
|
|
114
|
+
try {
|
|
115
|
+
const value = JSON.parse(primaryKey);
|
|
116
|
+
return Array.isArray(value) && value.length === 2 && value[0] === scopeKey && typeof value[1] === "string" && value[1].length > 0 && value[1].length <= 512 ? value[1] : null;
|
|
117
|
+
} catch {
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
function storedEntry(raw, primaryKey, store) {
|
|
122
|
+
const record2 = raw !== null && typeof raw === "object" ? raw : null;
|
|
123
|
+
const fromKey = keyedIdentity(primaryKey);
|
|
124
|
+
if (record2?.scopeKey !== scopeKey && !fromKey) return null;
|
|
125
|
+
const allowed = /* @__PURE__ */ new Set([
|
|
126
|
+
"key",
|
|
127
|
+
"scopeKey",
|
|
128
|
+
"operationId",
|
|
129
|
+
"payload",
|
|
130
|
+
"status",
|
|
131
|
+
"attempts",
|
|
132
|
+
"createdAt",
|
|
133
|
+
"nextAttemptAt",
|
|
134
|
+
"errorCode",
|
|
135
|
+
"leaseOwner",
|
|
136
|
+
"leaseUntil",
|
|
137
|
+
"custodyInvalid",
|
|
138
|
+
"invalidRecord"
|
|
139
|
+
]);
|
|
140
|
+
const finiteTime = (value) => typeof value === "number" && Number.isFinite(value) && value >= 0;
|
|
141
|
+
const integer3 = (value) => typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
|
|
142
|
+
if (record2 && Object.keys(record2).every((name) => allowed.has(name)) && fromKey !== null && fromKey === record2.operationId && record2.key === primaryKey && record2.scopeKey === scopeKey && Object.hasOwn(record2, "payload") && ["pending", "quarantined"].includes(String(record2.status)) && integer3(record2.attempts) && finiteTime(record2.createdAt) && finiteTime(record2.nextAttemptAt) && (record2.errorCode === void 0 || typeof record2.errorCode === "string" && record2.errorCode.length <= 120) && (record2.leaseOwner === void 0 && record2.leaseUntil === void 0 || typeof record2.leaseOwner === "string" && record2.leaseOwner.length > 0 && record2.leaseOwner.length <= 128 && finiteTime(record2.leaseUntil)) && (record2.custodyInvalid === true ? record2.status === "quarantined" && record2.errorCode === "malformed_custody" : record2.custodyInvalid === void 0 && record2.invalidRecord === void 0 && payloadValid(record2.payload)))
|
|
143
|
+
return record2;
|
|
144
|
+
const operationId = fromKey ?? `invalid-${globalThis.crypto.randomUUID()}`;
|
|
145
|
+
const repaired = {
|
|
146
|
+
key: key(operationId),
|
|
147
|
+
scopeKey,
|
|
148
|
+
operationId,
|
|
149
|
+
payload: record2 && Object.hasOwn(record2, "payload") ? record2.payload : null,
|
|
150
|
+
status: "quarantined",
|
|
151
|
+
attempts: 0,
|
|
152
|
+
createdAt: 0,
|
|
153
|
+
nextAttemptAt: 0,
|
|
154
|
+
errorCode: "malformed_custody",
|
|
155
|
+
custodyInvalid: true,
|
|
156
|
+
invalidRecord: raw
|
|
157
|
+
};
|
|
158
|
+
store.put(repaired);
|
|
159
|
+
if (primaryKey !== repaired.key) store.delete(primaryKey);
|
|
160
|
+
return repaired;
|
|
161
|
+
}
|
|
162
|
+
function readScoped(store, result, guard) {
|
|
163
|
+
const entries = [];
|
|
164
|
+
const seen = /* @__PURE__ */ new Set();
|
|
165
|
+
const request = store.openCursor();
|
|
166
|
+
request.onsuccess = guard(() => {
|
|
167
|
+
const cursor = request.result;
|
|
168
|
+
if (!cursor) {
|
|
169
|
+
result(entries);
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
const entry = storedEntry(cursor.value, cursor.primaryKey, store);
|
|
173
|
+
if (entry && !seen.has(entry.key)) {
|
|
174
|
+
seen.add(entry.key);
|
|
175
|
+
entries.push(entry);
|
|
176
|
+
}
|
|
177
|
+
cursor.continue();
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
async function mutate(id7, update) {
|
|
181
|
+
await transaction("readwrite", (store, _result, guard) => {
|
|
182
|
+
const operationKey = key(id7);
|
|
183
|
+
const request = store.get(operationKey);
|
|
184
|
+
request.onsuccess = guard(() => {
|
|
185
|
+
if (request.result === void 0) return;
|
|
186
|
+
const entry = storedEntry(request.result, operationKey, store);
|
|
187
|
+
if (entry) update(entry, store);
|
|
188
|
+
});
|
|
189
|
+
});
|
|
190
|
+
notify();
|
|
191
|
+
}
|
|
192
|
+
async function claim(owner) {
|
|
193
|
+
return transaction("readwrite", (store, result, guard) => {
|
|
194
|
+
readScoped(
|
|
195
|
+
store,
|
|
196
|
+
(entries) => {
|
|
197
|
+
const time = now();
|
|
198
|
+
const next = entries.filter(
|
|
199
|
+
(entry) => entry.status === "pending" && entry.nextAttemptAt <= time && (!entry.leaseUntil || entry.leaseUntil <= time)
|
|
200
|
+
).sort(
|
|
201
|
+
(a, b) => a.createdAt - b.createdAt || a.operationId.localeCompare(b.operationId)
|
|
202
|
+
)[0];
|
|
203
|
+
if (!next) {
|
|
204
|
+
result(null);
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
next.leaseOwner = owner;
|
|
208
|
+
next.leaseUntil = time + leaseMs;
|
|
209
|
+
store.put(next);
|
|
210
|
+
result(next);
|
|
211
|
+
},
|
|
212
|
+
guard
|
|
213
|
+
);
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
return {
|
|
217
|
+
async enqueue({ operationId, payload }) {
|
|
218
|
+
const immutable = structuredClone(payload), operationKey = key(operationId);
|
|
219
|
+
if (!payloadValid(immutable)) throw new TypeError("Prepared review custody is invalid.");
|
|
220
|
+
await transaction("readwrite", (store, _result, guard) => {
|
|
221
|
+
const request = store.get(operationKey);
|
|
222
|
+
request.onsuccess = guard(() => {
|
|
223
|
+
if (request.result !== void 0) {
|
|
224
|
+
storedEntry(request.result, operationKey, store);
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
store.add({
|
|
228
|
+
key: operationKey,
|
|
229
|
+
scopeKey,
|
|
230
|
+
operationId,
|
|
231
|
+
payload: immutable,
|
|
232
|
+
status: "pending",
|
|
233
|
+
attempts: 0,
|
|
234
|
+
createdAt: now(),
|
|
235
|
+
nextAttemptAt: 0
|
|
236
|
+
});
|
|
237
|
+
});
|
|
238
|
+
});
|
|
239
|
+
notify();
|
|
240
|
+
},
|
|
241
|
+
async list() {
|
|
242
|
+
return transaction("readwrite", (store, result, guard) => {
|
|
243
|
+
readScoped(
|
|
244
|
+
store,
|
|
245
|
+
(entries) => result(
|
|
246
|
+
entries.sort(
|
|
247
|
+
(a, b) => a.createdAt - b.createdAt || a.operationId.localeCompare(b.operationId)
|
|
248
|
+
).map(publicEntry)
|
|
249
|
+
),
|
|
250
|
+
guard
|
|
251
|
+
);
|
|
252
|
+
});
|
|
253
|
+
},
|
|
254
|
+
async retry(id7) {
|
|
255
|
+
await mutate(id7, (entry, store) => {
|
|
256
|
+
if (entry.custodyInvalid || entry.leaseUntil && entry.leaseUntil > now()) return;
|
|
257
|
+
entry.status = "pending";
|
|
258
|
+
entry.nextAttemptAt = 0;
|
|
259
|
+
delete entry.errorCode;
|
|
260
|
+
store.put(entry);
|
|
261
|
+
});
|
|
262
|
+
},
|
|
263
|
+
async quarantine(id7, code) {
|
|
264
|
+
await mutate(id7, (entry, store) => {
|
|
265
|
+
if (entry.leaseUntil && entry.leaseUntil > now()) return;
|
|
266
|
+
entry.status = "quarantined";
|
|
267
|
+
entry.nextAttemptAt = 0;
|
|
268
|
+
entry.errorCode = entry.custodyInvalid ? "malformed_custody" : String(code).slice(0, 120);
|
|
269
|
+
store.put(entry);
|
|
270
|
+
});
|
|
271
|
+
},
|
|
272
|
+
async remove(id7) {
|
|
273
|
+
await mutate(id7, (entry, store) => {
|
|
274
|
+
if (!entry.leaseUntil || entry.leaseUntil <= now()) store.delete(entry.key);
|
|
275
|
+
});
|
|
276
|
+
},
|
|
277
|
+
drain(send) {
|
|
278
|
+
if (draining) return draining;
|
|
279
|
+
const owner = globalThis.crypto.randomUUID();
|
|
280
|
+
draining = (async () => {
|
|
281
|
+
while (!closed6) {
|
|
282
|
+
const entry = await claim(owner);
|
|
283
|
+
if (!entry) return;
|
|
284
|
+
const heartbeat = globalThis.setInterval(
|
|
285
|
+
() => {
|
|
286
|
+
void mutate(entry.operationId, (current, store) => {
|
|
287
|
+
if (current.leaseOwner === owner) {
|
|
288
|
+
current.leaseUntil = now() + leaseMs;
|
|
289
|
+
store.put(current);
|
|
290
|
+
}
|
|
291
|
+
}).catch(() => {
|
|
292
|
+
});
|
|
293
|
+
},
|
|
294
|
+
Math.floor(leaseMs / 3)
|
|
295
|
+
);
|
|
296
|
+
heartbeats.add(heartbeat);
|
|
297
|
+
let outcome;
|
|
298
|
+
try {
|
|
299
|
+
outcome = await send(publicEntry(entry));
|
|
300
|
+
} catch {
|
|
301
|
+
outcome = { status: "retry", code: "network" };
|
|
302
|
+
} finally {
|
|
303
|
+
globalThis.clearInterval(heartbeat);
|
|
304
|
+
heartbeats.delete(heartbeat);
|
|
305
|
+
}
|
|
306
|
+
if (!outcome || !["delivered", "retry", "rejected"].includes(outcome.status))
|
|
307
|
+
outcome = { status: "retry", code: "invalid-delivery-result" };
|
|
308
|
+
await mutate(entry.operationId, (current, store) => {
|
|
309
|
+
if (current.leaseOwner !== owner) return;
|
|
310
|
+
if (outcome.status === "delivered") {
|
|
311
|
+
store.delete(current.key);
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
delete current.leaseOwner;
|
|
315
|
+
delete current.leaseUntil;
|
|
316
|
+
current.attempts += 1;
|
|
317
|
+
current.errorCode = typeof outcome.code === "string" ? outcome.code.slice(0, 120) : outcome.status;
|
|
318
|
+
current.status = outcome.status === "rejected" ? "quarantined" : "pending";
|
|
319
|
+
const requestedDelay = Number.isFinite(outcome.retryAfterMs) ? Math.max(0, Math.min(36e5, outcome.retryAfterMs)) : 0;
|
|
320
|
+
current.nextAttemptAt = outcome.status === "retry" ? now() + Math.max(
|
|
321
|
+
requestedDelay,
|
|
322
|
+
Math.min(6e4, 1e3 * 2 ** Math.min(current.attempts - 1, 6))
|
|
323
|
+
) : 0;
|
|
324
|
+
store.put(current);
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
})().finally(() => {
|
|
328
|
+
draining = null;
|
|
329
|
+
});
|
|
330
|
+
return draining;
|
|
331
|
+
},
|
|
332
|
+
subscribe(listener) {
|
|
333
|
+
listeners.add(listener);
|
|
334
|
+
return () => listeners.delete(listener);
|
|
335
|
+
},
|
|
336
|
+
close() {
|
|
337
|
+
closed6 = true;
|
|
338
|
+
for (const heartbeat of heartbeats) globalThis.clearInterval(heartbeat);
|
|
339
|
+
heartbeats.clear();
|
|
340
|
+
channel?.close();
|
|
341
|
+
listeners.clear();
|
|
342
|
+
void database.then((db) => db.close()).catch(() => {
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// lib/artifact/ui/durable-paste-share.mjs
|
|
349
|
+
var retainedPreparation = /* @__PURE__ */ Symbol("private-paste-preparation-retained");
|
|
350
|
+
function preparation(value) {
|
|
351
|
+
assertPreparedArtifactPaste(value);
|
|
352
|
+
return value;
|
|
353
|
+
}
|
|
354
|
+
function receipt(prepared, value) {
|
|
355
|
+
const url = new URL(`/p/${prepared.body.id}`, prepared.origin);
|
|
356
|
+
url.hash = `k=${prepared.key}&v=2`;
|
|
357
|
+
if (!value || value.url !== url.toString() || value.deletionToken !== prepared.custodyToken || typeof value.expiresAt !== "string" || !Number.isFinite(Date.parse(value.expiresAt)))
|
|
358
|
+
throw new Error("Paste receipt differs from saved private custody.");
|
|
359
|
+
return value;
|
|
360
|
+
}
|
|
361
|
+
function createDurablePasteShare({
|
|
362
|
+
scope,
|
|
363
|
+
create,
|
|
364
|
+
commit,
|
|
365
|
+
indexedDB = globalThis.indexedDB,
|
|
366
|
+
databaseName = "openplanr-paste-custody-v1"
|
|
367
|
+
}) {
|
|
368
|
+
const queues = /* @__PURE__ */ new Map();
|
|
369
|
+
function queueFor(request) {
|
|
370
|
+
const base = scope(), revisionId2 = `${base.revisionId}:${request.ttl ?? "7d"}`, identity = JSON.stringify({ ...base, revisionId: revisionId2 });
|
|
371
|
+
let queue = queues.get(identity);
|
|
372
|
+
if (!queue) {
|
|
373
|
+
queue = createReviewOutbox({ scope: { ...base, revisionId: revisionId2 }, indexedDB, databaseName });
|
|
374
|
+
queues.set(identity, queue);
|
|
375
|
+
}
|
|
376
|
+
return queue;
|
|
377
|
+
}
|
|
378
|
+
const activeReceipt = (entries) => entries.map((entry) => entry.payload).find(
|
|
379
|
+
(value) => value?.kind === "paste-receipt" && typeof value.result?.expiresAt === "string" && Date.parse(value.result.expiresAt) > Date.now()
|
|
380
|
+
);
|
|
381
|
+
return {
|
|
382
|
+
async run(request) {
|
|
383
|
+
if (request.transport !== "short") return create(request);
|
|
384
|
+
const queue = queueFor(request);
|
|
385
|
+
let entries = await queue.list();
|
|
386
|
+
const prior = activeReceipt(entries);
|
|
387
|
+
if (prior) return receipt(preparation(prior.prepared), prior.result ?? null);
|
|
388
|
+
let pending = entries.find(
|
|
389
|
+
(entry) => entry.payload?.kind === "prepared-paste"
|
|
390
|
+
);
|
|
391
|
+
if (pending?.status === "quarantined")
|
|
392
|
+
throw new Error(
|
|
393
|
+
"The saved paste operation was rejected. Its private recovery custody is retained."
|
|
394
|
+
);
|
|
395
|
+
if (!pending) {
|
|
396
|
+
try {
|
|
397
|
+
await create({
|
|
398
|
+
...request,
|
|
399
|
+
onPreparedPaste: async (value) => {
|
|
400
|
+
const prepared = preparation(value);
|
|
401
|
+
if (prepared.body.ttl !== (request.ttl ?? "7d"))
|
|
402
|
+
throw new Error("Prepared paste expiry differs.");
|
|
403
|
+
await queue.enqueue({
|
|
404
|
+
operationId: prepared.body.creationId,
|
|
405
|
+
payload: { kind: "prepared-paste", prepared }
|
|
406
|
+
});
|
|
407
|
+
throw retainedPreparation;
|
|
408
|
+
}
|
|
409
|
+
});
|
|
410
|
+
} catch (error) {
|
|
411
|
+
if (error !== retainedPreparation) throw error;
|
|
412
|
+
}
|
|
413
|
+
entries = await queue.list();
|
|
414
|
+
pending = entries.find(
|
|
415
|
+
(entry) => entry.payload?.kind === "prepared-paste"
|
|
416
|
+
);
|
|
417
|
+
if (!pending)
|
|
418
|
+
throw new Error(
|
|
419
|
+
"This host did not retain paste custody before publication. Update OpenPlanr and retry."
|
|
420
|
+
);
|
|
421
|
+
}
|
|
422
|
+
await queue.retry(pending.operationId);
|
|
423
|
+
let result = null;
|
|
424
|
+
await queue.drain(async (entry) => {
|
|
425
|
+
const value = entry.payload;
|
|
426
|
+
if (value?.kind === "paste-receipt")
|
|
427
|
+
return { status: "rejected", code: "private-custody-receipt" };
|
|
428
|
+
try {
|
|
429
|
+
const prepared = preparation(value?.prepared);
|
|
430
|
+
const created = receipt(prepared, await commit(prepared));
|
|
431
|
+
const receiptId = `${prepared.body.creationId}:receipt`;
|
|
432
|
+
await queue.enqueue({
|
|
433
|
+
operationId: receiptId,
|
|
434
|
+
payload: { kind: "paste-receipt", prepared, result: created }
|
|
435
|
+
});
|
|
436
|
+
await queue.quarantine(receiptId, "private-custody-receipt");
|
|
437
|
+
result = created;
|
|
438
|
+
return { status: "delivered" };
|
|
439
|
+
} catch (error) {
|
|
440
|
+
const code = error?.code;
|
|
441
|
+
return typeof code === "string" && ["E_ARTIFACT_SHARE_NETWORK", "E_ARTIFACT_PASTE_UNAVAILABLE"].includes(code) ? { status: "retry", code } : { status: "rejected", code: "paste-rejected" };
|
|
442
|
+
}
|
|
443
|
+
});
|
|
444
|
+
if (result) return result;
|
|
445
|
+
const completed = activeReceipt(await queue.list());
|
|
446
|
+
if (completed) return receipt(preparation(completed.prepared), completed.result ?? null);
|
|
447
|
+
throw new Error(
|
|
448
|
+
"Paste publication is pending. Retry this share to resume the saved encrypted operation; another tab may be sending it."
|
|
449
|
+
);
|
|
450
|
+
},
|
|
451
|
+
close() {
|
|
452
|
+
for (const queue of queues.values()) queue.close();
|
|
453
|
+
queues.clear();
|
|
454
|
+
}
|
|
455
|
+
};
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
// lib/artifact/ui/feedback-rail.mjs
|
|
459
|
+
var ARTIFACT_REVIEW_CHANGE_EVENT = "planr:artifact-review-change";
|
|
460
|
+
var ARTIFACT_REVIEW_SELECT_EVENT = "planr:artifact-review-select";
|
|
461
|
+
var ARTIFACT_REVIEW_DRAFT_CHANGE_EVENT = "planr:artifact-review-draft-change";
|
|
462
|
+
var ARTIFACT_REVIEW_LIMITS = Object.freeze({
|
|
463
|
+
id: 128,
|
|
464
|
+
authorName: 256,
|
|
465
|
+
artifactId: 128,
|
|
466
|
+
variant: 128,
|
|
467
|
+
anchor: 512,
|
|
468
|
+
screen: 128,
|
|
469
|
+
text: 65536,
|
|
470
|
+
pins: 1e4,
|
|
471
|
+
replies: 1e4,
|
|
472
|
+
viewport: 16384
|
|
473
|
+
});
|
|
474
|
+
var ARTIFACT_REVIEW_DECISIONS = Object.freeze([
|
|
475
|
+
"pending",
|
|
476
|
+
"approved",
|
|
477
|
+
"changes_requested"
|
|
478
|
+
]);
|
|
479
|
+
var ARTIFACT_REVIEW_INTENTS = Object.freeze(["fix", "improve", "question"]);
|
|
480
|
+
var ARTIFACT_REVIEW_STATUSES = Object.freeze(["open", "addressed", "resolved"]);
|
|
481
|
+
var REVIEW_OF_RE = /^[a-f0-9]{64}$/;
|
|
482
|
+
var ArtifactReviewStateError = class extends Error {
|
|
483
|
+
constructor(code, message) {
|
|
484
|
+
super(message);
|
|
485
|
+
this.name = "ArtifactReviewStateError";
|
|
486
|
+
this.code = code;
|
|
487
|
+
}
|
|
488
|
+
};
|
|
489
|
+
function invalid(message) {
|
|
490
|
+
throw new ArtifactReviewStateError("E_ARTIFACT_REVIEW_INVALID", message);
|
|
491
|
+
}
|
|
492
|
+
function identityRequired() {
|
|
493
|
+
throw new ArtifactReviewStateError(
|
|
494
|
+
"E_ARTIFACT_REVIEW_IDENTITY_REQUIRED",
|
|
495
|
+
"Enter your name before adding a comment."
|
|
496
|
+
);
|
|
497
|
+
}
|
|
498
|
+
function clonePlain(value) {
|
|
499
|
+
if (Array.isArray(value)) return value.map(clonePlain);
|
|
500
|
+
if (!value || typeof value !== "object") return value;
|
|
501
|
+
const clone = {};
|
|
502
|
+
for (const [key, entry] of Object.entries(value)) clone[key] = clonePlain(entry);
|
|
503
|
+
return clone;
|
|
504
|
+
}
|
|
505
|
+
function cloneFrozenArtifactReview(review) {
|
|
506
|
+
return deepFreeze(clonePlain(review));
|
|
507
|
+
}
|
|
508
|
+
function boundedString(value, label, { min = 0, max, trim = false, pattern } = {}) {
|
|
509
|
+
if (typeof value !== "string") invalid(`${label} must be a string.`);
|
|
510
|
+
const normalized3 = trim ? value.trim() : value;
|
|
511
|
+
if (normalized3.length < min || max !== void 0 && normalized3.length > max) {
|
|
512
|
+
invalid(`${label} must contain ${min} through ${max ?? "unlimited"} characters.`);
|
|
513
|
+
}
|
|
514
|
+
if (pattern && !pattern.test(normalized3)) invalid(`${label} has an invalid format.`);
|
|
515
|
+
return normalized3;
|
|
516
|
+
}
|
|
517
|
+
function optionalString(value, label, options) {
|
|
518
|
+
if (value === void 0) return void 0;
|
|
519
|
+
return boundedString(value, label, options);
|
|
520
|
+
}
|
|
521
|
+
function enumValue(value, values, label) {
|
|
522
|
+
if (!values.includes(value)) invalid(`${label} must be one of: ${values.join(", ")}.`);
|
|
523
|
+
return value;
|
|
524
|
+
}
|
|
525
|
+
function isoTimestamp(value, label) {
|
|
526
|
+
const timestamp2 = value instanceof Date ? value.toISOString() : value;
|
|
527
|
+
if (typeof timestamp2 !== "string" || !/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})$/.test(timestamp2) || !Number.isFinite(Date.parse(timestamp2))) {
|
|
528
|
+
invalid(`${label} must be an ISO-8601 date-time.`);
|
|
529
|
+
}
|
|
530
|
+
return timestamp2;
|
|
531
|
+
}
|
|
532
|
+
function dependencyTimestamp(now) {
|
|
533
|
+
return isoTimestamp(now(), "now()");
|
|
534
|
+
}
|
|
535
|
+
function defaultNow() {
|
|
536
|
+
return (/* @__PURE__ */ new Date()).toISOString();
|
|
537
|
+
}
|
|
538
|
+
function createSecureArtifactReviewId(cryptoProvider = globalThis.crypto) {
|
|
539
|
+
const randomUuid = cryptoProvider?.randomUUID?.();
|
|
540
|
+
if (randomUuid) return randomUuid;
|
|
541
|
+
const bytes = new Uint8Array(16);
|
|
542
|
+
if (typeof cryptoProvider?.getRandomValues !== "function") {
|
|
543
|
+
throw new ArtifactReviewStateError(
|
|
544
|
+
"E_ARTIFACT_REVIEW_UUID_UNAVAILABLE",
|
|
545
|
+
"Secure UUID generation is unavailable in this browser."
|
|
546
|
+
);
|
|
547
|
+
}
|
|
548
|
+
cryptoProvider.getRandomValues(bytes);
|
|
549
|
+
bytes[6] = bytes[6] & 15 | 64;
|
|
550
|
+
bytes[8] = bytes[8] & 63 | 128;
|
|
551
|
+
const hex = [...bytes].map((value) => value.toString(16).padStart(2, "0")).join("");
|
|
552
|
+
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
|
|
553
|
+
}
|
|
554
|
+
function defaultCreateId() {
|
|
555
|
+
return createSecureArtifactReviewId();
|
|
556
|
+
}
|
|
557
|
+
function dependencyId(createId, kind) {
|
|
558
|
+
return boundedString(createId(kind), `${kind} id`, {
|
|
559
|
+
min: 1,
|
|
560
|
+
max: ARTIFACT_REVIEW_LIMITS.id,
|
|
561
|
+
trim: true
|
|
562
|
+
});
|
|
563
|
+
}
|
|
564
|
+
function uniqueDependencyId(createId, kind, existingIds) {
|
|
565
|
+
for (let attempt = 0; attempt < 100; attempt += 1) {
|
|
566
|
+
const candidate = dependencyId(createId, kind);
|
|
567
|
+
if (!existingIds.has(candidate)) return candidate;
|
|
568
|
+
}
|
|
569
|
+
throw new ArtifactReviewStateError(
|
|
570
|
+
"E_ARTIFACT_REVIEW_ID_COLLISION",
|
|
571
|
+
`Could not create a unique ${kind} id.`
|
|
572
|
+
);
|
|
573
|
+
}
|
|
574
|
+
function normalizeArtifactReviewIdentity(value, { allowEmpty = false } = {}) {
|
|
575
|
+
if (value === null || value === void 0 || value === "") {
|
|
576
|
+
if (allowEmpty) return null;
|
|
577
|
+
identityRequired();
|
|
578
|
+
}
|
|
579
|
+
const source = typeof value === "string" ? { name: value } : value;
|
|
580
|
+
if (!source || typeof source !== "object" || Array.isArray(source)) identityRequired();
|
|
581
|
+
const name = boundedString(source.name, "author.name", {
|
|
582
|
+
min: 1,
|
|
583
|
+
max: ARTIFACT_REVIEW_LIMITS.authorName,
|
|
584
|
+
trim: true
|
|
585
|
+
});
|
|
586
|
+
const id7 = optionalString(source.id, "author.id", {
|
|
587
|
+
min: 1,
|
|
588
|
+
max: ARTIFACT_REVIEW_LIMITS.id,
|
|
589
|
+
trim: true
|
|
590
|
+
});
|
|
591
|
+
return deepFreeze(id7 === void 0 ? { name } : { id: id7, name });
|
|
592
|
+
}
|
|
593
|
+
function normalizeRegion(region) {
|
|
594
|
+
if (!region || typeof region !== "object" || Array.isArray(region)) {
|
|
595
|
+
invalid("pin.region must be an object.");
|
|
596
|
+
}
|
|
597
|
+
const finite3 = (value, label) => {
|
|
598
|
+
if (typeof value !== "number" || !Number.isFinite(value)) invalid(`${label} must be finite.`);
|
|
599
|
+
return value;
|
|
600
|
+
};
|
|
601
|
+
const x = finite3(region.x, "pin.region.x");
|
|
602
|
+
const y = finite3(region.y, "pin.region.y");
|
|
603
|
+
const w = finite3(region.w, "pin.region.w");
|
|
604
|
+
const h = finite3(region.h, "pin.region.h");
|
|
605
|
+
if (x >= 0 && y >= 0 && w >= 0 && h >= 0 && x + w <= 1 && y + h <= 1) {
|
|
606
|
+
return { x, y, w, h };
|
|
607
|
+
}
|
|
608
|
+
const unit = (value) => Math.round(Math.min(1, Math.max(0, value)) * 1e6) / 1e6;
|
|
609
|
+
const boundedX = unit(x);
|
|
610
|
+
const boundedY = unit(y);
|
|
611
|
+
return {
|
|
612
|
+
x: boundedX,
|
|
613
|
+
y: boundedY,
|
|
614
|
+
w: Math.round(Math.min(unit(w), 1 - boundedX) * 1e6) / 1e6,
|
|
615
|
+
h: Math.round(Math.min(unit(h), 1 - boundedY) * 1e6) / 1e6
|
|
616
|
+
};
|
|
617
|
+
}
|
|
618
|
+
function normalizeViewport(viewport) {
|
|
619
|
+
if (!viewport || typeof viewport !== "object" || Array.isArray(viewport)) {
|
|
620
|
+
invalid("pin.viewport must be an object.");
|
|
621
|
+
}
|
|
622
|
+
const dimension = (value, label) => {
|
|
623
|
+
if (!Number.isInteger(value) || value < 1 || value > ARTIFACT_REVIEW_LIMITS.viewport) {
|
|
624
|
+
invalid(`${label} must be an integer from 1 through ${ARTIFACT_REVIEW_LIMITS.viewport}.`);
|
|
625
|
+
}
|
|
626
|
+
return value;
|
|
627
|
+
};
|
|
628
|
+
return {
|
|
629
|
+
width: dimension(viewport.width, "pin.viewport.width"),
|
|
630
|
+
height: dimension(viewport.height, "pin.viewport.height")
|
|
631
|
+
};
|
|
632
|
+
}
|
|
633
|
+
function normalizeAnchor(anchor2) {
|
|
634
|
+
if (anchor2 === void 0 || anchor2 === null) return void 0;
|
|
635
|
+
if (typeof anchor2 !== "object" || Array.isArray(anchor2)) invalid("pin.anchor must be an object.");
|
|
636
|
+
const planrId = boundedString(anchor2.planrId, "pin.anchor.planrId", {
|
|
637
|
+
min: 1,
|
|
638
|
+
max: ARTIFACT_REVIEW_LIMITS.anchor,
|
|
639
|
+
trim: true
|
|
640
|
+
});
|
|
641
|
+
const screen = optionalString(anchor2.screen, "pin.anchor.screen", {
|
|
642
|
+
min: 1,
|
|
643
|
+
max: ARTIFACT_REVIEW_LIMITS.screen,
|
|
644
|
+
trim: true
|
|
645
|
+
});
|
|
646
|
+
return screen === void 0 ? { planrId } : { planrId, screen };
|
|
647
|
+
}
|
|
648
|
+
function normalizeReply(reply2, label = "reply") {
|
|
649
|
+
if (!reply2 || typeof reply2 !== "object" || Array.isArray(reply2))
|
|
650
|
+
invalid(`${label} must be an object.`);
|
|
651
|
+
return {
|
|
652
|
+
id: boundedString(reply2.id, `${label}.id`, {
|
|
653
|
+
min: 1,
|
|
654
|
+
max: ARTIFACT_REVIEW_LIMITS.id,
|
|
655
|
+
trim: true
|
|
656
|
+
}),
|
|
657
|
+
author: normalizeArtifactReviewIdentity(reply2.author),
|
|
658
|
+
comment: boundedString(reply2.comment, `${label}.comment`, {
|
|
659
|
+
min: 1,
|
|
660
|
+
max: ARTIFACT_REVIEW_LIMITS.text,
|
|
661
|
+
trim: true
|
|
662
|
+
}),
|
|
663
|
+
createdAt: isoTimestamp(reply2.createdAt, `${label}.createdAt`)
|
|
664
|
+
};
|
|
665
|
+
}
|
|
666
|
+
function compareTimestampThenId(left, right) {
|
|
667
|
+
const byTime = left.createdAt.localeCompare(right.createdAt);
|
|
668
|
+
return byTime === 0 ? left.id.localeCompare(right.id) : byTime;
|
|
669
|
+
}
|
|
670
|
+
function normalizePin(pin, label = "pin") {
|
|
671
|
+
if (!pin || typeof pin !== "object" || Array.isArray(pin)) invalid(`${label} must be an object.`);
|
|
672
|
+
if (!Array.isArray(pin.replies) || pin.replies.length > ARTIFACT_REVIEW_LIMITS.replies) {
|
|
673
|
+
invalid(`${label}.replies must contain no more than ${ARTIFACT_REVIEW_LIMITS.replies} items.`);
|
|
674
|
+
}
|
|
675
|
+
const replies = pin.replies.map(
|
|
676
|
+
(reply2, index2) => normalizeReply(reply2, `${label}.replies[${index2}]`)
|
|
677
|
+
);
|
|
678
|
+
const replyIds = new Set(replies.map(({ id: id7 }) => id7));
|
|
679
|
+
if (replyIds.size !== replies.length) invalid(`${label}.replies must have unique ids.`);
|
|
680
|
+
const normalized3 = {
|
|
681
|
+
id: boundedString(pin.id, `${label}.id`, {
|
|
682
|
+
min: 1,
|
|
683
|
+
max: ARTIFACT_REVIEW_LIMITS.id,
|
|
684
|
+
trim: true
|
|
685
|
+
}),
|
|
686
|
+
author: normalizeArtifactReviewIdentity(pin.author),
|
|
687
|
+
artifactId: boundedString(pin.artifactId, `${label}.artifactId`, {
|
|
688
|
+
min: 1,
|
|
689
|
+
max: ARTIFACT_REVIEW_LIMITS.artifactId,
|
|
690
|
+
trim: true
|
|
691
|
+
}),
|
|
692
|
+
region: normalizeRegion(pin.region),
|
|
693
|
+
viewport: normalizeViewport(pin.viewport),
|
|
694
|
+
intent: enumValue(pin.intent, ARTIFACT_REVIEW_INTENTS, `${label}.intent`),
|
|
695
|
+
status: enumValue(pin.status, ARTIFACT_REVIEW_STATUSES, `${label}.status`),
|
|
696
|
+
comment: boundedString(pin.comment, `${label}.comment`, {
|
|
697
|
+
min: 1,
|
|
698
|
+
max: ARTIFACT_REVIEW_LIMITS.text,
|
|
699
|
+
trim: true
|
|
700
|
+
}),
|
|
701
|
+
replies: replies.sort(compareTimestampThenId),
|
|
702
|
+
createdAt: isoTimestamp(pin.createdAt, `${label}.createdAt`),
|
|
703
|
+
updatedAt: isoTimestamp(pin.updatedAt, `${label}.updatedAt`)
|
|
704
|
+
};
|
|
705
|
+
const variant = optionalString(pin.variant, `${label}.variant`, {
|
|
706
|
+
min: 1,
|
|
707
|
+
max: ARTIFACT_REVIEW_LIMITS.variant,
|
|
708
|
+
trim: true
|
|
709
|
+
});
|
|
710
|
+
const anchor2 = normalizeAnchor(pin.anchor);
|
|
711
|
+
if (variant !== void 0) normalized3.variant = variant;
|
|
712
|
+
if (anchor2 !== void 0) normalized3.anchor = anchor2;
|
|
713
|
+
return normalized3;
|
|
714
|
+
}
|
|
715
|
+
function normalizeArtifactReview(review) {
|
|
716
|
+
if (!review || typeof review !== "object" || Array.isArray(review)) {
|
|
717
|
+
invalid("Artifact review must be an object.");
|
|
718
|
+
}
|
|
719
|
+
if (!Array.isArray(review.pins) || review.pins.length > ARTIFACT_REVIEW_LIMITS.pins) {
|
|
720
|
+
invalid(`review.pins must contain no more than ${ARTIFACT_REVIEW_LIMITS.pins} items.`);
|
|
721
|
+
}
|
|
722
|
+
const pins = review.pins.map((pin, index2) => normalizePin(pin, `review.pins[${index2}]`));
|
|
723
|
+
const pinIds = new Set(pins.map(({ id: id7 }) => id7));
|
|
724
|
+
if (pinIds.size !== pins.length) invalid("review.pins must have unique ids.");
|
|
725
|
+
const normalized3 = {
|
|
726
|
+
schemaVersion: enumValue(review.schemaVersion, ["1.0.0"], "review.schemaVersion"),
|
|
727
|
+
reviewId: boundedString(review.reviewId, "review.reviewId", {
|
|
728
|
+
min: 1,
|
|
729
|
+
max: ARTIFACT_REVIEW_LIMITS.id,
|
|
730
|
+
trim: true
|
|
731
|
+
}),
|
|
732
|
+
reviewOf: boundedString(review.reviewOf, "review.reviewOf", {
|
|
733
|
+
min: 64,
|
|
734
|
+
max: 64,
|
|
735
|
+
pattern: REVIEW_OF_RE
|
|
736
|
+
}),
|
|
737
|
+
decision: enumValue(review.decision, ARTIFACT_REVIEW_DECISIONS, "review.decision"),
|
|
738
|
+
overall: boundedString(review.overall, "review.overall", {
|
|
739
|
+
max: ARTIFACT_REVIEW_LIMITS.text
|
|
740
|
+
}),
|
|
741
|
+
pins: pins.sort(compareTimestampThenId)
|
|
742
|
+
};
|
|
743
|
+
if (review.createdAt !== void 0)
|
|
744
|
+
normalized3.createdAt = isoTimestamp(review.createdAt, "review.createdAt");
|
|
745
|
+
if (review.updatedAt !== void 0)
|
|
746
|
+
normalized3.updatedAt = isoTimestamp(review.updatedAt, "review.updatedAt");
|
|
747
|
+
return deepFreeze(normalized3);
|
|
748
|
+
}
|
|
749
|
+
function createArtifactReview({ reviewId, reviewOf, createId = defaultCreateId } = {}) {
|
|
750
|
+
const normalizedReviewId = reviewId === void 0 ? dependencyId(createId, "review") : boundedString(reviewId, "reviewId", {
|
|
751
|
+
min: 1,
|
|
752
|
+
max: ARTIFACT_REVIEW_LIMITS.id,
|
|
753
|
+
trim: true
|
|
754
|
+
});
|
|
755
|
+
return normalizeArtifactReview({
|
|
756
|
+
schemaVersion: "1.0.0",
|
|
757
|
+
reviewId: normalizedReviewId,
|
|
758
|
+
reviewOf: boundedString(reviewOf, "reviewOf", {
|
|
759
|
+
min: 64,
|
|
760
|
+
max: 64,
|
|
761
|
+
pattern: REVIEW_OF_RE
|
|
762
|
+
}),
|
|
763
|
+
decision: "pending",
|
|
764
|
+
overall: "",
|
|
765
|
+
pins: []
|
|
766
|
+
});
|
|
767
|
+
}
|
|
768
|
+
function replacePin(review, pin) {
|
|
769
|
+
return review.pins.map((candidate) => candidate.id === pin.id ? pin : candidate);
|
|
770
|
+
}
|
|
771
|
+
function findPin(review, pinId) {
|
|
772
|
+
const normalizedId = boundedString(pinId, "pinId", {
|
|
773
|
+
min: 1,
|
|
774
|
+
max: ARTIFACT_REVIEW_LIMITS.id,
|
|
775
|
+
trim: true
|
|
776
|
+
});
|
|
777
|
+
const pin = review.pins.find(({ id: id7 }) => id7 === normalizedId);
|
|
778
|
+
if (!pin) {
|
|
779
|
+
throw new ArtifactReviewStateError(
|
|
780
|
+
"E_ARTIFACT_REVIEW_PIN_NOT_FOUND",
|
|
781
|
+
`Unknown feedback pin: ${normalizedId}`
|
|
782
|
+
);
|
|
783
|
+
}
|
|
784
|
+
return pin;
|
|
785
|
+
}
|
|
786
|
+
function preserveOptionalReviewTimestamps(review, next, timestamp2) {
|
|
787
|
+
if (review.createdAt !== void 0) next.createdAt = review.createdAt;
|
|
788
|
+
if (review.updatedAt !== void 0) next.updatedAt = timestamp2;
|
|
789
|
+
return next;
|
|
790
|
+
}
|
|
791
|
+
function reduceArtifactReview(review, action, { createId = defaultCreateId, now = defaultNow } = {}) {
|
|
792
|
+
const current = normalizeArtifactReview(review);
|
|
793
|
+
if (!action || typeof action !== "object" || Array.isArray(action))
|
|
794
|
+
invalid("Review action must be an object.");
|
|
795
|
+
const timestamp2 = dependencyTimestamp(now);
|
|
796
|
+
let next;
|
|
797
|
+
switch (action.type) {
|
|
798
|
+
case "add-pin": {
|
|
799
|
+
if (current.pins.length >= ARTIFACT_REVIEW_LIMITS.pins) {
|
|
800
|
+
invalid(`A review can contain at most ${ARTIFACT_REVIEW_LIMITS.pins} pins.`);
|
|
801
|
+
}
|
|
802
|
+
const author = normalizeArtifactReviewIdentity(action.author);
|
|
803
|
+
const pinInput = action.pin && typeof action.pin === "object" ? action.pin : {};
|
|
804
|
+
const pinId = pinInput.id === void 0 ? uniqueDependencyId(createId, "pin", new Set(current.pins.map(({ id: id7 }) => id7))) : boundedString(pinInput.id, "pin.id", {
|
|
805
|
+
min: 1,
|
|
806
|
+
max: ARTIFACT_REVIEW_LIMITS.id,
|
|
807
|
+
trim: true
|
|
808
|
+
});
|
|
809
|
+
if (current.pins.some(({ id: id7 }) => id7 === pinId)) {
|
|
810
|
+
throw new ArtifactReviewStateError(
|
|
811
|
+
"E_ARTIFACT_REVIEW_ID_COLLISION",
|
|
812
|
+
`Duplicate pin id: ${pinId}`
|
|
813
|
+
);
|
|
814
|
+
}
|
|
815
|
+
const pin = normalizePin({
|
|
816
|
+
...pinInput,
|
|
817
|
+
id: pinId,
|
|
818
|
+
author,
|
|
819
|
+
status: pinInput.status ?? "open",
|
|
820
|
+
replies: [],
|
|
821
|
+
createdAt: pinInput.createdAt ?? timestamp2,
|
|
822
|
+
updatedAt: pinInput.updatedAt ?? timestamp2
|
|
823
|
+
});
|
|
824
|
+
next = preserveOptionalReviewTimestamps(
|
|
825
|
+
current,
|
|
826
|
+
{
|
|
827
|
+
...current,
|
|
828
|
+
pins: [...current.pins, pin]
|
|
829
|
+
},
|
|
830
|
+
timestamp2
|
|
831
|
+
);
|
|
832
|
+
break;
|
|
833
|
+
}
|
|
834
|
+
case "add-reply": {
|
|
835
|
+
const pin = findPin(current, action.pinId);
|
|
836
|
+
if (pin.replies.length >= ARTIFACT_REVIEW_LIMITS.replies) {
|
|
837
|
+
invalid(`A feedback thread can contain at most ${ARTIFACT_REVIEW_LIMITS.replies} replies.`);
|
|
838
|
+
}
|
|
839
|
+
const replyId = action.id === void 0 ? uniqueDependencyId(createId, "reply", new Set(pin.replies.map(({ id: id7 }) => id7))) : boundedString(action.id, "reply.id", {
|
|
840
|
+
min: 1,
|
|
841
|
+
max: ARTIFACT_REVIEW_LIMITS.id,
|
|
842
|
+
trim: true
|
|
843
|
+
});
|
|
844
|
+
if (pin.replies.some(({ id: id7 }) => id7 === replyId)) {
|
|
845
|
+
throw new ArtifactReviewStateError(
|
|
846
|
+
"E_ARTIFACT_REVIEW_ID_COLLISION",
|
|
847
|
+
`Duplicate reply id: ${replyId}`
|
|
848
|
+
);
|
|
849
|
+
}
|
|
850
|
+
const reply2 = normalizeReply({
|
|
851
|
+
id: replyId,
|
|
852
|
+
author: normalizeArtifactReviewIdentity(action.author),
|
|
853
|
+
comment: action.comment,
|
|
854
|
+
createdAt: action.createdAt ?? timestamp2
|
|
855
|
+
});
|
|
856
|
+
const updatedPin = normalizePin({
|
|
857
|
+
...pin,
|
|
858
|
+
replies: [...pin.replies, reply2],
|
|
859
|
+
updatedAt: timestamp2
|
|
860
|
+
});
|
|
861
|
+
next = preserveOptionalReviewTimestamps(
|
|
862
|
+
current,
|
|
863
|
+
{
|
|
864
|
+
...current,
|
|
865
|
+
pins: replacePin(current, updatedPin)
|
|
866
|
+
},
|
|
867
|
+
timestamp2
|
|
868
|
+
);
|
|
869
|
+
break;
|
|
870
|
+
}
|
|
871
|
+
case "set-status": {
|
|
872
|
+
const pin = findPin(current, action.pinId);
|
|
873
|
+
const updatedPin = normalizePin({
|
|
874
|
+
...pin,
|
|
875
|
+
status: enumValue(action.status, ARTIFACT_REVIEW_STATUSES, "status"),
|
|
876
|
+
updatedAt: timestamp2
|
|
877
|
+
});
|
|
878
|
+
next = preserveOptionalReviewTimestamps(
|
|
879
|
+
current,
|
|
880
|
+
{
|
|
881
|
+
...current,
|
|
882
|
+
pins: replacePin(current, updatedPin)
|
|
883
|
+
},
|
|
884
|
+
timestamp2
|
|
885
|
+
);
|
|
886
|
+
break;
|
|
887
|
+
}
|
|
888
|
+
case "set-overall":
|
|
889
|
+
next = preserveOptionalReviewTimestamps(
|
|
890
|
+
current,
|
|
891
|
+
{
|
|
892
|
+
...current,
|
|
893
|
+
overall: boundedString(action.overall, "overall", { max: ARTIFACT_REVIEW_LIMITS.text })
|
|
894
|
+
},
|
|
895
|
+
timestamp2
|
|
896
|
+
);
|
|
897
|
+
break;
|
|
898
|
+
case "set-decision":
|
|
899
|
+
next = preserveOptionalReviewTimestamps(
|
|
900
|
+
current,
|
|
901
|
+
{
|
|
902
|
+
...current,
|
|
903
|
+
decision: enumValue(action.decision, ARTIFACT_REVIEW_DECISIONS, "decision")
|
|
904
|
+
},
|
|
905
|
+
timestamp2
|
|
906
|
+
);
|
|
907
|
+
break;
|
|
908
|
+
default:
|
|
909
|
+
throw new ArtifactReviewStateError(
|
|
910
|
+
"E_ARTIFACT_REVIEW_ACTION_UNKNOWN",
|
|
911
|
+
`Unknown artifact review action: ${String(action.type)}`
|
|
912
|
+
);
|
|
913
|
+
}
|
|
914
|
+
return normalizeArtifactReview(next);
|
|
915
|
+
}
|
|
916
|
+
function createArtifactReviewController({
|
|
917
|
+
initialReview = null,
|
|
918
|
+
reviewOf,
|
|
919
|
+
reviewId,
|
|
920
|
+
identity = null,
|
|
921
|
+
createId = defaultCreateId,
|
|
922
|
+
now = defaultNow
|
|
923
|
+
} = {}) {
|
|
924
|
+
let review = initialReview === null || initialReview === void 0 ? null : normalizeArtifactReview(initialReview);
|
|
925
|
+
if (review && reviewOf !== void 0 && review.reviewOf !== reviewOf) {
|
|
926
|
+
throw new ArtifactReviewStateError(
|
|
927
|
+
"E_ARTIFACT_REVIEW_DIGEST_MISMATCH",
|
|
928
|
+
"The initial review does not match the artifact envelope digest."
|
|
929
|
+
);
|
|
930
|
+
}
|
|
931
|
+
let localIdentity = normalizeArtifactReviewIdentity(identity, { allowEmpty: true });
|
|
932
|
+
let activePinId = null;
|
|
933
|
+
let destroyed = false;
|
|
934
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
935
|
+
const assertAlive = () => {
|
|
936
|
+
if (destroyed) {
|
|
937
|
+
throw new ArtifactReviewStateError(
|
|
938
|
+
"E_ARTIFACT_REVIEW_DESTROYED",
|
|
939
|
+
"Artifact review controller is destroyed."
|
|
940
|
+
);
|
|
941
|
+
}
|
|
942
|
+
};
|
|
943
|
+
const ensureReview = () => {
|
|
944
|
+
review ??= createArtifactReview({ reviewId, reviewOf, createId });
|
|
945
|
+
return review;
|
|
946
|
+
};
|
|
947
|
+
const getState = () => deepFreeze({
|
|
948
|
+
review,
|
|
949
|
+
identity: localIdentity,
|
|
950
|
+
activePinId
|
|
951
|
+
});
|
|
952
|
+
const notify = (change) => {
|
|
953
|
+
const state = getState();
|
|
954
|
+
for (const listener of [...listeners]) listener(state, deepFreeze({ ...change }));
|
|
955
|
+
};
|
|
956
|
+
const controller = {
|
|
957
|
+
getReview() {
|
|
958
|
+
return review;
|
|
959
|
+
},
|
|
960
|
+
getState,
|
|
961
|
+
getIdentity() {
|
|
962
|
+
return localIdentity;
|
|
963
|
+
},
|
|
964
|
+
setIdentity(value) {
|
|
965
|
+
assertAlive();
|
|
966
|
+
localIdentity = normalizeArtifactReviewIdentity(value, { allowEmpty: true });
|
|
967
|
+
notify({ type: "identity" });
|
|
968
|
+
return localIdentity;
|
|
969
|
+
},
|
|
970
|
+
dispatch(action) {
|
|
971
|
+
assertAlive();
|
|
972
|
+
const authored = action?.type === "add-pin" || action?.type === "add-reply";
|
|
973
|
+
const nextAction = authored && action.author === void 0 ? { ...action, author: localIdentity ?? identityRequired() } : action;
|
|
974
|
+
const previousPinIds = nextAction?.type === "add-pin" ? new Set(review?.pins.map(({ id: id7 }) => id7) ?? []) : null;
|
|
975
|
+
review = reduceArtifactReview(ensureReview(), nextAction, { createId, now });
|
|
976
|
+
if (previousPinIds) {
|
|
977
|
+
activePinId = review.pins.find(({ id: id7 }) => !previousPinIds.has(id7))?.id ?? activePinId;
|
|
978
|
+
}
|
|
979
|
+
notify({ type: "review", action: nextAction.type });
|
|
980
|
+
return review;
|
|
981
|
+
},
|
|
982
|
+
replaceReview(value) {
|
|
983
|
+
assertAlive();
|
|
984
|
+
const next = value === null || value === void 0 ? null : normalizeArtifactReview(value);
|
|
985
|
+
if (next && reviewOf !== void 0 && next.reviewOf !== reviewOf) {
|
|
986
|
+
throw new ArtifactReviewStateError(
|
|
987
|
+
"E_ARTIFACT_REVIEW_DIGEST_MISMATCH",
|
|
988
|
+
"The replacement review does not match the artifact envelope digest."
|
|
989
|
+
);
|
|
990
|
+
}
|
|
991
|
+
review = next;
|
|
992
|
+
if (activePinId && !review?.pins.some(({ id: id7 }) => id7 === activePinId)) activePinId = null;
|
|
993
|
+
notify({ type: "review-replaced" });
|
|
994
|
+
return review;
|
|
995
|
+
},
|
|
996
|
+
selectPin(pinId) {
|
|
997
|
+
assertAlive();
|
|
998
|
+
if (pinId === null || pinId === void 0) {
|
|
999
|
+
activePinId = null;
|
|
1000
|
+
} else {
|
|
1001
|
+
activePinId = findPin(ensureReview(), pinId).id;
|
|
1002
|
+
}
|
|
1003
|
+
notify({ type: "selection" });
|
|
1004
|
+
return activePinId;
|
|
1005
|
+
},
|
|
1006
|
+
subscribe(listener) {
|
|
1007
|
+
assertAlive();
|
|
1008
|
+
if (typeof listener !== "function") invalid("Review subscriber must be a function.");
|
|
1009
|
+
listeners.add(listener);
|
|
1010
|
+
return () => listeners.delete(listener);
|
|
1011
|
+
},
|
|
1012
|
+
destroy() {
|
|
1013
|
+
if (destroyed) return;
|
|
1014
|
+
destroyed = true;
|
|
1015
|
+
listeners.clear();
|
|
1016
|
+
review = null;
|
|
1017
|
+
localIdentity = null;
|
|
1018
|
+
activePinId = null;
|
|
1019
|
+
}
|
|
1020
|
+
};
|
|
1021
|
+
return Object.freeze(controller);
|
|
1022
|
+
}
|
|
1023
|
+
function createElement(document2, tagName, { className, text: text5, attributes = {} } = {}) {
|
|
1024
|
+
const element = document2.createElement(tagName);
|
|
1025
|
+
if (className) element.className = className;
|
|
1026
|
+
if (text5 !== void 0) element.textContent = text5;
|
|
1027
|
+
for (const [name, value] of Object.entries(attributes)) {
|
|
1028
|
+
if (value !== void 0 && value !== null) element.setAttribute(name, String(value));
|
|
1029
|
+
}
|
|
1030
|
+
return element;
|
|
1031
|
+
}
|
|
1032
|
+
function artifactReviewThreadDomId(pinId) {
|
|
1033
|
+
return annotationDomIds(pinId).thread;
|
|
1034
|
+
}
|
|
1035
|
+
function displayTimestamp(timestamp2) {
|
|
1036
|
+
const canonical = new Date(timestamp2).toISOString();
|
|
1037
|
+
return `${canonical.slice(0, 10)} ${canonical.slice(11, 16)} UTC`;
|
|
1038
|
+
}
|
|
1039
|
+
function renderReply(document2, reply2) {
|
|
1040
|
+
const item2 = createElement(document2, "li", {
|
|
1041
|
+
className: "planr-reply",
|
|
1042
|
+
attributes: { "data-planr-reply-id": reply2.id }
|
|
1043
|
+
});
|
|
1044
|
+
const heading = createElement(document2, "header");
|
|
1045
|
+
heading.append(createElement(document2, "strong", { text: reply2.author.name }));
|
|
1046
|
+
const time = createElement(document2, "time", {
|
|
1047
|
+
text: displayTimestamp(reply2.createdAt),
|
|
1048
|
+
attributes: { datetime: reply2.createdAt }
|
|
1049
|
+
});
|
|
1050
|
+
heading.append(time);
|
|
1051
|
+
item2.append(heading, createElement(document2, "p", { text: reply2.comment }));
|
|
1052
|
+
return item2;
|
|
1053
|
+
}
|
|
1054
|
+
function renderReplyForm(document2, pin, expanded = false) {
|
|
1055
|
+
const fieldId = `${annotationDomIds(pin.id).thread}-reply`;
|
|
1056
|
+
const wrapper = createElement(document2, "div", { className: "planr-reply-editor" });
|
|
1057
|
+
const toggle = createElement(document2, "button", {
|
|
1058
|
+
className: "planr-reply-toggle",
|
|
1059
|
+
text: expanded ? "− Reply" : "+ Reply",
|
|
1060
|
+
attributes: {
|
|
1061
|
+
type: "button",
|
|
1062
|
+
id: `${fieldId}-toggle`,
|
|
1063
|
+
"data-planr-reply-toggle": pin.id,
|
|
1064
|
+
"aria-expanded": String(expanded),
|
|
1065
|
+
"aria-controls": `${fieldId}-form`,
|
|
1066
|
+
title: expanded ? "Collapse reply" : "Reply to this comment"
|
|
1067
|
+
}
|
|
1068
|
+
});
|
|
1069
|
+
const form = createElement(document2, "form", {
|
|
1070
|
+
className: "planr-reply-form",
|
|
1071
|
+
attributes: {
|
|
1072
|
+
"data-planr-reply-form": pin.id,
|
|
1073
|
+
id: `${fieldId}-form`,
|
|
1074
|
+
...expanded ? {} : { hidden: "" }
|
|
1075
|
+
}
|
|
1076
|
+
});
|
|
1077
|
+
const label = createElement(document2, "label", {
|
|
1078
|
+
className: "planr-reply-label",
|
|
1079
|
+
text: "Reply to thread",
|
|
1080
|
+
attributes: { for: fieldId }
|
|
1081
|
+
});
|
|
1082
|
+
const textarea = createElement(document2, "textarea", {
|
|
1083
|
+
attributes: {
|
|
1084
|
+
id: fieldId,
|
|
1085
|
+
name: "reply",
|
|
1086
|
+
maxlength: ARTIFACT_REVIEW_LIMITS.text,
|
|
1087
|
+
rows: 2,
|
|
1088
|
+
placeholder: "Write a reply…",
|
|
1089
|
+
required: "",
|
|
1090
|
+
"aria-describedby": "planr-review-error"
|
|
1091
|
+
}
|
|
1092
|
+
});
|
|
1093
|
+
const controls = createElement(document2, "div", { className: "planr-reply-controls" });
|
|
1094
|
+
const hint = createElement(document2, "span", {
|
|
1095
|
+
className: "planr-reply-hint",
|
|
1096
|
+
text: "Ctrl/⌘ + Enter to send"
|
|
1097
|
+
});
|
|
1098
|
+
const submit = createElement(document2, "button", {
|
|
1099
|
+
className: "planr-reply-send",
|
|
1100
|
+
attributes: {
|
|
1101
|
+
type: "submit",
|
|
1102
|
+
disabled: "",
|
|
1103
|
+
"aria-label": "Send reply",
|
|
1104
|
+
title: "Send reply (Ctrl/⌘ + Enter)"
|
|
1105
|
+
}
|
|
1106
|
+
});
|
|
1107
|
+
const svg = document2.createElementNS("http://www.w3.org/2000/svg", "svg");
|
|
1108
|
+
for (const [name, value] of Object.entries({
|
|
1109
|
+
viewBox: "0 0 24 24",
|
|
1110
|
+
width: "16",
|
|
1111
|
+
height: "16",
|
|
1112
|
+
fill: "none",
|
|
1113
|
+
stroke: "currentColor",
|
|
1114
|
+
"stroke-width": "1.8",
|
|
1115
|
+
"stroke-linecap": "round",
|
|
1116
|
+
"stroke-linejoin": "round",
|
|
1117
|
+
"aria-hidden": "true",
|
|
1118
|
+
focusable: "false"
|
|
1119
|
+
}))
|
|
1120
|
+
svg.setAttribute(name, value);
|
|
1121
|
+
const path = document2.createElementNS("http://www.w3.org/2000/svg", "path");
|
|
1122
|
+
path.setAttribute("d", "M12 19V5m-6 6 6-6 6 6");
|
|
1123
|
+
svg.append(path);
|
|
1124
|
+
submit.append(svg);
|
|
1125
|
+
controls.append(hint, submit);
|
|
1126
|
+
form.append(label, textarea, controls);
|
|
1127
|
+
wrapper.append(toggle, form);
|
|
1128
|
+
return wrapper;
|
|
1129
|
+
}
|
|
1130
|
+
function renderThread(document2, pin, active, description = {}) {
|
|
1131
|
+
const intentLabel = typeof description.intentLabel === "string" ? description.intentLabel.slice(0, 128) : pin.intent;
|
|
1132
|
+
const article = createElement(document2, "article", {
|
|
1133
|
+
className: `planr-thread${active ? " is-active" : ""}`,
|
|
1134
|
+
attributes: {
|
|
1135
|
+
id: artifactReviewThreadDomId(pin.id),
|
|
1136
|
+
tabindex: "-1",
|
|
1137
|
+
"data-planr-pin-id": pin.id,
|
|
1138
|
+
"data-planr-intent": pin.intent,
|
|
1139
|
+
"data-planr-status": pin.status,
|
|
1140
|
+
"aria-controls": annotationDomIds(pin.id).pin,
|
|
1141
|
+
"aria-label": `${intentLabel} comment by ${pin.author.name}`,
|
|
1142
|
+
"data-planr-unread": description.unread === true ? "true" : "false",
|
|
1143
|
+
...description.compact ? { "data-planr-compact": "true" } : {}
|
|
1144
|
+
}
|
|
1145
|
+
});
|
|
1146
|
+
const header = createElement(document2, "header");
|
|
1147
|
+
const byline = createElement(document2, "div", { className: "planr-review-byline" });
|
|
1148
|
+
byline.append(
|
|
1149
|
+
createElement(document2, "strong", { text: pin.author.name }),
|
|
1150
|
+
createElement(document2, "span", { className: "planr-intent", text: intentLabel })
|
|
1151
|
+
);
|
|
1152
|
+
const time = createElement(document2, "time", {
|
|
1153
|
+
text: description.compact ? `${new Date(pin.createdAt).toISOString().slice(11, 16)} UTC` : displayTimestamp(pin.createdAt),
|
|
1154
|
+
attributes: { datetime: pin.createdAt, title: displayTimestamp(pin.createdAt) }
|
|
1155
|
+
});
|
|
1156
|
+
header.append(byline, time);
|
|
1157
|
+
const comment = createElement(document2, "p", {
|
|
1158
|
+
className: "planr-thread-comment",
|
|
1159
|
+
text: pin.comment
|
|
1160
|
+
});
|
|
1161
|
+
const lifecycle = createElement(document2, "div", { className: "planr-thread-actions" });
|
|
1162
|
+
lifecycle.append(
|
|
1163
|
+
createElement(document2, "span", {
|
|
1164
|
+
className: "planr-thread-status",
|
|
1165
|
+
text: pin.status,
|
|
1166
|
+
attributes: { title: `Status: ${pin.status}` }
|
|
1167
|
+
}),
|
|
1168
|
+
createElement(document2, "button", {
|
|
1169
|
+
text: pin.status === "resolved" ? "Reopen" : "Resolve",
|
|
1170
|
+
attributes: {
|
|
1171
|
+
type: "button",
|
|
1172
|
+
"data-planr-thread-action": pin.status === "resolved" ? "reopen" : "resolve",
|
|
1173
|
+
"data-planr-pin-id": pin.id
|
|
1174
|
+
}
|
|
1175
|
+
}),
|
|
1176
|
+
createElement(document2, "button", {
|
|
1177
|
+
text: "Show pin",
|
|
1178
|
+
attributes: {
|
|
1179
|
+
type: "button",
|
|
1180
|
+
"data-planr-thread-focus": pin.id,
|
|
1181
|
+
"aria-controls": annotationDomIds(pin.id).pin
|
|
1182
|
+
}
|
|
1183
|
+
})
|
|
1184
|
+
);
|
|
1185
|
+
const replies = createElement(document2, "ol", {
|
|
1186
|
+
className: "planr-replies",
|
|
1187
|
+
attributes: { "aria-label": "Replies" }
|
|
1188
|
+
});
|
|
1189
|
+
const earlierReplies = description.compact ? Math.max(0, pin.replies.length - (description.replyLimit || 2)) : 0;
|
|
1190
|
+
for (const reply2 of pin.replies.slice(earlierReplies))
|
|
1191
|
+
replies.append(renderReply(document2, reply2));
|
|
1192
|
+
const editor = renderReplyForm(document2, pin, description.replyExpanded);
|
|
1193
|
+
article.append(header, comment);
|
|
1194
|
+
if (description.compact && pin.comment.length > 240) {
|
|
1195
|
+
comment.dataset.planrCommentCollapsed = String(!description.commentExpanded);
|
|
1196
|
+
article.append(
|
|
1197
|
+
createElement(document2, "button", {
|
|
1198
|
+
className: "planr-comment-expand",
|
|
1199
|
+
text: description.commentExpanded ? "Show less" : "Read full comment",
|
|
1200
|
+
attributes: {
|
|
1201
|
+
type: "button",
|
|
1202
|
+
id: `${annotationDomIds(pin.id).thread}-expand`,
|
|
1203
|
+
"data-planr-comment-expand": pin.id,
|
|
1204
|
+
"aria-expanded": String(Boolean(description.commentExpanded))
|
|
1205
|
+
}
|
|
1206
|
+
})
|
|
1207
|
+
);
|
|
1208
|
+
}
|
|
1209
|
+
if (earlierReplies)
|
|
1210
|
+
article.append(
|
|
1211
|
+
createElement(document2, "button", {
|
|
1212
|
+
className: "planr-history-expand",
|
|
1213
|
+
text: `Show ${earlierReplies} earlier ${earlierReplies === 1 ? "reply" : "replies"}`,
|
|
1214
|
+
attributes: {
|
|
1215
|
+
type: "button",
|
|
1216
|
+
id: `${annotationDomIds(pin.id).thread}-history`,
|
|
1217
|
+
"data-planr-history-expand": pin.id,
|
|
1218
|
+
"aria-expanded": "false"
|
|
1219
|
+
}
|
|
1220
|
+
})
|
|
1221
|
+
);
|
|
1222
|
+
if (pin.replies.length) article.append(replies);
|
|
1223
|
+
if (description.compact) {
|
|
1224
|
+
lifecycle.append(editor.querySelector("[data-planr-reply-toggle]"));
|
|
1225
|
+
article.append(lifecycle, editor);
|
|
1226
|
+
} else article.append(lifecycle, editor);
|
|
1227
|
+
return article;
|
|
1228
|
+
}
|
|
1229
|
+
function decisionCopy(decision) {
|
|
1230
|
+
if (decision === "approved") return "Review approved";
|
|
1231
|
+
if (decision === "changes_requested") return "Changes requested";
|
|
1232
|
+
return "Decision pending";
|
|
1233
|
+
}
|
|
1234
|
+
function mountArtifactFeedbackRail({
|
|
1235
|
+
root,
|
|
1236
|
+
document: document2 = root?.ownerDocument,
|
|
1237
|
+
window = document2?.defaultView,
|
|
1238
|
+
controller: providedController,
|
|
1239
|
+
initialReview = null,
|
|
1240
|
+
reviewOf,
|
|
1241
|
+
reviewId,
|
|
1242
|
+
identity = null,
|
|
1243
|
+
createId = defaultCreateId,
|
|
1244
|
+
now = defaultNow,
|
|
1245
|
+
onSelectPin,
|
|
1246
|
+
presentation: initialPresentation = {}
|
|
1247
|
+
} = {}) {
|
|
1248
|
+
if (!root || !document2 || !window) invalid("A browser root, document, and window are required.");
|
|
1249
|
+
const slot = root.querySelector('[data-planr-slot="feedback-rail"]');
|
|
1250
|
+
const identityInput = root.querySelector("[data-planr-reviewer-name]");
|
|
1251
|
+
const identityStatus = root.querySelector("[data-planr-identity-status]");
|
|
1252
|
+
const overall = root.querySelector("#planr-overall-note");
|
|
1253
|
+
const decisionStatus = root.querySelector('[data-planr-slot="decision-status"]');
|
|
1254
|
+
const decisionButtons = [...root.querySelectorAll("[data-planr-decision]")];
|
|
1255
|
+
if (!slot || !identityInput || !overall || !decisionStatus || decisionButtons.length === 0) {
|
|
1256
|
+
invalid("Artifact feedback renderer slots are missing.");
|
|
1257
|
+
}
|
|
1258
|
+
const ownsController = !providedController;
|
|
1259
|
+
const controller = providedController ?? createArtifactReviewController({
|
|
1260
|
+
initialReview,
|
|
1261
|
+
reviewOf,
|
|
1262
|
+
reviewId,
|
|
1263
|
+
identity,
|
|
1264
|
+
createId,
|
|
1265
|
+
now
|
|
1266
|
+
});
|
|
1267
|
+
let destroyed = false;
|
|
1268
|
+
let presentation = { ...initialPresentation };
|
|
1269
|
+
const replyDrafts = /* @__PURE__ */ new Map();
|
|
1270
|
+
const expandedReplies = /* @__PURE__ */ new Set();
|
|
1271
|
+
const expandedHistory = /* @__PURE__ */ new Map(), expandedComments = /* @__PURE__ */ new Set();
|
|
1272
|
+
let visibleLimit = Number.isInteger(initialPresentation.pageSize) ? initialPresentation.pageSize : Infinity;
|
|
1273
|
+
const extraDrafts = /* @__PURE__ */ new Map();
|
|
1274
|
+
let overallDirty = false;
|
|
1275
|
+
let composing = false;
|
|
1276
|
+
let renderQueued = false;
|
|
1277
|
+
const captureDrafts = () => {
|
|
1278
|
+
for (const form of slot.querySelectorAll("[data-planr-reply-form]")) {
|
|
1279
|
+
const field = form.elements.namedItem("reply");
|
|
1280
|
+
if (field) replyDrafts.set(form.dataset.planrReplyForm, field.value);
|
|
1281
|
+
}
|
|
1282
|
+
for (const field of slot.querySelectorAll("[data-planr-draft-key]")) {
|
|
1283
|
+
if (typeof field.value === "string")
|
|
1284
|
+
extraDrafts.set(field.dataset.planrDraftKey, field.value);
|
|
1285
|
+
}
|
|
1286
|
+
};
|
|
1287
|
+
const snapshotDrafts = () => {
|
|
1288
|
+
captureDrafts();
|
|
1289
|
+
return cloneFrozenArtifactReview({
|
|
1290
|
+
reviewOf: controller.getReview()?.reviewOf ?? reviewOf,
|
|
1291
|
+
replies: Object.fromEntries(replyDrafts),
|
|
1292
|
+
expandedReplies: [...expandedReplies],
|
|
1293
|
+
fields: Object.fromEntries(extraDrafts),
|
|
1294
|
+
overall: { value: overall.value, dirty: overallDirty },
|
|
1295
|
+
identity: identityInput.value
|
|
1296
|
+
});
|
|
1297
|
+
};
|
|
1298
|
+
const emitDraftChange = () => root.dispatchEvent(
|
|
1299
|
+
new window.CustomEvent(ARTIFACT_REVIEW_DRAFT_CHANGE_EVENT, {
|
|
1300
|
+
bubbles: true,
|
|
1301
|
+
detail: snapshotDrafts()
|
|
1302
|
+
})
|
|
1303
|
+
);
|
|
1304
|
+
const focusDescriptor = () => {
|
|
1305
|
+
const element = document2.activeElement;
|
|
1306
|
+
if (!element || !slot.contains(element)) return null;
|
|
1307
|
+
const thread2 = element.closest("[data-planr-pin-id]");
|
|
1308
|
+
return {
|
|
1309
|
+
id: element.id,
|
|
1310
|
+
pinId: thread2?.dataset.planrPinId,
|
|
1311
|
+
draftKey: element.dataset.planrDraftKey,
|
|
1312
|
+
action: element.dataset.planrThreadAction,
|
|
1313
|
+
showPin: element.dataset.planrThreadFocus,
|
|
1314
|
+
tag: element.tagName,
|
|
1315
|
+
name: element.name,
|
|
1316
|
+
start: element.selectionStart,
|
|
1317
|
+
end: element.selectionEnd,
|
|
1318
|
+
direction: element.selectionDirection
|
|
1319
|
+
};
|
|
1320
|
+
};
|
|
1321
|
+
const restoreFocus = (descriptor) => {
|
|
1322
|
+
if (!descriptor) return;
|
|
1323
|
+
const thread2 = descriptor.pinId && document2.getElementById(artifactReviewThreadDomId(descriptor.pinId));
|
|
1324
|
+
const target = descriptor.id && document2.getElementById(descriptor.id) || descriptor.draftKey && [...slot.querySelectorAll("[data-planr-draft-key]")].find(
|
|
1325
|
+
(field) => field.dataset.planrDraftKey === descriptor.draftKey
|
|
1326
|
+
) || descriptor.action && [...thread2?.querySelectorAll("[data-planr-thread-action]") ?? []].find(
|
|
1327
|
+
(button) => button.dataset.planrThreadAction === descriptor.action
|
|
1328
|
+
) || descriptor.showPin && thread2?.querySelector("[data-planr-thread-focus]") || [...thread2?.querySelectorAll("button,input,select,textarea") ?? []].find(
|
|
1329
|
+
(field) => field.tagName === descriptor.tag && field.name === descriptor.name
|
|
1330
|
+
);
|
|
1331
|
+
target?.focus?.({ preventScroll: true });
|
|
1332
|
+
if (Number.isInteger(descriptor.start))
|
|
1333
|
+
target?.setSelectionRange?.(descriptor.start, descriptor.end, descriptor.direction);
|
|
1334
|
+
};
|
|
1335
|
+
const showError = (error) => {
|
|
1336
|
+
const target = root.querySelector("#planr-review-error");
|
|
1337
|
+
if (!target) return;
|
|
1338
|
+
target.textContent = error?.message ?? String(error);
|
|
1339
|
+
target.hidden = false;
|
|
1340
|
+
};
|
|
1341
|
+
const clearError = () => {
|
|
1342
|
+
const target = root.querySelector("#planr-review-error");
|
|
1343
|
+
if (!target) return;
|
|
1344
|
+
target.textContent = "";
|
|
1345
|
+
target.hidden = true;
|
|
1346
|
+
};
|
|
1347
|
+
const announce2 = (message) => {
|
|
1348
|
+
decisionStatus.textContent = message;
|
|
1349
|
+
const live = root.parentElement?.querySelector('[data-planr-slot="review-announcer"]') ?? document2.querySelector('[data-planr-slot="review-announcer"]');
|
|
1350
|
+
if (live) live.textContent = message;
|
|
1351
|
+
};
|
|
1352
|
+
const updateCounts = (pins) => {
|
|
1353
|
+
const label = `${pins.length} ${pins.length === 1 ? "comment" : "comments"}`;
|
|
1354
|
+
for (const count2 of root.querySelectorAll(
|
|
1355
|
+
'[data-planr-action="feedback"] .planr-count, .planr-review-rail > header .planr-count'
|
|
1356
|
+
)) {
|
|
1357
|
+
count2.textContent = String(pins.length);
|
|
1358
|
+
count2.setAttribute("aria-label", label);
|
|
1359
|
+
}
|
|
1360
|
+
const commentsButton = root.querySelector('[data-planr-action="feedback"]');
|
|
1361
|
+
commentsButton?.setAttribute("aria-label", commentsButton.dataset.planrReviewLabel || label);
|
|
1362
|
+
if (commentsButton?.dataset.planrReviewLabel)
|
|
1363
|
+
commentsButton.setAttribute("aria-description", label);
|
|
1364
|
+
};
|
|
1365
|
+
const render = ({ capture = true } = {}) => {
|
|
1366
|
+
if (composing) {
|
|
1367
|
+
renderQueued = true;
|
|
1368
|
+
return;
|
|
1369
|
+
}
|
|
1370
|
+
if (capture) captureDrafts();
|
|
1371
|
+
const focus = focusDescriptor();
|
|
1372
|
+
const scroll = [];
|
|
1373
|
+
for (let node2 = slot; node2 && root.contains(node2); node2 = node2.parentElement)
|
|
1374
|
+
scroll.push([node2, node2.scrollTop, node2.scrollLeft]);
|
|
1375
|
+
const state = controller.getState();
|
|
1376
|
+
const { review, activePinId } = state;
|
|
1377
|
+
const allPins = review?.pins ?? [];
|
|
1378
|
+
const pins = typeof presentation.filterPin === "function" ? allPins.filter((pin) => presentation.filterPin(pin, state)) : allPins;
|
|
1379
|
+
const activeIndex = pins.findIndex((pin) => pin.id === activePinId);
|
|
1380
|
+
const visiblePins = pins.slice(0, visibleLimit);
|
|
1381
|
+
if (activeIndex >= visibleLimit) visiblePins.push(pins[activeIndex]);
|
|
1382
|
+
const fragment = document2.createDocumentFragment();
|
|
1383
|
+
const list3 = createElement(document2, "div", {
|
|
1384
|
+
className: "planr-thread-list",
|
|
1385
|
+
attributes: { "aria-label": "Comment threads" }
|
|
1386
|
+
});
|
|
1387
|
+
if (pins.length === 0) {
|
|
1388
|
+
list3.append(
|
|
1389
|
+
createElement(document2, "p", {
|
|
1390
|
+
className: "planr-review-empty",
|
|
1391
|
+
text: typeof presentation.emptyMessage === "string" ? presentation.emptyMessage : allPins.length ? "No comments match these filters." : "No comments yet. Choose Add comment, then select a point or region in the artifact."
|
|
1392
|
+
})
|
|
1393
|
+
);
|
|
1394
|
+
} else {
|
|
1395
|
+
for (const pin of visiblePins) {
|
|
1396
|
+
const thread2 = renderThread(document2, pin, pin.id === activePinId, {
|
|
1397
|
+
...presentation.describePin?.(pin, state),
|
|
1398
|
+
compact: presentation.compact === true,
|
|
1399
|
+
replyLimit: expandedHistory.get(pin.id) || 2,
|
|
1400
|
+
commentExpanded: expandedComments.has(pin.id),
|
|
1401
|
+
replyExpanded: expandedReplies.has(pin.id)
|
|
1402
|
+
});
|
|
1403
|
+
presentation.decorateThread?.({ element: thread2, pin, state, document: document2 });
|
|
1404
|
+
const reply2 = thread2.querySelector('[name="reply"]');
|
|
1405
|
+
if (replyDrafts.has(pin.id)) reply2.value = replyDrafts.get(pin.id);
|
|
1406
|
+
thread2.querySelector(".planr-reply-send").disabled = !reply2.value.trim();
|
|
1407
|
+
for (const field of thread2.querySelectorAll("[data-planr-draft-key]")) {
|
|
1408
|
+
if (extraDrafts.has(field.dataset.planrDraftKey))
|
|
1409
|
+
field.value = extraDrafts.get(field.dataset.planrDraftKey);
|
|
1410
|
+
}
|
|
1411
|
+
list3.append(thread2);
|
|
1412
|
+
}
|
|
1413
|
+
}
|
|
1414
|
+
if (pins.length > visibleLimit)
|
|
1415
|
+
list3.append(
|
|
1416
|
+
createElement(document2, "button", {
|
|
1417
|
+
className: "planr-threads-more",
|
|
1418
|
+
text: `Show more comments · ${Math.min(visibleLimit, pins.length)} of ${pins.length}`,
|
|
1419
|
+
attributes: { type: "button", "data-planr-threads-more": "" }
|
|
1420
|
+
})
|
|
1421
|
+
);
|
|
1422
|
+
fragment.append(list3);
|
|
1423
|
+
slot.replaceChildren(fragment);
|
|
1424
|
+
const identityName2 = controller.getIdentity()?.name ?? "";
|
|
1425
|
+
if (document2.activeElement !== identityInput) identityInput.value = identityName2;
|
|
1426
|
+
if (identityStatus) {
|
|
1427
|
+
identityStatus.dataset.planrIdentityReady = String(Boolean(identityName2));
|
|
1428
|
+
identityStatus.textContent = identityName2 ? `Comments will appear as ${identityName2}.` : "Used to sign your comments.";
|
|
1429
|
+
}
|
|
1430
|
+
overall.maxLength = ARTIFACT_REVIEW_LIMITS.text;
|
|
1431
|
+
if (!overallDirty && document2.activeElement !== overall) overall.value = review?.overall ?? "";
|
|
1432
|
+
for (const button of decisionButtons) {
|
|
1433
|
+
button.setAttribute(
|
|
1434
|
+
"aria-pressed",
|
|
1435
|
+
String(button.dataset.planrDecision === (review?.decision ?? "pending"))
|
|
1436
|
+
);
|
|
1437
|
+
}
|
|
1438
|
+
decisionStatus.textContent = decisionCopy(review?.decision ?? "pending");
|
|
1439
|
+
updateCounts(allPins);
|
|
1440
|
+
restoreFocus(focus);
|
|
1441
|
+
for (const [node2, top, left] of scroll) {
|
|
1442
|
+
node2.scrollTop = top;
|
|
1443
|
+
node2.scrollLeft = left;
|
|
1444
|
+
}
|
|
1445
|
+
const openMetric = root.querySelector('[data-planr-metric="open"]');
|
|
1446
|
+
if (openMetric)
|
|
1447
|
+
openMetric.textContent = `${allPins.filter(({ status }) => status !== "resolved").length} open`;
|
|
1448
|
+
};
|
|
1449
|
+
const focusThread = (pinId) => {
|
|
1450
|
+
const thread2 = document2.getElementById(artifactReviewThreadDomId(pinId));
|
|
1451
|
+
thread2?.focus({ preventScroll: true });
|
|
1452
|
+
thread2?.scrollIntoView?.({ block: "nearest" });
|
|
1453
|
+
};
|
|
1454
|
+
const emitReview = (review) => {
|
|
1455
|
+
const detail = cloneFrozenArtifactReview(review);
|
|
1456
|
+
root.dispatchEvent(
|
|
1457
|
+
new window.CustomEvent(ARTIFACT_REVIEW_CHANGE_EVENT, {
|
|
1458
|
+
detail,
|
|
1459
|
+
bubbles: true
|
|
1460
|
+
})
|
|
1461
|
+
);
|
|
1462
|
+
};
|
|
1463
|
+
const unsubscribe = controller.subscribe((state, change) => {
|
|
1464
|
+
if (destroyed) return;
|
|
1465
|
+
if (["review", "review-replaced", "selection"].includes(change.type)) render();
|
|
1466
|
+
if (["review", "review-replaced"].includes(change.type) && state.review)
|
|
1467
|
+
emitReview(state.review);
|
|
1468
|
+
});
|
|
1469
|
+
const onInput = (event) => {
|
|
1470
|
+
if (event.target !== identityInput) return;
|
|
1471
|
+
try {
|
|
1472
|
+
controller.setIdentity(event.target.value ? { name: event.target.value } : null);
|
|
1473
|
+
const name = controller.getIdentity()?.name ?? "";
|
|
1474
|
+
if (identityStatus) {
|
|
1475
|
+
identityStatus.dataset.planrIdentityReady = String(Boolean(name));
|
|
1476
|
+
identityStatus.textContent = name ? `Comments will appear as ${name}.` : "Used to sign your comments.";
|
|
1477
|
+
}
|
|
1478
|
+
clearError();
|
|
1479
|
+
} catch (error) {
|
|
1480
|
+
showError(error);
|
|
1481
|
+
}
|
|
1482
|
+
};
|
|
1483
|
+
const setReplyExpanded = (pinId, expanded, { focus = false } = {}) => {
|
|
1484
|
+
const thread2 = document2.getElementById(artifactReviewThreadDomId(pinId));
|
|
1485
|
+
const form = thread2?.querySelector("[data-planr-reply-form]");
|
|
1486
|
+
const toggle = thread2?.querySelector("[data-planr-reply-toggle]");
|
|
1487
|
+
if (!form || !toggle) return;
|
|
1488
|
+
if (expanded) expandedReplies.add(pinId);
|
|
1489
|
+
else expandedReplies.delete(pinId);
|
|
1490
|
+
form.hidden = !expanded;
|
|
1491
|
+
toggle.setAttribute("aria-expanded", String(expanded));
|
|
1492
|
+
toggle.textContent = expanded ? "− Reply" : "+ Reply";
|
|
1493
|
+
toggle.title = expanded ? "Collapse reply" : "Reply to this comment";
|
|
1494
|
+
if (focus)
|
|
1495
|
+
(expanded ? form.elements.namedItem("reply") : toggle).focus({ preventScroll: true });
|
|
1496
|
+
emitDraftChange();
|
|
1497
|
+
};
|
|
1498
|
+
const onKeyDown = (event) => {
|
|
1499
|
+
const form = event.target?.closest?.("[data-planr-reply-form]");
|
|
1500
|
+
if (!form || event.isComposing) return;
|
|
1501
|
+
if (event.key === "Escape") {
|
|
1502
|
+
event.preventDefault();
|
|
1503
|
+
event.stopPropagation();
|
|
1504
|
+
setReplyExpanded(form.dataset.planrReplyForm, false, { focus: true });
|
|
1505
|
+
} else if (event.key === "Enter" && (event.metaKey || event.ctrlKey)) {
|
|
1506
|
+
event.preventDefault();
|
|
1507
|
+
if (form.elements.namedItem("reply").value.trim()) form.requestSubmit();
|
|
1508
|
+
}
|
|
1509
|
+
};
|
|
1510
|
+
const emitSelection = (pinId) => {
|
|
1511
|
+
controller.selectPin(pinId);
|
|
1512
|
+
root.dispatchEvent(
|
|
1513
|
+
new window.CustomEvent(ARTIFACT_REVIEW_SELECT_EVENT, {
|
|
1514
|
+
detail: deepFreeze({ pinId, source: "thread" }),
|
|
1515
|
+
bubbles: true
|
|
1516
|
+
})
|
|
1517
|
+
);
|
|
1518
|
+
onSelectPin?.(pinId);
|
|
1519
|
+
};
|
|
1520
|
+
const onClick = (event) => {
|
|
1521
|
+
if (event.target.closest?.("[data-planr-threads-more]")) {
|
|
1522
|
+
visibleLimit += presentation.pageSize || 40;
|
|
1523
|
+
render();
|
|
1524
|
+
return;
|
|
1525
|
+
}
|
|
1526
|
+
const historyToggle = event.target.closest?.("[data-planr-history-expand]");
|
|
1527
|
+
if (historyToggle) {
|
|
1528
|
+
const id7 = historyToggle.dataset.planrHistoryExpand;
|
|
1529
|
+
expandedHistory.set(id7, (expandedHistory.get(id7) || 2) + 20);
|
|
1530
|
+
render();
|
|
1531
|
+
return;
|
|
1532
|
+
}
|
|
1533
|
+
const commentToggle = event.target.closest?.("[data-planr-comment-expand]");
|
|
1534
|
+
if (commentToggle) {
|
|
1535
|
+
const id7 = commentToggle.dataset.planrCommentExpand;
|
|
1536
|
+
if (expandedComments.has(id7)) expandedComments.delete(id7);
|
|
1537
|
+
else expandedComments.add(id7);
|
|
1538
|
+
render();
|
|
1539
|
+
return;
|
|
1540
|
+
}
|
|
1541
|
+
const replyToggle = event.target?.closest?.("[data-planr-reply-toggle]");
|
|
1542
|
+
if (replyToggle) {
|
|
1543
|
+
setReplyExpanded(
|
|
1544
|
+
replyToggle.dataset.planrReplyToggle,
|
|
1545
|
+
replyToggle.getAttribute("aria-expanded") !== "true",
|
|
1546
|
+
{ focus: true }
|
|
1547
|
+
);
|
|
1548
|
+
return;
|
|
1549
|
+
}
|
|
1550
|
+
const action = event.target?.closest?.("[data-planr-thread-action]");
|
|
1551
|
+
const focus = event.target?.closest?.("[data-planr-thread-focus]");
|
|
1552
|
+
if (action) {
|
|
1553
|
+
try {
|
|
1554
|
+
const pinId = action.dataset.planrPinId;
|
|
1555
|
+
controller.dispatch({
|
|
1556
|
+
type: "set-status",
|
|
1557
|
+
pinId,
|
|
1558
|
+
status: action.dataset.planrThreadAction === "resolve" ? "resolved" : "open"
|
|
1559
|
+
});
|
|
1560
|
+
announce2(
|
|
1561
|
+
action.dataset.planrThreadAction === "resolve" ? "Comment resolved" : "Comment reopened"
|
|
1562
|
+
);
|
|
1563
|
+
focusThread(pinId);
|
|
1564
|
+
clearError();
|
|
1565
|
+
} catch (error) {
|
|
1566
|
+
showError(error);
|
|
1567
|
+
}
|
|
1568
|
+
return;
|
|
1569
|
+
}
|
|
1570
|
+
if (focus) emitSelection(focus.dataset.planrThreadFocus);
|
|
1571
|
+
};
|
|
1572
|
+
const onSubmit = (event) => {
|
|
1573
|
+
const form = event.target?.closest?.("[data-planr-reply-form]");
|
|
1574
|
+
if (!form) return;
|
|
1575
|
+
event.preventDefault();
|
|
1576
|
+
const textarea = form.elements.namedItem("reply");
|
|
1577
|
+
const comment = textarea.value;
|
|
1578
|
+
if (!comment.trim()) return;
|
|
1579
|
+
const wasExpanded = expandedReplies.has(form.dataset.planrReplyForm);
|
|
1580
|
+
try {
|
|
1581
|
+
textarea.setAttribute("aria-invalid", "false");
|
|
1582
|
+
textarea.value = "";
|
|
1583
|
+
replyDrafts.delete(form.dataset.planrReplyForm);
|
|
1584
|
+
expandedReplies.delete(form.dataset.planrReplyForm);
|
|
1585
|
+
controller.dispatch({
|
|
1586
|
+
type: "add-reply",
|
|
1587
|
+
pinId: form.dataset.planrReplyForm,
|
|
1588
|
+
comment
|
|
1589
|
+
});
|
|
1590
|
+
emitDraftChange();
|
|
1591
|
+
announce2("Reply added");
|
|
1592
|
+
focusThread(form.dataset.planrReplyForm);
|
|
1593
|
+
clearError();
|
|
1594
|
+
} catch (error) {
|
|
1595
|
+
textarea.value = comment;
|
|
1596
|
+
replyDrafts.set(form.dataset.planrReplyForm, comment);
|
|
1597
|
+
if (wasExpanded) expandedReplies.add(form.dataset.planrReplyForm);
|
|
1598
|
+
textarea?.setAttribute("aria-invalid", "true");
|
|
1599
|
+
showError(error);
|
|
1600
|
+
textarea?.focus();
|
|
1601
|
+
}
|
|
1602
|
+
};
|
|
1603
|
+
const onOverallChange = () => {
|
|
1604
|
+
try {
|
|
1605
|
+
const value = overall.value;
|
|
1606
|
+
controller.dispatch({ type: "set-overall", overall: value });
|
|
1607
|
+
overallDirty = false;
|
|
1608
|
+
announce2("Overall note updated");
|
|
1609
|
+
clearError();
|
|
1610
|
+
} catch (error) {
|
|
1611
|
+
showError(error);
|
|
1612
|
+
}
|
|
1613
|
+
};
|
|
1614
|
+
const onDecision = (event) => {
|
|
1615
|
+
try {
|
|
1616
|
+
const selected = event.currentTarget.dataset.planrDecision;
|
|
1617
|
+
const decision = controller.getReview()?.decision === selected ? "pending" : selected;
|
|
1618
|
+
controller.dispatch({ type: "set-decision", decision });
|
|
1619
|
+
announce2(decisionCopy(decision));
|
|
1620
|
+
clearError();
|
|
1621
|
+
} catch (error) {
|
|
1622
|
+
showError(error);
|
|
1623
|
+
}
|
|
1624
|
+
};
|
|
1625
|
+
const onSelect = (event) => {
|
|
1626
|
+
if (event.detail?.source === "thread" || typeof event.detail?.pinId !== "string") return;
|
|
1627
|
+
try {
|
|
1628
|
+
controller.selectPin(event.detail.pinId);
|
|
1629
|
+
focusThread(event.detail.pinId);
|
|
1630
|
+
} catch (error) {
|
|
1631
|
+
showError(error);
|
|
1632
|
+
}
|
|
1633
|
+
};
|
|
1634
|
+
let lastOpened = null;
|
|
1635
|
+
const onThreadOpen = (event) => {
|
|
1636
|
+
const thread2 = event.target.closest?.(".planr-thread[data-planr-pin-id]");
|
|
1637
|
+
if (!thread2 || !slot.contains(thread2)) return;
|
|
1638
|
+
const pin = controller.getReview()?.pins.find((entry) => entry.id === thread2.dataset.planrPinId);
|
|
1639
|
+
const key = pin && `${pin.id}:${pin.replies.map((reply2) => reply2.id).join(",")}`;
|
|
1640
|
+
if (!key || lastOpened === key) return;
|
|
1641
|
+
lastOpened = key;
|
|
1642
|
+
presentation.onThreadOpen?.(pin.id, controller.getState());
|
|
1643
|
+
root.dispatchEvent(
|
|
1644
|
+
new window.CustomEvent("planr:artifact-thread-open", {
|
|
1645
|
+
bubbles: true,
|
|
1646
|
+
detail: Object.freeze({ pinId: pin.id })
|
|
1647
|
+
})
|
|
1648
|
+
);
|
|
1649
|
+
};
|
|
1650
|
+
const onDraftInput = (event) => {
|
|
1651
|
+
const form = event.target.closest?.("[data-planr-reply-form]");
|
|
1652
|
+
if (form)
|
|
1653
|
+
form.querySelector(".planr-reply-send").disabled = !form.elements.namedItem("reply").value.trim();
|
|
1654
|
+
emitDraftChange();
|
|
1655
|
+
};
|
|
1656
|
+
const onOverallInput = () => {
|
|
1657
|
+
overallDirty = true;
|
|
1658
|
+
emitDraftChange();
|
|
1659
|
+
};
|
|
1660
|
+
const onCompositionStart = () => {
|
|
1661
|
+
composing = true;
|
|
1662
|
+
};
|
|
1663
|
+
const onCompositionEnd = () => {
|
|
1664
|
+
composing = false;
|
|
1665
|
+
if (renderQueued) {
|
|
1666
|
+
renderQueued = false;
|
|
1667
|
+
render();
|
|
1668
|
+
}
|
|
1669
|
+
};
|
|
1670
|
+
slot.addEventListener("input", onDraftInput);
|
|
1671
|
+
slot.addEventListener("focusin", onThreadOpen);
|
|
1672
|
+
slot.addEventListener("click", onThreadOpen);
|
|
1673
|
+
slot.addEventListener("compositionstart", onCompositionStart);
|
|
1674
|
+
slot.addEventListener("compositionend", onCompositionEnd);
|
|
1675
|
+
overall.addEventListener("input", onOverallInput);
|
|
1676
|
+
identityInput.addEventListener("input", onInput);
|
|
1677
|
+
slot.addEventListener("click", onClick);
|
|
1678
|
+
slot.addEventListener("submit", onSubmit);
|
|
1679
|
+
slot.addEventListener("keydown", onKeyDown);
|
|
1680
|
+
overall.addEventListener("change", onOverallChange);
|
|
1681
|
+
for (const button of decisionButtons) button.addEventListener("click", onDecision);
|
|
1682
|
+
root.addEventListener(ARTIFACT_REVIEW_SELECT_EVENT, onSelect);
|
|
1683
|
+
render();
|
|
1684
|
+
return Object.freeze({
|
|
1685
|
+
controller,
|
|
1686
|
+
getReview: () => controller.getReview(),
|
|
1687
|
+
getState: () => controller.getState(),
|
|
1688
|
+
getIdentity: () => controller.getIdentity(),
|
|
1689
|
+
setIdentity: (value) => controller.setIdentity(value),
|
|
1690
|
+
dispatch: (action) => controller.dispatch(action),
|
|
1691
|
+
replaceReview: (review) => controller.replaceReview(review),
|
|
1692
|
+
selectPin: (pinId) => controller.selectPin(pinId),
|
|
1693
|
+
render,
|
|
1694
|
+
setPresentation(options = {}) {
|
|
1695
|
+
if (options.filterKey !== presentation.filterKey || options.pageSize !== void 0 && options.pageSize !== presentation.pageSize)
|
|
1696
|
+
visibleLimit = options.pageSize || presentation.pageSize || Infinity;
|
|
1697
|
+
presentation = { ...presentation, ...options };
|
|
1698
|
+
render();
|
|
1699
|
+
},
|
|
1700
|
+
snapshotDrafts,
|
|
1701
|
+
getReviewOf: () => controller.getReview()?.reviewOf ?? reviewOf,
|
|
1702
|
+
restoreDrafts(snapshot2) {
|
|
1703
|
+
const digest7 = controller.getReview()?.reviewOf ?? reviewOf;
|
|
1704
|
+
if (!snapshot2 || snapshot2.reviewOf !== digest7) return false;
|
|
1705
|
+
if (typeof snapshot2.identity === "string") {
|
|
1706
|
+
const name = snapshot2.identity.slice(0, ARTIFACT_REVIEW_LIMITS.authorName);
|
|
1707
|
+
controller.setIdentity(name.trim() ? { ...controller.getIdentity(), name } : null);
|
|
1708
|
+
}
|
|
1709
|
+
for (const [id7, value] of Object.entries(snapshot2.replies ?? {}).slice(
|
|
1710
|
+
0,
|
|
1711
|
+
ARTIFACT_REVIEW_LIMITS.pins
|
|
1712
|
+
)) {
|
|
1713
|
+
if (id7.length <= ARTIFACT_REVIEW_LIMITS.id && typeof value === "string")
|
|
1714
|
+
replyDrafts.set(id7, value.slice(0, ARTIFACT_REVIEW_LIMITS.text));
|
|
1715
|
+
}
|
|
1716
|
+
expandedReplies.clear();
|
|
1717
|
+
const expanded = Array.isArray(snapshot2.expandedReplies) ? snapshot2.expandedReplies : [...replyDrafts.keys()];
|
|
1718
|
+
for (const id7 of expanded.slice(0, ARTIFACT_REVIEW_LIMITS.pins))
|
|
1719
|
+
if (typeof id7 === "string" && id7.length <= ARTIFACT_REVIEW_LIMITS.id)
|
|
1720
|
+
expandedReplies.add(id7);
|
|
1721
|
+
for (const [key, value] of Object.entries(snapshot2.fields ?? {}).slice(
|
|
1722
|
+
0,
|
|
1723
|
+
ARTIFACT_REVIEW_LIMITS.pins
|
|
1724
|
+
)) {
|
|
1725
|
+
if (key.length <= 512 && typeof value === "string")
|
|
1726
|
+
extraDrafts.set(key, value.slice(0, ARTIFACT_REVIEW_LIMITS.text));
|
|
1727
|
+
}
|
|
1728
|
+
overallDirty = snapshot2.overall?.dirty === true;
|
|
1729
|
+
if (overallDirty && typeof snapshot2.overall?.value === "string")
|
|
1730
|
+
overall.value = snapshot2.overall.value.slice(0, ARTIFACT_REVIEW_LIMITS.text);
|
|
1731
|
+
render({ capture: false });
|
|
1732
|
+
return true;
|
|
1733
|
+
},
|
|
1734
|
+
focusThread,
|
|
1735
|
+
destroy() {
|
|
1736
|
+
if (destroyed) return;
|
|
1737
|
+
destroyed = true;
|
|
1738
|
+
unsubscribe();
|
|
1739
|
+
slot.removeEventListener("input", onDraftInput);
|
|
1740
|
+
slot.removeEventListener("focusin", onThreadOpen);
|
|
1741
|
+
slot.removeEventListener("click", onThreadOpen);
|
|
1742
|
+
slot.removeEventListener("compositionstart", onCompositionStart);
|
|
1743
|
+
slot.removeEventListener("compositionend", onCompositionEnd);
|
|
1744
|
+
overall.removeEventListener("input", onOverallInput);
|
|
1745
|
+
identityInput.removeEventListener("input", onInput);
|
|
1746
|
+
slot.removeEventListener("click", onClick);
|
|
1747
|
+
slot.removeEventListener("submit", onSubmit);
|
|
1748
|
+
slot.removeEventListener("keydown", onKeyDown);
|
|
1749
|
+
overall.removeEventListener("change", onOverallChange);
|
|
1750
|
+
for (const button of decisionButtons) button.removeEventListener("click", onDecision);
|
|
1751
|
+
root.removeEventListener(ARTIFACT_REVIEW_SELECT_EVENT, onSelect);
|
|
1752
|
+
if (ownsController) controller.destroy();
|
|
1753
|
+
}
|
|
1754
|
+
});
|
|
1755
|
+
}
|