dsh-ptc-plus 0.3.2 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.en.md +65 -78
- package/README.md +65 -78
- package/assets/ptc-plus-bindings-en.png +0 -0
- package/assets/ptc-plus-bindings-zh.png +0 -0
- package/assets/ptc-plus-repair-en.png +0 -0
- package/assets/ptc-plus-repair-zh.png +0 -0
- package/assets/ptc-plus-repl-workspace-en.png +0 -0
- package/assets/ptc-plus-repl-workspace-zh.png +0 -0
- package/assets/ptc-plus-settings-en.png +0 -0
- package/assets/ptc-plus-settings-zh.png +0 -0
- package/client.js +30596 -742
- package/compiler-amaro.cjs +5 -0
- package/compiler-amaro.cjs.map +6 -0
- package/compiler-asset-124f80c1bba92ff5fe79cfec6e8ad518f476de4a42514cae3919538887f510e8.txt +1 -0
- package/compiler-assets.json +3 -0
- package/compiler-core.cjs +554 -0
- package/compiler-core.cjs.map +6 -0
- package/compiler-platform.cjs +2 -0
- package/compiler-platform.cjs.map +6 -0
- package/compiler-typescript.cjs +388 -0
- package/compiler-typescript.cjs.map +6 -0
- package/docs/README.md +6 -0
- package/docs/adr/0004-declare-program-bindings-at-the-host.md +1 -1
- package/docs/adr/0006-rewrite-module-syntax-with-an-ast.md +5 -3
- package/docs/adr/0007-separate-worker-transport-from-session-semantics.md +1 -1
- package/docs/adr/0009-bound-expensive-acceptance-default.md +2 -0
- package/docs/adr/0010-session-log-derived-recovery-tips.md +59 -5
- package/docs/adr/0011-isolate-the-local-recovery-tip-provider.md +1 -1
- package/docs/adr/0012-preserve-mixed-destructuring-initialization.md +3 -1
- package/docs/adr/0013-isolate-repl-redeclaration-convenience.md +3 -1
- package/docs/adr/0014-persist-cell-rewrite-policy.md +6 -2
- package/docs/adr/0015-preserve-source-positions-through-rewrites.md +1 -1
- package/docs/adr/0016-archive-resolved-review-findings.md +1 -1
- package/docs/adr/0017-track-the-latest-dsh-public-surface.md +49 -0
- package/docs/adr/0018-use-versioned-fixture-for-ab-benchmark.md +3 -3
- package/docs/adr/0019-plugin-settings-and-kill-switch.md +5 -3
- package/docs/adr/0020-optional-cordis-tools-in-ptc-mode.md +2 -2
- package/docs/adr/0021-separate-host-bootstrap-from-session-runtime.md +12 -0
- package/docs/adr/0023-global-user-bindings.md +104 -0
- package/docs/adr/0024-repl-console-observation.md +33 -0
- package/docs/adr/0025-use-versioned-logical-binding-identities.md +119 -0
- package/docs/adr/0026-persist-the-delivery-verdict-against-the-verified-candidate.md +21 -0
- package/docs/adr/0027-run-the-session-kernel-in-a-killable-helper-process.md +37 -0
- package/docs/adr/0028-attach-to-the-host-ptc-execution-seam.md +103 -0
- package/docs/adr/0029-keep-plugin-unavailability-out-of-the-host-agent-lifecycle.md +27 -0
- package/docs/adr/0030-serve-both-typert-codec-generations-from-one-codec.md +27 -0
- package/docs/architecture.md +73 -21
- package/docs/binding-review-placement-design.md +200 -0
- package/docs/capability-projection.md +1 -1
- package/docs/client-ui.md +95 -19
- package/docs/durability-design.md +25 -10
- package/docs/evaluation.md +78 -6
- package/docs/installation.md +41 -6
- package/docs/publishing.md +5 -3
- package/docs/runtime-reference.md +72 -25
- package/docs/semantic-validation.md +29 -0
- package/docs/stateful-binding-updates-proposal.md +755 -0
- package/docs/user-bindings.en.md +64 -0
- package/docs/user-bindings.md +64 -0
- package/docs/value-wire.md +3 -3
- package/docs/verification.md +57 -0
- package/index.js +369 -92
- package/internal/ast-traversal.js +5 -2
- package/internal/binding-descriptors.js +1 -0
- package/internal/binding-pattern-traversal.js +25 -0
- package/internal/binding-pattern.js +26 -29
- package/internal/binding-source-analysis.js +391 -0
- package/internal/binding-update-policy.js +66 -0
- package/internal/bounded-worker-round.js +56 -0
- package/internal/callable-definition-environment.js +42 -0
- package/internal/callable-source-catalog.js +129 -0
- package/internal/callable-source-encoding.js +47 -0
- package/internal/callable-source-facts.js +459 -0
- package/internal/callable-source-format.js +2 -0
- package/internal/callable-source-lowering.js +123 -0
- package/internal/cell-analysis-contract.js +25 -0
- package/internal/cell-analysis.js +240 -290
- package/internal/cell-completion.js +15 -0
- package/internal/cell-error.js +7 -0
- package/internal/cell-parser.js +66 -0
- package/internal/cell-rewriter.js +3 -677
- package/internal/commonjs-export-evidence.js +63 -0
- package/internal/commonjs-source-evidence.js +72 -0
- package/internal/compiler-amaro.cjs +2 -0
- package/internal/compiler-compression.js +12 -0
- package/internal/compiler-data.js +58 -0
- package/internal/compiler-descriptors.js +54 -0
- package/internal/compiler-dynamic-cache.js +48 -0
- package/internal/compiler-entry.js +35 -0
- package/internal/compiler-generated-names.js +17 -0
- package/internal/compiler-intrinsics.js +84 -0
- package/internal/compiler-module-links.js +34 -0
- package/internal/compiler-operations.js +61 -0
- package/internal/compiler-platform-factory.js +84 -0
- package/internal/compiler-platform.js +136 -0
- package/internal/compiler-record-roles.js +53 -0
- package/internal/compiler-region-output.js +259 -0
- package/internal/compiler-resource-lowering.js +103 -0
- package/internal/compiler-scope-facts.js +115 -0
- package/internal/compiler-service.js +109 -0
- package/internal/compiler-source-cache.js +32 -0
- package/internal/compiler-source-pieces.js +93 -0
- package/internal/compiler-source-regions.js +132 -0
- package/internal/compiler-storage-source.js +54 -0
- package/internal/compiler-text.js +15 -0
- package/internal/compiler-typescript.cjs +2 -0
- package/internal/config-spec.js +83 -27
- package/internal/cordis-tools-owner.js +130 -21
- package/internal/diagnostic.js +27 -1
- package/internal/direct-surface-owner.js +76 -23
- package/internal/dynamic-binding-evidence.js +16 -0
- package/internal/dynamic-environment-compiler.js +779 -0
- package/internal/dynamic-environment-integration.js +87 -0
- package/internal/dynamic-environment-runtime.js +362 -0
- package/internal/dynamic-native-calls.js +150 -0
- package/internal/dynamic-native-runtime.js +248 -0
- package/internal/dynamic-scope-analysis.js +90 -0
- package/internal/edit-transport-owner.js +27 -2
- package/internal/execution-seam-compat.js +193 -0
- package/internal/failure-reporting.js +118 -18
- package/internal/host-rpc.js +66 -0
- package/internal/isolated-worker.js +548 -0
- package/internal/kernel-child.js +139 -0
- package/internal/kernel-worker.js +904 -120
- package/internal/language-semantics.js +14 -0
- package/internal/legacy-binding-catalog.js +48 -0
- package/internal/legacy-cell-analysis.js +600 -0
- package/internal/legacy-cell-parser.js +50 -0
- package/internal/legacy-cell-rewriter.js +631 -0
- package/internal/legacy-repl-convenience.js +291 -0
- package/internal/managed-module-operations.js +170 -0
- package/internal/module-compilation.js +321 -0
- package/internal/module-runtime-platform.js +19 -0
- package/internal/module-transform-contract.js +12 -0
- package/internal/native-await-declarations.js +68 -0
- package/internal/native-root-compilation.js +54 -0
- package/internal/native-root-dynamic.js +67 -0
- package/internal/recovery-tips.js +27 -15
- package/internal/rejected-cell-editor.js +13 -8
- package/internal/repl-convenience.js +2 -289
- package/internal/repl-memory-projection.js +81 -12
- package/internal/repl-observation-interest.js +70 -0
- package/internal/repl-rewrite-contract.js +6 -0
- package/internal/repl-scope-normalizer.js +2091 -0
- package/internal/repl-value-observer.js +114 -0
- package/internal/rpc-contract.js +52 -0
- package/internal/runtime-bridge-owner.js +172 -28
- package/internal/runtime-config.js +48 -52
- package/internal/runtime-contexts.js +82 -10
- package/internal/runtime-messages.js +111 -0
- package/internal/session-cell-executor.js +401 -87
- package/internal/session-journal-recovery.js +684 -0
- package/internal/session-journal-schema.js +106 -0
- package/internal/session-journal.js +184 -654
- package/internal/session-log-view.js +42 -9
- package/internal/session-runtime.js +256 -95
- package/internal/session-state.js +247 -71
- package/internal/source-position-map.js +211 -71
- package/internal/stateful-module-compiler.js +154 -0
- package/internal/stateful-module-runtime.js +303 -0
- package/internal/stateful-root-compiler.js +598 -0
- package/internal/stateful-root-runtime.js +340 -0
- package/internal/typert-codec-compat.js +28 -0
- package/internal/typescript-transform.js +29 -0
- package/internal/user-binding-authoring.js +70 -0
- package/internal/user-binding-console-worker.js +184 -0
- package/internal/user-binding-console.js +148 -0
- package/internal/user-binding-draft-projection.js +335 -0
- package/internal/user-binding-model-context.js +31 -0
- package/internal/user-binding-runner.js +56 -0
- package/internal/user-bindings-owner.js +1065 -0
- package/internal/user-bindings-store.js +293 -0
- package/internal/user-bindings.js +353 -0
- package/internal/value-wire-schema.js +28 -0
- package/internal/value-wire.js +29 -20
- package/internal/worker-client.js +57 -67
- package/internal/worker-environment.js +45 -0
- package/internal/worker-output-relay.js +29 -0
- package/internal/worker-shutdown.js +14 -0
- package/package.json +75 -7
- package/src/client-activity.js +88 -53
- package/src/client-authoring-view.js +580 -0
- package/src/client-binding-review.js +230 -0
- package/src/client-bindings-data.js +29 -0
- package/src/client-catalog.js +127 -0
- package/src/client-code-editor.js +84 -0
- package/src/client-console.js +112 -0
- package/src/client-copy.js +370 -0
- package/src/client-feature-gates.js +62 -0
- package/src/client-host-compat.js +150 -0
- package/src/client-indicator-view.js +267 -0
- package/src/client-repl-view.js +292 -0
- package/src/client-rpc.js +20 -0
- package/src/client-settings-view.js +221 -0
- package/src/client-styles.js +106 -0
- package/src/client-tool-view.js +108 -0
- package/src/client-transport.js +159 -0
- package/src/client-workbench.js +521 -0
- 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
|
+
}
|