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
@@ -0,0 +1,185 @@
1
+ const HOVER_DWELL_MS = 400
2
+ const SCROLL_IDLE_MS = 350
3
+
4
+ /** Presentation excerpt only; the expanded row retains the complete record. */
5
+ export function toolHoverExcerpt(text, maxLines) {
6
+ const prefix = text.slice(0, 1800).replace(/[\uD800-\uDBFF]$/, '')
7
+ const lines = prefix.split(/\r\n|\r|\n/)
8
+ const selected = lines.slice(0, maxLines)
9
+ let truncated = prefix.length < text.length || lines.length > maxLines
10
+ const bounded = selected.map(line => {
11
+ if (line.length <= 100) return line
12
+ truncated = true
13
+ // Do not leave a dangling UTF-16 high surrogate at the cut.
14
+ return line.slice(0, 99).replace(/[\uD800-\uDBFF]$/, '') + '…'
15
+ })
16
+ return { text: bounded.join('\n'), truncated }
17
+ }
18
+
19
+ /** Only the owned anchor and preview participate in placement. */
20
+ function placeToolHover(anchor, preview, pointer) {
21
+ const margin = 12
22
+ const gap = 10
23
+ const viewport = window.visualViewport
24
+ const leftEdge = viewport?.offsetLeft ?? 0
25
+ const topEdge = viewport?.offsetTop ?? 0
26
+ const width = viewport?.width ?? window.innerWidth
27
+ const height = viewport?.height ?? window.innerHeight
28
+ const rect = anchor.getBoundingClientRect()
29
+ const previewWidth = Math.min(420, width - margin * 2)
30
+ const right = pointer === null ? rect.right + gap : pointer[0] + 16
31
+ const onRight = right + previewWidth <= leftEdge + width - margin
32
+ const below = Math.max(0, topEdge + height - rect.bottom - gap - margin)
33
+ const above = Math.max(0, rect.top - topEdge - gap - margin)
34
+ const onTop = below < 340 && above > below
35
+ const available = Math.min(360, onRight ? height - margin * 2 : onTop ? above : below)
36
+ if (available < 100 || width < 100) return false
37
+ preview.style.width = `${previewWidth}px`
38
+ preview.style.maxHeight = `${available}px`
39
+ preview.style.left = onRight ? `${right}px` : `${Math.max(leftEdge + margin,
40
+ Math.min(rect.left + 16, leftEdge + width - previewWidth - margin))}px`
41
+ preview.style.top = onRight
42
+ ? `${Math.max(topEdge + margin, Math.min(pointer?.[1] ?? rect.top,
43
+ topEdge + height - margin - Math.min(preview.offsetHeight, available)))}px`
44
+ : onTop ? `${rect.top - gap - Math.min(preview.offsetHeight, available)}px`
45
+ : `${rect.bottom + gap}px`
46
+ preview.dataset.placement = onRight ? 'right' : onTop ? 'top' : 'bottom'
47
+ preview.style.visibility = 'visible'
48
+ preview.dataset.clipped = String([...preview.querySelectorAll(
49
+ '.ptcPlusToolHoverContent,.ptcPlusToolHoverSection,.ptcPlusToolHoverDescription,.ptcPlusFeatures,pre',
50
+ )].some(element => element.scrollHeight > element.clientHeight + 1
51
+ || element.scrollWidth > element.clientWidth + 1))
52
+ return true
53
+ }
54
+
55
+ /**
56
+ * Passive quick peeks never enter hit testing. Shared capture observers cancel
57
+ * every pending/visible tool preview on wheel or scroll without cancelling or
58
+ * redispatching the event. A fresh pointer gesture after idle owns reopening.
59
+ */
60
+ export function createToolHover(React, createPortal) {
61
+ const h = React.createElement
62
+ const subscribers = new Set()
63
+ let lastScroll = -Infinity
64
+ const scrolling = () => {
65
+ lastScroll = Date.now()
66
+ for (const cancel of subscribers) cancel()
67
+ }
68
+ const escaping = event => {
69
+ if (event.key === 'Escape') for (const cancel of subscribers) cancel()
70
+ }
71
+ const subscribe = cancel => {
72
+ if (subscribers.size === 0) {
73
+ window.addEventListener('wheel', scrolling, { capture: true, passive: true })
74
+ window.addEventListener('scroll', scrolling, { capture: true, passive: true })
75
+ window.addEventListener('keydown', escaping)
76
+ }
77
+ subscribers.add(cancel)
78
+ return () => {
79
+ subscribers.delete(cancel)
80
+ if (subscribers.size === 0) {
81
+ window.removeEventListener('wheel', scrolling, true)
82
+ window.removeEventListener('scroll', scrolling, true)
83
+ window.removeEventListener('keydown', escaping)
84
+ lastScroll = -Infinity
85
+ }
86
+ }
87
+ }
88
+
89
+ function ToolHoverAnchor({ disabled, content, children }) {
90
+ const [visible, setVisible] = React.useState(false)
91
+ const anchorRef = React.useRef(null)
92
+ const previewRef = React.useRef(null)
93
+ const pending = React.useRef(null)
94
+ const visibleRef = React.useRef(false)
95
+ const lastPointer = React.useRef(null)
96
+ const cancelled = React.useRef(false)
97
+ const clearPending = React.useCallback(() => {
98
+ if (pending.current !== null) clearTimeout(pending.current)
99
+ pending.current = null
100
+ }, [])
101
+ const dismiss = React.useCallback(() => {
102
+ clearPending()
103
+ visibleRef.current = false
104
+ // Hide the owned surface immediately, even before React's next render.
105
+ if (previewRef.current !== null) previewRef.current.hidden = true
106
+ setVisible(false)
107
+ }, [clearPending])
108
+ const cancel = React.useCallback(() => {
109
+ cancelled.current = true
110
+ dismiss()
111
+ }, [dismiss])
112
+ React.useEffect(() => disabled ? undefined : subscribe(cancel), [disabled, cancel])
113
+ React.useEffect(() => () => clearPending(), [clearPending])
114
+ React.useLayoutEffect(() => {
115
+ if (disabled) cancel()
116
+ }, [disabled, cancel])
117
+ const show = () => {
118
+ if (disabled) return
119
+ cancelled.current = false
120
+ visibleRef.current = true
121
+ setVisible(true)
122
+ }
123
+ const arm = event => {
124
+ if (disabled || event.pointerType === 'touch' || visibleRef.current
125
+ || pending.current !== null || Date.now() - lastScroll < SCROLL_IDLE_MS) return
126
+ cancelled.current = false
127
+ pending.current = setTimeout(() => {
128
+ pending.current = null
129
+ if (Date.now() - lastScroll >= SCROLL_IDLE_MS && !cancelled.current) show()
130
+ }, HOVER_DWELL_MS)
131
+ }
132
+ React.useLayoutEffect(() => {
133
+ if (!visible || disabled) return
134
+ const preview = previewRef.current
135
+ const anchor = anchorRef.current
136
+ if (preview === null || anchor === null) return
137
+ preview.hidden = false
138
+ if (typeof preview.showPopover === 'function') preview.showPopover()
139
+ const place = () => {
140
+ if (!placeToolHover(anchor, preview, lastPointer.current)) dismiss()
141
+ }
142
+ place()
143
+ const observer = typeof ResizeObserver === 'function' ? new ResizeObserver(place) : null
144
+ observer?.observe(anchor)
145
+ observer?.observe(preview)
146
+ window.addEventListener('resize', place)
147
+ window.visualViewport?.addEventListener('resize', place)
148
+ return () => {
149
+ observer?.disconnect()
150
+ window.removeEventListener('resize', place)
151
+ window.visualViewport?.removeEventListener('resize', place)
152
+ }
153
+ }, [visible, disabled, dismiss, cancel])
154
+ const preview = !visible || disabled ? null : h('div', {
155
+ ref: previewRef, className: 'ptcPlusToolHover',
156
+ popover: typeof HTMLElement.prototype.showPopover === 'function' ? 'manual' : undefined,
157
+ style: { visibility: 'hidden' },
158
+ role: 'tooltip',
159
+ }, content)
160
+ return h('div', {
161
+ className: 'ptcPlusTool', ref: anchorRef,
162
+ onPointerEnter: event => {
163
+ lastPointer.current = [event.clientX, event.clientY]
164
+ arm(event)
165
+ },
166
+ onPointerMove: event => {
167
+ const point = lastPointer.current
168
+ lastPointer.current = [event.clientX, event.clientY]
169
+ if (point === null || point[0] !== event.clientX || point[1] !== event.clientY) arm(event)
170
+ },
171
+ onPointerLeave: () => { lastPointer.current = null; dismiss() },
172
+ onPointerDownCapture: cancel,
173
+ onKeyDownCapture: event => { if (event.key === 'Escape') cancel() },
174
+ onFocusCapture: event => {
175
+ if (event.target.matches(':focus-visible')) { lastPointer.current = null; clearPending(); show() }
176
+ },
177
+ onBlurCapture: event => {
178
+ if (!event.currentTarget.contains(event.relatedTarget)) dismiss()
179
+ },
180
+ }, children, preview === null ? null : typeof createPortal === 'function'
181
+ ? createPortal(preview, document.body) : preview)
182
+ }
183
+
184
+ return { ToolHoverAnchor }
185
+ }
@@ -1,4 +1,6 @@
1
1
  import { derivePtcToolView } from './client-activity.js'
2
+ import { isHostComponent } from './client-host-compat.js'
3
+ import { createToolHover, toolHoverExcerpt } from './client-tool-hover.js'
2
4
 
3
5
  /**
4
6
  * run_code / edit_run_code tool rows. The row is a pure projection of the
@@ -6,15 +8,16 @@ import { derivePtcToolView } from './client-activity.js'
6
8
  * DisclosureRow when the host provides it and an accessible fallback otherwise.
7
9
  */
8
10
  export function createPtcToolView(React, deps) {
9
- const { CodeBlock, DisclosureRow, icons } = deps
11
+ const { CodeBlock, DisclosureRow, createPortal, icons } = deps
10
12
  const h = React.createElement
11
- const hasDisclosureRow = typeof DisclosureRow === 'function'
12
- || (typeof DisclosureRow === 'object' && DisclosureRow !== null)
13
+ const hasDisclosureRow = isHostComponent(DisclosureRow)
14
+ const { ToolHoverAnchor } = createToolHover(React, createPortal)
13
15
 
14
- function PTCPlusToolRow({ toolName, block, inspect, t }) {
16
+ function PTCPlusToolRow({ toolName, block, inspect, t, hoverEnabled = true }) {
15
17
  const [open, setOpen] = React.useState(false)
16
18
  const view = derivePtcToolView(block, toolName)
17
- const expandable = view.code !== '' || view.output !== '' || typeof inspect === 'function'
19
+ const previewable = view.code !== '' || view.output !== ''
20
+ const expandable = previewable || typeof inspect === 'function'
18
21
  const stateKey = {
19
22
  running: 'tool.running',
20
23
  ok: 'tool.completed',
@@ -35,10 +38,10 @@ export function createPtcToolView(React, deps) {
35
38
  stateText === null ? null : h('span', { className: 'ptcPlusToolState' }, stateText),
36
39
  summaryText === null ? null : h('span', { className: 'ptcPlusToolSep', 'aria-hidden': true }),
37
40
  summaryText === null ? null : h('span', { className: 'ptcPlusToolDescription' }, summaryText))
38
- const body = !open ? null : h('div', { className: 'ptcPlusToolBody' },
41
+ const renderBody = (withInspect) => h('div', { className: 'ptcPlusToolBody' },
39
42
  view.code === '' ? null : h('div', { className: 'ptcPlusToolSection' },
40
43
  h('span', { className: 'ptcPlusToolSectionLabel' }, t('tool.source')),
41
- typeof CodeBlock === 'function'
44
+ isHostComponent(CodeBlock)
42
45
  ? h(CodeBlock, {
43
46
  code: view.code, lang: 'typescript', className: 'ptcPlusToolCode',
44
47
  copyLabel: t('tool.copy'), copiedLabel: t('tool.copied'),
@@ -51,7 +54,7 @@ export function createPtcToolView(React, deps) {
51
54
  className: 'ptcPlusIoText',
52
55
  'data-error': view.state === 'error' || undefined,
53
56
  }, view.output))),
54
- typeof inspect !== 'function' ? null : h('button', {
57
+ !withInspect || typeof inspect !== 'function' ? null : h('button', {
55
58
  type: 'button', className: 'ptcPlusInspect', onClick: inspect,
56
59
  }, h(icons.inspect, { 'aria-hidden': true }), t('tool.inspect')))
57
60
  const features = view.features.length === 0 ? null : h('div', { className: 'ptcPlusFeatures' },
@@ -62,8 +65,34 @@ export function createPtcToolView(React, deps) {
62
65
  feature.detail === '' ? null
63
66
  : h('span', { className: 'ptcPlusFeatureDetail', title: feature.detail }, feature.detail))))
64
67
  const collapsedContent = h('div', { className: 'ptcPlusToolPreview' }, summaryLine, features)
68
+ const body = open ? renderBody(true) : null
69
+ const sourceExcerpt = toolHoverExcerpt(view.code, 6)
70
+ const outputExcerpt = toolHoverExcerpt(view.output, 3)
71
+ const preview = h(React.Fragment, null,
72
+ h('div', { className: 'ptcPlusToolHoverHeader', 'data-state': view.state },
73
+ h('span', { className: 'ptcPlusToolHoverHeading' },
74
+ t(toolName === 'edit_run_code' ? 'tool.editPreview' : 'tool.preview')),
75
+ h('span', { className: 'ptcPlusToolHoverState' }, t(stateKey))),
76
+ h('div', { className: 'ptcPlusToolHoverContent' },
77
+ view.description === '' ? null : h('div', { className: 'ptcPlusToolHoverDescription' }, view.description),
78
+ view.code === '' ? null : h('div', { className: 'ptcPlusToolHoverSection' },
79
+ h('span', { className: 'ptcPlusToolSectionLabel' }, `${t('tool.source')} · TypeScript`),
80
+ isHostComponent(CodeBlock) ? h(CodeBlock, {
81
+ code: sourceExcerpt.text, lang: 'typescript', showHeader: false, lineNumbers: true,
82
+ className: 'ptcPlusToolCode ptcPlusToolHoverCode',
83
+ }) : h('pre', { className: 'ptcPlusToolCode ptcPlusToolHoverCode' }, sourceExcerpt.text)),
84
+ view.output === '' ? null : h('div', { className: 'ptcPlusToolHoverSection' },
85
+ h('span', { className: 'ptcPlusToolSectionLabel' }, t('tool.result')),
86
+ h('pre', { className: 'ptcPlusIoText', 'data-error': view.state === 'error' || undefined }, outputExcerpt.text)),
87
+ features),
88
+ h('div', { className: 'ptcPlusToolHoverFooter' },
89
+ h('span', null, t('tool.previewHint')),
90
+ h('span', { className: 'ptcPlusToolHoverBounded',
91
+ 'data-truncated': sourceExcerpt.truncated || outputExcerpt.truncated || undefined,
92
+ }, t('tool.previewBounded'))))
93
+ const wrapRow = anchor => h(ToolHoverAnchor, { disabled: !hoverEnabled || open || !previewable, content: preview }, anchor)
65
94
  if (hasDisclosureRow) {
66
- return h('div', { className: 'ptcPlusTool' },
95
+ return wrapRow(
67
96
  h(DisclosureRow, {
68
97
  icon: expandable
69
98
  ? h(icons.chevron, { size: 14 })
@@ -75,11 +104,16 @@ export function createPtcToolView(React, deps) {
75
104
  expandOnRowClick: true,
76
105
  previewChevron: false,
77
106
  keepContentWhenOpen: true,
107
+ className: 'ptcPlusToolDisclosure',
108
+ rowClassName: 'ptcPlusToolRow',
109
+ contentClassName: 'ptcPlusToolDisclosureContent',
110
+ contentLayoutClassName: 'ptcPlusToolDisclosureLayout',
111
+ titleClassName: 'ptcPlusToolTitle',
78
112
  collapsedContent,
79
113
  children: body,
80
114
  }))
81
115
  }
82
- return h('div', { className: 'ptcPlusTool' },
116
+ return wrapRow(h(React.Fragment, null,
83
117
  h('div', {
84
118
  className: 'ptcPlusToolSummary', 'data-state': view.state,
85
119
  'data-expandable': expandable || undefined,
@@ -103,7 +137,7 @@ export function createPtcToolView(React, deps) {
103
137
  h('span', { className: 'ptcPlusToolSep', 'aria-hidden': true }),
104
138
  h('span', { className: 'ptcPlusToolDescription' }, summary)),
105
139
  features,
106
- body)
140
+ body))
107
141
  }
108
142
 
109
143
  return { PTCPlusToolRow }
@@ -7,9 +7,9 @@ import { normalizeReplMemorySnapshot } from '../internal/repl-memory-projection.
7
7
  * - observeRepl watches one visible REPL region, retries the watch with bounded
8
8
  * backoff and only reports an observation that still matches the requested
9
9
  * inventory. Everything it starts is aborted by its disposer.
10
- * - createBindingCommandAvailability publishes one boolean per session for the
11
- * `binding` command, keyed by epoch so a late `commands.list` answer can never
12
- * revive a subscription that already ended.
10
+ * - createCommandAccess publishes command availability per session, keyed by
11
+ * epoch so late directory replies cannot revive an ended subscription.
12
+ * Execution receipts must also match the connection and preset generations.
13
13
  */
14
14
  export function createReplObserver({ rpc, ctx }) {
15
15
  function observeRepl(sessionId, element, memory, onObservation) {
@@ -28,25 +28,33 @@ export function createReplObserver({ rpc, ctx }) {
28
28
  previous?.abort()
29
29
  }
30
30
  const watch = async current => {
31
+ let receipt
31
32
  try {
32
- await rpc.call(RPC_CONTRACTS.repl, 'watch', { sessionId }, current.signal)
33
+ receipt = await rpc.call(RPC_CONTRACTS.repl, 'watch', { sessionId }, current.signal)
33
34
  } catch {}
34
35
  if (disposed || controller !== current) return
35
36
  controller = undefined
36
37
  current.abort()
37
38
  if (!visible || document.visibilityState === 'hidden') { stop(); return }
39
+ if (receipt?.ok === true && receipt.value?.changed === true) {
40
+ retries = 0
41
+ sync()
42
+ return
43
+ }
38
44
  const delay = retryDelays[retries++]
39
45
  if (delay === undefined) return
40
46
  retryTimer = setTimeout(() => { retryTimer = undefined; sync() }, delay)
41
47
  }
42
48
  const read = async current => {
43
- if (!memory.available || memory.entries.length === 0 || memory.observation !== undefined) return
49
+ if (!memory.available || memory.entries.length === 0) return
44
50
  try {
45
51
  const result = await rpc.call(RPC_CONTRACTS.repl, 'observe', { sessionId, memory }, current.signal)
46
52
  if (disposed || controller !== current || current.signal.aborted || result?.ok !== true || result.value === null) return
47
53
  const observed = normalizeReplMemorySnapshot(result.value)
48
54
  const { observation, ...inventory } = observed
49
- if (observation !== undefined && JSON.stringify(inventory) === JSON.stringify(memory)) onObservation(observed)
55
+ const { observation: _previousObservation, ...expected } = memory
56
+ if (!observed.available || (observation !== undefined
57
+ && JSON.stringify(inventory) === JSON.stringify(expected))) onObservation(observed)
50
58
  } catch {}
51
59
  }
52
60
  const sync = () => {
@@ -78,14 +86,15 @@ export function createReplObserver({ rpc, ctx }) {
78
86
  return { observeRepl }
79
87
  }
80
88
 
81
- export function createBindingCommandAvailability(scope) {
89
+ export function createCommandAccess(scope, name) {
82
90
  const entries = new Map()
83
91
  let commandRemote
92
+ let connectionGeneration = 0
84
93
  const entryFor = (sessionId) => {
85
94
  const id = String(sessionId)
86
95
  let entry = entries.get(id)
87
96
  if (entry === undefined) {
88
- entry = { available: false, epoch: 0, listeners: new Set() }
97
+ entry = { available: false, epoch: 0, commandGeneration: 0, listeners: new Set() }
89
98
  entry.source = {
90
99
  getSnapshot: () => entry.available,
91
100
  subscribe(listener) {
@@ -119,7 +128,7 @@ export function createBindingCommandAvailability(scope) {
119
128
  const result = await commandRemote?.list(String(sessionId))
120
129
  available = result?.ok === true
121
130
  && Array.isArray(result.value)
122
- && result.value.some(command => command?.name === 'binding')
131
+ && result.value.some(command => command?.name === name)
123
132
  } catch {}
124
133
  if (entry.epoch === epoch) publish(entry, available)
125
134
  }
@@ -128,10 +137,12 @@ export function createBindingCommandAvailability(scope) {
128
137
  const entry = entries.get(String(sessionId))
129
138
  if (entry === undefined || entry.listeners.size === 0) return
130
139
  entry.epoch += 1
140
+ entry.commandGeneration += 1
131
141
  publish(entry, false)
132
142
  void refresh(sessionId)
133
143
  }
134
144
  scope.inject(['remote', 'remote.commands'], commandScope => {
145
+ connectionGeneration++
135
146
  commandRemote = commandScope.get('remote.commands')
136
147
  commandScope.effect(() => commandScope.remote.$on('commands/change', () => {
137
148
  for (const sessionId of entries.keys()) void refresh(sessionId)
@@ -139,6 +150,7 @@ export function createBindingCommandAvailability(scope) {
139
150
  commandScope.effect(() => commandScope.remote.$on('agent-preset/selected', reset))
140
151
  for (const sessionId of entries.keys()) void refresh(sessionId)
141
152
  commandScope.effect(() => () => {
153
+ connectionGeneration++
142
154
  commandRemote = undefined
143
155
  for (const entry of entries.values()) {
144
156
  entry.epoch++
@@ -147,13 +159,36 @@ export function createBindingCommandAvailability(scope) {
147
159
  })
148
160
  })
149
161
  scope.on('connection/reset', () => {
162
+ connectionGeneration++
150
163
  for (const sessionId of entries.keys()) reset(sessionId)
151
164
  })
152
165
  scope.effect(() => () => {
166
+ connectionGeneration++
153
167
  for (const entry of entries.values()) entry.epoch++
154
168
  entries.clear()
155
169
  })
156
170
  return Object.freeze({
157
171
  source: sessionId => entryFor(sessionId).source,
172
+ async run(sessionId, input, signal) {
173
+ signal?.throwIfAborted()
174
+ const remote = commandRemote
175
+ const generation = connectionGeneration
176
+ const entry = entries.get(String(sessionId))
177
+ const commandGeneration = entry?.commandGeneration
178
+ if (!entries.get(String(sessionId))?.available || typeof remote?.execute !== 'function') {
179
+ throw new Error(`/${name} is unavailable in this session`)
180
+ }
181
+ const result = await remote.execute(String(sessionId), `/${name}${input ? ` ${input}` : ''}`, [], signal)
182
+ signal?.throwIfAborted()
183
+ if (remote !== commandRemote || generation !== connectionGeneration
184
+ || commandGeneration !== entry?.commandGeneration) throw new Error('Command connection changed')
185
+ if (result?.ok !== true || !result.value) throw new Error(result?.error?.message ?? 'Command was not accepted')
186
+ if (result.value.result?.kind !== 'success') throw new Error(result.value.result?.text ?? 'Command failed')
187
+ return result.value.result
188
+ },
158
189
  })
159
190
  }
191
+
192
+ export function createBindingCommandAvailability(scope) {
193
+ return createCommandAccess(scope, 'binding')
194
+ }
@@ -1,5 +1,5 @@
1
1
  import { blankBinding, editableBinding, bindingPayload } from './client-bindings-data.js'
2
- import { isHostIconComponent } from './client-host-compat.js'
2
+ import { isHostComponent, isHostIconComponent } from './client-host-compat.js'
3
3
 
4
4
  /**
5
5
  * User Bindings workbench. The controller owns every piece of edit state in one
@@ -12,7 +12,7 @@ import { isHostIconComponent } from './client-host-compat.js'
12
12
  * discard an unsaved draft.
13
13
  */
14
14
  export function createUserBindingsWorkbench(React, deps) {
15
- const { TypeScriptEditor, BindingConsole, IconButton, ActionButton, CodeBlock, Modal, icons, catalogOwner } = deps
15
+ const { TypeScriptEditor, BindingConsole, IconButton, ActionButton, CodeBlock, Modal, Select, icons, catalogOwner } = deps
16
16
  const h = React.createElement
17
17
 
18
18
  const initialState = Object.freeze({
@@ -120,7 +120,7 @@ export function createUserBindingsWorkbench(React, deps) {
120
120
  }
121
121
  if (action.imported === true) next.importPath = ''
122
122
  next.message = action.removed === true ? { key: 'bindings.removed' }
123
- : action.imported === true ? { key: 'bindings.imported' } : state.message
123
+ : action.imported === true ? { key: 'bindings.imported' } : stored ? { key: stored.enabled ? 'bindings.toggleEnabled' : 'bindings.toggleDisabled', params: { name: stored.name } } : state.message
124
124
  return next
125
125
  }
126
126
  case 'reloadResult': {
@@ -391,7 +391,7 @@ export function createUserBindingsWorkbench(React, deps) {
391
391
  h('details', { className: 'ptcPlusBindingSection ptcPlusBindingSourcePreview', open: true },
392
392
  h('summary', { className: 'ptcPlusBindingFieldLabel' }, t('bindings.declaration')),
393
393
  state.declaration === '' ? h('p', { className: 'ptcPlusMessage' }, t('bindings.unvalidated'))
394
- : typeof CodeBlock === 'function'
394
+ : isHostComponent(CodeBlock)
395
395
  ? h(CodeBlock, { code: state.declaration, lang: 'typescript', className: 'ptcPlusCodeBlock',
396
396
  copyLabel: t('tool.copy'), copiedLabel: t('tool.copied') })
397
397
  : h('pre', { className: 'ptcPlusDeclaration' }, state.declaration)),
@@ -420,7 +420,7 @@ export function createUserBindingsWorkbench(React, deps) {
420
420
  state.editing
421
421
  ? h(TypeScriptEditor, { documentId: state.documentId, value: state.draft.source, disabled: state.busy,
422
422
  label: t('bindings.source'), onChange: value => edit('source', value) })
423
- : state.sourceOpen && typeof CodeBlock === 'function'
423
+ : state.sourceOpen && isHostComponent(CodeBlock)
424
424
  ? h(CodeBlock, { code: state.draft.source, lang: 'typescript', className: 'ptcPlusSourceCode',
425
425
  copyLabel: t('tool.copy'), copiedLabel: t('tool.copied') })
426
426
  : state.sourceOpen ? h('pre', { className: 'ptcPlusSourceCode' }, state.draft.source) : null)),
@@ -450,10 +450,9 @@ export function createUserBindingsWorkbench(React, deps) {
450
450
  })),
451
451
  h('label', { className: 'ptcPlusBindingField' },
452
452
  h('span', { className: 'ptcPlusBindingFieldLabel' }, t('bindings.scope')),
453
- h('select', {
454
- className: 'ptcPlusSelect', value: state.draft.scope, disabled: state.busy || !state.editing,
455
- onChange: event => edit('scope', event.target.value),
456
- }, h('option', { value: 'namespace' }, 'namespace'), h('option', { value: 'top-level' }, 'top-level'))),
453
+ h(Select, { value: state.draft.scope, disabled: state.busy || !state.editing,
454
+ label: t('bindings.scope'), onChange: next => edit('scope', next),
455
+ options: [{ value: 'namespace', label: 'namespace' }, { value: 'top-level', label: 'top-level' }] })),
457
456
  h('label', { className: 'ptcPlusBindingField' },
458
457
  h('span', { className: 'ptcPlusBindingFieldLabel' }, t('bindings.symbols')),
459
458
  h('input', {
@@ -502,7 +501,7 @@ export function createUserBindingsWorkbench(React, deps) {
502
501
  const trapFocus = event => {
503
502
  if (event.key !== 'Tab') return
504
503
  const controls = [...content.current.querySelectorAll('button:not(:disabled),input:not(:disabled),textarea:not(:disabled),select:not(:disabled),[contenteditable=true],summary')]
505
- .filter(element => element.getClientRects().length > 0)
504
+ .filter(element => element.getClientRects().length > 0 && !element.closest('[aria-hidden=true],[hidden]'))
506
505
  const first = controls[0]
507
506
  const last = controls.at(-1)
508
507
  if ((event.shiftKey && document.activeElement === first) || (!event.shiftKey && document.activeElement === last)) {