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,267 @@
1
+ /**
2
+ * Hover dwell before the card opens: the indicator shares the session header with
3
+ * native actions, so a pointer merely crossing it must not open a card over the
4
+ * conversation. Click and keyboard focus stay immediate.
5
+ */
6
+ const HOVER_DWELL_MS = 150
7
+
8
+ /**
9
+ * Grace before a pointer-dismissed card closes. It carries the pointer across the
10
+ * 8px trigger-card gap without leaving the card behind a pointer that moved on.
11
+ */
12
+ const CLOSE_GRACE_MS = 200
13
+
14
+ /**
15
+ * Header indicator for PTC Plus sessions: a popover trigger whose card is
16
+ * supplied by the REPL view. Popover open/placement logic is shared with the
17
+ * card rather than re-implemented here.
18
+ */
19
+ export function createIndicatorView(React, deps) {
20
+ const {
21
+ featureEnabled, useSessionPreset, sessionUsesPtcPreset,
22
+ normalizeReplMemorySnapshot, unavailableReplMemorySnapshot,
23
+ ReplMemoryCard, replPopoverIsOpen, placeReplPopover,
24
+ catalogOwner, subscribeReset,
25
+ } = deps
26
+ const h = React.createElement
27
+
28
+ function PTCPlusSessionIndicator({
29
+ sessionId, t, useProjection, useSessions, usePtcSettings, callUserBindings,
30
+ }) {
31
+ const preset = useSessionPreset({ sessionId, useProjection, useSessions })
32
+ const projectionMemory = useProjection('ptcPlusRepl')
33
+ const settings = usePtcSettings(snapshot => snapshot)
34
+ const resolvedSessionId = sessionId
35
+ const globalEnabled = featureEnabled(settings, 'bindings')
36
+ const identity = JSON.stringify([sessionId, globalEnabled])
37
+ // The catalog is read through its owner: this surface keeps no epoch of its
38
+ // own, so a read here cannot invalidate another surface's request.
39
+ const catalogSource = React.useMemo(() => catalogOwner.claim(), [catalogOwner])
40
+ const catalogState = React.useSyncExternalStore(catalogSource.subscribe, catalogSource.getSnapshot)
41
+ const globalBindings = catalogState.catalog ?? undefined
42
+ const refreshGlobalBindings = React.useCallback(() => {
43
+ if (!globalEnabled) return
44
+ void catalogSource.read()
45
+ }, [globalEnabled, catalogSource])
46
+ React.useEffect(() => () => catalogSource.release(), [catalogSource])
47
+ React.useEffect(() => {
48
+ // Session and feature identity own this surface's catalog lifetime.
49
+ if (!globalEnabled) { catalogSource.reset(); return }
50
+ void catalogSource.read()
51
+ }, [identity, globalEnabled, catalogSource])
52
+ React.useEffect(() => subscribeReset(() => catalogSource.reset()), [catalogSource])
53
+ const triggerRef = React.useRef(null)
54
+ const shellRef = React.useRef(null)
55
+ const popoverRef = React.useRef(null)
56
+ const openTimer = React.useRef(undefined)
57
+ const closeTimer = React.useRef(undefined)
58
+ const placementFrame = React.useRef(undefined)
59
+ // The card leaves with the pointer unless a click pinned it. Keyboard modality
60
+ // separates a reader focusing the trigger from the focus a pointer press leaves
61
+ // behind, which must not turn a hovering card into a resident one.
62
+ const pinned = React.useRef(false)
63
+ const keyboard = React.useRef(false)
64
+ // Restoring focus to the trigger is bookkeeping, not a reader asking for the
65
+ // card back, so it must not read as a keyboard request to open it again.
66
+ const restoringFocus = React.useRef(false)
67
+ const [expanded, setExpanded] = React.useState(false)
68
+ React.useEffect(() => {
69
+ const markKeyboard = () => { keyboard.current = true }
70
+ const markPointer = () => { keyboard.current = false }
71
+ document.addEventListener('keydown', markKeyboard, true)
72
+ document.addEventListener('pointerdown', markPointer, true)
73
+ return () => {
74
+ document.removeEventListener('keydown', markKeyboard, true)
75
+ document.removeEventListener('pointerdown', markPointer, true)
76
+ }
77
+ }, [])
78
+ const cancelOpen = React.useCallback(() => {
79
+ if (openTimer.current === undefined) return
80
+ clearTimeout(openTimer.current)
81
+ openTimer.current = undefined
82
+ }, [])
83
+ const cancelClose = React.useCallback(() => {
84
+ if (closeTimer.current === undefined) return
85
+ clearTimeout(closeTimer.current)
86
+ closeTimer.current = undefined
87
+ }, [])
88
+ const positionPopover = React.useCallback(() => {
89
+ if (!replPopoverIsOpen(popoverRef.current)) return
90
+ placeReplPopover(triggerRef.current, popoverRef.current)
91
+ }, [])
92
+ // A captured scroll fires per event and every placement forces layout, so
93
+ // several can collapse into one frame.
94
+ const schedulePlacement = React.useCallback(() => {
95
+ if (placementFrame.current !== undefined) return
96
+ placementFrame.current = requestAnimationFrame(() => {
97
+ placementFrame.current = undefined
98
+ positionPopover()
99
+ })
100
+ }, [positionPopover])
101
+ const showPopover = React.useCallback(() => {
102
+ cancelOpen()
103
+ cancelClose()
104
+ const popover = popoverRef.current
105
+ if (popover === null) return
106
+ popover.style.visibility = 'hidden'
107
+ if (!replPopoverIsOpen(popover)) {
108
+ if (typeof popover.showPopover === 'function') {
109
+ try {
110
+ popover.showPopover()
111
+ } catch {
112
+ popover.dataset.open = 'true'
113
+ }
114
+ } else {
115
+ popover.dataset.open = 'true'
116
+ }
117
+ }
118
+ placeReplPopover(triggerRef.current, popover)
119
+ popover.style.visibility = 'visible'
120
+ setExpanded(true)
121
+ void refreshGlobalBindings()
122
+ }, [cancelClose, cancelOpen, refreshGlobalBindings])
123
+ const hidePopover = React.useCallback(() => {
124
+ cancelOpen()
125
+ cancelClose()
126
+ pinned.current = false
127
+ const popover = popoverRef.current
128
+ if (popover === null) return
129
+ if (popover.dataset.open === 'true') delete popover.dataset.open
130
+ if (typeof popover.hidePopover === 'function' && replPopoverIsOpen(popover)) {
131
+ try { popover.hidePopover() } catch {}
132
+ }
133
+ setExpanded(false)
134
+ }, [cancelClose, cancelOpen])
135
+ // Touch has no hover, so its tap reaches the same card through the click path.
136
+ const scheduleOpen = React.useCallback(() => {
137
+ // The pointer came back to the trigger: the card may still be open from before,
138
+ // with the close its own leave armed waiting to return it.
139
+ cancelClose()
140
+ if (replPopoverIsOpen(popoverRef.current)) return
141
+ cancelOpen()
142
+ openTimer.current = setTimeout(() => {
143
+ openTimer.current = undefined
144
+ // A hover that reaches this point is the pointer driving the card again, so a
145
+ // later leave closes it instead of inheriting the keyboard exemption.
146
+ keyboard.current = false
147
+ showPopover()
148
+ }, HOVER_DWELL_MS)
149
+ }, [cancelClose, cancelOpen, showPopover])
150
+ const scheduleHide = React.useCallback(() => {
151
+ cancelOpen()
152
+ cancelClose()
153
+ if (pinned.current) return
154
+ closeTimer.current = setTimeout(() => {
155
+ closeTimer.current = undefined
156
+ // A keyboard reader keeps the card: focus inside it, or on its own trigger.
157
+ if (popoverRef.current?.contains(document.activeElement)) return
158
+ if (keyboard.current && document.activeElement === triggerRef.current) return
159
+ hidePopover()
160
+ }, CLOSE_GRACE_MS)
161
+ }, [cancelClose, cancelOpen, hidePopover])
162
+ const togglePopover = React.useCallback(() => {
163
+ cancelOpen()
164
+ if (!replPopoverIsOpen(popoverRef.current)) {
165
+ // The click owns the card until the reader closes it explicitly.
166
+ pinned.current = true
167
+ showPopover()
168
+ return
169
+ }
170
+ if (!pinned.current) { pinned.current = true; return }
171
+ hidePopover()
172
+ }, [cancelOpen, hidePopover, showPopover])
173
+ const dismissOutside = React.useCallback(event => {
174
+ if (event.target instanceof Node && shellRef.current?.contains(event.target) !== true) hidePopover()
175
+ }, [hidePopover])
176
+ const closeOnEscape = React.useCallback(event => {
177
+ if (event.key !== 'Escape') return
178
+ hidePopover()
179
+ const trigger = triggerRef.current
180
+ if (!trigger?.getClientRects().length) return
181
+ restoringFocus.current = true
182
+ trigger.focus({ preventScroll: true })
183
+ restoringFocus.current = false
184
+ }, [hidePopover])
185
+ React.useEffect(() => {
186
+ if (!expanded) return undefined
187
+ // A manual popover keeps the browser out of dismissal, so both close paths
188
+ // stay owned here instead of racing the click that toggles it.
189
+ document.addEventListener('pointerdown', dismissOutside)
190
+ document.addEventListener('keydown', closeOnEscape)
191
+ return () => {
192
+ document.removeEventListener('pointerdown', dismissOutside)
193
+ document.removeEventListener('keydown', closeOnEscape)
194
+ }
195
+ }, [closeOnEscape, dismissOutside, expanded])
196
+ React.useEffect(() => {
197
+ const syncPopoverState = (event) => {
198
+ if (event.target !== popoverRef.current) return
199
+ const open = replPopoverIsOpen(popoverRef.current)
200
+ if (!open) pinned.current = false
201
+ setExpanded(open)
202
+ }
203
+ const onScroll = (event) => {
204
+ // Scrolling the card's own list cannot move it; only the page behind can.
205
+ if (popoverRef.current?.contains(event.target)) return
206
+ schedulePlacement()
207
+ }
208
+ window.addEventListener('resize', schedulePlacement)
209
+ document.addEventListener('scroll', onScroll, true)
210
+ document.addEventListener('toggle', syncPopoverState, true)
211
+ return () => {
212
+ cancelOpen()
213
+ cancelClose()
214
+ if (placementFrame.current !== undefined) {
215
+ cancelAnimationFrame(placementFrame.current)
216
+ placementFrame.current = undefined
217
+ }
218
+ window.removeEventListener('resize', schedulePlacement)
219
+ document.removeEventListener('scroll', onScroll, true)
220
+ document.removeEventListener('toggle', syncPopoverState, true)
221
+ const popover = popoverRef.current
222
+ if (popover?.dataset?.open === 'true') delete popover.dataset.open
223
+ if (typeof popover?.hidePopover === 'function' && replPopoverIsOpen(popover)) {
224
+ try { popover.hidePopover() } catch {}
225
+ }
226
+ }
227
+ }, [cancelClose, cancelOpen, schedulePlacement])
228
+ if (!sessionUsesPtcPreset(preset) || !featureEnabled(settings, 'plugin')) return null
229
+ let memory
230
+ try {
231
+ memory = normalizeReplMemorySnapshot(projectionMemory)
232
+ } catch {
233
+ memory = unavailableReplMemorySnapshot()
234
+ }
235
+ const popoverId = `ptc-plus-repl-${String(resolvedSessionId).replace(/[^A-Za-z0-9_-]/g, '-')}`
236
+ const titleId = `${popoverId}-title`
237
+ return h('span', { className: 'ptcPlusActiveShell', ref: shellRef },
238
+ h('button', {
239
+ type: 'button', className: 'ptcPlusActive', ref: triggerRef,
240
+ title: t('indicator.title'),
241
+ 'aria-label': t('indicator.title'), 'aria-controls': popoverId,
242
+ 'aria-expanded': expanded, 'aria-haspopup': 'dialog',
243
+ onPointerEnter: event => {
244
+ if (event.pointerType === 'touch') return
245
+ scheduleOpen()
246
+ },
247
+ onPointerLeave: scheduleHide,
248
+ // Only a keyboard reader opens the card by focusing the trigger; the focus
249
+ // a click leaves behind must not reopen what the click just toggled.
250
+ onFocus: () => { if (keyboard.current && !restoringFocus.current) showPopover() },
251
+ onBlur: scheduleHide, onClick: togglePopover,
252
+ onKeyDown: event => { if (event.key === 'Escape') hidePopover() },
253
+ }, h('span', { className: 'ptcPlusActiveLabel' }, 'PTC Plus')),
254
+ h(ReplMemoryCard, {
255
+ memory,
256
+ globalEnabled,
257
+ globalBindings,
258
+ loadGlobalBinding: id => callUserBindings('load', { id }),
259
+ t, id: popoverId, titleId, popoverRef,
260
+ onEnter: () => { cancelOpen(); cancelClose() },
261
+ onLeave: scheduleHide,
262
+ onGlobalTab: refreshGlobalBindings,
263
+ }))
264
+ }
265
+
266
+ return { PTCPlusSessionIndicator }
267
+ }
@@ -0,0 +1,292 @@
1
+ /**
2
+ * REPL surface views: the conversation tab (session bindings + global workbench)
3
+ * and the header memory card. Every piece here is a pure projection of props,
4
+ * session projections and settings; the entry owns registration and transport.
5
+ */
6
+ export function createReplView(React, deps) {
7
+ const {
8
+ CodeBlock, featureEnabled, normalizeReplMemorySnapshot,
9
+ unavailableReplMemorySnapshot, useSessionPreset, sessionUsesPtcPreset,
10
+ useWorkbenchController, UserBindingsWorkbench,
11
+ icons: { search: IconSearchOutline16, chevron: IconChevronDownOutline14 },
12
+ } = deps
13
+ const h = React.createElement
14
+
15
+ function ReplComposer() { return null }
16
+
17
+ function ReplBindingInspector({ entry, preview, t }) {
18
+ const readable = preview?.status === 'readable'
19
+ return h('aside', { className: 'ptcPlusBindingInspector', 'aria-label': entry.name },
20
+ h('header', { className: 'ptcPlusInspectorHead' },
21
+ h('strong', null, entry.name),
22
+ h('span', null, t('memory.location', entry.definition))),
23
+ h('div', { className: 'ptcPlusObservationLabel' }, t('console.definition')),
24
+ typeof CodeBlock === 'function'
25
+ ? h(CodeBlock, { code: entry.definition.source, lang: 'typescript', className: 'ptcPlusObservationCode',
26
+ copyLabel: t('tool.copy'), copiedLabel: t('tool.copied') })
27
+ : h('pre', { className: 'ptcPlusObservationCode' }, entry.definition.source),
28
+ h('div', { className: 'ptcPlusObservationLabel' }, t('console.value'),
29
+ preview?.truncated ? h('span', { className: 'ptcPlusObservationState' }, t('console.bounded')) : null),
30
+ readable ? h('pre', { className: 'ptcPlusObservationPreview' }, preview.text)
31
+ : h('p', { className: 'ptcPlusObservationUnavailable' }, t(preview === undefined ? 'console.unobservedValue' : 'console.unreadable')))
32
+ }
33
+
34
+ function ReplSessionBindings({ memory, t }) {
35
+ const [query, setQuery] = React.useState('')
36
+ const [kind, setKind] = React.useState('all')
37
+ const [selectedName, setSelectedName] = React.useState(null)
38
+ const entries = memory.entries.filter(entry => (kind === 'all' || entry.kind === kind)
39
+ && entry.name.toLowerCase().includes(query.trim().toLowerCase()))
40
+ const selected = entries.find(entry => entry.name === selectedName) ?? entries[0]
41
+ const observation = memory.observation
42
+ const previews = new Map(observation?.entries.map(entry => [entry.name, entry]) ?? [])
43
+ return h('section', { className: 'ptcPlusConsoleSection ptcPlusSessionBindings', 'aria-label': t('console.session') },
44
+ h('div', { className: 'ptcPlusObservationHead' },
45
+ h('div', { className: 'ptcPlusObservationTitle' },
46
+ h('h2', null, t('console.session')),
47
+ memory.available ? h('span', { className: 'ptcPlusObservationCount', title: t('memory.count', { count: memory.total }) },
48
+ query.trim() || kind !== 'all' ? `${entries.length} / ${memory.total}` : memory.total) : null,
49
+ memory.available ? h('span', { className: 'ptcPlusObservationReuseTotal', title: t('memory.reuseHint') },
50
+ t('memory.reuseTotal', { count: memory.reuseTotal })) : null),
51
+ h('span', { className: 'ptcPlusObservationTime' }, observation === undefined
52
+ ? t('console.unobserved') : t('console.observed', { time: new Date(observation.at).toLocaleString() }))),
53
+ memory.entries.length === 0 ? h('div', { className: 'ptcPlusSessionEmpty' },
54
+ t(memory.available ? 'memory.empty' : 'memory.unavailable'))
55
+ : h('div', { className: 'ptcPlusObservationGrid', 'data-empty': selected === undefined },
56
+ h('div', { className: 'ptcPlusObservationCatalog' },
57
+ h('div', { className: 'ptcPlusObservationFilters' },
58
+ h('label', { className: 'ptcPlusSearch' },
59
+ typeof IconSearchOutline16 === 'function' ? h(IconSearchOutline16, { size: 16 }) : null,
60
+ h('input', { value: query, onChange: event => setQuery(event.target.value),
61
+ placeholder: t('console.search'), 'aria-label': t('console.search') })),
62
+ h('select', { className: 'ptcPlusSelect', value: kind, onChange: event => setKind(event.target.value),
63
+ 'aria-label': t('console.kind') },
64
+ ['all', 'variable', 'function', 'class', 'import'].map(value => h('option', { key: value, value },
65
+ t(value === 'all' ? 'console.allKinds' : `memory.kind.${value}`))))),
66
+ h('div', { className: 'ptcPlusObservations' },
67
+ h('table', { className: 'ptcPlusObservationTable', 'aria-label': t('console.session') },
68
+ h('thead', null, h('tr', null,
69
+ h('th', { scope: 'col' }, t('console.name')),
70
+ h('th', { scope: 'col' }, t('console.kind')),
71
+ h('th', { scope: 'col' }, t('console.reuse')),
72
+ h('th', { scope: 'col' }, t('console.value')))),
73
+ h('tbody', null, entries.map(entry => {
74
+ const preview = previews.get(entry.name)
75
+ return h('tr', { key: entry.name, 'data-selected': entry === selected,
76
+ onClick: () => setSelectedName(entry.name) },
77
+ h('td', null, h('button', { type: 'button', className: 'ptcPlusObservationSelect',
78
+ 'aria-current': entry === selected ? 'true' : undefined, title: entry.name }, entry.name)),
79
+ h('td', { className: 'ptcPlusObservationKind' }, t(`memory.kind.${entry.kind}`)),
80
+ h('td', { className: 'ptcPlusObservationReuse', title: t('memory.reuseHint') }, entry.reuseCount),
81
+ h('td', null, h('span', { className: 'ptcPlusObservationValue' },
82
+ preview?.status === 'readable' ? h('code', null, preview.text)
83
+ : h('span', { className: 'ptcPlusObservationState' }, t(preview === undefined ? 'console.unobservedValue' : 'console.unreadable')),
84
+ preview?.truncated ? h('span', { className: 'ptcPlusObservationState' }, t('console.bounded')) : null)))
85
+ }))),
86
+ entries.length === 0 ? h('p', { className: 'ptcPlusMessage ptcPlusObservationEmpty' },
87
+ t(!memory.available ? 'memory.unavailable' : memory.entries.length === 0 ? 'memory.empty' : 'console.noMatches')) : null)),
88
+ selected ? h(ReplBindingInspector, { entry: selected, preview: previews.get(selected.name), t }) : null),
89
+ memory.omitted > 0 ? h('p', { className: 'ptcPlusMessage' }, t('memory.more', { count: memory.omitted })) : null)
90
+ }
91
+
92
+ function ReplConsole({ t, sessionId, useProjection, useSessions, usePtcSettings, callUserBindings, hideComposer, observeRepl }) {
93
+ const preset = useSessionPreset({ sessionId, useProjection, useSessions })
94
+ const projected = useProjection('ptcPlusRepl')
95
+ const settings = usePtcSettings(snapshot => snapshot)
96
+ const globalEnabled = featureEnabled(settings, 'bindings')
97
+ const workbench = useWorkbenchController({ enabled: globalEnabled, active: true, callUserBindings })
98
+ const eligible = featureEnabled(settings, 'replView') && sessionUsesPtcPreset(preset)
99
+ const memory = React.useMemo(() => {
100
+ try { return normalizeReplMemorySnapshot(projected) } catch { return unavailableReplMemorySnapshot() }
101
+ }, [projected])
102
+ const [observed, setObserved] = React.useState(null)
103
+ const observationRegion = React.useRef(null)
104
+ React.useEffect(() => eligible ? hideComposer(sessionId) : undefined, [eligible, hideComposer, sessionId])
105
+ React.useEffect(() => eligible ? observeRepl(sessionId, observationRegion.current, memory,
106
+ value => setObserved({ sessionId, source: memory, value })) : undefined, [eligible, observeRepl, sessionId, memory])
107
+ if (!eligible) return null
108
+ return h('div', { className: 'ptcPlusConsole', 'data-conversation-composer-overlay': '' },
109
+ h('div', { className: 'ptcPlusConsoleSection', ref: observationRegion }, h(ReplSessionBindings, {
110
+ key: sessionId, memory: observed?.sessionId === sessionId && observed.source === memory ? observed.value : memory, t })),
111
+ globalEnabled ? h('div', { className: 'ptcPlusConsoleSection ptcPlusBindingsSurface' },
112
+ h(UserBindingsWorkbench, { controller: workbench, t, headingLabel: t('console.global') })) : null)
113
+ }
114
+
115
+ function replPopoverIsOpen(popover) {
116
+ if (popover?.dataset?.open === 'true') return true
117
+ try {
118
+ return popover?.matches?.(':popover-open') === true
119
+ } catch {
120
+ return false
121
+ }
122
+ }
123
+
124
+ function placeReplPopover(trigger, popover) {
125
+ if (trigger === null || popover === null) return
126
+ const margin = 12
127
+ const gap = 8
128
+ const viewportWidth = document.documentElement.clientWidth || window.innerWidth
129
+ const viewportHeight = document.documentElement.clientHeight || window.innerHeight
130
+ const triggerRect = trigger.getBoundingClientRect()
131
+ const width = Math.min(344, Math.max(0, viewportWidth - margin * 2))
132
+ const left = Math.min(
133
+ Math.max(margin, triggerRect.right - width),
134
+ Math.max(margin, viewportWidth - width - margin),
135
+ )
136
+ const below = Math.max(0, viewportHeight - triggerRect.bottom - gap - margin)
137
+ const above = Math.max(0, triggerRect.top - gap - margin)
138
+ const opensAbove = below < 260 && above > below
139
+ const availableHeight = Math.max(80, opensAbove ? above : below)
140
+ popover.style.width = `${width}px`
141
+ popover.style.maxHeight = `${availableHeight}px`
142
+ popover.style.left = `${left}px`
143
+ popover.style.top = opensAbove
144
+ ? `${Math.max(margin, triggerRect.top - gap - Math.min(popover.offsetHeight, availableHeight))}px`
145
+ : `${Math.min(viewportHeight - margin, triggerRect.bottom + gap)}px`
146
+ }
147
+
148
+ function ReplMemoryCard({
149
+ memory,
150
+ globalEnabled,
151
+ globalBindings,
152
+ loadGlobalBinding,
153
+ t,
154
+ id,
155
+ titleId,
156
+ popoverRef,
157
+ onEnter,
158
+ onLeave,
159
+ onGlobalTab,
160
+ }) {
161
+ const [expandedBinding, setExpandedBinding] = React.useState(null)
162
+ const [tab, setTab] = React.useState('session')
163
+ const [globalSource, setGlobalSource] = React.useState(null)
164
+ const activeTab = globalEnabled ? tab : 'session'
165
+ const summary = activeTab === 'session'
166
+ ? memory.available ? `${t('memory.count', { count: memory.total })} · ${t('memory.reuseTotal', { count: memory.reuseTotal })}` : ''
167
+ : globalBindings === undefined ? '' : t('memory.globalCount', { count: globalBindings.entries.length })
168
+ const inspectGlobal = entry => {
169
+ if (globalSource?.id === entry.id) {
170
+ setGlobalSource(null)
171
+ return
172
+ }
173
+ setGlobalSource({ id: entry.id, source: '' })
174
+ loadGlobalBinding(entry.id).then(
175
+ loaded => setGlobalSource(current => current?.id === entry.id
176
+ ? { id: entry.id, source: loaded.entry.source }
177
+ : current),
178
+ () => setGlobalSource(current => current?.id === entry.id
179
+ ? { id: entry.id, error: true, source: '' }
180
+ : current),
181
+ )
182
+ }
183
+ return h('div', {
184
+ // A manual popover owns its lifetime: the trigger toggles it, and the entry
185
+ // that opened it closes it on an outside press or Escape.
186
+ className: 'ptcPlusReplPopover', id, ref: popoverRef, popover: 'manual',
187
+ role: 'dialog', 'aria-labelledby': titleId,
188
+ onPointerEnter: onEnter, onPointerLeave: onLeave,
189
+ },
190
+ h('div', { className: 'ptcPlusReplCard' },
191
+ h('div', { className: 'ptcPlusReplHead' },
192
+ h('span', { className: 'ptcPlusReplStatusDot', 'aria-hidden': true }),
193
+ h('span', { className: 'ptcPlusReplTitle', id: titleId }, t('memory.title')),
194
+ h('span', {
195
+ className: 'ptcPlusReplSummary',
196
+ 'aria-hidden': summary === '' ? true : undefined,
197
+ }, summary)),
198
+ globalEnabled
199
+ ? h('div', { className: 'ptcPlusReplTabs', role: 'tablist' },
200
+ h('button', {
201
+ type: 'button', role: 'tab', className: 'ptcPlusReplTab',
202
+ 'aria-selected': activeTab === 'session', onClick: () => setTab('session'),
203
+ }, t('memory.sessionTab')),
204
+ h('button', {
205
+ type: 'button', role: 'tab', className: 'ptcPlusReplTab',
206
+ 'aria-selected': activeTab === 'global',
207
+ onClick: () => { setTab('global'); onGlobalTab?.() },
208
+ }, t('memory.globalTab')))
209
+ : null,
210
+ activeTab === 'global'
211
+ ? h('div', { className: 'ptcPlusGlobalPane' },
212
+ globalBindings === undefined
213
+ ? h('span', { className: 'ptcPlusReplEmpty' }, t('memory.globalUnavailable'))
214
+ : globalBindings.entries.length === 0
215
+ ? h('span', { className: 'ptcPlusReplEmpty' }, t('memory.globalEmpty'))
216
+ : h('ul', { className: 'ptcPlusGlobalList' }, globalBindings.entries.map(entry => (
217
+ h('li', { key: entry.id, className: 'ptcPlusGlobalItem' },
218
+ h('button', {
219
+ type: 'button', className: 'ptcPlusBindingSelect',
220
+ 'aria-expanded': globalSource?.id === entry.id,
221
+ onClick: () => inspectGlobal(entry),
222
+ },
223
+ h('span', { className: 'ptcPlusBindingName', title: entry.name }, entry.name),
224
+ h('span', { className: 'ptcPlusBindingMeta', title: entry.symbols.join(', ') }, `${entry.scope} - ${entry.symbols.join(', ')}`),
225
+ h('span', { className: 'ptcPlusBindingState', 'data-enabled': entry.enabled },
226
+ t(entry.enabled ? 'bindings.enabled' : 'bindings.disabledEntry'))),
227
+ globalSource?.id !== entry.id
228
+ ? null
229
+ : globalSource.error === true
230
+ ? h('span', { className: 'ptcPlusReplEmpty' }, t('memory.globalUnavailable'))
231
+ : h('pre', { className: 'ptcPlusGlobalSource' }, globalSource.source))
232
+ )))
233
+ )
234
+ : !memory.available
235
+ ? h('span', { className: 'ptcPlusReplEmpty' }, t('memory.unavailable'))
236
+ : memory.entries.length === 0
237
+ ? h('span', { className: 'ptcPlusReplEmpty' }, t('memory.empty'))
238
+ : h('ul', { className: 'ptcPlusReplList' }, memory.entries.map((binding, index) => {
239
+ const expanded = expandedBinding === binding.name
240
+ const preview = binding.definition.source.replace(/\s+/g, ' ').trim()
241
+ const definitionId = `${id}-binding-${index}`
242
+ const toggle = () => setExpandedBinding(current => (
243
+ current === binding.name ? null : binding.name
244
+ ))
245
+ return h('li', {
246
+ className: 'ptcPlusReplBinding', key: binding.name,
247
+ 'data-expanded': expanded,
248
+ },
249
+ h('button', {
250
+ className: 'ptcPlusReplBindingTrigger', type: 'button',
251
+ 'aria-expanded': expanded,
252
+ 'aria-controls': expanded ? definitionId : undefined,
253
+ onClick: toggle,
254
+ },
255
+ h('span', { className: 'ptcPlusReplIdentity' },
256
+ h('span', {
257
+ className: 'ptcPlusReplName', 'data-kind': binding.kind,
258
+ title: `${binding.name} - ${t(`memory.kind.${binding.kind}`)}`,
259
+ }, binding.name),
260
+ h('span', { className: 'ptcPlusReplReuse', title: t('memory.reuseHint') },
261
+ t('memory.reuseCount', { count: binding.reuseCount }))),
262
+ h('span', { className: 'ptcPlusReplPreview', title: preview }, preview),
263
+ h('span', {
264
+ className: 'ptcPlusReplChevron', 'data-open': expanded, 'aria-hidden': true,
265
+ }, h(IconChevronDownOutline14, { size: 14 }))),
266
+ expanded
267
+ ? h('div', {
268
+ className: 'ptcPlusReplDefinitionWrap', 'data-open': true,
269
+ id: definitionId, role: 'region', 'aria-label': binding.name,
270
+ }, h('div', { className: 'ptcPlusReplDefinitionInner' },
271
+ h('div', { className: 'ptcPlusReplDefinition' },
272
+ h('span', { className: 'ptcPlusReplLocation' }, t('memory.location', {
273
+ line: binding.definition.line,
274
+ column: binding.definition.column,
275
+ })),
276
+ typeof CodeBlock === 'function'
277
+ ? h(CodeBlock, {
278
+ code: binding.definition.source,
279
+ lang: 'typescript',
280
+ className: 'ptcPlusReplCode',
281
+ copyLabel: t('tool.copy'),
282
+ copiedLabel: t('tool.copied'),
283
+ })
284
+ : h('pre', { className: 'ptcPlusReplCode' }, binding.definition.source))))
285
+ : null)
286
+ })),
287
+ tab !== 'session' || memory.omitted === 0 ? null
288
+ : h('span', { className: 'ptcPlusReplMore' }, t('memory.more', { count: memory.omitted }))))
289
+ }
290
+
291
+ return { ReplComposer, ReplConsole, ReplMemoryCard, replPopoverIsOpen, placeReplPopover }
292
+ }
@@ -0,0 +1,20 @@
1
+ import { RPC_CONTRACTS, RPC_REMOTE } from '../internal/rpc-contract.js'
2
+
3
+ /** Mount and consume the shared contract through the Host's public Remote caller. */
4
+ export async function createClientRpc(ctx) {
5
+ await ctx.remote.$mount(RPC_REMOTE)
6
+ const services = Object.values(RPC_CONTRACTS).map(({ service }) => `remote.${service}`)
7
+ let scope
8
+ await ctx.inject(services, current => {
9
+ scope = current
10
+ current.effect(() => () => { if (scope === current) scope = undefined })
11
+ })
12
+ return {
13
+ async call(contract, operation, payload, signal) {
14
+ if (scope === undefined) throw new Error('PTC Plus Remote service is unavailable')
15
+ const args = signal === undefined ? [operation, payload] : [operation, payload, signal]
16
+ const result = await scope.remote[contract.service].invoke(...args)
17
+ return result.ok ? result.value : result
18
+ },
19
+ }
20
+ }