dsh-ptc-plus 0.4.8 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (139) hide show
  1. package/README.en.md +61 -123
  2. package/README.md +61 -123
  3. package/assets/dsh-ptc-plus-banner-en.webp +0 -0
  4. package/assets/dsh-ptc-plus-banner-zh.webp +0 -0
  5. package/assets/ptc-plus-repl-workspace-en.png +0 -0
  6. package/assets/ptc-plus-repl-workspace-zh.png +0 -0
  7. package/assets/ptc-plus-settings-en.png +0 -0
  8. package/assets/ptc-plus-settings-zh.png +0 -0
  9. package/client.cjs +42336 -0
  10. package/cordis.patch.yml +13 -2
  11. package/docs/README.md +7 -2
  12. package/docs/adr/0005-temporary-rejected-cell-edit-transport.md +31 -179
  13. package/docs/adr/0008-define-agent-operating-contract.md +5 -1
  14. package/docs/adr/0009-bound-expensive-acceptance-default.md +2 -2
  15. package/docs/adr/0016-archive-resolved-review-findings.md +1 -1
  16. package/docs/adr/0017-track-the-latest-dsh-public-surface.md +2 -2
  17. package/docs/adr/0019-plugin-settings-and-kill-switch.md +14 -3
  18. package/docs/adr/0020-optional-cordis-tools-in-ptc-mode.md +2 -2
  19. package/docs/adr/0022-stage-npm-releases-with-oidc.md +4 -2
  20. package/docs/adr/0023-global-user-bindings.md +3 -1
  21. package/docs/adr/0026-persist-the-delivery-verdict-against-the-verified-candidate.md +2 -0
  22. package/docs/adr/0028-attach-to-the-host-ptc-execution-seam.md +101 -35
  23. package/docs/adr/0030-serve-both-typert-codec-generations-from-one-codec.md +3 -3
  24. package/docs/adr/0031-close-semantic-obligations-by-owner.md +1 -1
  25. package/docs/adr/0032-own-session-log-facts-in-a-host-only-projection.md +27 -0
  26. package/docs/adr/0033-gate-client-visual-usability-in-a-browser.md +23 -0
  27. package/docs/adr/0034-keep-program-values-separate-from-output-projections.md +29 -0
  28. package/docs/adr/0035-run-coverage-only-before-a-new-release.md +27 -0
  29. package/docs/adr/0036-separate-focused-development-from-delivery-verification.md +29 -0
  30. package/docs/adr/0037-keep-tool-hover-previews-passive.md +52 -0
  31. package/docs/adr/0038-freeze-session-capability-plans.md +71 -0
  32. package/docs/adr/0039-verify-documentation-without-runtime-suites.md +29 -0
  33. package/docs/adr/0040-separate-capability-configurations-from-session-observations.md +39 -0
  34. package/docs/adr/0041-record-cell-execution-directories.md +65 -0
  35. package/docs/adr/0042-persist-provider-availability-and-handback.md +41 -0
  36. package/docs/adr/0043-compose-capability-and-context-policies.md +37 -0
  37. package/docs/adr/README.md +6 -0
  38. package/docs/architecture.md +25 -27
  39. package/docs/binding-review-placement-design.md +29 -21
  40. package/docs/capability-planning-components.md +93 -0
  41. package/docs/capability-projection.md +7 -0
  42. package/docs/client-ui.md +88 -29
  43. package/docs/client-visual-contract.md +65 -0
  44. package/docs/context-management.md +22 -0
  45. package/docs/context-optimization-plan.md +353 -0
  46. package/docs/context-public-interface-coverage.md +25 -0
  47. package/docs/durability-design.md +31 -14
  48. package/docs/evaluation.md +41 -6
  49. package/docs/evaluations/command-goat-deepseek-v4.1-flash-20261006.md +126 -0
  50. package/docs/evaluations/command-goat-deepseek-v4.1-flash.md +281 -0
  51. package/docs/installation.md +50 -8
  52. package/docs/program-data-plane.md +5 -3
  53. package/docs/publishing.md +5 -5
  54. package/docs/release-coverage.md +73 -0
  55. package/docs/release-notes.md +1 -1
  56. package/docs/review-procedure.md +56 -0
  57. package/docs/runtime-reference.md +48 -16
  58. package/docs/semantic-validation.md +14 -2
  59. package/docs/stateful-binding-updates-proposal.md +2 -2
  60. package/docs/user-bindings.en.md +2 -2
  61. package/docs/user-bindings.md +3 -3
  62. package/docs/value-wire.md +11 -1
  63. package/docs/verification-optimization-work.md +52 -15
  64. package/docs/verification.md +30 -86
  65. package/index.js +147 -23
  66. package/internal/binding-toggle-receipt.js +24 -0
  67. package/internal/capability-command-receipt.js +29 -0
  68. package/internal/capability-environment.js +1247 -0
  69. package/internal/capability-plan.js +285 -0
  70. package/internal/capability-policy.js +96 -0
  71. package/internal/capability-statistics-store.js +180 -0
  72. package/internal/capability-statistics.js +210 -0
  73. package/internal/compiler-platform.js +5 -3
  74. package/internal/compiler-service.js +2 -9
  75. package/internal/config-spec.js +79 -9
  76. package/internal/cordis-tools-owner.js +22 -19
  77. package/internal/direct-surface-owner.js +29 -29
  78. package/internal/dynamic-environment-runtime.js +9 -9
  79. package/internal/edit-transport-owner.js +183 -139
  80. package/internal/execution-provider-owner.js +160 -0
  81. package/internal/execution-seam-compat.js +76 -120
  82. package/internal/isolated-worker.js +12 -8
  83. package/internal/kernel-worker.js +104 -32
  84. package/internal/native-root-dynamic.js +12 -13
  85. package/internal/program-bindings.js +190 -0
  86. package/internal/recovery-tips.js +53 -23
  87. package/internal/rejected-cell-editor.js +8 -8
  88. package/internal/repl-observation-interest.js +11 -3
  89. package/internal/rpc-contract.js +1 -0
  90. package/internal/runtime-bridge-owner.js +351 -82
  91. package/internal/runtime-config.js +58 -0
  92. package/internal/runtime-contexts.js +85 -16
  93. package/internal/runtime-intrinsics.js +1 -0
  94. package/internal/runtime-messages.js +41 -0
  95. package/internal/sdk-projection.js +10 -82
  96. package/internal/session-cell-executor.js +42 -19
  97. package/internal/session-events.js +74 -4
  98. package/internal/session-journal-recovery.js +904 -184
  99. package/internal/session-journal-schema.js +6 -3
  100. package/internal/session-journal.js +31 -2
  101. package/internal/session-log-view.js +189 -43
  102. package/internal/session-runtime.js +175 -23
  103. package/internal/settings-compat.js +23 -6
  104. package/internal/stateful-module-compiler.js +3 -2
  105. package/internal/stateful-module-runtime.js +7 -3
  106. package/internal/stateful-root-runtime.js +9 -4
  107. package/internal/user-binding-authoring.js +2 -7
  108. package/internal/user-bindings-owner.js +47 -7
  109. package/internal/validated-parse-repair.js +1 -1
  110. package/internal/value-wire.js +68 -21
  111. package/internal/worker-cwd-virtualization.js +20 -18
  112. package/internal/worker-realm-surfaces.js +3 -3
  113. package/locale/en.json +6 -0
  114. package/locale/zh.json +6 -0
  115. package/package.json +55 -38
  116. package/src/client-authoring-view.js +286 -168
  117. package/src/client-binding-review.js +4 -1
  118. package/src/client-binding-toggle.js +18 -0
  119. package/src/client-capabilities-view.js +1341 -0
  120. package/src/client-capability-sort.js +29 -0
  121. package/src/client-code-editor.js +6 -0
  122. package/src/client-copy.js +881 -8
  123. package/src/client-feature-gates.js +15 -5
  124. package/src/client-host-compat.js +23 -2
  125. package/src/client-indicator-view.js +4 -6
  126. package/src/client-primitives.js +280 -0
  127. package/src/client-repl-view.js +23 -16
  128. package/src/client-settings-view.js +412 -69
  129. package/src/client-styles.js +193 -41
  130. package/src/client-tool-hover.js +185 -0
  131. package/src/client-tool-view.js +45 -11
  132. package/src/client-transport.js +44 -9
  133. package/src/client-workbench.js +9 -10
  134. package/src/client.js +148 -68
  135. package/assets/ptc-plus-bindings-en.png +0 -0
  136. package/assets/ptc-plus-bindings-zh.png +0 -0
  137. package/assets/ptc-plus-repair-en.png +0 -0
  138. package/assets/ptc-plus-repair-zh.png +0 -0
  139. package/client.js +0 -31857
@@ -1,3 +1,5 @@
1
+ import { CONFIG_DEFAULTS } from '../internal/config-spec.js'
2
+
1
3
  /**
2
4
  * Settings snapshot to feature eligibility. Each feature names the settings that
3
5
  * gate exactly one contribution, so a new gated surface reads one table instead
@@ -11,22 +13,30 @@ const FEATURE_SETTINGS = Object.freeze({
11
13
  bindings: ['userBindingsEnabled'],
12
14
  // Enhanced run_code tool rows.
13
15
  toolView: ['enhancedToolView'],
16
+ toolHover: ['enhancedToolView', 'toolHoverPreviewEnabled'],
14
17
  // The REPL view tab; the session preset condition is added by its registration.
15
18
  replView: ['replViewEnabled'],
16
19
  // The composer authoring menu.
17
20
  authorButton: ['userBindingsEnabled', 'bindingAuthorButtonVisible'],
21
+ // Settings and capability refresh do not require Global User Bindings.
22
+ ptcMenu: ['bindingAuthorButtonVisible'],
18
23
  })
19
24
 
20
25
  /**
21
- * Pure settings projection: a feature is eligible only from a ready snapshot
22
- * with the master switch on. These features default to on unless the user
23
- * turned them off. Writability gates setting writes, not contribution presence.
26
+ * Pure settings projection: a feature is eligible unless the settings say
27
+ * otherwise. Only a readable snapshot can turn a feature off — an unreadable or
28
+ * not-yet-arrived settings transport falls back to the defaults the plugin ships
29
+ * in `CONFIG_FIELDS`, so a transport outage degrades the settings surface
30
+ * instead of removing every contribution the plugin owns. Writability gates
31
+ * setting writes, not contribution presence.
24
32
  */
25
33
  export function featureEnabled(snapshot, feature) {
26
34
  const rule = FEATURE_SETTINGS[feature]
27
35
  if (rule === undefined) throw new Error(`Unknown client feature: ${feature}`)
28
- if (snapshot?.status !== 'ready' || snapshot.value?.enabled !== true) return false
29
- const value = snapshot.value
36
+ const value = snapshot?.status === 'ready' && snapshot.value !== undefined
37
+ ? snapshot.value
38
+ : CONFIG_DEFAULTS
39
+ if (value.enabled === false) return false
30
40
  return rule.every(key => value[key] !== false)
31
41
  }
32
42
 
@@ -1,4 +1,25 @@
1
1
  import { SETTINGS_NAMESPACE } from '../internal/config-spec.js'
2
+ import { ForwardRef, Lazy, Memo, isValidElementType } from 'react-is'
3
+
4
+ /** Host exports are components, including React's wrapped object types. */
5
+ export function isHostComponent(component) {
6
+ if (typeof component === 'function') return true
7
+ if (typeof component !== 'object' || component === null) return false
8
+ const seen = new Set()
9
+ while (isValidElementType(component)) {
10
+ if (typeof component !== 'object' || component === null) return true
11
+ if (seen.has(component)) return false
12
+ seen.add(component)
13
+ // react-is also admits server references and wrappers with missing payloads.
14
+ if (component.$$typeof === ForwardRef) return typeof component.render === 'function'
15
+ if (component.$$typeof === Lazy) return typeof component._init === 'function'
16
+ && typeof component._payload === 'object' && component._payload !== null
17
+ if (component.$$typeof !== Memo) return false
18
+ if (component.compare != null && typeof component.compare !== 'function') return false
19
+ component = component.type
20
+ }
21
+ return false
22
+ }
2
23
 
3
24
  const HOST_ICON_EXPORTS = Object.freeze({
4
25
  check: ['IconCheckOutlineRegular', 'IconCheckOutline14'],
@@ -28,7 +49,7 @@ ABSENT_HOST_ICON.absentHostIcon = true
28
49
 
29
50
  /** Whether a normalized icon entry is a glyph the host actually published. */
30
51
  export function isHostIconComponent(icon) {
31
- return typeof icon === 'function' && icon.absentHostIcon !== true
52
+ return isHostComponent(icon) && icon.absentHostIcon !== true
32
53
  }
33
54
 
34
55
  /**
@@ -42,7 +63,7 @@ export function hostIconComponents(primitives) {
42
63
  return Object.fromEntries(Object.entries(HOST_ICON_EXPORTS).map(([name, candidates]) => [
43
64
  name,
44
65
  candidates.map(candidate => primitives?.[candidate])
45
- .find(value => typeof value === 'function') ?? ABSENT_HOST_ICON,
66
+ .find(isHostComponent) ?? ABSENT_HOST_ICON,
46
67
  ]))
47
68
  }
48
69
 
@@ -22,6 +22,7 @@ export function createIndicatorView(React, deps) {
22
22
  normalizeReplMemorySnapshot, unavailableReplMemorySnapshot,
23
23
  ReplMemoryCard, replPopoverIsOpen, placeReplPopover,
24
24
  catalogOwner, subscribeReset,
25
+ useObservedReplMemory, observeRepl,
25
26
  } = deps
26
27
  const h = React.createElement
27
28
 
@@ -225,13 +226,10 @@ export function createIndicatorView(React, deps) {
225
226
  }
226
227
  }
227
228
  }, [cancelClose, cancelOpen, schedulePlacement])
229
+ const memory = useObservedReplMemory({ sessionId, projected: projectionMemory,
230
+ active: expanded && sessionUsesPtcPreset(preset) && featureEnabled(settings, 'plugin'),
231
+ elementRef: popoverRef, observeRepl })
228
232
  if (!sessionUsesPtcPreset(preset) || !featureEnabled(settings, 'plugin')) return null
229
- let memory
230
- try {
231
- memory = normalizeReplMemorySnapshot(projectionMemory)
232
- } catch {
233
- memory = unavailableReplMemorySnapshot()
234
- }
235
233
  const popoverId = `ptc-plus-repl-${String(resolvedSessionId).replace(/[^A-Za-z0-9_-]/g, '-')}`
236
234
  const titleId = `${popoverId}-title`
237
235
  return h('span', { className: 'ptcPlusActiveShell', ref: shellRef },
@@ -0,0 +1,280 @@
1
+ import { isHostComponent } from './client-host-compat.js'
2
+ import * as RadixSelect from '@radix-ui/react-select'
3
+
4
+ /**
5
+ * Resolve the Harness Client components this plugin renders against the
6
+ * installed generation.
7
+ *
8
+ * The plugin may load a generation that renamed, removed, or reshaped a
9
+ * primitive; a missing component would otherwise throw while rendering and take
10
+ * the whole slot entry down with it. Each component therefore resolves to the
11
+ * shipped primitive when that generation provides it and to a plugin-local
12
+ * fallback otherwise, so a primitive change degrades one control instead of
13
+ * blanking the entry that renders it.
14
+ */
15
+
16
+ /** One menu row: separators and labels paint; every other row selects by id. */
17
+ function menuEntry(React, item, onSelect) {
18
+ const h = React.createElement
19
+ const content = item.label ?? item.text ?? null
20
+ if (item.type === 'separator') {
21
+ return h('div', { key: item.id, className: 'ptcPlusFallbackMenuSeparator', role: 'separator' })
22
+ }
23
+ if (item.type === 'label') {
24
+ return h('div', { key: item.id, className: 'ptcPlusFallbackMenuLabel' }, content)
25
+ }
26
+ return h('button', {
27
+ key: item.id, type: 'button', role: 'menuitem', className: 'ptcPlusFallbackMenuItem',
28
+ disabled: item.disabled === true,
29
+ onClick: () => onSelect?.(item.id),
30
+ }, content)
31
+ }
32
+
33
+ function useCompositionGuard(React, open) {
34
+ const composing = React.useRef(false)
35
+ const ended = React.useRef(false)
36
+ React.useEffect(() => {
37
+ composing.current = false
38
+ ended.current = false
39
+ if (!open) return undefined
40
+ const start = () => { composing.current = true }
41
+ const end = () => { composing.current = false; ended.current = true }
42
+ const release = () => { ended.current = false }
43
+ const blur = () => { composing.current = false; ended.current = false }
44
+ document.addEventListener('compositionstart', start, true)
45
+ document.addEventListener('compositionend', end, true)
46
+ document.addEventListener('keyup', release, true)
47
+ window.addEventListener('blur', blur)
48
+ return () => {
49
+ document.removeEventListener('compositionstart', start, true)
50
+ document.removeEventListener('compositionend', end, true)
51
+ document.removeEventListener('keyup', release, true)
52
+ window.removeEventListener('blur', blur)
53
+ composing.current = false
54
+ ended.current = false
55
+ }
56
+ }, [open])
57
+ return event => {
58
+ const guarded = composing.current || ended.current || event.isComposing === true || event.keyCode === 229
59
+ ended.current = false
60
+ return guarded
61
+ }
62
+ }
63
+
64
+ /** Menu fallback: the anchor, the item list, the pinned footer and the children region. */
65
+ function createFallbackMenu(React, createPortal = content => content) {
66
+ const h = React.createElement
67
+ return function FallbackMenu({ open, items = [], children, footer, className, listClassName, autoFocus, anchor,
68
+ onClose, onSelect, portal = false, side = 'bottom', align = 'start', getAnchorRect, closeOnPointerLeave = false }) {
69
+ const root = React.useRef(null)
70
+ const list = React.useRef(null)
71
+ const closeTimer = React.useRef(null)
72
+ const close = React.useRef(onClose)
73
+ close.current = onClose
74
+ const isComposing = useCompositionGuard(React, open)
75
+ const cancelClose = () => { clearTimeout(closeTimer.current); closeTimer.current = null }
76
+ const scheduleClose = () => {
77
+ if (!closeOnPointerLeave) return
78
+ cancelClose()
79
+ closeTimer.current = setTimeout(() => close.current?.(), 180)
80
+ }
81
+ React.useEffect(() => cancelClose, [open])
82
+ React.useLayoutEffect(() => {
83
+ if (!open) return undefined
84
+ const place = () => {
85
+ const rect = getAnchorRect?.() ?? root.current?.getBoundingClientRect()
86
+ const surface = list.current
87
+ if (!rect || !surface) return
88
+ const viewport = window.visualViewport
89
+ const viewportTop = viewport?.offsetTop ?? 0
90
+ const viewportLeft = viewport?.offsetLeft ?? 0
91
+ const viewportHeight = viewport?.height ?? window.innerHeight
92
+ const viewportWidth = viewport?.width ?? window.innerWidth
93
+ const available = Math.max(0, side === 'top' ? rect.top - viewportTop - 12
94
+ : viewportTop + viewportHeight - rect.bottom - 12)
95
+ surface.style.maxHeight = `${available}px`
96
+ const measured = surface.getBoundingClientRect()
97
+ const left = align === 'end' ? rect.right - measured.width : rect.left
98
+ surface.style.left = `${Math.max(viewportLeft + 8, Math.min(left, viewportLeft + viewportWidth - measured.width - 8))}px`
99
+ surface.style.top = `${side === 'top' ? Math.max(viewportTop + 8, rect.top - measured.height - 4) : rect.bottom + 4}px`
100
+ }
101
+ place()
102
+ window.addEventListener('resize', place)
103
+ window.addEventListener('scroll', place, true)
104
+ window.visualViewport?.addEventListener('resize', place)
105
+ window.visualViewport?.addEventListener('scroll', place)
106
+ return () => {
107
+ window.removeEventListener('resize', place)
108
+ window.removeEventListener('scroll', place, true)
109
+ window.visualViewport?.removeEventListener('resize', place)
110
+ window.visualViewport?.removeEventListener('scroll', place)
111
+ }
112
+ }, [open, side, align, getAnchorRect, children, items, footer])
113
+ React.useEffect(() => {
114
+ if (!open) return undefined
115
+ if (autoFocus) list.current?.querySelector('button:not(:disabled)')?.focus()
116
+ const onKeyDown = event => {
117
+ if (event.defaultPrevented || isComposing(event)) return
118
+ if (event.key === 'Escape') {
119
+ event.preventDefault()
120
+ event.stopPropagation()
121
+ if (!event.repeat) close.current?.()
122
+ return
123
+ }
124
+ if (!root.current?.contains(event.target) && !list.current?.contains(event.target)) return
125
+ if (!['ArrowDown', 'ArrowUp', 'Home', 'End'].includes(event.key)) return
126
+ const controls = [...list.current.querySelectorAll('button:not(:disabled)')]
127
+ if (controls.length === 0) return
128
+ const current = controls.indexOf(document.activeElement)
129
+ const next = event.key === 'Home' ? 0 : event.key === 'End' ? controls.length - 1
130
+ : (current + (event.key === 'ArrowDown' ? 1 : -1) + controls.length) % controls.length
131
+ event.preventDefault()
132
+ controls[next].focus()
133
+ }
134
+ const onPointerDown = event => {
135
+ if (root.current?.contains(event.target) || list.current?.contains(event.target)) return
136
+ close.current?.()
137
+ }
138
+ document.addEventListener('keydown', onKeyDown)
139
+ document.addEventListener('pointerdown', onPointerDown, true)
140
+ return () => {
141
+ document.removeEventListener('keydown', onKeyDown)
142
+ document.removeEventListener('pointerdown', onPointerDown, true)
143
+ }
144
+ }, [open, autoFocus])
145
+ const rows = list => list.map(item => menuEntry(React, item, onSelect))
146
+ const surface = open ? h('div', { ref: list, className: ['ptcPlusFallbackMenuList', listClassName].filter(Boolean).join(' '),
147
+ role: 'menu', onPointerEnter: cancelClose, onPointerLeave: scheduleClose }, ...rows(items),
148
+ ...(Array.isArray(footer) ? rows(footer) : []),
149
+ children ?? null) : null
150
+ return h('span', { ref: root, className: ['ptcPlusFallbackMenu', className].filter(Boolean).join(' '),
151
+ onPointerEnter: cancelClose, onPointerLeave: scheduleClose }, anchor ?? null,
152
+ portal && surface !== null ? createPortal(surface, document.body) : surface)
153
+ }
154
+ }
155
+
156
+ /** Modal fallback: headless content or a titled dialog that closes on Escape and backdrop. */
157
+ function createFallbackModal(React, createPortal = content => content) {
158
+ const h = React.createElement
159
+ const modalStack = []
160
+ return function FallbackModal({ open, onClose, title, closeLabel, description, className, contentClassName, headless, children }) {
161
+ const root = React.useRef(null)
162
+ const close = React.useRef(onClose)
163
+ close.current = onClose
164
+ const isComposing = useCompositionGuard(React, open)
165
+ const focusable = 'button:not(:disabled),input:not(:disabled),textarea:not(:disabled),select:not(:disabled),[href],[tabindex]:not([tabindex="-1"]),[contenteditable=true],summary'
166
+ React.useLayoutEffect(() => {
167
+ if (!open) return undefined
168
+ const previous = document.activeElement
169
+ const surface = root.current
170
+ const target = surface?.querySelector('[data-modal-autofocus]:not(:disabled)') ?? surface?.querySelector(focusable) ?? surface
171
+ target?.focus({ preventScroll: true })
172
+ modalStack.push(surface)
173
+ const onDocumentKeyDown = event => {
174
+ if (modalStack.at(-1) === surface) onKeyDown(event)
175
+ }
176
+ document.addEventListener('keydown', onDocumentKeyDown)
177
+ return () => {
178
+ document.removeEventListener('keydown', onDocumentKeyDown)
179
+ modalStack.splice(modalStack.indexOf(surface), 1)
180
+ if (document.activeElement === document.body || surface?.contains(document.activeElement)) previous?.focus?.({ preventScroll: true })
181
+ }
182
+ }, [open])
183
+ const onKeyDown = event => {
184
+ if (event.defaultPrevented || isComposing(event)) return
185
+ if (event.key === 'Escape') {
186
+ event.preventDefault()
187
+ event.stopPropagation()
188
+ if (!event.repeat) close.current?.()
189
+ } else if (event.key === 'Tab') {
190
+ const controls = [...root.current.querySelectorAll(focusable)]
191
+ .filter(control => control.getClientRects().length > 0 && !control.closest('[aria-hidden=true],[hidden]'))
192
+ const target = event.shiftKey ? controls.at(-1) : controls[0]
193
+ const boundary = event.shiftKey ? controls[0] : controls.at(-1)
194
+ if (controls.length === 0 || document.activeElement === boundary || !root.current.contains(document.activeElement)) {
195
+ event.preventDefault()
196
+ ;(target ?? root.current).focus({ preventScroll: true })
197
+ }
198
+ }
199
+ }
200
+ if (!open) return null
201
+ const body = h('div', { className: contentClassName ?? 'ptcPlusFallbackModalBody' }, children)
202
+ const surface = ['ptcPlusFallbackModal', className].filter(Boolean).join(' ')
203
+ return createPortal(h('div', {
204
+ className: 'ptcPlusFallbackModalBackdrop',
205
+ onPointerDown: event => { if (event.target === event.currentTarget) onClose?.() },
206
+ }, h('div', { ref: root, tabIndex: -1, className: surface, role: 'dialog', 'aria-modal': 'true', 'aria-label': title },
207
+ headless === true ? null : h('div', { className: 'ptcPlusFallbackModalHead' },
208
+ h('h2', null, title),
209
+ h('button', { type: 'button', className: 'ptcPlusDialogClose', 'aria-label': closeLabel ?? title, onClick: () => onClose?.() },
210
+ closeLabel ?? '\u00d7')),
211
+ headless === true || description === undefined ? null : h('p', { className: 'ptcPlusDetail' }, description),
212
+ headless === true ? children : body)), document.body)
213
+ }
214
+ }
215
+
216
+ /**
217
+ * Resolve every component the plugin renders.
218
+ * @param primitives - the installed generation's Client primitives.
219
+ * @param React - the browser React instance.
220
+ * @returns the component set the views consume.
221
+ */
222
+ export function resolvePrimitives(primitives, React, createPortal) {
223
+ return Object.freeze({
224
+ Button: primitives.Button,
225
+ CodeBlock: primitives.CodeBlock,
226
+ DisclosureRow: primitives.DisclosureRow,
227
+ Menu: isHostComponent(primitives.Menu) ? primitives.Menu : createFallbackMenu(React, createPortal),
228
+ MenuFallback: createFallbackMenu(React, createPortal),
229
+ Select: createSelect(React),
230
+ Modal: isHostComponent(primitives.Modal) ? primitives.Modal : createFallbackModal(React, createPortal),
231
+ Toast: primitives.Toast,
232
+ Tooltip: primitives.Tooltip,
233
+ })
234
+ }
235
+
236
+ /** Shared themed choice control; Radix owns listbox focus, typeahead, and dismissal. */
237
+ export function createSelect(React) {
238
+ const h = React.createElement
239
+ return function Select({ value, options, onChange, label, disabled = false, className = '',
240
+ placeholder, dataStrategy, autoFocus = false }) {
241
+ const [open, setOpen] = React.useState(false)
242
+ React.useEffect(() => { if (disabled) setOpen(false) }, [disabled])
243
+ const chosen = options.find(option => option.value === value)
244
+ let emptyToken = '__ptc_plus_empty_choice__'
245
+ while (options.some(option => option.value === emptyToken)) emptyToken += '_'
246
+ return h(RadixSelect.Root, { value: value === '' ? emptyToken : value, open, disabled,
247
+ onOpenChange: setOpen, onValueChange: next => onChange(next === emptyToken ? '' : next) },
248
+ h(RadixSelect.Trigger, { className: ['ptcPlusSelectTrigger', className].filter(Boolean).join(' '),
249
+ 'aria-label': label, 'data-strategy': dataStrategy, 'data-modal-autofocus': autoFocus || undefined },
250
+ h(RadixSelect.Value, { placeholder: placeholder ?? chosen?.label ?? '' }),
251
+ h(RadixSelect.Icon, { className: 'ptcPlusSelectChevron', 'aria-hidden': true },
252
+ h('svg', { viewBox: '0 0 16 16', width: 16, height: 16, fill: 'none' },
253
+ h('path', { d: 'm4 6 4 4 4-4', stroke: 'currentColor', strokeWidth: 1.5, strokeLinecap: 'round', strokeLinejoin: 'round' })))),
254
+ h(RadixSelect.Portal, null,
255
+ h(RadixSelect.Content, { className: 'ptcPlusSelectMenu', position: 'popper', sideOffset: 4,
256
+ onEscapeKeyDown: event => event.stopPropagation() },
257
+ h(RadixSelect.Viewport, { className: 'ptcPlusSelectViewport' },
258
+ ...options.map(option => h(RadixSelect.Item, { key: option.value, value: option.value === '' ? emptyToken : option.value,
259
+ disabled: option.disabled === true, className: 'ptcPlusSelectOption' },
260
+ h(RadixSelect.ItemText, null, option.label),
261
+ h(RadixSelect.ItemIndicator, { className: 'ptcPlusSelectCheck' }, '✓')))))))
262
+ }
263
+ }
264
+
265
+ /** Adapt host action styling, or own it completely when the primitive is absent. */
266
+ export function createActionButton(React, Button) {
267
+ const h = React.createElement
268
+ return function ActionButton({ className = '', 'data-kind': kind, ...props }) {
269
+ const classes = className.split(/\s+/).filter(name => name !== '' && name !== 'ptcPlusButton')
270
+ return isHostComponent(Button)
271
+ ? h(Button, {
272
+ type: 'button', ...props, size: 'sm',
273
+ variant: kind === 'primary' ? 'primary' : kind === 'ghost' ? 'ghost' : 'outline',
274
+ className: classes.join(' '),
275
+ })
276
+ : h('button', { type: 'button', ...props, className: ['ptcPlusButton', ...classes].join(' '), 'data-kind': kind })
277
+ }
278
+ }
279
+
280
+ export { createFallbackMenu, createFallbackModal }
@@ -1,4 +1,4 @@
1
- import { isHostIconComponent } from './client-host-compat.js'
1
+ import { isHostComponent, isHostIconComponent } from './client-host-compat.js'
2
2
 
3
3
  /**
4
4
  * REPL surface views: the conversation tab (session bindings + global workbench)
@@ -7,7 +7,7 @@ import { isHostIconComponent } from './client-host-compat.js'
7
7
  */
8
8
  export function createReplView(React, deps) {
9
9
  const {
10
- CodeBlock, featureEnabled, normalizeReplMemorySnapshot,
10
+ CodeBlock, Select, featureEnabled, normalizeReplMemorySnapshot,
11
11
  unavailableReplMemorySnapshot, useSessionPreset, sessionUsesPtcPreset,
12
12
  useWorkbenchController, UserBindingsWorkbench,
13
13
  icons: { search: IconSearchOutline16, chevron: IconChevronDownOutline14 },
@@ -16,6 +16,18 @@ export function createReplView(React, deps) {
16
16
 
17
17
  function ReplComposer() { return null }
18
18
 
19
+ function useObservedReplMemory({ sessionId, projected, active, elementRef, observeRepl }) {
20
+ const memory = React.useMemo(() => {
21
+ try { return normalizeReplMemorySnapshot(projected) } catch { return unavailableReplMemorySnapshot() }
22
+ }, [projected])
23
+ const [observed, setObserved] = React.useState(null)
24
+ React.useEffect(() => active && typeof observeRepl === 'function'
25
+ ? observeRepl(sessionId, elementRef.current, memory,
26
+ value => setObserved({ sessionId, source: memory, value }))
27
+ : undefined, [active, observeRepl, sessionId, memory, elementRef])
28
+ return observed?.sessionId === sessionId && observed.source === memory ? observed.value : memory
29
+ }
30
+
19
31
  function ReplBindingInspector({ entry, preview, t }) {
20
32
  const readable = preview?.status === 'readable'
21
33
  return h('aside', { className: 'ptcPlusBindingInspector', 'aria-label': entry.name },
@@ -23,7 +35,7 @@ export function createReplView(React, deps) {
23
35
  h('strong', null, entry.name),
24
36
  h('span', null, t('memory.location', entry.definition))),
25
37
  h('div', { className: 'ptcPlusObservationLabel' }, t('console.definition')),
26
- typeof CodeBlock === 'function'
38
+ isHostComponent(CodeBlock)
27
39
  ? h(CodeBlock, { code: entry.definition.source, lang: 'typescript', className: 'ptcPlusObservationCode',
28
40
  copyLabel: t('tool.copy'), copiedLabel: t('tool.copied') })
29
41
  : h('pre', { className: 'ptcPlusObservationCode' }, entry.definition.source),
@@ -61,10 +73,9 @@ export function createReplView(React, deps) {
61
73
  isHostIconComponent(IconSearchOutline16) ? h(IconSearchOutline16, { size: 16 }) : null,
62
74
  h('input', { value: query, onChange: event => setQuery(event.target.value),
63
75
  placeholder: t('console.search'), 'aria-label': t('console.search') })),
64
- h('select', { className: 'ptcPlusSelect', value: kind, onChange: event => setKind(event.target.value),
65
- 'aria-label': t('console.kind') },
66
- ['all', 'variable', 'function', 'class', 'import'].map(value => h('option', { key: value, value },
67
- t(value === 'all' ? 'console.allKinds' : `memory.kind.${value}`))))),
76
+ h(Select, { value: kind, onChange: setKind, label: t('console.kind'),
77
+ options: ['all', 'variable', 'function', 'class', 'import'].map(value => ({ value,
78
+ label: t(value === 'all' ? 'console.allKinds' : `memory.kind.${value}`) })) })),
68
79
  h('div', { className: 'ptcPlusObservations' },
69
80
  h('table', { className: 'ptcPlusObservationTable', 'aria-label': t('console.session') },
70
81
  h('thead', null, h('tr', null,
@@ -98,18 +109,14 @@ export function createReplView(React, deps) {
98
109
  const globalEnabled = featureEnabled(settings, 'bindings')
99
110
  const workbench = useWorkbenchController({ enabled: globalEnabled, active: true, callUserBindings })
100
111
  const eligible = featureEnabled(settings, 'replView') && sessionUsesPtcPreset(preset)
101
- const memory = React.useMemo(() => {
102
- try { return normalizeReplMemorySnapshot(projected) } catch { return unavailableReplMemorySnapshot() }
103
- }, [projected])
104
- const [observed, setObserved] = React.useState(null)
105
112
  const observationRegion = React.useRef(null)
113
+ const memory = useObservedReplMemory({ sessionId, projected, active: eligible,
114
+ elementRef: observationRegion, observeRepl })
106
115
  React.useEffect(() => eligible ? hideComposer(sessionId) : undefined, [eligible, hideComposer, sessionId])
107
- React.useEffect(() => eligible ? observeRepl(sessionId, observationRegion.current, memory,
108
- value => setObserved({ sessionId, source: memory, value })) : undefined, [eligible, observeRepl, sessionId, memory])
109
116
  if (!eligible) return null
110
117
  return h('div', { className: 'ptcPlusConsole', 'data-conversation-composer-overlay': '' },
111
118
  h('div', { className: 'ptcPlusConsoleSection', ref: observationRegion }, h(ReplSessionBindings, {
112
- key: sessionId, memory: observed?.sessionId === sessionId && observed.source === memory ? observed.value : memory, t })),
119
+ key: sessionId, memory, t })),
113
120
  globalEnabled ? h('div', { className: 'ptcPlusConsoleSection ptcPlusBindingsSurface' },
114
121
  h(UserBindingsWorkbench, { controller: workbench, t, headingLabel: t('console.global') })) : null)
115
122
  }
@@ -275,7 +282,7 @@ export function createReplView(React, deps) {
275
282
  line: binding.definition.line,
276
283
  column: binding.definition.column,
277
284
  })),
278
- typeof CodeBlock === 'function'
285
+ isHostComponent(CodeBlock)
279
286
  ? h(CodeBlock, {
280
287
  code: binding.definition.source,
281
288
  lang: 'typescript',
@@ -290,5 +297,5 @@ export function createReplView(React, deps) {
290
297
  : h('span', { className: 'ptcPlusReplMore' }, t('memory.more', { count: memory.omitted }))))
291
298
  }
292
299
 
293
- return { ReplComposer, ReplConsole, ReplMemoryCard, replPopoverIsOpen, placeReplPopover }
300
+ return { ReplComposer, ReplConsole, ReplMemoryCard, replPopoverIsOpen, placeReplPopover, useObservedReplMemory }
294
301
  }