dsh-ptc-plus 0.4.10 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (125) hide show
  1. package/README.en.md +65 -127
  2. package/README.md +65 -127
  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 +42702 -0
  10. package/cordis.patch.yml +1 -0
  11. package/docs/README.md +6 -2
  12. package/docs/adr/0005-temporary-rejected-cell-edit-transport.md +31 -198
  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 +4 -4
  17. package/docs/adr/0019-plugin-settings-and-kill-switch.md +1 -1
  18. package/docs/adr/0020-optional-cordis-tools-in-ptc-mode.md +1 -1
  19. package/docs/adr/0022-stage-npm-releases-with-oidc.md +4 -2
  20. package/docs/adr/0023-global-user-bindings.md +2 -0
  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 +25 -23
  23. package/docs/adr/0031-close-semantic-obligations-by-owner.md +1 -1
  24. package/docs/adr/0034-keep-program-values-separate-from-output-projections.md +29 -0
  25. package/docs/adr/0035-run-coverage-only-before-a-new-release.md +27 -0
  26. package/docs/adr/0036-separate-focused-development-from-delivery-verification.md +29 -0
  27. package/docs/adr/0037-keep-tool-hover-previews-passive.md +52 -0
  28. package/docs/adr/0038-freeze-session-capability-plans.md +71 -0
  29. package/docs/adr/0039-verify-documentation-without-runtime-suites.md +29 -0
  30. package/docs/adr/0040-separate-capability-configurations-from-session-observations.md +39 -0
  31. package/docs/adr/0041-record-cell-execution-directories.md +65 -0
  32. package/docs/adr/0042-persist-provider-availability-and-handback.md +41 -0
  33. package/docs/adr/0043-compose-capability-and-context-policies.md +37 -0
  34. package/docs/adr/0044-coexist-with-the-official-persistent-repl.md +35 -0
  35. package/docs/adr/README.md +8 -0
  36. package/docs/architecture.md +23 -25
  37. package/docs/capability-planning-components.md +93 -0
  38. package/docs/capability-projection.md +7 -0
  39. package/docs/client-ui.md +70 -23
  40. package/docs/client-visual-contract.md +26 -9
  41. package/docs/context-management.md +22 -0
  42. package/docs/context-optimization-plan.md +353 -0
  43. package/docs/context-public-interface-coverage.md +25 -0
  44. package/docs/durability-design.md +20 -15
  45. package/docs/evaluation.md +41 -6
  46. package/docs/evaluations/command-goat-deepseek-v4.1-flash-20261006.md +126 -0
  47. package/docs/evaluations/command-goat-deepseek-v4.1-flash.md +281 -0
  48. package/docs/installation.md +33 -10
  49. package/docs/program-data-plane.md +5 -3
  50. package/docs/publishing.md +7 -7
  51. package/docs/release-coverage.md +73 -0
  52. package/docs/release-notes.md +1 -1
  53. package/docs/review-procedure.md +56 -0
  54. package/docs/runtime-reference.md +53 -16
  55. package/docs/semantic-validation.md +14 -2
  56. package/docs/stateful-binding-updates-proposal.md +2 -2
  57. package/docs/user-bindings.md +1 -1
  58. package/docs/value-wire.md +11 -1
  59. package/docs/verification-optimization-work.md +26 -15
  60. package/docs/verification.md +30 -88
  61. package/index.js +146 -20
  62. package/internal/binding-toggle-receipt.js +24 -0
  63. package/internal/capability-command-receipt.js +31 -0
  64. package/internal/capability-environment.js +1253 -0
  65. package/internal/capability-plan.js +285 -0
  66. package/internal/capability-policy.js +96 -0
  67. package/internal/capability-statistics-store.js +180 -0
  68. package/internal/capability-statistics.js +214 -0
  69. package/internal/config-spec.js +89 -10
  70. package/internal/direct-surface-owner.js +27 -29
  71. package/internal/dynamic-environment-runtime.js +9 -9
  72. package/internal/edit-transport-owner.js +195 -140
  73. package/internal/execution-provider-owner.js +40 -12
  74. package/internal/execution-seam-compat.js +26 -8
  75. package/internal/kernel-worker.js +104 -32
  76. package/internal/native-root-dynamic.js +12 -13
  77. package/internal/official-repl-priority.js +51 -0
  78. package/internal/program-bindings.js +190 -0
  79. package/internal/recovery-tips.js +3 -2
  80. package/internal/rejected-cell-editor.js +8 -8
  81. package/internal/repl-observation-interest.js +11 -3
  82. package/internal/rpc-contract.js +1 -0
  83. package/internal/runtime-bridge-owner.js +275 -34
  84. package/internal/runtime-config.js +58 -0
  85. package/internal/runtime-contexts.js +74 -5
  86. package/internal/runtime-intrinsics.js +1 -0
  87. package/internal/runtime-messages.js +41 -0
  88. package/internal/sdk-projection.js +10 -82
  89. package/internal/session-cell-executor.js +42 -19
  90. package/internal/session-events.js +34 -0
  91. package/internal/session-journal-recovery.js +234 -11
  92. package/internal/session-journal-schema.js +6 -3
  93. package/internal/session-journal.js +31 -2
  94. package/internal/session-log-view.js +1 -1
  95. package/internal/session-runtime.js +119 -15
  96. package/internal/settings-compat.js +12 -5
  97. package/internal/stateful-module-compiler.js +3 -2
  98. package/internal/stateful-module-runtime.js +7 -3
  99. package/internal/stateful-root-runtime.js +9 -4
  100. package/internal/user-binding-authoring.js +2 -7
  101. package/internal/user-bindings-owner.js +19 -2
  102. package/internal/validated-parse-repair.js +1 -1
  103. package/internal/value-wire.js +68 -21
  104. package/internal/worker-cwd-virtualization.js +20 -18
  105. package/internal/worker-realm-surfaces.js +3 -3
  106. package/package.json +49 -30
  107. package/src/client-activity.js +3 -2
  108. package/src/client-authoring-view.js +67 -30
  109. package/src/client-binding-review.js +2 -1
  110. package/src/client-binding-toggle.js +18 -0
  111. package/src/client-capabilities-view.js +1555 -0
  112. package/src/client-capability-sort.js +29 -0
  113. package/src/client-copy.js +923 -8
  114. package/src/client-feature-gates.js +3 -0
  115. package/src/client-indicator-view.js +4 -6
  116. package/src/client-primitives.js +33 -2
  117. package/src/client-repl-view.js +20 -13
  118. package/src/client-settings-view.js +411 -41
  119. package/src/client-styles.js +112 -15
  120. package/src/client-tool-hover.js +185 -0
  121. package/src/client-tool-view.js +45 -9
  122. package/src/client-transport.js +44 -9
  123. package/src/client-workbench.js +6 -7
  124. package/src/client.js +76 -22
  125. package/client.js +0 -32479
@@ -1,5 +1,6 @@
1
1
  import { derivePtcToolView } from './client-activity.js'
2
2
  import { isHostComponent } from './client-host-compat.js'
3
+ import { createToolHover, toolHoverExcerpt } from './client-tool-hover.js'
3
4
 
4
5
  /**
5
6
  * run_code / edit_run_code tool rows. The row is a pure projection of the
@@ -7,14 +8,16 @@ import { isHostComponent } from './client-host-compat.js'
7
8
  * DisclosureRow when the host provides it and an accessible fallback otherwise.
8
9
  */
9
10
  export function createPtcToolView(React, deps) {
10
- const { CodeBlock, DisclosureRow, icons } = deps
11
+ const { CodeBlock, DisclosureRow, createPortal, icons } = deps
11
12
  const h = React.createElement
12
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',
@@ -30,12 +33,17 @@ export function createPtcToolView(React, deps) {
30
33
  if (expandable) setOpen(current => !current)
31
34
  }
32
35
  const summaryText = summary === '' ? null : summary
36
+ // An auto-filled summary stays visually identical but declares its provenance in the DOM
37
+ // and on hover, so readers can tell who wrote it without changing the model-facing result.
38
+ const generatedSummary = view.descriptionGenerated && outputSummary === ''
33
39
  const summaryLine = stateText === null && summaryText === null ? null
34
40
  : h('div', { className: 'ptcPlusToolSummaryLine', 'data-state': view.state, role: 'status' },
35
41
  stateText === null ? null : h('span', { className: 'ptcPlusToolState' }, stateText),
36
42
  summaryText === null ? null : h('span', { className: 'ptcPlusToolSep', 'aria-hidden': true }),
37
- summaryText === null ? null : h('span', { className: 'ptcPlusToolDescription' }, summaryText))
38
- const body = !open ? null : h('div', { className: 'ptcPlusToolBody' },
43
+ summaryText === null ? null : h('span', { className: 'ptcPlusToolDescription',
44
+ 'data-generated': generatedSummary ? 'true' : undefined,
45
+ title: generatedSummary ? t('tool.descriptionGenerated') : undefined }, summaryText))
46
+ const renderBody = (withInspect) => h('div', { className: 'ptcPlusToolBody' },
39
47
  view.code === '' ? null : h('div', { className: 'ptcPlusToolSection' },
40
48
  h('span', { className: 'ptcPlusToolSectionLabel' }, t('tool.source')),
41
49
  isHostComponent(CodeBlock)
@@ -51,7 +59,7 @@ export function createPtcToolView(React, deps) {
51
59
  className: 'ptcPlusIoText',
52
60
  'data-error': view.state === 'error' || undefined,
53
61
  }, view.output))),
54
- typeof inspect !== 'function' ? null : h('button', {
62
+ !withInspect || typeof inspect !== 'function' ? null : h('button', {
55
63
  type: 'button', className: 'ptcPlusInspect', onClick: inspect,
56
64
  }, h(icons.inspect, { 'aria-hidden': true }), t('tool.inspect')))
57
65
  const features = view.features.length === 0 ? null : h('div', { className: 'ptcPlusFeatures' },
@@ -62,8 +70,36 @@ export function createPtcToolView(React, deps) {
62
70
  feature.detail === '' ? null
63
71
  : h('span', { className: 'ptcPlusFeatureDetail', title: feature.detail }, feature.detail))))
64
72
  const collapsedContent = h('div', { className: 'ptcPlusToolPreview' }, summaryLine, features)
73
+ const body = open ? renderBody(true) : null
74
+ const sourceExcerpt = toolHoverExcerpt(view.code, 6)
75
+ const outputExcerpt = toolHoverExcerpt(view.output, 3)
76
+ const preview = h(React.Fragment, null,
77
+ h('div', { className: 'ptcPlusToolHoverHeader', 'data-state': view.state },
78
+ h('span', { className: 'ptcPlusToolHoverHeading' },
79
+ t(toolName === 'edit_run_code' ? 'tool.editPreview' : 'tool.preview')),
80
+ h('span', { className: 'ptcPlusToolHoverState' }, t(stateKey))),
81
+ h('div', { className: 'ptcPlusToolHoverContent' },
82
+ view.description === '' ? null : h('div', { className: 'ptcPlusToolHoverDescription',
83
+ 'data-generated': view.descriptionGenerated ? 'true' : undefined,
84
+ title: view.descriptionGenerated ? t('tool.descriptionGenerated') : undefined }, view.description),
85
+ view.code === '' ? null : h('div', { className: 'ptcPlusToolHoverSection' },
86
+ h('span', { className: 'ptcPlusToolSectionLabel' }, `${t('tool.source')} · TypeScript`),
87
+ isHostComponent(CodeBlock) ? h(CodeBlock, {
88
+ code: sourceExcerpt.text, lang: 'typescript', showHeader: false, lineNumbers: true,
89
+ className: 'ptcPlusToolCode ptcPlusToolHoverCode',
90
+ }) : h('pre', { className: 'ptcPlusToolCode ptcPlusToolHoverCode' }, sourceExcerpt.text)),
91
+ view.output === '' ? null : h('div', { className: 'ptcPlusToolHoverSection' },
92
+ h('span', { className: 'ptcPlusToolSectionLabel' }, t('tool.result')),
93
+ h('pre', { className: 'ptcPlusIoText', 'data-error': view.state === 'error' || undefined }, outputExcerpt.text)),
94
+ features),
95
+ h('div', { className: 'ptcPlusToolHoverFooter' },
96
+ h('span', null, t('tool.previewHint')),
97
+ h('span', { className: 'ptcPlusToolHoverBounded',
98
+ 'data-truncated': sourceExcerpt.truncated || outputExcerpt.truncated || undefined,
99
+ }, t('tool.previewBounded'))))
100
+ const wrapRow = anchor => h(ToolHoverAnchor, { disabled: !hoverEnabled || open || !previewable, content: preview }, anchor)
65
101
  if (hasDisclosureRow) {
66
- return h('div', { className: 'ptcPlusTool' },
102
+ return wrapRow(
67
103
  h(DisclosureRow, {
68
104
  icon: expandable
69
105
  ? h(icons.chevron, { size: 14 })
@@ -84,7 +120,7 @@ export function createPtcToolView(React, deps) {
84
120
  children: body,
85
121
  }))
86
122
  }
87
- return h('div', { className: 'ptcPlusTool' },
123
+ return wrapRow(h(React.Fragment, null,
88
124
  h('div', {
89
125
  className: 'ptcPlusToolSummary', 'data-state': view.state,
90
126
  'data-expandable': expandable || undefined,
@@ -108,7 +144,7 @@ export function createPtcToolView(React, deps) {
108
144
  h('span', { className: 'ptcPlusToolSep', 'aria-hidden': true }),
109
145
  h('span', { className: 'ptcPlusToolDescription' }, summary)),
110
146
  features,
111
- body)
147
+ body))
112
148
  }
113
149
 
114
150
  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
+ }
@@ -12,7 +12,7 @@ import { isHostComponent, 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': {
@@ -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)) {
package/src/client.js CHANGED
@@ -3,8 +3,11 @@ import { RPC_CONTRACTS } from '../internal/rpc-contract.js'
3
3
  import { CONFIG_DEFAULTS, SETTINGS_NAMESPACE } from '../internal/config-spec.js'
4
4
  import { createPtcToolView } from './client-tool-view.js'
5
5
  import { createPtcSettingsView } from './client-settings-view.js'
6
+ import { createCapabilityCommandCard, createCapabilityRefreshView, createSessionCapabilityControlsView } from './client-capabilities-view.js'
7
+ import { capabilityProfileConfiguration } from '../internal/runtime-config.js'
6
8
  import { createReplView } from './client-repl-view.js'
7
9
  import { createAuthoringView } from './client-authoring-view.js'
10
+ import { bindingToggleConversationDefinition } from './client-binding-toggle.js'
8
11
  import { createIndicatorView } from './client-indicator-view.js'
9
12
  import { createTypeScriptEditor } from './client-code-editor.js'
10
13
  import { createBindingConsole } from './client-console.js'
@@ -29,7 +32,7 @@ import { installStyles } from './client-styles.js'
29
32
  import { featureEnabled, registerGated } from './client-feature-gates.js'
30
33
  import { createCatalogOwner } from './client-catalog.js'
31
34
  import { createUserBindingsWorkbench } from './client-workbench.js'
32
- import { createBindingCommandAvailability, createReplObserver } from './client-transport.js'
35
+ import { createBindingCommandAvailability, createCommandAccess, createReplObserver } from './client-transport.js'
33
36
  import { createActionButton, resolvePrimitives } from './client-primitives.js'
34
37
 
35
38
  /** Settings snapshot used until the installed generation serves a transport. */
@@ -87,10 +90,7 @@ function createPreferenceScope(ctx) {
87
90
  })
88
91
  }
89
92
 
90
- window.__ModuleLoader__.load({
91
- // Replaced by the bundle entry with the package name from package.json.
92
- id: __PTC_PLUS_CLIENT_MODULE_ID__,
93
- factory: (require) => {
93
+ export function createClientPlugin(require) {
94
94
  const React = require('react')
95
95
  const { createPortal } = require('react-dom')
96
96
  let primitives
@@ -106,6 +106,7 @@ window.__ModuleLoader__.load({
106
106
  Menu,
107
107
  MenuFallback,
108
108
  Modal,
109
+ Select,
109
110
  Toast,
110
111
  Tooltip,
111
112
  } = resolvePrimitives(primitives, React, createPortal)
@@ -123,7 +124,6 @@ window.__ModuleLoader__.load({
123
124
  play: IconPlayOutline16,
124
125
  stop: IconStopFill16,
125
126
  } = hostIconComponents(primitives)
126
- const module = { exports: {} }
127
127
  const h = React.createElement
128
128
  const TypeScriptEditor = createTypeScriptEditor(React)
129
129
 
@@ -138,7 +138,7 @@ window.__ModuleLoader__.load({
138
138
  const BindingConsole = createBindingConsole(React, { TypeScriptEditor, IconButton, ActionButton,
139
139
  icons: { play: IconPlayOutline16, stop: IconStopFill16, reset: IconRefreshOutline16, clear: IconTrashOutline16 } })
140
140
  const { PTCPlusToolRow } = createPtcToolView(React, {
141
- CodeBlock, DisclosureRow,
141
+ CodeBlock, DisclosureRow, createPortal,
142
142
  icons: { chevron: IconChevronDownOutline14, check: IconCheckOutline14, inspect: IconInspectOutline12 },
143
143
  })
144
144
 
@@ -167,7 +167,7 @@ window.__ModuleLoader__.load({
167
167
  const catalogOwner = createCatalogOwner({ callUserBindings })
168
168
  ctx.effect(() => () => catalogOwner.dispose(), 'ptc-plus: binding catalog sources')
169
169
  const { useWorkbenchController, UserBindingsWorkbench, BindingsDialog } = createUserBindingsWorkbench(React, {
170
- TypeScriptEditor, BindingConsole, IconButton, ActionButton, CodeBlock, Modal,
170
+ TypeScriptEditor, BindingConsole, IconButton, ActionButton, CodeBlock, Modal, Select,
171
171
  catalogOwner,
172
172
  icons: {
173
173
  refresh: IconRefreshOutline16, plus: IconPlusOutline16, check: IconCheckOutline14,
@@ -176,12 +176,43 @@ window.__ModuleLoader__.load({
176
176
  },
177
177
  })
178
178
  const { observeRepl } = createReplObserver({ rpc, ctx })
179
+ const capabilityCommands = createCommandAccess(ctx, 'capabilities')
180
+ const CapabilityCommandCard = createCapabilityCommandCard(React)
181
+ const CapabilityRefreshButton = createCapabilityRefreshView(React, capabilityCommands)
182
+ const SessionCapabilityControls = createSessionCapabilityControlsView(React, {
183
+ commands: capabilityCommands, Modal, Select,
184
+ async inspectSessionCatalog(sessionId, signal) {
185
+ const result = await rpc.call(RPC_CONTRACTS.capabilities, 'session-catalog', { sessionId }, signal)
186
+ if (result?.ok !== true) throw new Error(result?.error?.message ?? 'Session capability catalog unavailable')
187
+ return result.value
188
+ },
189
+ })
179
190
  const { PTCPlusSettingsCard, PTCPlusSettingsDialog } = createPtcSettingsView(React, {
180
- ActionButton, BindingsDialog, Modal, useWorkbenchController,
181
- icons: { chevron: IconChevronDownOutline14 },
191
+ ActionButton, BindingsDialog, Modal, Select, useWorkbenchController, CapabilityRefreshButton,
192
+ async inspectCapabilityTargets(signal) {
193
+ const result = await rpc.call(RPC_CONTRACTS.capabilities, 'targets', {}, signal)
194
+ if (result?.ok !== true) throw new Error(result?.error?.message ?? 'Capability refresh targets unavailable')
195
+ return result.value
196
+ },
197
+ async inspectCapabilityPlans(signal) {
198
+ const result = await rpc.call(RPC_CONTRACTS.capabilities, 'inspect', {}, signal)
199
+ if (result?.ok !== true) throw new Error(result?.error?.message ?? 'Capability plans unavailable')
200
+ return result.value
201
+ },
202
+ async inspectCapabilityCatalog(signal) {
203
+ const result = await rpc.call(RPC_CONTRACTS.capabilities, 'catalog', {}, signal)
204
+ if (result?.ok !== true) throw new Error(result?.error?.message ?? 'Capability catalog unavailable')
205
+ return result.value
206
+ },
207
+ async inspectCapabilityAllocation(payload, signal) {
208
+ const result = await rpc.call(RPC_CONTRACTS.capabilities, 'allocation', payload, signal)
209
+ if (result?.ok !== true) throw new Error(result?.error?.message ?? 'Capability allocation unavailable')
210
+ return result.value
211
+ },
212
+ icons: { search: IconSearchOutline16, chevron: IconChevronDownOutline14 },
182
213
  })
183
- const { ReplComposer, ReplConsole, ReplMemoryCard, replPopoverIsOpen, placeReplPopover } = createReplView(React, {
184
- CodeBlock, featureEnabled, normalizeReplMemorySnapshot, unavailableReplMemorySnapshot,
214
+ const { ReplComposer, ReplConsole, ReplMemoryCard, replPopoverIsOpen, placeReplPopover, useObservedReplMemory } = createReplView(React, {
215
+ CodeBlock, Select, featureEnabled, normalizeReplMemorySnapshot, unavailableReplMemorySnapshot,
185
216
  useSessionPreset, sessionUsesPtcPreset, useWorkbenchController, UserBindingsWorkbench,
186
217
  icons: { search: IconSearchOutline16, chevron: IconChevronDownOutline14 },
187
218
  })
@@ -204,6 +235,15 @@ window.__ModuleLoader__.load({
204
235
  && (key !== 'bindingUpdates' || after.value.legacyBindingSettings !== true)
205
236
  ? 'status.applied' : 'status.conflict'
206
237
  }
238
+ const updateCapabilityConfiguration = async configuration => {
239
+ const before = preferenceScope.getSnapshot()
240
+ if (before.status !== 'ready' || before.writable !== true || !featureEnabled(before, 'plugin')) throw new Error('Capability settings are not writable')
241
+ const validated = capabilityProfileConfiguration(configuration)
242
+ await preferenceScope.mutate(Object.entries(validated).map(([key, value]) => ({ op: 'set', path: [key], value })))
243
+ const after = preferenceScope.getSnapshot()
244
+ if (after.status !== 'ready' || Object.entries(validated).some(([key, value]) => after.value?.[key] !== value)) throw new Error('Capability configuration was not applied')
245
+ return 'status.applied'
246
+ }
207
247
  // One gated-registration shape for every settings-driven contribution.
208
248
  const settingsGate = (feature, register) => ({
209
249
  subscribe: listener => preferenceScope.subscribe(listener),
@@ -217,20 +257,25 @@ window.__ModuleLoader__.load({
217
257
  const settingsCard = publishSettingsCard(ctx, {
218
258
  bundleName: __PTC_PLUS_CLIENT_MODULE_ID__,
219
259
  locale: LOCALE_NS,
220
- injectProps: () => ({ ...settingsProps(), updateSetting }),
260
+ injectProps: () => ({ ...settingsProps(), updateSetting, updateCapabilityConfiguration }),
221
261
  component: PTCPlusSettingsCard,
222
262
  })
223
263
 
264
+ function ConfiguredToolRow(props) {
265
+ const snapshot = React.useSyncExternalStore(preferenceScope.subscribe, preferenceScope.getSnapshot)
266
+ return h(PTCPlusToolRow, { ...props, hoverEnabled: featureEnabled(snapshot, 'toolHover') })
267
+ }
268
+
224
269
  ctx.slots.inject('tool.call.toolview', () => registerGated(ctx, settingsGate('toolView', () => {
225
270
  // Two rows share one eligibility decision; roll both back if either fails.
226
271
  const disposers = []
227
272
  try {
228
273
  disposers.push(ctx.slots.register({
229
274
  name: 'tool.call.toolview', key: 'run_code', locale: LOCALE_NS,
230
- }, PTCPlusToolRow))
275
+ }, ConfiguredToolRow))
231
276
  disposers.push(ctx.slots.register({
232
277
  name: 'tool.call.toolview', key: 'edit_run_code', locale: LOCALE_NS,
233
- }, PTCPlusToolRow))
278
+ }, ConfiguredToolRow))
234
279
  } catch (error) {
235
280
  disposers.reverse().forEach(dispose => dispose())
236
281
  throw error
@@ -283,7 +328,7 @@ window.__ModuleLoader__.load({
283
328
  const { BindingAuthorButton, BindingReviewDock, BindingCommandCard } = createAuthoringView(React, {
284
329
  ActionButton, IconButton, Menu, MenuFallback, Toast, Tooltip, CodeBlock, Modal, createPortal, BindingsDialog, PTCPlusSettingsDialog,
285
330
  useWorkbenchController, useBindingReview, catalogOwner, callUserBindings, subscribeReset,
286
- settingsCardSeat: settingsCard.seat, updateSetting, menuChildren,
331
+ settingsCardSeat: settingsCard.seat, updateSetting, updateCapabilityConfiguration, menuChildren, SessionCapabilityControls,
287
332
  icons: {
288
333
  sparkle: IconSparkle16, chevron: IconChevronDownOutline14,
289
334
  close: IconCloseOutline16, check: IconCheckOutline14,
@@ -294,6 +339,7 @@ window.__ModuleLoader__.load({
294
339
  normalizeReplMemorySnapshot, unavailableReplMemorySnapshot,
295
340
  ReplMemoryCard, replPopoverIsOpen, placeReplPopover,
296
341
  catalogOwner, subscribeReset,
342
+ useObservedReplMemory, observeRepl,
297
343
  })
298
344
 
299
345
  ctx.slots.inject('conversation.session.header.actions', () => registerGated(ctx, settingsGate('plugin', () => ctx.slots.register({
@@ -304,6 +350,15 @@ window.__ModuleLoader__.load({
304
350
  name: 'conversation.chat.commandview', key: 'binding', locale: LOCALE_NS,
305
351
  inject: settingsProps,
306
352
  }, props => h(BindingCommandCard, { ...props, key: props.node.commandId })))))
353
+ ctx.slots.inject('conversation.chat.commandview', () => registerGated(ctx, settingsGate('plugin', () => ctx.slots.register({
354
+ name: 'conversation.chat.commandview', key: 'capabilities', locale: LOCALE_NS,
355
+ }, props => h(CapabilityCommandCard, props)))))
356
+ ctx.inject(['uiConversation'], scope => scope.effect(() => scope.uiConversation.events.register(bindingToggleConversationDefinition),
357
+ 'ptc-plus binding configuration receipts'))
358
+ ctx.slots.inject('conversation.chat.node', () => ctx.effect(() => ctx.slots.register({
359
+ name: 'conversation.chat.node', key: 'ptc-plus-binding-toggle', locale: LOCALE_NS,
360
+ }, ({ node, t }) => h('p', { className: 'ptcPlusCapabilityReceipt', role: 'status' },
361
+ t(node.data.enabled ? 'bindings.toggleEnabled' : 'bindings.toggleDisabled', { name: node.data.name }))), 'ptc-plus binding configuration receipt renderer'))
307
362
  ctx.slots.inject('conversation.input.dock', () => ctx.slots.inject('conversation.input.left', () =>
308
363
  registerGated(ctx, settingsGate('bindings', () => ctx.slots.register({
309
364
  name: 'conversation.input.dock', id: 'ptc-plus-binding-review', order: -30, locale: LOCALE_NS,
@@ -326,21 +381,20 @@ window.__ModuleLoader__.load({
326
381
  return () => { unsubscribePreset(); unsubscribeSettings() }
327
382
  },
328
383
  isEnabled: () => sessionUsesPtcPreset(preset)
329
- && featureEnabled(preferenceScope.getSnapshot(), 'bindings'),
384
+ && featureEnabled(preferenceScope.getSnapshot(), 'plugin'),
330
385
  register: () => ctx.slots.register({
331
386
  name: 'conversation.input.left', id: 'ptc-plus-binding-author', order: 20, locale: LOCALE_NS,
332
387
  inject: sessionId => ({
333
- hooks: { ptcSettings: preferenceScope, bindingCommand: availability.source(sessionId) },
388
+ hooks: { ptcSettings: preferenceScope, bindingCommand: availability.source(sessionId),
389
+ capabilityCommand: capabilityCommands.source(sessionId) },
334
390
  }),
335
391
  }, props => props.sessionId === undefined ? null : h(BindingAuthorButton, { ...props, key: props.sessionId })),
336
392
  }))
337
393
  })
338
394
  }
339
395
 
340
- module.exports = {
396
+ return {
341
397
  apply,
342
398
  inject: ['slots', 'locale', 'connection', 'remote'],
343
399
  }
344
- return module.exports
345
- },
346
- })
400
+ }