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
@@ -0,0 +1,230 @@
1
+ import { normalizeUserBindingDraftView } from '../internal/user-binding-draft-projection.js'
2
+ import { bindingModelPreferences } from '../internal/user-binding-model-context.js'
3
+
4
+ export function bindingDraftProjection(value) {
5
+ try { return normalizeUserBindingDraftView(value) } catch { return undefined }
6
+ }
7
+
8
+ function candidateIdentity(candidate) {
9
+ return candidate ? JSON.stringify([candidate.requestId, candidate.commandId, candidate.version]) : null
10
+ }
11
+
12
+ function entryIdentity(entry) {
13
+ return JSON.stringify([entry?.id, entry?.name, entry?.scope, entry?.purpose, entry?.source,
14
+ entry?.symbols, entry?.enabled, bindingModelPreferences(entry?.modelContext)])
15
+ }
16
+
17
+ function sameCandidate(left, right) {
18
+ return left != null && right != null && candidateIdentity(left) === candidateIdentity(right)
19
+ && left.mode === right.mode && entryIdentity(left.entry) === entryIdentity(right.entry)
20
+ }
21
+
22
+ function matchesDraft(candidate, draft) {
23
+ return candidate != null && draft != null && candidate.version === draft.version
24
+ && candidate.mode === draft.mode && entryIdentity(candidate.entry) === entryIdentity(draft.entry)
25
+ }
26
+
27
+ function matchesAction(candidate, action) {
28
+ return action != null && action.requestId === candidate?.requestId && action.id === candidate?.entry.id
29
+ && (action.state === 'saved' || action.state === 'discarded') && typeof action.enabled === 'boolean'
30
+ && (action.state !== 'discarded' || action.enabled === false)
31
+ }
32
+
33
+ export function bindingReviewStatus(view) {
34
+ return view.action?.state === 'saved'
35
+ ? view.action.enabled ? 'bindings.commandSavedEnabled' : 'bindings.commandSaved'
36
+ : view.action?.state === 'discarded' ? 'bindings.commandDiscarded'
37
+ : view.busy ? 'bindings.reviewSaving' : view.message ? 'bindings.reviewAttention'
38
+ : view.writable ? 'bindings.commandReady'
39
+ : view.loading ? 'bindings.reviewLoading' : 'bindings.reviewReadOnly'
40
+ }
41
+
42
+ /** One Client review per session. Display choices and receipts never grant Host authority. */
43
+ export function createBindingReviews(call) {
44
+ const sessions = new Map()
45
+ let disposed = false
46
+ function forSession(sessionId) {
47
+ if (sessions.has(sessionId)) return sessions.get(sessionId)
48
+ const listeners = new Set()
49
+ const choices = new Map()
50
+ let snapshot = { candidate: null, candidateKey: null, action: null, writable: false, busy: false, loading: false,
51
+ message: null, messageSource: null, visibility: 'expanded', mounted: false, reachable: false }
52
+ let projection
53
+ let authority = null
54
+ let revoked = false
55
+ let epoch = 0
56
+ let reading
57
+ const pending = new Map()
58
+ let timer
59
+ let attached = false
60
+ const publish = patch => {
61
+ if (disposed) return
62
+ snapshot = { ...snapshot, ...patch }
63
+ // Completed candidates belong to history, regardless of which Host evidence settled them.
64
+ if (snapshot.action !== null) snapshot.visibility = 'hidden'
65
+ for (const listener of listeners) listener()
66
+ }
67
+ const invalidate = () => {
68
+ epoch++
69
+ reading?.abort()
70
+ reading = undefined
71
+ clearInterval(timer)
72
+ timer = undefined
73
+ }
74
+ const select = (candidate, action = null) => {
75
+ const same = sameCandidate(candidate, snapshot.candidate)
76
+ const visibility = choices.get(candidateIdentity(candidate)) ?? 'expanded'
77
+ publish({ candidate, candidateKey: candidateIdentity(candidate), action: action ?? (same ? snapshot.action : null), visibility,
78
+ ...(!same ? { message: null, messageSource: null, writable: false, loading: false } : {}),
79
+ busy: pending.has(candidateIdentity(candidate)) })
80
+ }
81
+ const refresh = async () => {
82
+ if (!attached || authority === null || reading || pending.has(candidateIdentity(snapshot.candidate))
83
+ || snapshot.action !== null || disposed) return
84
+ const currentEpoch = epoch
85
+ const capability = authority
86
+ const commandId = projection.commandId
87
+ const controller = new AbortController()
88
+ reading = controller
89
+ const results = await Promise.allSettled(['list', 'draft', 'draft-review'].map(endpoint => (
90
+ call(endpoint, endpoint === 'list' ? {} : { capability }, controller.signal)
91
+ )))
92
+ if (disposed || epoch !== currentEpoch || controller.signal.aborted) return
93
+ reading = undefined
94
+ const [catalogResult, draftResult, reviewResult] = results
95
+ const review = reviewResult.status === 'fulfilled' ? reviewResult.value : null
96
+ const candidate = review?.candidate
97
+ const known = snapshot.candidate
98
+ const matches = candidate?.commandId === commandId && typeof candidate?.requestId === 'string'
99
+ && Number.isSafeInteger(candidate.version) && candidate.version > 0 && typeof candidate.entry?.source === 'string'
100
+ && (known === null || sameCandidate(known, candidate))
101
+ if (matches && matchesAction(candidate, review.action)) {
102
+ clearInterval(timer)
103
+ timer = undefined
104
+ publish({ ...(known !== null ? { action: review.action } : {}),
105
+ writable: false, loading: false, message: null, messageSource: null })
106
+ return
107
+ }
108
+ if (matches && known === null) select(candidate)
109
+ const draft = draftResult.status === 'fulfilled' ? draftResult.value : null
110
+ if (draftResult.status === 'fulfilled' && draft === null) revoked = true
111
+ const catalog = catalogResult.status === 'fulfilled' ? catalogResult.value : null
112
+ const writable = !revoked && matches && review.action === null && matchesDraft(candidate, draft)
113
+ && Number.isSafeInteger(catalog?.revision)
114
+ const error = results.find(result => result.status === 'rejected')
115
+ publish({ writable, loading: false, revision: writable ? catalog.revision : undefined,
116
+ ...(snapshot.messageSource === 'write' ? {} : {
117
+ message: error ? String(error.reason?.message ?? error.reason) : null,
118
+ messageSource: error ? 'read' : null,
119
+ }) })
120
+ }
121
+ const start = () => {
122
+ clearInterval(timer)
123
+ timer = undefined
124
+ if (!attached || authority === null || snapshot.action !== null) return
125
+ void refresh()
126
+ // Retain the existing bounded-frequency eligibility refresh, including during composer takeovers.
127
+ timer = setInterval(() => { void refresh() }, 1500)
128
+ }
129
+ const sync = next => {
130
+ projection = next
131
+ const historical = next?.history?.find(record => record.commandId === next.commandId)
132
+ const nextAuthority = next?.phase === 'ready' && next.commandId !== null && !historical?.action
133
+ ? next.capability : null
134
+ const previousKey = candidateIdentity(snapshot.candidate)
135
+ const recorded = next?.history?.find(record => sameCandidate(record.candidate, snapshot.candidate))
136
+ if (recorded?.action) publish({ action: recorded.action, writable: false, message: null, messageSource: null })
137
+ if (historical && next.phase === 'ready') {
138
+ // Historical receipts reconcile an existing review; they do not create a new dock.
139
+ if (!historical.action || sameCandidate(historical.candidate, snapshot.candidate)) {
140
+ select(historical.candidate, historical.action)
141
+ } else select(null)
142
+ }
143
+ else if (next?.commandId != null && next.commandId !== snapshot.candidate?.commandId) select(null)
144
+ else if (next === undefined || (next?.phase !== 'ready' && snapshot.candidate !== null
145
+ && snapshot.action === null && !next?.history?.some(record => sameCandidate(record.candidate, snapshot.candidate)))) {
146
+ select(null)
147
+ }
148
+ const nextKey = candidateIdentity(snapshot.candidate)
149
+ if (authority !== nextAuthority || previousKey !== nextKey) {
150
+ invalidate()
151
+ if (authority !== nextAuthority) revoked = false
152
+ authority = nextAuthority
153
+ publish({ writable: false, loading: nextAuthority !== null && snapshot.action === null })
154
+ start()
155
+ }
156
+ }
157
+ const act = async (operation, activate = false) => {
158
+ const key = candidateIdentity(snapshot.candidate)
159
+ if (!attached || disposed || !snapshot.writable || snapshot.action !== null || pending.has(key) || authority === null) return
160
+ const transaction = { candidate: snapshot.candidate, capability: authority }
161
+ pending.set(key, transaction)
162
+ invalidate()
163
+ publish({ busy: true, writable: false, message: null, messageSource: null })
164
+ try {
165
+ await call(operation, { capability: transaction.capability, version: transaction.candidate.version,
166
+ ...(operation === 'save-draft' ? { expectedRevision: snapshot.revision, activate } : {}) })
167
+ // Save returns only after persistence. Discard can also return for an absent locator,
168
+ // so it needs the existing review receipt before claiming a completed discard.
169
+ const receipt = operation === 'save-draft'
170
+ ? { requestId: transaction.candidate.requestId, id: transaction.candidate.entry.id, state: 'saved', enabled: activate }
171
+ : (await call('draft-review', { capability: transaction.capability }))
172
+ const action = operation === 'save-draft' ? receipt
173
+ : sameCandidate(receipt?.candidate, transaction.candidate) && matchesAction(transaction.candidate, receipt?.action)
174
+ ? receipt.action : null
175
+ if (!disposed && sameCandidate(snapshot.candidate, transaction.candidate) && snapshot.action === null) {
176
+ publish({ action, writable: false, loading: false,
177
+ message: action === null ? 'bindings.reviewUnconfirmed' : null,
178
+ messageSource: action === null ? 'write' : null })
179
+ }
180
+ } catch (error) {
181
+ if (!disposed && sameCandidate(snapshot.candidate, transaction.candidate) && snapshot.action === null) {
182
+ publish({ message: String(error?.message ?? error), messageSource: 'write', writable: false, loading: false })
183
+ }
184
+ } finally {
185
+ pending.delete(key)
186
+ if (!disposed && sameCandidate(snapshot.candidate, transaction.candidate)) {
187
+ publish({ busy: false })
188
+ start()
189
+ }
190
+ }
191
+ }
192
+ const review = {
193
+ getSnapshot: () => snapshot,
194
+ subscribe(listener) { listeners.add(listener); return () => listeners.delete(listener) },
195
+ sync,
196
+ attach() {
197
+ attached = true
198
+ publish({ mounted: true })
199
+ start()
200
+ return () => {
201
+ attached = false
202
+ invalidate()
203
+ publish({ mounted: false, reachable: false, writable: false })
204
+ }
205
+ },
206
+ reset() {
207
+ invalidate()
208
+ publish({ writable: false, loading: authority !== null && snapshot.action === null })
209
+ start()
210
+ },
211
+ reachable(value) { if (snapshot.reachable !== value) publish({ reachable: value }) },
212
+ display(visibility, candidate = snapshot.candidate) {
213
+ if (snapshot.candidate === null || snapshot.action !== null || !sameCandidate(snapshot.candidate, candidate)) return false
214
+ choices.set(candidateIdentity(snapshot.candidate), visibility)
215
+ publish({ visibility })
216
+ return true
217
+ },
218
+ act,
219
+ refresh,
220
+ dispose() { invalidate(); attached = false; listeners.clear(); choices.clear() },
221
+ }
222
+ sessions.set(sessionId, review)
223
+ return review
224
+ }
225
+ return {
226
+ forSession,
227
+ reset() { for (const review of sessions.values()) review.reset() },
228
+ dispose() { disposed = true; for (const review of sessions.values()) review.dispose(); sessions.clear() },
229
+ }
230
+ }
@@ -0,0 +1,29 @@
1
+ import { bindingModelPreferences } from '../internal/user-binding-model-context.js'
2
+
3
+ const blankBinding = () => ({
4
+ id: '',
5
+ name: '',
6
+ scope: 'namespace',
7
+ symbolsText: '',
8
+ purpose: '',
9
+ enabled: false,
10
+ source: 'export function helper() {\n return undefined\n}\n',
11
+ modelContext: bindingModelPreferences(),
12
+ })
13
+ const editableBinding = value => ({
14
+ id: value.id,
15
+ name: value.name,
16
+ scope: value.scope,
17
+ symbolsText: Array.isArray(value.symbols) ? value.symbols.join(', ') : '',
18
+ purpose: value.purpose,
19
+ enabled: value.enabled,
20
+ source: value.source,
21
+ modelContext: bindingModelPreferences(value.modelContext),
22
+ })
23
+ const bindingPayload = (value) => {
24
+ const { symbolsText, ...entry } = value
25
+ const symbols = symbolsText.split(',').map(symbol => symbol.trim()).filter(Boolean)
26
+ return symbols.length === 0 ? entry : { ...entry, symbols }
27
+ }
28
+
29
+ export { blankBinding, editableBinding, bindingPayload }
@@ -0,0 +1,127 @@
1
+ /**
2
+ * Global User Bindings catalog owner. One source per consumer: reads are
3
+ * latest-wins inside that source (a newer read, write or reset aborts the
4
+ * previous one and its response is dropped), writes are serialized per source,
5
+ * and every state change publishes an immutable snapshot to its subscribers.
6
+ * Consumers therefore keep no epoch of their own, and a slow request in one
7
+ * surface can never invalidate another surface's request.
8
+ */
9
+ export function createCatalogOwner({ callUserBindings }) {
10
+ const sources = new Set()
11
+
12
+ const createSource = () => {
13
+ let state = Object.freeze({ status: 'loading', catalog: null, error: null })
14
+ let epoch = 0
15
+ let controller
16
+ let writing = false
17
+ let inactive = false
18
+ const listeners = new Set()
19
+
20
+ const publish = next => {
21
+ state = Object.freeze({ ...state, ...next })
22
+ for (const listener of [...listeners]) listener()
23
+ }
24
+ // Any new read/write/reset invalidates every older response in this source.
25
+ const invalidate = () => {
26
+ epoch += 1
27
+ const previous = controller
28
+ controller = undefined
29
+ previous?.abort()
30
+ return epoch
31
+ }
32
+ const message = error => (error instanceof Error ? error.message : String(error))
33
+
34
+ const source = {
35
+ getSnapshot: () => state,
36
+ subscribe(listener) {
37
+ // A mounted consumer makes the source live again after a release, and
38
+ // keeps it inside owner disposal.
39
+ if (inactive) {
40
+ inactive = false
41
+ sources.add(source)
42
+ }
43
+ listeners.add(listener)
44
+ return () => {
45
+ listeners.delete(listener)
46
+ if (listeners.size === 0) invalidate()
47
+ }
48
+ },
49
+ /** Latest-wins catalog read; resolves to the catalog only when it still owns the source. */
50
+ async read({ reload = false } = {}) {
51
+ if (inactive || writing) return undefined
52
+ const current = invalidate()
53
+ const read = new AbortController()
54
+ controller = read
55
+ publish({ status: 'loading', error: null })
56
+ try {
57
+ const catalog = await callUserBindings(reload ? 'reload' : 'list', {}, read.signal)
58
+ if (epoch !== current || controller !== read) return undefined
59
+ controller = undefined
60
+ if (catalog?.error !== undefined) {
61
+ publish({ status: 'error', catalog: null, error: String(catalog.error) })
62
+ return undefined
63
+ }
64
+ publish({ status: 'ready', catalog, error: null })
65
+ return catalog
66
+ } catch (error) {
67
+ if (epoch !== current || controller !== read) return undefined
68
+ controller = undefined
69
+ publish({ status: 'error', catalog: null, error: message(error) })
70
+ return undefined
71
+ }
72
+ },
73
+ /** Serialized compare-and-swap write; a failed write becomes the source error state. */
74
+ async write(operation) {
75
+ if (inactive || writing) return undefined
76
+ writing = true
77
+ const current = invalidate()
78
+ publish({ status: 'writing', error: null })
79
+ try {
80
+ const catalog = await operation()
81
+ if (epoch !== current) return undefined
82
+ publish({ status: 'ready', catalog, error: null })
83
+ return catalog
84
+ } catch (error) {
85
+ if (epoch !== current) return undefined
86
+ publish({ status: 'error', catalog: null, error: message(error) })
87
+ return undefined
88
+ } finally {
89
+ writing = false
90
+ // A reset landed while writing: the state it dropped has to be read again.
91
+ if (!inactive && epoch !== current) void source.read()
92
+ }
93
+ },
94
+ /** Publish a catalog produced outside this source (the workbench owns its own writes). */
95
+ accept(catalog) {
96
+ if (inactive) return
97
+ invalidate()
98
+ publish({ status: 'ready', catalog, error: null })
99
+ },
100
+ /** Drop in-flight work and require a fresh read before the next render trusts the catalog. */
101
+ reset() {
102
+ if (inactive) return
103
+ invalidate()
104
+ publish({ status: 'loading', catalog: null, error: null })
105
+ },
106
+ release() {
107
+ if (inactive) return
108
+ inactive = true
109
+ invalidate()
110
+ listeners.clear()
111
+ sources.delete(source)
112
+ },
113
+ }
114
+ sources.add(source)
115
+ return source
116
+ }
117
+
118
+ return {
119
+ /** Claim an independent catalog source; the consumer releases it when it unmounts. */
120
+ claim() {
121
+ return createSource()
122
+ },
123
+ dispose() {
124
+ for (const source of [...sources]) source.release()
125
+ },
126
+ }
127
+ }
@@ -0,0 +1,84 @@
1
+ import { basicSetup } from 'codemirror'
2
+ import { Compartment, EditorState, Transaction } from '@codemirror/state'
3
+ import { EditorView, keymap } from '@codemirror/view'
4
+ import { HighlightStyle, syntaxHighlighting } from '@codemirror/language'
5
+ import { javascript } from '@codemirror/lang-javascript'
6
+ import { tags } from '@lezer/highlight'
7
+
8
+ const editorTheme = EditorView.theme({
9
+ '&': { backgroundColor: 'var(--dsw-alias-bg-layer-3)', color: 'var(--dsw-alias-label-primary)', fontSize: '12px' },
10
+ '&.cm-focused': { outline: 'none' },
11
+ '.cm-scroller': { fontFamily: 'ui-monospace, SFMono-Regular, Consolas, monospace', lineHeight: '20px', overflow: 'auto' },
12
+ '.cm-content': { minHeight: '320px', padding: '10px 0', caretColor: 'var(--dsw-alias-label-primary)' },
13
+ '.cm-line': { padding: '0 12px' },
14
+ '.cm-gutters': { backgroundColor: 'var(--dsw-alias-bg-layer-3)', color: 'var(--dsw-alias-label-tertiary)', border: 'none' },
15
+ '.cm-activeLine, .cm-activeLineGutter': { backgroundColor: 'var(--dsw-alias-interactive-bg-hover)' },
16
+ '.cm-cursor': { borderLeftColor: 'var(--dsw-alias-label-primary)' },
17
+ '&.cm-focused .cm-selectionBackground, .cm-selectionBackground, ::selection': {
18
+ backgroundColor: 'color-mix(in srgb, var(--dsw-alias-interactive-primary, #4d6bfe) 22%, transparent)',
19
+ },
20
+ '.cm-panels, .cm-tooltip': { backgroundColor: 'var(--dsw-alias-bg-layer-3)', color: 'var(--dsw-alias-label-primary)' },
21
+ '.cm-searchMatch': { backgroundColor: 'color-mix(in srgb, #e3b24e 30%, transparent)' },
22
+ })
23
+
24
+ const editorHighlight = HighlightStyle.define([
25
+ { tag: [tags.keyword, tags.modifier], color: 'var(--ptc-code-keyword)' },
26
+ { tag: [tags.string, tags.regexp], color: 'var(--ptc-code-string)' },
27
+ { tag: [tags.number, tags.bool, tags.null], color: 'var(--ptc-code-number)' },
28
+ { tag: [tags.typeName, tags.className], color: 'var(--ptc-code-type)' },
29
+ { tag: [tags.function(tags.variableName), tags.function(tags.propertyName)], color: 'var(--ptc-code-function)' },
30
+ { tag: [tags.comment, tags.meta], color: 'var(--dsw-alias-label-tertiary)', fontStyle: 'italic' },
31
+ ])
32
+
33
+ export function createTypeScriptEditor(React) {
34
+ return function TypeScriptEditor({ value, onChange, disabled, label, documentId, onRun }) {
35
+ const parent = React.useRef(null)
36
+ const view = React.useRef(null)
37
+ const latest = React.useRef({ value, onChange })
38
+ const access = React.useRef(new Compartment())
39
+ latest.current = { value, onChange, onRun }
40
+ const accessExtensions = () => [EditorState.readOnly.of(disabled), EditorView.editable.of(!disabled)]
41
+
42
+ React.useLayoutEffect(() => {
43
+ const editor = new EditorView({
44
+ parent: parent.current,
45
+ state: EditorState.create({ doc: value, extensions: [
46
+ keymap.of([{ key: 'Mod-Enter', run: () => {
47
+ if (!latest.current.onRun) return false
48
+ void latest.current.onRun()
49
+ return true
50
+ } }]),
51
+ basicSetup,
52
+ javascript({ typescript: true }),
53
+ editorTheme,
54
+ syntaxHighlighting(editorHighlight),
55
+ EditorView.lineWrapping,
56
+ EditorView.contentAttributes.of({ 'aria-label': label }),
57
+ access.current.of(accessExtensions()),
58
+ EditorView.updateListener.of(update => {
59
+ if (update.docChanged) {
60
+ const next = update.state.doc.toString()
61
+ if (next !== latest.current.value) latest.current.onChange(next)
62
+ }
63
+ }),
64
+ ] }),
65
+ })
66
+ view.current = editor
67
+ return () => { view.current = null; editor.destroy() }
68
+ }, [documentId])
69
+
70
+ React.useLayoutEffect(() => {
71
+ const editor = view.current
72
+ const current = editor.state.doc.toString()
73
+ if (current !== value) editor.dispatch({
74
+ changes: { from: 0, to: current.length, insert: value },
75
+ annotations: Transaction.addToHistory.of(false),
76
+ })
77
+ }, [value])
78
+ React.useLayoutEffect(() => {
79
+ view.current.dispatch({ effects: access.current.reconfigure(accessExtensions()) })
80
+ view.current.contentDOM.setAttribute('aria-label', label)
81
+ }, [disabled, label])
82
+ return React.createElement('div', { className: 'ptcPlusCodeEditor', ref: parent })
83
+ }
84
+ }
@@ -0,0 +1,112 @@
1
+ const MAX_HISTORY_ENTRIES = 40
2
+ const MAX_HISTORY_CHARACTERS = 128 * 1024
3
+ const MAX_RECORD_CHARACTERS = MAX_HISTORY_CHARACTERS / 2
4
+
5
+ export function createBindingConsole(React, { TypeScriptEditor, IconButton, ActionButton, icons }) {
6
+ const h = React.createElement
7
+ return function BindingConsole({ source, entryId, resetVersion, callUserBindings, t }) {
8
+ const [code, setCode] = React.useState('')
9
+ const [records, setRecords] = React.useState([])
10
+ const [running, setRunning] = React.useState(false)
11
+ const [state, setState] = React.useState('ready')
12
+ const [open, setOpen] = React.useState(true)
13
+ const environment = React.useRef(null)
14
+ const request = React.useRef(null)
15
+ const idleTimer = React.useRef(null)
16
+ const alive = React.useRef(false)
17
+ const history = React.useRef(null)
18
+ const release = () => {
19
+ clearTimeout(idleTimer.current)
20
+ const capability = environment.current
21
+ environment.current = null
22
+ request.current?.abort()
23
+ if (capability !== null) void callUserBindings('console-release', { environment: capability }).catch(() => {})
24
+ }
25
+ React.useEffect(() => {
26
+ alive.current = true
27
+ return () => { alive.current = false; release() }
28
+ }, [])
29
+ React.useEffect(() => { release(); setState('ready') }, [resetVersion])
30
+ React.useEffect(() => {
31
+ if (history.current) history.current.scrollTop = history.current.scrollHeight
32
+ }, [records, running])
33
+ const append = record => setRecords(current => {
34
+ const truncated = record.code.length + record.text.length > MAX_RECORD_CHARACTERS
35
+ const code = record.code.slice(0, MAX_RECORD_CHARACTERS / 2)
36
+ const bounded = { ...record, code, text: record.text.slice(0, MAX_RECORD_CHARACTERS - code.length),
37
+ truncated: truncated || code.length < record.code.length }
38
+ const next = [...current, bounded].slice(-MAX_HISTORY_ENTRIES)
39
+ let length = next.reduce((total, item) => total + item.code.length + item.text.length, 0)
40
+ while (next.length > 1 && length > MAX_HISTORY_CHARACTERS) {
41
+ const removed = next.shift()
42
+ length -= removed.code.length + removed.text.length
43
+ }
44
+ return next
45
+ })
46
+ const run = async () => {
47
+ if (request.current !== null || code.trim() === '' || source.trim() === '') return
48
+ clearTimeout(idleTimer.current)
49
+ const controller = new AbortController()
50
+ request.current = controller
51
+ setRunning(true)
52
+ setState('running')
53
+ const submitted = code
54
+ try {
55
+ const result = await callUserBindings('console-run', {
56
+ ...(environment.current === null ? {} : { environment: environment.current }), source, code: submitted,
57
+ }, controller.signal)
58
+ if (!alive.current || controller.signal.aborted) {
59
+ if (result.environment) void callUserBindings('console-release', { environment: result.environment }).catch(() => {})
60
+ if (alive.current) append({ code: submitted, text: t('execution.stopped'), error: true })
61
+ return
62
+ }
63
+ environment.current = result.environment
64
+ const text = [...result.logs.map(log => log.text), result.error ?? result.output ?? ''].join('\n')
65
+ append({ code: submitted, text, error: result.error !== undefined,
66
+ reset: result.reset, durationMs: result.durationMs })
67
+ setCode('')
68
+ setState(result.environment === null ? 'released' : 'ready')
69
+ if (result.expiresAt !== null) idleTimer.current = setTimeout(() => {
70
+ release()
71
+ if (alive.current) setState('released')
72
+ }, Math.max(0, result.expiresAt - Date.now()))
73
+ } catch (error) {
74
+ const stopped = controller.signal.aborted
75
+ release()
76
+ if (alive.current) {
77
+ append({ code: submitted, text: stopped ? t('execution.stopped') : String(error), error: true })
78
+ setState('released')
79
+ }
80
+ } finally {
81
+ if (request.current === controller) request.current = null
82
+ if (alive.current) setRunning(false)
83
+ }
84
+ }
85
+ return h('details', { className: 'ptcPlusExecution', open, onToggle: event => setOpen(event.currentTarget.open) },
86
+ h('summary', null, t('bindings.debug')),
87
+ h('div', { className: 'ptcPlusExecutionToolbar' },
88
+ h('span', { className: 'ptcPlusExecutionLanguage' }, 'TypeScript'),
89
+ h('span', { className: 'ptcPlusExecutionState', role: 'status' }, t(`execution.${state}`)),
90
+ h('div', { className: 'ptcPlusExecutionActions' },
91
+ running
92
+ ? h(ActionButton, { onClick: () => { release(); setState('released') } }, typeof icons.stop === 'function' ? h(icons.stop, { size: 14 }) : null, t('execution.stop'))
93
+ : h(ActionButton, { 'data-kind': 'primary', onClick: run, disabled: !code.trim() || !source.trim() },
94
+ typeof icons.play === 'function' ? h(icons.play, { size: 14 }) : null, t('bindings.run')),
95
+ h(IconButton, { icon: icons.reset, label: t('execution.reset'), onClick: () => { release(); setState('ready') } }),
96
+ h(IconButton, { icon: icons.clear, label: t('execution.clear'), onClick: () => setRecords([]) }))),
97
+ h('div', { className: 'ptcPlusExecutionHistory', ref: history, 'aria-label': t('execution.history') },
98
+ records.map((record, index) => h('div', { className: 'ptcPlusExecutionRecord', key: index },
99
+ record.reset ? h('span', { className: 'ptcPlusMessage' }, t('execution.restarted')) : null,
100
+ h('pre', { className: 'ptcPlusExecutionCommand' }, h('span', { 'aria-hidden': true }, '> '), record.code),
101
+ h('div', { className: 'ptcPlusIoCard ptcPlusExecutionResult' },
102
+ h('span', { className: 'ptcPlusExecutionOutputLabel' }, t('execution.output')),
103
+ h('pre', { className: 'ptcPlusExecutionOutput', 'data-error': record.error,
104
+ tabIndex: 0, 'aria-label': t('execution.output') }, record.text)),
105
+ record.truncated ? h('span', { className: 'ptcPlusMessage' }, t('execution.truncated')) : null,
106
+ record.durationMs === undefined ? null : h('span', { className: 'ptcPlusExecutionDuration' }, `${record.durationMs} ms`)))),
107
+ h('div', { className: 'ptcPlusExecutionInput' }, open ? h(TypeScriptEditor, {
108
+ documentId: entryId, value: code, onChange: setCode, disabled: running,
109
+ label: t('execution.input'), onRun: run,
110
+ }) : null))
111
+ }
112
+ }