dsh-ptc-plus 0.3.2 → 0.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (206) hide show
  1. package/README.en.md +63 -80
  2. package/README.md +63 -80
  3. package/assets/ptc-plus-bindings-en.png +0 -0
  4. package/assets/ptc-plus-bindings-zh.png +0 -0
  5. package/assets/ptc-plus-repair-en.png +0 -0
  6. package/assets/ptc-plus-repair-zh.png +0 -0
  7. package/assets/ptc-plus-repl-workspace-en.png +0 -0
  8. package/assets/ptc-plus-repl-workspace-zh.png +0 -0
  9. package/assets/ptc-plus-settings-en.png +0 -0
  10. package/assets/ptc-plus-settings-zh.png +0 -0
  11. package/client.js +30827 -742
  12. package/compiler-amaro.cjs +5 -0
  13. package/compiler-amaro.cjs.map +6 -0
  14. package/compiler-asset-124f80c1bba92ff5fe79cfec6e8ad518f476de4a42514cae3919538887f510e8.txt +1 -0
  15. package/compiler-assets.json +3 -0
  16. package/compiler-core.cjs +556 -0
  17. package/compiler-core.cjs.map +6 -0
  18. package/compiler-platform.cjs +2 -0
  19. package/compiler-platform.cjs.map +6 -0
  20. package/compiler-typescript.cjs +388 -0
  21. package/compiler-typescript.cjs.map +6 -0
  22. package/docs/README.md +7 -0
  23. package/docs/adr/0004-declare-program-bindings-at-the-host.md +1 -1
  24. package/docs/adr/0006-rewrite-module-syntax-with-an-ast.md +5 -3
  25. package/docs/adr/0007-separate-worker-transport-from-session-semantics.md +1 -1
  26. package/docs/adr/0009-bound-expensive-acceptance-default.md +2 -0
  27. package/docs/adr/0010-session-log-derived-recovery-tips.md +59 -5
  28. package/docs/adr/0011-isolate-the-local-recovery-tip-provider.md +1 -1
  29. package/docs/adr/0012-preserve-mixed-destructuring-initialization.md +3 -1
  30. package/docs/adr/0013-isolate-repl-redeclaration-convenience.md +3 -1
  31. package/docs/adr/0014-persist-cell-rewrite-policy.md +6 -2
  32. package/docs/adr/0015-preserve-source-positions-through-rewrites.md +1 -1
  33. package/docs/adr/0016-archive-resolved-review-findings.md +1 -1
  34. package/docs/adr/0017-track-the-latest-dsh-public-surface.md +49 -0
  35. package/docs/adr/0018-use-versioned-fixture-for-ab-benchmark.md +3 -3
  36. package/docs/adr/0019-plugin-settings-and-kill-switch.md +5 -3
  37. package/docs/adr/0020-optional-cordis-tools-in-ptc-mode.md +2 -2
  38. package/docs/adr/0021-separate-host-bootstrap-from-session-runtime.md +12 -0
  39. package/docs/adr/0023-global-user-bindings.md +104 -0
  40. package/docs/adr/0024-repl-console-observation.md +33 -0
  41. package/docs/adr/0025-use-versioned-logical-binding-identities.md +119 -0
  42. package/docs/adr/0026-persist-the-delivery-verdict-against-the-verified-candidate.md +21 -0
  43. package/docs/adr/0027-run-the-session-kernel-in-a-killable-helper-process.md +37 -0
  44. package/docs/adr/0028-attach-to-the-host-ptc-execution-seam.md +103 -0
  45. package/docs/adr/0029-keep-plugin-unavailability-out-of-the-host-agent-lifecycle.md +27 -0
  46. package/docs/adr/0030-serve-both-typert-codec-generations-from-one-codec.md +27 -0
  47. package/docs/architecture.md +77 -21
  48. package/docs/binding-review-placement-design.md +200 -0
  49. package/docs/capability-projection.md +1 -1
  50. package/docs/client-ui.md +95 -19
  51. package/docs/durability-design.md +28 -10
  52. package/docs/evaluation.md +78 -6
  53. package/docs/installation.md +41 -6
  54. package/docs/publishing.md +5 -3
  55. package/docs/runtime-reference.md +76 -25
  56. package/docs/semantic-validation.md +29 -0
  57. package/docs/stateful-binding-updates-proposal.md +759 -0
  58. package/docs/user-bindings.en.md +66 -0
  59. package/docs/user-bindings.md +66 -0
  60. package/docs/value-wire.md +3 -3
  61. package/docs/verification-optimization-work.md +41 -0
  62. package/docs/verification.md +71 -0
  63. package/index.js +369 -92
  64. package/internal/ast-traversal.js +5 -2
  65. package/internal/async-local-storage-intrinsics.js +38 -0
  66. package/internal/binding-declaration-contract.js +8 -0
  67. package/internal/binding-descriptors.js +1 -0
  68. package/internal/binding-pattern-traversal.js +25 -0
  69. package/internal/binding-pattern.js +26 -29
  70. package/internal/binding-source-analysis.js +708 -0
  71. package/internal/binding-update-policy.js +72 -0
  72. package/internal/bounded-worker-round.js +56 -0
  73. package/internal/callable-definition-environment.js +42 -0
  74. package/internal/callable-source-catalog.js +129 -0
  75. package/internal/callable-source-encoding.js +47 -0
  76. package/internal/callable-source-facts.js +459 -0
  77. package/internal/callable-source-format.js +2 -0
  78. package/internal/callable-source-lowering.js +123 -0
  79. package/internal/cell-analysis-contract.js +25 -0
  80. package/internal/cell-analysis.js +253 -290
  81. package/internal/cell-completion.js +15 -0
  82. package/internal/cell-error.js +7 -0
  83. package/internal/cell-parser.js +66 -0
  84. package/internal/cell-rewriter.js +3 -677
  85. package/internal/commonjs-export-evidence.js +63 -0
  86. package/internal/commonjs-source-evidence.js +72 -0
  87. package/internal/compiler-amaro.cjs +2 -0
  88. package/internal/compiler-compression.js +12 -0
  89. package/internal/compiler-data.js +58 -0
  90. package/internal/compiler-descriptors.js +54 -0
  91. package/internal/compiler-dynamic-cache.js +48 -0
  92. package/internal/compiler-entry.js +35 -0
  93. package/internal/compiler-generated-names.js +17 -0
  94. package/internal/compiler-intrinsics.js +50 -0
  95. package/internal/compiler-module-links.js +37 -0
  96. package/internal/compiler-operations.js +61 -0
  97. package/internal/compiler-platform-factory.js +84 -0
  98. package/internal/compiler-platform.js +136 -0
  99. package/internal/compiler-record-roles.js +53 -0
  100. package/internal/compiler-region-output.js +259 -0
  101. package/internal/compiler-resource-lowering.js +103 -0
  102. package/internal/compiler-scope-facts.js +115 -0
  103. package/internal/compiler-service.js +109 -0
  104. package/internal/compiler-source-cache.js +32 -0
  105. package/internal/compiler-source-pieces.js +93 -0
  106. package/internal/compiler-source-regions.js +132 -0
  107. package/internal/compiler-storage-source.js +54 -0
  108. package/internal/compiler-text.js +15 -0
  109. package/internal/compiler-typescript.cjs +2 -0
  110. package/internal/config-spec.js +83 -27
  111. package/internal/cordis-tools-owner.js +130 -21
  112. package/internal/diagnostic.js +27 -1
  113. package/internal/direct-surface-owner.js +76 -23
  114. package/internal/dynamic-binding-evidence.js +16 -0
  115. package/internal/dynamic-environment-compiler.js +779 -0
  116. package/internal/dynamic-environment-integration.js +91 -0
  117. package/internal/dynamic-environment-runtime.js +383 -0
  118. package/internal/dynamic-native-calls.js +153 -0
  119. package/internal/dynamic-native-runtime.js +262 -0
  120. package/internal/dynamic-scope-analysis.js +90 -0
  121. package/internal/edit-transport-owner.js +27 -2
  122. package/internal/execution-seam-compat.js +193 -0
  123. package/internal/failure-reporting.js +136 -28
  124. package/internal/host-rpc.js +66 -0
  125. package/internal/isolated-worker.js +555 -0
  126. package/internal/kernel-child.js +139 -0
  127. package/internal/kernel-worker.js +1179 -233
  128. package/internal/language-semantics.js +14 -0
  129. package/internal/legacy-binding-catalog.js +48 -0
  130. package/internal/legacy-cell-analysis.js +600 -0
  131. package/internal/legacy-cell-parser.js +50 -0
  132. package/internal/legacy-cell-rewriter.js +631 -0
  133. package/internal/legacy-repl-convenience.js +291 -0
  134. package/internal/managed-module-operations.js +170 -0
  135. package/internal/module-compilation.js +322 -0
  136. package/internal/module-runtime-platform.js +19 -0
  137. package/internal/module-transform-contract.js +42 -0
  138. package/internal/native-await-declarations.js +68 -0
  139. package/internal/native-root-compilation.js +54 -0
  140. package/internal/native-root-dynamic.js +67 -0
  141. package/internal/record-utils.js +24 -15
  142. package/internal/recovery-tips.js +27 -15
  143. package/internal/rejected-cell-editor.js +13 -8
  144. package/internal/repl-convenience.js +2 -289
  145. package/internal/repl-memory-projection.js +81 -12
  146. package/internal/repl-observation-interest.js +70 -0
  147. package/internal/repl-rewrite-contract.js +6 -0
  148. package/internal/repl-scope-normalizer.js +2091 -0
  149. package/internal/repl-value-observer.js +117 -0
  150. package/internal/rpc-contract.js +52 -0
  151. package/internal/runtime-bridge-owner.js +172 -28
  152. package/internal/runtime-config.js +48 -52
  153. package/internal/runtime-contexts.js +82 -10
  154. package/internal/runtime-intrinsics.js +101 -0
  155. package/internal/runtime-messages.js +111 -0
  156. package/internal/session-cell-executor.js +402 -87
  157. package/internal/session-journal-recovery.js +684 -0
  158. package/internal/session-journal-schema.js +123 -0
  159. package/internal/session-journal.js +199 -654
  160. package/internal/session-log-view.js +42 -9
  161. package/internal/session-runtime.js +257 -95
  162. package/internal/session-state.js +247 -71
  163. package/internal/source-position-map.js +230 -71
  164. package/internal/stateful-module-compiler.js +154 -0
  165. package/internal/stateful-module-runtime.js +303 -0
  166. package/internal/stateful-root-compiler.js +600 -0
  167. package/internal/stateful-root-runtime.js +340 -0
  168. package/internal/typert-codec-compat.js +28 -0
  169. package/internal/typescript-transform.js +31 -0
  170. package/internal/user-binding-authoring.js +70 -0
  171. package/internal/user-binding-console-worker.js +244 -0
  172. package/internal/user-binding-console.js +148 -0
  173. package/internal/user-binding-draft-projection.js +335 -0
  174. package/internal/user-binding-model-context.js +31 -0
  175. package/internal/user-binding-runner.js +56 -0
  176. package/internal/user-bindings-owner.js +1065 -0
  177. package/internal/user-bindings-store.js +293 -0
  178. package/internal/user-bindings.js +361 -0
  179. package/internal/value-wire-schema.js +28 -0
  180. package/internal/value-wire.js +130 -91
  181. package/internal/worker-client.js +57 -67
  182. package/internal/worker-cwd-virtualization.js +80 -53
  183. package/internal/worker-environment.js +65 -0
  184. package/internal/worker-output-relay.js +29 -0
  185. package/internal/worker-repl-realm.js +134 -0
  186. package/internal/worker-shutdown.js +14 -0
  187. package/package.json +76 -7
  188. package/src/client-activity.js +95 -53
  189. package/src/client-authoring-view.js +580 -0
  190. package/src/client-binding-review.js +230 -0
  191. package/src/client-bindings-data.js +29 -0
  192. package/src/client-catalog.js +127 -0
  193. package/src/client-code-editor.js +84 -0
  194. package/src/client-console.js +112 -0
  195. package/src/client-copy.js +370 -0
  196. package/src/client-feature-gates.js +62 -0
  197. package/src/client-host-compat.js +150 -0
  198. package/src/client-indicator-view.js +267 -0
  199. package/src/client-repl-view.js +292 -0
  200. package/src/client-rpc.js +20 -0
  201. package/src/client-settings-view.js +221 -0
  202. package/src/client-styles.js +106 -0
  203. package/src/client-tool-view.js +108 -0
  204. package/src/client-transport.js +159 -0
  205. package/src/client-workbench.js +521 -0
  206. package/src/client.js +233 -615
package/src/client.js CHANGED
@@ -1,149 +1,31 @@
1
- import { CONFIG_FIELDS, CONFIG_GROUPS, SETTINGS_NAMESPACE } from '../internal/config-spec.js'
2
- import { derivePtcToolView } from './client-activity.js'
1
+ import { createClientRpc } from './client-rpc.js'
2
+ import { RPC_CONTRACTS } from '../internal/rpc-contract.js'
3
+ import { SETTINGS_NAMESPACE } from '../internal/config-spec.js'
4
+ import { createPtcToolView } from './client-tool-view.js'
5
+ import { createPtcSettingsView } from './client-settings-view.js'
6
+ import { createReplView } from './client-repl-view.js'
7
+ import { createAuthoringView } from './client-authoring-view.js'
8
+ import { createIndicatorView } from './client-indicator-view.js'
9
+ import { createTypeScriptEditor } from './client-code-editor.js'
10
+ import { createBindingConsole } from './client-console.js'
11
+ import {
12
+ isIdleSessionComposer,
13
+ publishSettingsCard,
14
+ sessionUsesPtcPreset,
15
+ useSessionPreset,
16
+ watchCurrentSessionPreset,
17
+ } from './client-host-compat.js'
3
18
  import {
4
19
  normalizeReplMemorySnapshot,
5
20
  unavailableReplMemorySnapshot,
6
21
  } from '../internal/repl-memory-projection.js'
7
-
8
- const CLIENT_STYLE_ID = 'ptc-plus-client-style'
9
- const CLIENT_CSS = `
10
- .ptcPlusCard{border:1px solid var(--dsw-alias-border-l2,rgba(0,0,0,.1));background:var(--dsw-alias-bg-layer-3,#fff);border-radius:8px;list-style:none;overflow:hidden}
11
- .ptcPlusHeader{appearance:none;width:100%;display:flex;align-items:center;gap:12px;padding:14px 16px;border:0;background:transparent;color:inherit;font:inherit;text-align:left;cursor:pointer;transition:background-color .16s ease}
12
- .ptcPlusHeader:hover{background:var(--dsw-alias-interactive-bg-hover,rgba(38,49,72,.06))}.ptcPlusHeader:focus-visible,.ptcPlusButton:focus-visible,.ptcPlusInput:focus-visible{outline:2px solid var(--dsw-alias-interactive-primary,#4d6bfe);outline-offset:-2px}
13
- .ptcPlusHeadText{display:flex;flex:1;min-width:0;flex-direction:column;align-items:flex-start;gap:1px}.ptcPlusName{font-size:14px;font-weight:600;line-height:20px}.ptcPlusDescription{color:var(--dsw-alias-label-tertiary,#74777d);font-size:12px;line-height:18px;overflow-wrap:anywhere}.ptcPlusStatus{display:inline-flex;align-items:center;flex:none;padding:2px 8px;border-radius:999px;font-size:11px;font-weight:500;line-height:16px}.ptcPlusStatus[data-enabled=true]{color:var(--dsw-alias-state-success-primary,#16794f);background:var(--dsw-alias-state-success-tertiary,#e7f7ef)}.ptcPlusStatus[data-enabled=false]{color:var(--dsw-alias-label-tertiary,#74777d);background:var(--dsw-alias-interactive-bg-hover,rgba(38,49,72,.06))}
14
- .ptcPlusChevron{display:flex;color:var(--dsw-alias-label-tertiary,#74777d);transition:transform .18s ease}.ptcPlusChevron[data-open=true]{transform:rotate(180deg)}.ptcPlusBody{display:grid;grid-template-rows:0fr;transition:grid-template-rows .2s ease}.ptcPlusBody[data-open=true]{grid-template-rows:1fr}.ptcPlusBodyInner{min-height:0;overflow:hidden}.ptcPlusFields{margin:0 16px;padding:8px 0 12px;border-top:1px solid var(--dsw-alias-border-l2,rgba(0,0,0,.1))}
15
- .ptcPlusGroup+.ptcPlusGroup{margin-top:14px}.ptcPlusGroupTitle{margin:0;padding:8px 0 5px;color:var(--dsw-alias-label-secondary,#52565d);font-size:11px;font-weight:600;letter-spacing:0;line-height:16px}.ptcPlusRow{display:flex;align-items:center;gap:12px;min-height:48px;border-top:1px solid var(--dsw-alias-border-l2,rgba(0,0,0,.1))}.ptcPlusRow:first-child{border-top:0}.ptcPlusMain{flex:1;min-width:0}.ptcPlusLabel{font-size:14px;font-weight:500;line-height:20px}.ptcPlusDetail,.ptcPlusMessage{color:var(--dsw-alias-label-tertiary,#74777d);font-size:12px;line-height:18px;overflow-wrap:anywhere}.ptcPlusInput{box-sizing:border-box;min-width:72px;width:140px;padding:5px 8px;border:1px solid var(--dsw-alias-border-l2,rgba(0,0,0,.1));border-radius:6px;background:var(--dsw-alias-bg-layer-1,#fff);color:inherit;font:12px/18px ui-monospace,SFMono-Regular,Consolas,monospace}.ptcPlusCheck{width:18px;height:18px;accent-color:var(--dsw-alias-interactive-primary,#4d6bfe)}
16
- .ptcPlusFooter{display:flex;align-items:center;justify-content:space-between;gap:12px;margin-top:8px}.ptcPlusButton{min-height:32px;padding:0 12px;border:1px solid var(--dsw-alias-border-l2,rgba(0,0,0,.1));border-radius:6px;background:transparent;color:inherit;cursor:pointer;font:500 13px/20px inherit;transition:background-color .16s ease,border-color .16s ease}.ptcPlusButton:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover,rgba(38,49,72,.06))}.ptcPlusButton:disabled,.ptcPlusInput:disabled,.ptcPlusCheck:disabled{cursor:not-allowed;opacity:.55}
17
- .ptcPlusActiveShell{display:inline-flex;align-items:center}.ptcPlusActive{appearance:none;display:inline-flex;height:24px;align-items:center;gap:5px;padding:0 8px;border:1px solid color-mix(in srgb,var(--dsw-alias-state-success-primary,#16794f) 32%,transparent);border-radius:6px;background:var(--dsw-alias-state-success-tertiary,#e7f7ef);color:var(--dsw-alias-state-success-primary,#16794f);cursor:help;font:600 12px/18px inherit;white-space:nowrap;transition:background-color .14s ease,border-color .14s ease}.ptcPlusActive:hover,.ptcPlusActive[aria-expanded=true]{border-color:color-mix(in srgb,var(--dsw-alias-state-success-primary,#16794f) 48%,transparent);background:color-mix(in srgb,var(--dsw-alias-state-success-primary,#16794f) 16%,var(--dsw-alias-bg-layer-3,#fff))}.ptcPlusActive:focus-visible{outline:2px solid var(--dsw-alias-state-success-primary,#16794f);outline-offset:2px}.ptcPlusReplPopover{position:fixed;z-index:2147483000;inset:auto;display:none;box-sizing:border-box;margin:0;padding:0;border:0;overflow:visible;background:transparent;color:var(--dsw-alias-label-primary,#18191c)}.ptcPlusReplPopover:popover-open,.ptcPlusReplPopover[data-open=true]{display:block}.ptcPlusReplPopover::backdrop{background:transparent}.ptcPlusReplCard{display:flex;max-height:inherit;overflow:hidden;flex-direction:column;border:1px solid color-mix(in srgb,var(--dsw-alias-state-success-primary,#16794f) 22%,var(--dsw-alias-border-l2,rgba(0,0,0,.1)));border-top:3px solid var(--dsw-alias-state-success-primary,#16794f);border-radius:8px;background:var(--dsw-alias-bg-layer-3,#fff);box-shadow:0 14px 36px rgba(16,24,40,.2),0 3px 10px rgba(16,24,40,.1);color:var(--dsw-alias-label-primary,#18191c);white-space:normal}.ptcPlusReplHead{display:grid;grid-template-columns:auto minmax(0,1fr);align-items:center;column-gap:8px;padding:11px 13px 10px;border-bottom:1px solid var(--dsw-alias-border-l2,rgba(0,0,0,.1));background:color-mix(in srgb,var(--dsw-alias-state-success-primary,#16794f) 7%,var(--dsw-alias-bg-layer-3,#fff))}.ptcPlusReplStatusDot{grid-row:1/3;width:8px;height:8px;border-radius:50%;background:var(--dsw-alias-state-success-primary,#16794f);box-shadow:0 0 0 3px color-mix(in srgb,var(--dsw-alias-state-success-primary,#16794f) 14%,transparent)}.ptcPlusReplTitle,.ptcPlusReplSummary{display:block;min-width:0}.ptcPlusReplTitle{font-size:13px;font-weight:600;line-height:19px}.ptcPlusReplSummary{color:var(--dsw-alias-label-tertiary,#74777d);font-size:11px;line-height:16px}.ptcPlusReplList{min-height:0;margin:0;padding:5px 0;overflow:auto;overscroll-behavior:contain;list-style:none;scrollbar-gutter:stable}.ptcPlusReplBinding{display:grid;grid-template-columns:minmax(0,1fr) auto;align-items:center;gap:4px 10px;padding:7px 12px}.ptcPlusReplBinding:hover{background:var(--dsw-alias-interactive-bg-hover,rgba(38,49,72,.06))}.ptcPlusReplIdentity{display:flex;min-width:0;align-items:center;gap:7px}.ptcPlusReplName{min-width:0;overflow:hidden;font:12px/18px ui-monospace,SFMono-Regular,Consolas,monospace;text-overflow:ellipsis;white-space:nowrap}.ptcPlusReplKind{flex:none;padding:1px 6px;border:1px solid color-mix(in srgb,currentColor 22%,transparent);border-radius:999px;background:color-mix(in srgb,currentColor 10%,transparent);font-size:10px;font-weight:600;line-height:15px}.ptcPlusReplKind[data-kind=variable]{color:var(--dsw-alias-interactive-primary,#315fbd)}.ptcPlusReplKind[data-kind=function]{color:#7651b5}.ptcPlusReplKind[data-kind=class]{color:var(--dsw-alias-state-warning-primary,#946200)}.ptcPlusReplKind[data-kind=import]{color:#14766f}.ptcPlusReplPreview{grid-column:1;min-width:0;overflow:hidden;color:var(--dsw-alias-label-tertiary,#74777d);font:11px/16px ui-monospace,SFMono-Regular,Consolas,monospace;text-overflow:ellipsis;white-space:nowrap}.ptcPlusReplInspect{grid-column:2;grid-row:1/3;display:inline-flex;align-items:center;gap:4px;padding:3px 5px;border:0;border-radius:4px;background:transparent;color:var(--dsw-alias-label-secondary,#52565d);cursor:pointer;font:500 11px/17px inherit;white-space:nowrap}.ptcPlusReplInspect:hover{background:var(--dsw-alias-interactive-bg-hover,rgba(38,49,72,.06));color:var(--dsw-alias-interactive-primary,#4d6bfe)}.ptcPlusReplInspect:focus-visible{outline:2px solid var(--dsw-alias-interactive-primary,#4d6bfe);outline-offset:1px}.ptcPlusReplDefinition{grid-column:1/-1;min-width:0;margin-top:4px;padding:8px;border-left:2px solid var(--dsw-alias-interactive-primary,#4d6bfe);background:var(--dsw-alias-bg-layer-2,rgba(38,49,72,.03))}.ptcPlusReplLocation{display:block;margin-bottom:5px;color:var(--dsw-alias-label-tertiary,#74777d);font-size:10px;line-height:15px}.ptcPlusReplCode{max-height:180px;margin:0;overflow:auto;color:inherit;font:11px/16px ui-monospace,SFMono-Regular,Consolas,monospace;white-space:pre-wrap;overflow-wrap:anywhere}.ptcPlusReplEmpty,.ptcPlusReplMore{display:block;color:var(--dsw-alias-label-tertiary,#74777d)}.ptcPlusReplEmpty{padding:18px 13px;font-size:12px;line-height:18px}.ptcPlusReplMore{padding:8px 13px;border-top:1px solid var(--dsw-alias-border-l2,rgba(0,0,0,.1));background:var(--dsw-alias-bg-layer-2,rgba(38,49,72,.03));font-size:11px;line-height:17px}
18
- .ptcPlusReplList{max-height:min(52vh,480px)}.ptcPlusReplBinding{grid-template-columns:minmax(0,1fr) 24px;gap:3px 8px;min-height:36px;padding:5px 12px;content-visibility:auto;contain-intrinsic-size:36px;cursor:pointer;transition:background-color .16s ease}.ptcPlusReplBinding:focus-visible{outline:2px solid var(--dsw-alias-interactive-primary,#4d6bfe);outline-offset:-2px}.ptcPlusReplBinding[data-expanded=true]{background:color-mix(in srgb,var(--dsw-alias-interactive-primary,#4d6bfe) 5%,transparent)}.ptcPlusReplName{grid-column:1}.ptcPlusReplName[data-kind=variable]{color:var(--dsw-alias-interactive-primary,#315fbd)}.ptcPlusReplName[data-kind=function]{color:#7651b5}.ptcPlusReplName[data-kind=class]{color:var(--dsw-alias-state-warning-primary,#946200)}.ptcPlusReplName[data-kind=import]{color:#14766f}.ptcPlusReplPreview{grid-column:1}.ptcPlusReplChevron{grid-column:2;grid-row:1/3;display:flex;align-items:center;justify-content:center;color:var(--dsw-alias-label-tertiary,#74777d);transition:transform .2s ease}.ptcPlusReplChevron[data-open=true]{transform:rotate(180deg)}.ptcPlusReplDefinitionWrap{grid-column:1/-1;display:grid;grid-template-rows:0fr;min-width:0;transition:grid-template-rows .24s cubic-bezier(.2,.7,.2,1)}.ptcPlusReplDefinitionWrap[data-open=true]{grid-template-rows:1fr}.ptcPlusReplDefinitionInner{min-height:0;overflow:hidden}
19
- .ptcPlusTool{display:flex;min-width:0;flex-direction:column}.ptcPlusToolPreview{display:flex;min-width:0;flex:1 1 auto;flex-direction:row;align-items:center;overflow:hidden;margin-left:7px}.ptcPlusToolPreview .ptcPlusFeatures{flex:0 1 auto;flex-wrap:nowrap;overflow:hidden;margin:0 0 0 7px}.ptcPlusToolSummaryLine{box-sizing:border-box;display:flex;min-width:0;min-height:20px;flex:1 1 auto;align-items:center;gap:7px;padding:0;color:inherit;line-height:20px}.ptcPlusToolSummary{box-sizing:border-box;display:flex;min-width:0;min-height:32px;align-items:center;gap:7px;padding:0;color:inherit;line-height:20px}.ptcPlusToolSummary[data-expandable=true]{cursor:pointer}.ptcPlusToolSummary[data-expandable=true]:hover .ptcPlusToolTitle{color:var(--dsw-alias-interactive-primary,#4d6bfe)}.ptcPlusToolSummary:focus-visible{outline:2px solid var(--dsw-alias-interactive-primary,#4d6bfe);outline-offset:2px}.ptcPlusToolLeading{display:flex;width:16px;height:20px;flex:none;align-items:center;justify-content:center;color:var(--dsw-alias-label-tertiary,#74777d)}.ptcPlusToolChevron{transition:transform .16s ease}.ptcPlusToolChevron[data-open=true]{transform:rotate(180deg)}.ptcPlusToolTitle{display:flex;height:20px;flex:none;align-items:center;font-size:13px;font-weight:500;line-height:20px}.ptcPlusToolState{display:flex;height:20px;flex:none;align-items:center;color:var(--dsw-alias-label-tertiary,#74777d);font-size:11px;line-height:20px}.ptcPlusToolSummaryLine[data-state=running] .ptcPlusToolState,.ptcPlusToolSummary[data-state=running] .ptcPlusToolState{color:var(--dsw-alias-interactive-primary,#4d6bfe)}.ptcPlusToolSummaryLine[data-state=error] .ptcPlusToolState,.ptcPlusToolSummary[data-state=error] .ptcPlusToolState{color:var(--dsw-alias-state-danger-primary,#c43d3d)}.ptcPlusToolSummaryLine[data-state=stopped] .ptcPlusToolState,.ptcPlusToolSummary[data-state=stopped] .ptcPlusToolState{color:var(--dsw-alias-state-warning-primary,#a15c00)}.ptcPlusToolSep{width:3px;height:3px;flex:none;border-radius:50%;background:var(--dsw-alias-label-tertiary,#74777d)}.ptcPlusToolDescription{display:flex;min-width:0;min-height:20px;flex:1 1 auto;align-items:center;overflow:hidden;color:var(--dsw-alias-label-secondary,#52565d);font-size:13px;line-height:20px;text-overflow:ellipsis;white-space:nowrap}.ptcPlusToolPreview .ptcPlusFeature{flex:none;max-width:180px;white-space:nowrap}.ptcPlusToolPreview .ptcPlusFeatureDetail{max-width:120px}.ptcPlusToolSummaryLine[data-state=error] .ptcPlusToolDescription,.ptcPlusToolSummary[data-state=error] .ptcPlusToolDescription{color:var(--dsw-alias-state-danger-primary,#c43d3d)}.ptcPlusToolSummaryLine[data-state=stopped] .ptcPlusToolDescription,.ptcPlusToolSummary[data-state=stopped] .ptcPlusToolDescription{color:var(--dsw-alias-state-warning-primary,#a15c00)}
20
- .ptcPlusFeatures{display:flex;min-width:0;flex-wrap:wrap;gap:3px 14px;margin:0 0 5px 23px}.ptcPlusFeature{display:inline-flex;min-width:0;align-items:center;gap:5px;color:var(--dsw-alias-label-secondary,#52565d);font-size:11px;line-height:17px}.ptcPlusFeature::before{width:4px;height:4px;flex:none;border-radius:50%;background:var(--dsw-alias-interactive-primary,#4d6bfe);content:''}.ptcPlusFeatureName{font-weight:500}.ptcPlusFeatureDetail{min-width:0;overflow:hidden;color:var(--dsw-alias-label-tertiary,#74777d);font-family:ui-monospace,SFMono-Regular,Consolas,monospace;text-overflow:ellipsis;white-space:nowrap}
21
- .ptcPlusToolBody{margin:4px 0 8px 23px;border-left:2px solid var(--dsw-alias-border-l2,rgba(0,0,0,.1));background:var(--dsw-alias-bg-layer-2,rgba(38,49,72,.03))}.ptcPlusToolSection{display:flex;min-width:0;flex-direction:column;gap:4px;padding:9px 11px}.ptcPlusToolSection+.ptcPlusToolSection{border-top:1px solid var(--dsw-alias-border-l2,rgba(0,0,0,.1))}.ptcPlusToolSectionLabel{color:var(--dsw-alias-label-tertiary,#74777d);font-size:10px;font-weight:600;line-height:16px;text-transform:uppercase}.ptcPlusToolCode{max-height:320px;margin:0;overflow:auto;color:inherit;font:12px/18px ui-monospace,SFMono-Regular,Consolas,monospace;white-space:pre-wrap;overflow-wrap:anywhere}.ptcPlusIoCard{display:flex;flex-direction:column;border:1px solid var(--dsw-alias-border-l1,rgba(0,0,0,.1));border-radius:12px;background:var(--dsw-alias-markdown-code-block,rgba(38,49,72,.06));overflow:hidden}.ptcPlusIoText{max-height:320px;margin:0;padding:12px 16px;overflow:auto;color:var(--dsw-alias-label-secondary,#52565d);font:12px/18px ui-monospace,SFMono-Regular,Consolas,monospace;white-space:pre-wrap;overflow-wrap:anywhere}.ptcPlusIoText[data-error]{color:var(--dsw-alias-state-error-primary,#c43d3d)}.ptcPlusInspect{display:inline-flex;align-self:flex-start;align-items:center;gap:4px;margin:4px 0 2px 4px;padding:2px 8px;border:1px solid var(--dsw-alias-border-l2,rgba(0,0,0,.1));border-radius:999px;background:var(--dsw-alias-bg-base,#fff);color:var(--dsw-alias-label-secondary,#52565d);cursor:pointer;opacity:0;font-size:11px;line-height:16px;transition:opacity .1s ease;display:inline-flex}.ptcPlusTool:hover .ptcPlusInspect,.ptcPlusInspect:focus-visible{opacity:1}.ptcPlusInspect:hover{background:var(--dsw-alias-interactive-bg-hover-solid,rgba(38,49,72,.06));color:var(--dsw-alias-label-primary,#18191c)}
22
- @media(max-width:560px){.ptcPlusHeader{padding:12px}.ptcPlusFields{margin:0 12px}.ptcPlusRow{align-items:flex-start;flex-direction:column;gap:6px;padding:10px 0}.ptcPlusInput{width:100%}.ptcPlusFooter{align-items:stretch;flex-direction:column}.ptcPlusButton{width:100%}.ptcPlusFeatures,.ptcPlusToolBody{margin-left:0}.ptcPlusToolSummary .ptcPlusToolDescription{white-space:normal;overflow-wrap:anywhere}}
23
- @media(prefers-reduced-motion:reduce){.ptcPlusHeader,.ptcPlusChevron,.ptcPlusBody,.ptcPlusButton,.ptcPlusActive,.ptcPlusToolChevron,.ptcPlusReplChevron,.ptcPlusReplDefinitionWrap,.ptcPlusInspect{transition:none}}
24
- /* The summary button owns disclosure; definition content is a separate grid item. */
25
- .ptcPlusReplBinding{padding:0;cursor:default}.ptcPlusReplBindingTrigger{appearance:none;display:grid;width:100%;grid-column:1/-1;grid-template-columns:minmax(0,1fr) 24px;gap:3px 8px;min-height:36px;padding:5px 12px;border:0;background:transparent;color:inherit;text-align:left;cursor:pointer;font:inherit;transition:background-color .16s ease}.ptcPlusReplBindingTrigger:hover,.ptcPlusReplBindingTrigger[aria-expanded=true]{background:color-mix(in srgb,var(--dsw-alias-interactive-primary,#4d6bfe) 5%,transparent)}.ptcPlusReplBindingTrigger:focus-visible{outline:2px solid var(--dsw-alias-interactive-primary,#4d6bfe);outline-offset:-2px}
26
- /* High-contrast TypeScript-like token colors adapt to the active text theme. */
27
- .ptcPlusReplCard .ptcPlusReplName[data-kind=variable]{color:color-mix(in srgb,#005cc5 78%,var(--dsw-alias-label-primary,#18191c))}
28
- .ptcPlusReplCard .ptcPlusReplName[data-kind=function]{color:color-mix(in srgb,#795e26 78%,var(--dsw-alias-label-primary,#18191c))}
29
- .ptcPlusReplCard .ptcPlusReplName[data-kind=class]{color:color-mix(in srgb,#267f99 78%,var(--dsw-alias-label-primary,#18191c))}
30
- .ptcPlusReplCard .ptcPlusReplName[data-kind=import]{color:color-mix(in srgb,#af00db 78%,var(--dsw-alias-label-primary,#18191c))}
31
- /* Keep the session-header action on the same compact 32px rhythm as DSH chrome. */
32
- .ptcPlusActiveShell{display:inline-flex;height:28px;align-items:center;justify-content:center;line-height:0;vertical-align:middle}.ptcPlusActive{box-sizing:border-box;height:28px;justify-content:center;gap:6px;padding:0 6px;border:0;background:transparent;font-family:inherit;font-size:13px;font-weight:500;line-height:18px}.ptcPlusActive::before{width:6px;height:6px;flex:none;border-radius:50%;background:currentColor;box-shadow:0 0 0 2px color-mix(in srgb,currentColor 18%,transparent);content:''}.ptcPlusActive:hover,.ptcPlusActive[aria-expanded=true]{background:var(--dsw-alias-interactive-bg-hover,rgba(38,49,72,.06))}.ptcPlusActiveLabel{display:inline-flex;height:18px;align-items:center;line-height:18px}
33
- `
34
-
35
- /** Locale namespace owning every settings-card string (field copy plus chrome). */
36
- const LOCALE_NS = 'settings.ptcPlus'
37
-
38
- /** Card chrome copy; field labels and hints ride the shared config spec. */
39
- const CHROME_COPY = Object.freeze({
40
- zh: Object.freeze({
41
- 'card.description': 'PTC 模式的会话级 TypeScript REPL。',
42
- 'status.enabled': '已启用',
43
- 'status.disabled': '已停用',
44
- 'action.expand': '展开 PTC Plus 设置',
45
- 'action.collapse': '收起 PTC Plus 设置',
46
- 'state.syncing': '正在同步设置...',
47
- 'state.unavailable': '当前 DSH 实例未提供设置服务',
48
- 'footer.live': '设置会在修改后立即生效',
49
- 'footer.readOnly': '当前设置为只读',
50
- 'status.applied': '设置已立即生效',
51
- 'status.conflict': '设置未生效,请检查设置冲突',
52
- 'status.failed': '设置失败:{error}',
53
- 'indicator.title': 'PTC Plus 已启用;查看当前可复用的 REPL 绑定',
54
- 'memory.title': 'REPL 可复用绑定',
55
- 'memory.count': '{count} 个可复用绑定',
56
- 'memory.empty': '当前没有可复用绑定',
57
- 'memory.unavailable': '当前绑定状态尚不可确认',
58
- 'memory.more': '另有 {count} 个绑定未显示',
59
- 'memory.kind.variable': '变量',
60
- 'memory.kind.function': '函数',
61
- 'memory.kind.class': '类',
62
- 'memory.kind.import': '导入',
63
- 'memory.location': '第 {line} 行,第 {column} 列',
64
- 'tool.code': '执行',
65
- 'tool.codeEdit': '修正执行',
66
- 'tool.running': '正在运行',
67
- 'tool.completed': '执行完成',
68
- 'tool.failed': '执行失败',
69
- 'tool.stopped': '执行已中断',
70
- 'tool.source': '源码',
71
- 'tool.result': '结果',
72
- 'tool.copy': '复制代码',
73
- 'tool.copied': '已复制',
74
- 'tool.inspect': '检查调用',
75
- 'feature.safeEdit': '安全编辑执行',
76
- 'feature.codeRun': '隔离执行 code.run',
77
- 'feature.stateSaved': '保存 REPL 状态',
78
- 'feature.stateRestored': '恢复 REPL 状态',
79
- 'feature.stateDeleted': '删除 REPL 状态',
80
- }),
81
- en: Object.freeze({
82
- 'card.description': 'The session-bound TypeScript REPL for PTC mode.',
83
- 'status.enabled': 'Enabled',
84
- 'status.disabled': 'Disabled',
85
- 'action.expand': 'Expand PTC Plus settings',
86
- 'action.collapse': 'Collapse PTC Plus settings',
87
- 'state.syncing': 'Syncing settings...',
88
- 'state.unavailable': 'This DSH instance does not provide a settings service',
89
- 'footer.live': 'Changes take effect immediately.',
90
- 'footer.readOnly': 'These settings are read-only.',
91
- 'status.applied': 'Setting applied immediately.',
92
- 'status.conflict': 'The setting did not take effect; check for conflicting settings.',
93
- 'status.failed': 'Could not save: {error}',
94
- 'indicator.title': 'PTC Plus is active; view reusable REPL bindings',
95
- 'memory.title': 'Reusable REPL bindings',
96
- 'memory.count': '{count} reusable bindings',
97
- 'memory.empty': 'No reusable bindings',
98
- 'memory.unavailable': 'Current binding state cannot be confirmed',
99
- 'memory.more': '{count} more bindings not shown',
100
- 'memory.kind.variable': 'Variable',
101
- 'memory.kind.function': 'Function',
102
- 'memory.kind.class': 'Class',
103
- 'memory.kind.import': 'Import',
104
- 'memory.location': 'Line {line}, column {column}',
105
- 'tool.code': 'Code',
106
- 'tool.codeEdit': 'Code edit',
107
- 'tool.running': 'Running',
108
- 'tool.completed': 'Completed',
109
- 'tool.failed': 'Failed',
110
- 'tool.stopped': 'Stopped',
111
- 'tool.source': 'Source',
112
- 'tool.result': 'Result',
113
- 'tool.inspect': 'Inspect call',
114
- 'feature.safeEdit': 'Safe edit execution',
115
- 'feature.codeRun': 'Isolated code.run execution',
116
- 'feature.stateSaved': 'Saved REPL state',
117
- 'feature.stateRestored': 'Restored REPL state',
118
- 'feature.stateDeleted': 'Deleted REPL state',
119
- 'tool.copy': 'Copy code',
120
- 'tool.copied': 'Copied',
121
- }),
122
- })
123
-
124
- /** Project one field's copy into locale dictionary keys; an empty hint has no key. */
125
- function fieldCopy(field, locale) {
126
- const copy = { [`${field.key}.label`]: locale === 'en' ? field.labelEn : field.label }
127
- const description = locale === 'en' ? field.descriptionEn : field.description
128
- if (description !== '') copy[`${field.key}.description`] = description
129
- return copy
130
- }
131
-
132
- /** Complete dictionaries for both shipped locales, derived once from the spec. */
133
- const SETTINGS_COPY = Object.freeze(Object.fromEntries(
134
- ['zh', 'en'].map(locale => [locale, Object.freeze({
135
- ...CHROME_COPY[locale],
136
- ...Object.assign({}, ...CONFIG_GROUPS.map(group => ({
137
- [`group.${group.key}`]: locale === 'en' ? group.labelEn : group.label,
138
- }))),
139
- ...Object.assign({}, ...CONFIG_FIELDS.map(field => fieldCopy(field, locale))),
140
- })]),
141
- ))
142
-
143
- function sessionUsesPtcPreset(session) {
144
- const preset = session?.projectionValues?.agentPreset ?? session?.agentPreset
145
- return preset === 'ptc' || preset === 'code'
146
- }
22
+ import { createBindingReviews } from './client-binding-review.js'
23
+ import { LOCALE_NS, SETTINGS_COPY } from './client-copy.js'
24
+ import { installStyles } from './client-styles.js'
25
+ import { featureEnabled, registerGated } from './client-feature-gates.js'
26
+ import { createCatalogOwner } from './client-catalog.js'
27
+ import { createUserBindingsWorkbench } from './client-workbench.js'
28
+ import { createBindingCommandAvailability, createReplObserver } from './client-transport.js'
147
29
 
148
30
  window.__ModuleLoader__.load({
149
31
  // Replaced by the bundle entry with the package name from package.json.
@@ -151,513 +33,249 @@ window.__ModuleLoader__.load({
151
33
  factory: (require) => {
152
34
  const React = require('react')
153
35
  const {
36
+ Button,
154
37
  CodeBlock,
155
38
  DisclosureRow,
156
39
  IconCheckOutline14,
157
40
  IconChevronDownOutline14,
158
41
  IconInspectOutline12,
42
+ IconSparkle16,
43
+ IconCloseOutline16,
44
+ IconSearchOutline16,
45
+ IconPlusOutline16,
46
+ IconRefreshOutline16,
47
+ IconTrashOutline16,
48
+ IconEditOutline16,
49
+ IconPlayOutline16,
50
+ IconStopFill16,
51
+ Menu,
52
+ Modal,
53
+ Toast,
54
+ Tooltip,
159
55
  } = require('@deepseek-ai/dsh-client-ui-primitives')
160
56
  const module = { exports: {} }
161
57
  const h = React.createElement
58
+ const TypeScriptEditor = createTypeScriptEditor(React)
162
59
 
163
- function installStyles() {
164
- if (document.getElementById(CLIENT_STYLE_ID) !== null) return () => {}
165
- const style = document.createElement('style')
166
- style.id = CLIENT_STYLE_ID
167
- style.textContent = CLIENT_CSS
168
- document.head.append(style)
169
- return () => style.remove()
60
+ function IconButton({ icon: Icon, label, ...props }) {
61
+ const button = h('button', { ...props, type: 'button', className: 'ptcPlusIconButton',
62
+ 'aria-label': label, title: label }, typeof Icon === 'function' ? h(Icon, { size: 16 }) : label)
63
+ return typeof Tooltip === 'function' ? h(Tooltip, { label, delayMs: 400 }, button) : button
170
64
  }
171
65
 
172
- function fieldInput(field, value, disabled, onChange, label) {
173
- if (field.type === 'boolean') {
174
- return h('input', {
175
- type: 'checkbox', role: 'switch', className: 'ptcPlusCheck', checked: value === true,
176
- disabled, 'aria-label': label,
177
- onChange: event => onChange(field, event.target.checked),
66
+ function ActionButton({ className = '', 'data-kind': kind, ...props }) {
67
+ return typeof Button === 'function'
68
+ ? h(Button, {
69
+ ...props, size: 'sm', variant: kind === 'primary' ? 'primary' : kind === 'ghost' ? 'ghost' : 'outline',
70
+ className: className.split(' ').filter(name => name !== 'ptcPlusButton').join(' '),
178
71
  })
179
- }
180
- return h('input', {
181
- type: 'number', className: 'ptcPlusInput',
182
- value: Number.isSafeInteger(value) ? String(value) : '',
183
- min: field.min, max: field.max, step: 1, disabled, 'aria-label': label,
184
- onChange: event => {
185
- const input = event.target.value
186
- const parsed = input === '' ? '' : Number(input)
187
- onChange(field, Number.isSafeInteger(parsed) ? parsed : input)
188
- },
189
- })
72
+ : h('button', { ...props, className, 'data-kind': kind })
190
73
  }
191
74
 
192
- function apply(ctx) {
75
+ const BindingConsole = createBindingConsole(React, { TypeScriptEditor, IconButton, ActionButton,
76
+ icons: { play: IconPlayOutline16, stop: IconStopFill16, reset: IconRefreshOutline16, clear: IconTrashOutline16 } })
77
+ const { PTCPlusToolRow } = createPtcToolView(React, {
78
+ CodeBlock, DisclosureRow,
79
+ icons: { chevron: IconChevronDownOutline14, check: IconCheckOutline14, inspect: IconInspectOutline12 },
80
+ })
81
+
82
+ async function apply(ctx) {
83
+ const rpc = await createClientRpc(ctx)
193
84
  const preferenceScope = ctx.settingsScope.bind({ namespace: SETTINGS_NAMESPACE })
194
85
  ctx.effect(() => ctx.locale.register(LOCALE_NS, SETTINGS_COPY), 'ptc-plus: settings dictionaries')
195
86
  ctx.effect(installStyles, 'ptc-plus: client styles')
196
87
 
197
- function PTCPlusSettingsCard({ t }) {
198
- const [open, setOpen] = React.useState(false)
199
- const [status, setStatus] = React.useState(null)
200
- const [pending, setPending] = React.useState(() => new Set())
201
- const writeTail = React.useRef(Promise.resolve())
202
- const subscribe = React.useCallback(listener => preferenceScope.subscribe(listener), [])
203
- const getSnapshot = React.useCallback(() => preferenceScope.getSnapshot(), [])
204
- const snapshot = React.useSyncExternalStore(subscribe, getSnapshot, getSnapshot)
205
- const value = snapshot.status === 'ready' ? (snapshot.value ?? {}) : {}
206
- const enabled = value.enabled === true
207
- const unavailable = snapshot.status !== 'ready' || snapshot.writable !== true
208
- const persist = (field, nextValue) => {
209
- if (unavailable || pending.has(field.key)) return
210
- const operation = writeTail.current.then(async () => {
211
- const before = preferenceScope.getSnapshot()
212
- if (before.status !== 'ready' || before.writable !== true) return
213
- if (field.key !== 'enabled' && before.value?.enabled !== true) return
214
- if (before.value?.[field.key] === nextValue) return
215
- setPending(current => new Set(current).add(field.key))
216
- setStatus(null)
217
- try {
218
- await preferenceScope.set(field.key, nextValue)
219
- const after = preferenceScope.getSnapshot()
220
- if (after.status !== 'ready' || after.value?.[field.key] !== nextValue) {
221
- setStatus({ key: 'status.conflict' })
222
- } else {
223
- setStatus({ key: 'status.applied' })
224
- }
225
- } catch (error) {
226
- setStatus({
227
- key: 'status.failed',
228
- params: { error: error instanceof Error ? error.message : String(error) },
229
- })
230
- } finally {
231
- setPending(current => {
232
- const next = new Set(current)
233
- next.delete(field.key)
234
- return next
235
- })
236
- }
237
- })
238
- writeTail.current = operation.catch(() => {})
88
+ async function callUserBindings(endpoint, payload = {}, signal = undefined) {
89
+ if (!featureEnabled(preferenceScope.getSnapshot(), 'bindings')) {
90
+ throw new Error('Global User Bindings are disabled')
239
91
  }
240
- const fieldDisabled = field => unavailable
241
- || pending.has(field.key)
242
- || (field.key !== 'enabled' && !enabled)
243
- const settingGroups = CONFIG_GROUPS.map(group => h('section', {
244
- key: group.key,
245
- className: 'ptcPlusGroup',
246
- 'aria-labelledby': `ptc-plus-settings-group-${group.key}`,
247
- },
248
- h('h3', {
249
- id: `ptc-plus-settings-group-${group.key}`,
250
- className: 'ptcPlusGroupTitle',
251
- }, t(`group.${group.key}`)),
252
- ...group.fields.map(key => {
253
- const field = CONFIG_FIELDS.find(candidate => candidate.key === key)
254
- if (field === undefined) return null
255
- return h('div', { key: field.key, className: 'ptcPlusRow' },
256
- h('div', { className: 'ptcPlusMain' },
257
- h('div', { className: 'ptcPlusLabel' }, t(`${field.key}.label`)),
258
- field.description === '' ? null : h('div', { className: 'ptcPlusDetail' }, t(`${field.key}.description`))),
259
- fieldInput(field, value[field.key], fieldDisabled(field), persist, t(`${field.key}.label`)))
260
- })))
261
- return h('li', { className: 'ptcPlusCard' },
262
- h('button', {
263
- type: 'button', className: 'ptcPlusHeader', 'aria-expanded': open,
264
- 'aria-label': t(open ? 'action.collapse' : 'action.expand'),
265
- 'aria-controls': 'ptc-plus-settings-body', onClick: () => setOpen(current => !current),
266
- },
267
- h('span', { className: 'ptcPlusHeadText' },
268
- h('span', { className: 'ptcPlusName' }, 'PTC Plus'),
269
- h('span', { className: 'ptcPlusDescription' }, t('card.description'))),
270
- h('span', { className: 'ptcPlusStatus', 'data-enabled': enabled }, t(enabled ? 'status.enabled' : 'status.disabled')),
271
- h('span', { className: 'ptcPlusChevron', 'data-open': open, 'aria-hidden': true }, h(IconChevronDownOutline14, { size: 14 }))),
272
- h('div', { id: 'ptc-plus-settings-body', className: 'ptcPlusBody', 'data-open': open, 'aria-hidden': !open },
273
- h('div', { className: 'ptcPlusBodyInner' }, h('div', { className: 'ptcPlusFields' },
274
- snapshot.status === 'loading'
275
- ? h('p', { className: 'ptcPlusMessage' }, t('state.syncing'))
276
- : snapshot.status === 'unavailable'
277
- ? h('p', { className: 'ptcPlusMessage' }, t('state.unavailable'))
278
- : [
279
- ...settingGroups,
280
- h('div', { key: 'footer', className: 'ptcPlusFooter' },
281
- h('span', { className: 'ptcPlusMessage', role: 'status' }, status === null
282
- ? t(snapshot.writable ? 'footer.live' : 'footer.readOnly')
283
- : t(status.key, status.params))),
284
- ]))),
92
+ const result = await rpc.call(
93
+ RPC_CONTRACTS.bindings,
94
+ endpoint,
95
+ payload,
96
+ signal,
285
97
  )
98
+ if (result?.ok === true) return result.value
99
+ const error = new Error(result?.error?.message ?? 'Global User Binding request failed')
100
+ if (typeof result?.error?.code === 'string') error.code = result.error.code
101
+ throw error
286
102
  }
287
103
 
288
- ctx.slots.inject('settings.plugin.item', () => ctx.slots.register({
289
- name: 'settings.plugin.item', key: SETTINGS_NAMESPACE, locale: LOCALE_NS,
290
- }, PTCPlusSettingsCard))
104
+ const catalogOwner = createCatalogOwner({ callUserBindings })
105
+ ctx.effect(() => () => catalogOwner.dispose(), 'ptc-plus: binding catalog sources')
106
+ const { useWorkbenchController, UserBindingsWorkbench, BindingsDialog } = createUserBindingsWorkbench(React, {
107
+ TypeScriptEditor, BindingConsole, IconButton, ActionButton, CodeBlock, Modal,
108
+ catalogOwner,
109
+ icons: {
110
+ refresh: IconRefreshOutline16, plus: IconPlusOutline16, check: IconCheckOutline14,
111
+ trash: IconTrashOutline16, edit: IconEditOutline16, search: IconSearchOutline16,
112
+ chevron: IconChevronDownOutline14, close: IconCloseOutline16,
113
+ },
114
+ })
115
+ const { observeRepl } = createReplObserver({ rpc, ctx })
116
+ const { PTCPlusSettingsCard, PTCPlusSettingsDialog } = createPtcSettingsView(React, {
117
+ ActionButton, BindingsDialog, Modal, useWorkbenchController,
118
+ icons: { chevron: IconChevronDownOutline14 },
119
+ })
120
+ const { ReplComposer, ReplConsole, ReplMemoryCard, replPopoverIsOpen, placeReplPopover } = createReplView(React, {
121
+ CodeBlock, featureEnabled, normalizeReplMemorySnapshot, unavailableReplMemorySnapshot,
122
+ useSessionPreset, sessionUsesPtcPreset, useWorkbenchController, UserBindingsWorkbench,
123
+ icons: { search: IconSearchOutline16, chevron: IconChevronDownOutline14 },
124
+ })
291
125
 
292
- function PTCPlusToolRow({ toolName, block, inspect, t }) {
293
- const [open, setOpen] = React.useState(false)
294
- const view = derivePtcToolView(block, toolName)
295
- const expandable = view.code !== '' || view.output !== '' || typeof inspect === 'function'
296
- const stateKey = {
297
- running: 'tool.running',
298
- ok: 'tool.completed',
299
- error: 'tool.failed',
300
- stopped: 'tool.stopped',
301
- }[view.state]
302
- const outputSummary = view.state === 'error' && view.output !== ''
303
- ? view.output.split(/\r?\n/, 1)[0]
304
- : ''
305
- const summary = outputSummary || view.description
306
- const stateText = view.state === 'ok' ? null : t(stateKey)
307
- const toggle = () => {
308
- if (expandable) setOpen(current => !current)
309
- }
310
- const summaryText = summary === '' ? null : summary
311
- const summaryLine = stateText === null && summaryText === null ? null
312
- : h('div', { className: 'ptcPlusToolSummaryLine', 'data-state': view.state, role: 'status' },
313
- stateText === null ? null : h('span', { className: 'ptcPlusToolState' }, stateText),
314
- summaryText === null ? null : h('span', { className: 'ptcPlusToolSep', 'aria-hidden': true }),
315
- summaryText === null ? null : h('span', { className: 'ptcPlusToolDescription' }, summaryText))
316
- const body = !open ? null : h('div', { className: 'ptcPlusToolBody' },
317
- view.code === '' ? null : h('div', { className: 'ptcPlusToolSection' },
318
- h('span', { className: 'ptcPlusToolSectionLabel' }, t('tool.source')),
319
- typeof CodeBlock === 'function'
320
- ? h(CodeBlock, {
321
- code: view.code, lang: 'typescript', className: 'ptcPlusToolCode',
322
- copyLabel: t('tool.copy'), copiedLabel: t('tool.copied'),
323
- })
324
- : h('pre', { className: 'ptcPlusToolCode' }, view.code)),
325
- view.output === '' ? null : h('div', { className: 'ptcPlusToolSection' },
326
- h('span', { className: 'ptcPlusToolSectionLabel' }, t('tool.result')),
327
- h('div', { className: 'ptcPlusIoCard' },
328
- h('pre', {
329
- className: 'ptcPlusIoText',
330
- 'data-error': view.state === 'error' || undefined,
331
- }, view.output))),
332
- typeof inspect !== 'function' ? null : h('button', {
333
- type: 'button', className: 'ptcPlusInspect', onClick: inspect,
334
- }, h(IconInspectOutline12, { 'aria-hidden': true }), t('tool.inspect')))
335
- const features = view.features.length === 0 ? null : h('div', { className: 'ptcPlusFeatures' },
336
- view.features.map(feature => h('span', {
337
- key: `${feature.key}:${feature.detail}`, className: 'ptcPlusFeature',
338
- },
339
- h('span', { className: 'ptcPlusFeatureName' }, t(feature.key)),
340
- feature.detail === '' ? null
341
- : h('span', { className: 'ptcPlusFeatureDetail', title: feature.detail }, feature.detail))))
342
- const collapsedContent = h('div', { className: 'ptcPlusToolPreview' }, summaryLine, features)
343
- if (typeof DisclosureRow === 'function') {
344
- return h('div', { className: 'ptcPlusTool' },
345
- h(DisclosureRow, {
346
- icon: expandable
347
- ? h(IconChevronDownOutline14, { size: 14 })
348
- : h(IconCheckOutline14, { size: 14 }),
349
- title: t(toolName === 'edit_run_code' ? 'tool.codeEdit' : 'tool.code'),
350
- open,
351
- expandable,
352
- onToggle: toggle,
353
- expandOnRowClick: true,
354
- previewChevron: false,
355
- keepContentWhenOpen: true,
356
- collapsedContent,
357
- children: body,
358
- }))
359
- }
360
- return h('div', { className: 'ptcPlusTool' },
361
- h('div', {
362
- className: 'ptcPlusToolSummary', 'data-state': view.state,
363
- 'data-expandable': expandable || undefined,
364
- role: expandable ? 'button' : undefined,
365
- tabIndex: expandable ? 0 : undefined,
366
- 'aria-expanded': expandable ? open : undefined,
367
- onClick: expandable ? toggle : undefined,
368
- onKeyDown: expandable ? (event) => {
369
- if (event.key !== 'Enter' && event.key !== ' ') return
370
- event.preventDefault()
371
- toggle()
372
- } : undefined,
373
- },
374
- h('span', { className: 'ptcPlusToolLeading', 'aria-hidden': true }, expandable
375
- ? h(IconChevronDownOutline14, { size: 14, className: 'ptcPlusToolChevron', 'data-open': open })
376
- : h(IconCheckOutline14, { size: 14 })),
377
- h('span', { className: 'ptcPlusToolTitle' }, t(
378
- toolName === 'edit_run_code' ? 'tool.codeEdit' : 'tool.code')),
379
- view.state === 'ok' ? null
380
- : h('span', { className: 'ptcPlusToolState', role: 'status' }, t(stateKey)),
381
- h('span', { className: 'ptcPlusToolSep', 'aria-hidden': true }),
382
- h('span', { className: 'ptcPlusToolDescription' }, summary)),
383
- features,
384
- body)
126
+ const settingsProps = () => ({ hooks: { ptcSettings: preferenceScope }, callUserBindings })
127
+ const updateSetting = async (key, value) => {
128
+ const before = preferenceScope.getSnapshot()
129
+ if (before.status !== 'ready' || before.writable !== true
130
+ || (key !== 'enabled' && !featureEnabled(before, 'plugin'))
131
+ || (before.value?.[key] === value
132
+ && !(key === 'bindingUpdates' && before.value.legacyBindingSettings === true))) return null
133
+ if (key === 'bindingUpdates') {
134
+ await preferenceScope.mutate([
135
+ { op: 'set', path: ['bindingUpdates'], value },
136
+ { op: 'set', path: ['legacyBindingSettings'], value: false },
137
+ ])
138
+ } else await preferenceScope.set(key, value)
139
+ const after = preferenceScope.getSnapshot()
140
+ return after.status === 'ready' && after.value?.[key] === value
141
+ && (key !== 'bindingUpdates' || after.value.legacyBindingSettings !== true)
142
+ ? 'status.applied' : 'status.conflict'
385
143
  }
144
+ // One gated-registration shape for every settings-driven contribution.
145
+ const settingsGate = (feature, register) => ({
146
+ subscribe: listener => preferenceScope.subscribe(listener),
147
+ isEnabled: () => featureEnabled(preferenceScope.getSnapshot(), feature),
148
+ register,
149
+ })
386
150
 
387
- ctx.slots.inject('tool.call.toolview', () => {
388
- let releaseRows
389
- const release = () => {
390
- releaseRows?.()
391
- releaseRows = undefined
392
- }
393
- const sync = () => {
394
- const snapshot = preferenceScope.getSnapshot()
395
- const enabled = snapshot.status === 'ready'
396
- && snapshot.value?.enabled === true
397
- && snapshot.value?.enhancedToolView !== false
398
- if (enabled === (releaseRows !== undefined)) return
399
- release()
400
- if (!enabled) return
401
- const disposers = []
402
- try {
403
- disposers.push(ctx.slots.register({
404
- name: 'tool.call.toolview', key: 'run_code', locale: LOCALE_NS,
405
- }, PTCPlusToolRow))
406
- disposers.push(ctx.slots.register({
407
- name: 'tool.call.toolview', key: 'edit_run_code', locale: LOCALE_NS,
408
- }, PTCPlusToolRow))
409
- } catch (error) {
410
- disposers.reverse().forEach(dispose => dispose())
411
- throw error
412
- }
413
- releaseRows = () => disposers.reverse().forEach(dispose => dispose())
414
- }
415
- sync()
416
- const unsubscribe = preferenceScope.subscribe(sync)
417
- return () => {
418
- unsubscribe()
419
- release()
420
- }
151
+ // The settings card's seat differs by DSH generation; the compat module
152
+ // owns both contracts and why one publication serves them. The composer
153
+ // entry points at whichever seat the installed generation declared.
154
+ const settingsCard = publishSettingsCard(ctx, {
155
+ bundleName: __PTC_PLUS_CLIENT_MODULE_ID__,
156
+ locale: LOCALE_NS,
157
+ injectProps: () => ({ ...settingsProps(), updateSetting }),
158
+ component: PTCPlusSettingsCard,
421
159
  })
422
160
 
423
- ctx.inject(['slots', 'sessions'], (scope) => {
424
- function replPopoverIsOpen(popover) {
425
- if (popover?.dataset?.open === 'true') return true
426
- try {
427
- return popover?.matches?.(':popover-open') === true
428
- } catch {
429
- return false
430
- }
161
+ ctx.slots.inject('tool.call.toolview', () => registerGated(ctx, settingsGate('toolView', () => {
162
+ // Two rows share one eligibility decision; roll both back if either fails.
163
+ const disposers = []
164
+ try {
165
+ disposers.push(ctx.slots.register({
166
+ name: 'tool.call.toolview', key: 'run_code', locale: LOCALE_NS,
167
+ }, PTCPlusToolRow))
168
+ disposers.push(ctx.slots.register({
169
+ name: 'tool.call.toolview', key: 'edit_run_code', locale: LOCALE_NS,
170
+ }, PTCPlusToolRow))
171
+ } catch (error) {
172
+ disposers.reverse().forEach(dispose => dispose())
173
+ throw error
431
174
  }
175
+ return () => disposers.reverse().forEach(dispose => dispose())
176
+ })))
432
177
 
433
- function placeReplPopover(trigger, popover) {
434
- if (trigger === null || popover === null) return
435
- const margin = 12
436
- const gap = 8
437
- const viewportWidth = document.documentElement.clientWidth || window.innerWidth
438
- const viewportHeight = document.documentElement.clientHeight || window.innerHeight
439
- const triggerRect = trigger.getBoundingClientRect()
440
- const width = Math.min(344, Math.max(0, viewportWidth - margin * 2))
441
- const left = Math.min(
442
- Math.max(margin, triggerRect.right - width),
443
- Math.max(margin, viewportWidth - width - margin),
444
- )
445
- const below = Math.max(0, viewportHeight - triggerRect.bottom - gap - margin)
446
- const above = Math.max(0, triggerRect.top - gap - margin)
447
- const opensAbove = below < 260 && above > below
448
- const availableHeight = Math.max(80, opensAbove ? above : below)
449
- popover.style.width = `${width}px`
450
- popover.style.maxHeight = `${availableHeight}px`
451
- popover.style.left = `${left}px`
452
- popover.style.top = opensAbove
453
- ? `${Math.max(margin, triggerRect.top - gap - Math.min(popover.offsetHeight, availableHeight))}px`
454
- : `${Math.min(viewportHeight - margin, triggerRect.bottom + gap)}px`
455
- }
178
+ ctx.inject(['sessions'], (viewScope) => {
179
+ const hideComposer = sessionId => viewScope.effect(() => viewScope.slots.inject(
180
+ 'conversation.composer', () => viewScope.slots.register({
181
+ name: 'conversation.composer', priority: 100,
182
+ select: owner => isIdleSessionComposer(owner, sessionId) ? true : null,
183
+ }, ReplComposer),
184
+ ))
185
+ viewScope.slots.inject('conversation.view', () => {
186
+ // The REPL tab needs both the settings gate and the session preset.
187
+ let preset
188
+ return registerGated(viewScope, {
189
+ subscribe: listener => {
190
+ const unsubscribePreset = watchCurrentSessionPreset(viewScope.sessions, value => {
191
+ preset = value
192
+ listener()
193
+ })
194
+ const unsubscribeSettings = preferenceScope.subscribe(listener)
195
+ return () => { unsubscribePreset(); unsubscribeSettings() }
196
+ },
197
+ isEnabled: () => featureEnabled(preferenceScope.getSnapshot(), 'replView')
198
+ && sessionUsesPtcPreset(preset),
199
+ register: () => viewScope.slots.register({
200
+ name: 'conversation.view', id: 'ptc-plus-repl', label: 'REPL', order: 20,
201
+ locale: LOCALE_NS, inject: () => ({ ...settingsProps(), hideComposer, observeRepl }),
202
+ }, props => typeof props.useProjection === 'function' ? h(ReplConsole, props) : null),
203
+ })
204
+ })
205
+ })
456
206
 
457
- function ReplMemoryCard({ memory, t, id, titleId, popoverRef, onEnter, onLeave }) {
458
- const [expandedBinding, setExpandedBinding] = React.useState(null)
459
- return h('div', {
460
- className: 'ptcPlusReplPopover', id, ref: popoverRef, popover: 'auto',
461
- role: 'dialog', 'aria-labelledby': titleId,
462
- onPointerEnter: onEnter, onPointerLeave: onLeave,
463
- },
464
- h('div', { className: 'ptcPlusReplCard' },
465
- h('div', { className: 'ptcPlusReplHead' },
466
- h('span', { className: 'ptcPlusReplStatusDot', 'aria-hidden': true }),
467
- h('span', { className: 'ptcPlusReplTitle', id: titleId }, t('memory.title')),
468
- memory.available
469
- ? h('span', { className: 'ptcPlusReplSummary' }, t('memory.count', { count: memory.total }))
470
- : null),
471
- !memory.available
472
- ? h('span', { className: 'ptcPlusReplEmpty' }, t('memory.unavailable'))
473
- : memory.entries.length === 0
474
- ? h('span', { className: 'ptcPlusReplEmpty' }, t('memory.empty'))
475
- : h('ul', { className: 'ptcPlusReplList' }, memory.entries.map((binding, index) => {
476
- const expanded = expandedBinding === binding.name
477
- const preview = binding.definition.source.replace(/\s+/g, ' ').trim()
478
- const definitionId = `${id}-binding-${index}`
479
- const toggle = () => setExpandedBinding(current => (
480
- current === binding.name ? null : binding.name
481
- ))
482
- return h('li', {
483
- className: 'ptcPlusReplBinding', key: binding.name,
484
- 'data-expanded': expanded,
485
- },
486
- h('button', {
487
- className: 'ptcPlusReplBindingTrigger', type: 'button',
488
- 'aria-expanded': expanded,
489
- 'aria-controls': expanded ? definitionId : undefined,
490
- onClick: toggle,
491
- },
492
- h('span', {
493
- className: 'ptcPlusReplName', 'data-kind': binding.kind,
494
- title: `${binding.name} - ${t(`memory.kind.${binding.kind}`)}`,
495
- }, binding.name),
496
- h('span', { className: 'ptcPlusReplPreview', title: preview }, preview),
497
- h('span', {
498
- className: 'ptcPlusReplChevron', 'data-open': expanded, 'aria-hidden': true,
499
- }, h(IconChevronDownOutline14, { size: 14 }))),
500
- expanded
501
- ? h('div', {
502
- className: 'ptcPlusReplDefinitionWrap', 'data-open': true,
503
- id: definitionId, role: 'region', 'aria-label': binding.name,
504
- }, h('div', { className: 'ptcPlusReplDefinitionInner' },
505
- h('div', { className: 'ptcPlusReplDefinition' },
506
- h('span', { className: 'ptcPlusReplLocation' }, t('memory.location', {
507
- line: binding.definition.line,
508
- column: binding.definition.column,
509
- })),
510
- typeof CodeBlock === 'function'
511
- ? h(CodeBlock, {
512
- code: binding.definition.source,
513
- lang: 'typescript',
514
- className: 'ptcPlusReplCode',
515
- copyLabel: t('tool.copy'),
516
- copiedLabel: t('tool.copied'),
517
- })
518
- : h('pre', { className: 'ptcPlusReplCode' }, binding.definition.source))))
519
- : null)
520
- })),
521
- memory.omitted === 0 ? null
522
- : h('span', { className: 'ptcPlusReplMore' }, t('memory.more', { count: memory.omitted }))))
523
- }
207
+ const bindingReviews = createBindingReviews(callUserBindings)
208
+ ctx.effect(() => () => bindingReviews.dispose())
209
+ ctx.on('connection/reset', () => bindingReviews.reset())
524
210
 
525
- function PTCPlusSessionIndicator({ sessionId, t, useSession, useProjection, useSessions }) {
526
- // The session-scoped standard kit changed between DSH lines. Prefer its
527
- // public hooks when present, then fall back to the older sessions list.
528
- let conversation
529
- try {
530
- conversation = typeof useSession === 'function'
531
- ? useSession(snapshot => snapshot)
532
- : undefined
533
- } catch {
534
- conversation = undefined
535
- }
536
- let projectionMemory
537
- try {
538
- projectionMemory = typeof useProjection === 'function'
539
- ? useProjection('ptcPlusRepl')
540
- : undefined
541
- } catch {
542
- projectionMemory = undefined
543
- }
544
- let sessions
545
- try {
546
- sessions = typeof useSessions === 'function'
547
- ? useSessions(snapshot => snapshot)
548
- : React.useSyncExternalStore(
549
- listener => scope.sessions?.list?.subscribe?.(listener) ?? (() => {}),
550
- () => scope.sessions?.list?.getSnapshot?.() ?? {},
551
- () => scope.sessions?.list?.getSnapshot?.() ?? {},
552
- )
553
- } catch {
554
- sessions = {}
555
- }
556
- const settings = React.useSyncExternalStore(
557
- listener => preferenceScope.subscribe(listener),
558
- () => preferenceScope.getSnapshot(),
559
- () => preferenceScope.getSnapshot(),
560
- )
561
- const triggerRef = React.useRef(null)
562
- const popoverRef = React.useRef(null)
563
- const closeTimer = React.useRef(undefined)
564
- const [expanded, setExpanded] = React.useState(false)
565
- const positionPopover = React.useCallback(() => {
566
- if (!replPopoverIsOpen(popoverRef.current)) return
567
- placeReplPopover(triggerRef.current, popoverRef.current)
568
- }, [])
569
- const showPopover = React.useCallback(() => {
570
- if (closeTimer.current !== undefined) clearTimeout(closeTimer.current)
571
- const popover = popoverRef.current
572
- if (popover === null) return
573
- popover.style.visibility = 'hidden'
574
- if (!replPopoverIsOpen(popover)) {
575
- if (typeof popover.showPopover === 'function') {
576
- try {
577
- popover.showPopover()
578
- } catch {
579
- popover.dataset.open = 'true'
580
- }
581
- } else {
582
- popover.dataset.open = 'true'
583
- }
584
- }
585
- placeReplPopover(triggerRef.current, popover)
586
- popover.style.visibility = 'visible'
587
- setExpanded(true)
588
- }, [])
589
- const hidePopover = React.useCallback(() => {
590
- const popover = popoverRef.current
591
- if (popover === null) return
592
- if (popover.dataset.open === 'true') delete popover.dataset.open
593
- if (typeof popover.hidePopover === 'function' && replPopoverIsOpen(popover)) {
594
- try { popover.hidePopover() } catch {}
595
- }
596
- setExpanded(false)
597
- }, [])
598
- const scheduleHide = React.useCallback(() => {
599
- if (closeTimer.current !== undefined) clearTimeout(closeTimer.current)
600
- closeTimer.current = setTimeout(() => {
601
- closeTimer.current = undefined
602
- if (document.activeElement === triggerRef.current
603
- || popoverRef.current?.contains(document.activeElement)) return
604
- hidePopover()
605
- }, 120)
606
- }, [hidePopover])
607
- React.useEffect(() => {
608
- const syncPopoverState = (event) => {
609
- if (event.target !== popoverRef.current) return
610
- setExpanded(replPopoverIsOpen(popoverRef.current))
611
- }
612
- window.addEventListener('resize', positionPopover)
613
- document.addEventListener('scroll', positionPopover, true)
614
- document.addEventListener('toggle', syncPopoverState, true)
615
- return () => {
616
- if (closeTimer.current !== undefined) clearTimeout(closeTimer.current)
617
- window.removeEventListener('resize', positionPopover)
618
- document.removeEventListener('scroll', positionPopover, true)
619
- document.removeEventListener('toggle', syncPopoverState, true)
620
- const popover = popoverRef.current
621
- if (popover?.dataset?.open === 'true') delete popover.dataset.open
622
- if (typeof popover?.hidePopover === 'function' && replPopoverIsOpen(popover)) {
623
- try { popover.hidePopover() } catch {}
624
- }
625
- }
626
- }, [hidePopover, positionPopover])
627
- const resolvedSessionId = sessionId ?? conversation?.sessionId
628
- const session = sessions?.byId?.[resolvedSessionId] ?? conversation
629
- if (!sessionUsesPtcPreset(session)
630
- || settings.status !== 'ready' || settings.value?.enabled !== true) return null
631
- let memory
632
- try {
633
- memory = normalizeReplMemorySnapshot(projectionMemory
634
- ?? session?.projectionValues?.ptcPlusRepl)
635
- } catch {
636
- memory = unavailableReplMemorySnapshot()
637
- }
638
- const popoverId = `ptc-plus-repl-${String(resolvedSessionId).replace(/[^A-Za-z0-9_-]/g, '-')}`
639
- const titleId = `${popoverId}-title`
640
- return h('span', { className: 'ptcPlusActiveShell' },
641
- h('button', {
642
- type: 'button', className: 'ptcPlusActive', ref: triggerRef,
643
- 'aria-label': t('indicator.title'), 'aria-controls': popoverId,
644
- 'aria-expanded': expanded, 'aria-haspopup': 'dialog',
645
- onPointerEnter: showPopover, onPointerLeave: scheduleHide,
646
- onFocus: showPopover, onBlur: scheduleHide, onClick: showPopover,
647
- onKeyDown: event => { if (event.key === 'Escape') hidePopover() },
648
- }, h('span', { className: 'ptcPlusActiveLabel' }, 'PTC Plus')),
649
- h(ReplMemoryCard, {
650
- memory, t, id: popoverId, titleId, popoverRef,
651
- onEnter: showPopover, onLeave: scheduleHide,
652
- }))
653
- }
654
- scope.slots.inject('conversation.session.header.actions', () => scope.slots.register({
655
- name: 'conversation.session.header.actions', id: 'ptc-plus-active', order: -9, locale: LOCALE_NS,
656
- }, PTCPlusSessionIndicator))
211
+ function useBindingReview(sessionId) {
212
+ const review = bindingReviews.forSession(sessionId)
213
+ const view = React.useSyncExternalStore(review.subscribe, review.getSnapshot)
214
+ return [review, view]
215
+ }
216
+
217
+ const subscribeReset = listener => ctx.on('connection/reset', listener)
218
+ const { BindingAuthorButton, BindingReviewDock, BindingCommandCard } = createAuthoringView(React, {
219
+ ActionButton, IconButton, Menu, Toast, Tooltip, CodeBlock, BindingsDialog, PTCPlusSettingsDialog,
220
+ useWorkbenchController, useBindingReview, catalogOwner, callUserBindings, subscribeReset,
221
+ settingsCardSeat: settingsCard.seat, updateSetting,
222
+ icons: {
223
+ sparkle: IconSparkle16, chevron: IconChevronDownOutline14,
224
+ close: IconCloseOutline16, check: IconCheckOutline14,
225
+ },
226
+ })
227
+ const { PTCPlusSessionIndicator } = createIndicatorView(React, {
228
+ featureEnabled, useSessionPreset, sessionUsesPtcPreset,
229
+ normalizeReplMemorySnapshot, unavailableReplMemorySnapshot,
230
+ ReplMemoryCard, replPopoverIsOpen, placeReplPopover,
231
+ catalogOwner, subscribeReset,
232
+ })
233
+
234
+ ctx.slots.inject('conversation.session.header.actions', () => registerGated(ctx, settingsGate('plugin', () => ctx.slots.register({
235
+ name: 'conversation.session.header.actions', id: 'ptc-plus-active', order: -9, locale: LOCALE_NS,
236
+ inject: settingsProps,
237
+ }, props => typeof props.useProjection === 'function' ? h(PTCPlusSessionIndicator, props) : null))))
238
+ ctx.slots.inject('conversation.chat.commandview', () => registerGated(ctx, settingsGate('bindings', () => ctx.slots.register({
239
+ name: 'conversation.chat.commandview', key: 'binding', locale: LOCALE_NS,
240
+ inject: settingsProps,
241
+ }, props => h(BindingCommandCard, { ...props, key: props.node.commandId })))))
242
+ ctx.slots.inject('conversation.input.dock', () => ctx.slots.inject('conversation.input.left', () =>
243
+ registerGated(ctx, settingsGate('bindings', () => ctx.slots.register({
244
+ name: 'conversation.input.dock', id: 'ptc-plus-binding-review', order: 30, locale: LOCALE_NS,
245
+ }, props => typeof props.useProjection === 'function' && props.sessionId !== undefined
246
+ ? h(BindingReviewDock, { ...props, key: props.sessionId }) : null)))))
247
+ const availability = createBindingCommandAvailability(ctx)
248
+ // The composer binding entry belongs to the PTC surface, exactly like the
249
+ // header indicator and the REPL tab. A non-PTC session keeps plugin
250
+ // settings and `/binding` command cards, but shows no binding shortcut and
251
+ // issues no command-directory read for it.
252
+ ctx.inject(['sessions'], (viewScope) => {
253
+ let preset
254
+ viewScope.slots.inject('conversation.input.left', () => registerGated(viewScope, {
255
+ subscribe: listener => {
256
+ const unsubscribePreset = watchCurrentSessionPreset(viewScope.sessions, value => {
257
+ preset = value
258
+ listener()
259
+ })
260
+ const unsubscribeSettings = preferenceScope.subscribe(listener)
261
+ return () => { unsubscribePreset(); unsubscribeSettings() }
262
+ },
263
+ isEnabled: () => sessionUsesPtcPreset(preset)
264
+ && featureEnabled(preferenceScope.getSnapshot(), 'bindings'),
265
+ register: () => ctx.slots.register({
266
+ name: 'conversation.input.left', id: 'ptc-plus-binding-author', order: 20, locale: LOCALE_NS,
267
+ inject: sessionId => ({
268
+ hooks: { ptcSettings: preferenceScope, bindingCommand: availability.source(sessionId) },
269
+ }),
270
+ }, props => props.sessionId === undefined ? null : h(BindingAuthorButton, { ...props, key: props.sessionId })),
271
+ }))
657
272
  })
658
273
  }
659
274
 
660
- module.exports = { apply, inject: ['settingsScope', 'slots', 'sessions', 'locale'] }
275
+ module.exports = {
276
+ apply,
277
+ inject: ['settingsScope', 'slots', 'locale', 'connection', 'remote'],
278
+ }
661
279
  return module.exports
662
280
  },
663
281
  })