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,22 +1,124 @@
1
- import { CONFIG_FIELDS, CONFIG_GROUPS } from '../internal/config-spec.js'
2
- import { resolveConfig } from '../internal/runtime-config.js'
1
+ import { CONFIG_DEFAULTS, CONFIG_FIELDS, CONFIG_GROUPS, CONFIG_MODULES } from '../internal/config-spec.js'
2
+ import { CAPABILITY_CONFIGURATION_KEYS, capabilityProfileConfiguration, resolveConfig } from '../internal/runtime-config.js'
3
3
  import { featureEnabled } from './client-feature-gates.js'
4
+ import { isHostIconComponent } from './client-host-compat.js'
5
+ import { capabilityPlanReason, createCapabilitySettingsViews } from './client-capabilities-view.js'
6
+
7
+ // DOM integer dimensions lose fractional pixels under zoom. Convert viewport
8
+ // positions using the resolved CSS border box, shared by tracking and reveal.
9
+ function settingsScrollGeometry(element) {
10
+ const bounds = element.getBoundingClientRect()
11
+ const style = element.ownerDocument.defaultView.getComputedStyle(element)
12
+ const number = value => Number.parseFloat(value) || 0
13
+ const borderTop = number(style.borderTopWidth), borderBottom = number(style.borderBottomWidth)
14
+ const cssHeight = number(style.height) + (style.boxSizing === 'border-box' ? 0
15
+ : number(style.paddingTop) + number(style.paddingBottom) + borderTop + borderBottom)
16
+ const scale = cssHeight > 0 ? bounds.height / cssHeight : 1
17
+ return { bounds, scale, top: bounds.top + borderTop * scale,
18
+ bottom: bounds.bottom - borderBottom * scale, borderTop }
19
+ }
4
20
 
5
21
  /**
6
- * Settings card for the plugin. One field control per config-spec entry, one
7
- * serialized write tail so rapid edits cannot race, and the manage-bindings
22
+ * Settings card with structured capability controls and one serialized write
23
+ * tail so rapid edits cannot race, plus the manage-bindings
8
24
  * action only while global bindings are eligible.
9
25
  */
10
26
  export function createPtcSettingsView(React, deps) {
11
- const { ActionButton, BindingsDialog, Modal, useWorkbenchController, icons } = deps
27
+ const { ActionButton, BindingsDialog, Modal, Select, useWorkbenchController, icons, inspectCapabilityPlans, inspectCapabilityCatalog,
28
+ inspectCapabilityTargets, inspectCapabilityAllocation, CapabilityRefreshButton } = deps
12
29
  const h = React.createElement
13
- const focusableSelector = 'button:not(:disabled),input:not(:disabled),textarea:not(:disabled),select:not(:disabled),[href],[tabindex]:not([tabindex="-1"]),[contenteditable=true],summary'
30
+ const enabledField = CONFIG_FIELDS.find(field => field.key === 'enabled')
31
+
32
+ function SectionIcon({ section }) {
33
+ const paths = {
34
+ syntax: ['m8 5-5 5 5 5', 'm16 5 5 5-5 5', 'm14 3-4 14'],
35
+ recovery: ['M3 10a9 9 0 1 1 2.6 8', 'M3 4v6h6', 'M12 7v5l3 2'],
36
+ extensions: ['M9 3H5v6a3 3 0 1 1 0 6v6h6a3 3 0 1 1 6 0h4v-6a3 3 0 1 1 0-6V5h-6a3 3 0 1 0-6-2Z'],
37
+ calls: ['M12 3 3 7v6c0 5 9 8 9 8s9-3 9-8V7l-9-4Z', 'm8 12 3 3 5-6'],
38
+ interface: ['M3 4h18v13H3Z', 'M8 21h8', 'M12 17v4'],
39
+ limits: ['M3 6h5m4 0h9', 'M3 12h11m4 0h3', 'M3 18h3m4 0h11', 'M8 3v6', 'M14 9v6', 'M6 15v6'],
40
+ capabilities: ['M9 3h6', 'M10 3v6L4 19a1.3 1.3 0 0 0 1 2h14a1.3 1.3 0 0 0 1-2L14 9V3', 'M7 15h10'],
41
+ }
42
+ return h('svg', { width: 16, height: 16, viewBox: '0 0 24 24', fill: 'none', stroke: 'currentColor',
43
+ strokeWidth: 1.6, strokeLinecap: 'round', strokeLinejoin: 'round', 'aria-hidden': true,
44
+ className: 'ptcPlusSettingsNavIcon' }, ...paths[section].map((d, key) => h('path', { key, d })))
45
+ }
46
+
47
+ function SearchIcon() {
48
+ return isHostIconComponent(icons.search) ? h(icons.search, { size: 16, 'aria-hidden': true })
49
+ : h('svg', { width: 16, height: 16, viewBox: '0 0 24 24', fill: 'none', stroke: 'currentColor',
50
+ strokeWidth: 1.6, strokeLinecap: 'round', 'aria-hidden': true },
51
+ h('circle', { cx: 10.5, cy: 10.5, r: 6.5 }), h('path', { d: 'm16 16 5 5' }))
52
+ }
53
+
54
+ function NavigationArrow() {
55
+ return isHostIconComponent(icons.chevron) ? h(icons.chevron, { size: 12 })
56
+ : h('svg', { width: 12, height: 12, viewBox: '0 0 24 24', fill: 'none', stroke: 'currentColor',
57
+ strokeWidth: 1.6, strokeLinecap: 'round', strokeLinejoin: 'round' }, h('path', { d: 'm6 9 6 6 6-6' }))
58
+ }
59
+
60
+ const validStatistics = settings => typeof settings.enabled === 'boolean'
61
+ && Number.isSafeInteger(settings.windowSize) && settings.windowSize >= 1
62
+ && Number.isSafeInteger(settings.minimumSamples) && settings.minimumSamples >= 1 && settings.minimumSamples <= settings.windowSize
63
+ && Number.isFinite(settings.fullThreshold) && settings.fullThreshold >= 0 && settings.fullThreshold <= 1
64
+
65
+ function StatisticsSetting({ value, disabled, onChange, t }) {
66
+ const saved = React.useMemo(() => {
67
+ const defaults = JSON.parse(CONFIG_DEFAULTS.capabilityStatistics)
68
+ try {
69
+ const parsed = JSON.parse(value)
70
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) throw new TypeError()
71
+ const settings = { ...defaults, ...parsed }
72
+ if (!validStatistics(settings)) throw new TypeError()
73
+ return { settings, invalid: false }
74
+ } catch { return { settings: defaults, invalid: true } }
75
+ }, [value])
76
+ const [draft, setDraft] = React.useState(saved.settings)
77
+ React.useEffect(() => setDraft(saved.settings), [saved])
78
+ const valid = validStatistics(draft)
79
+ const commit = next => {
80
+ if (!validStatistics(next)) return
81
+ const serialized = JSON.stringify({ enabled: next.enabled, windowSize: next.windowSize,
82
+ minimumSamples: next.minimumSamples, fullThreshold: next.fullThreshold })
83
+ if (serialized !== value) onChange(CONFIG_FIELDS.find(field => field.key === 'capabilityStatistics'), serialized)
84
+ }
85
+ const numeric = (key, min, max, scale = 1) => h('label', { className: 'ptcPlusBindingField', key },
86
+ h('span', { className: 'ptcPlusBindingFieldLabel' }, t(`capabilities.${key}`)),
87
+ h('input', { type: 'number', className: 'ptcPlusInput', disabled, min, max, step: scale === 1 ? 1 : 'any',
88
+ value: draft[key] === '' ? '' : scale === 1 ? draft[key] : Number((draft[key] * scale).toFixed(8)), 'aria-label': t(`capabilities.${key}`),
89
+ onChange: event => setDraft(current => ({ ...current, [key]: event.target.value === '' ? '' : Number(event.target.value) / scale })),
90
+ onBlur: () => commit(draft) }))
91
+ return h('div', { className: 'ptcPlusCapabilityStatistics' },
92
+ h('label', { className: 'ptcPlusRow' }, h('span', { className: 'ptcPlusMain' }, t('capabilities.statisticsEnabled')),
93
+ h('input', { type: 'checkbox', role: 'switch', className: 'ptcPlusCheck', disabled,
94
+ checked: draft.enabled === true, 'aria-label': t('capabilities.statisticsEnabled'),
95
+ onChange: event => {
96
+ // A malformed numeric draft must never make the independent
97
+ // enable/disable switch appear to save while leaving persisted
98
+ // statistics enabled. Restore the last valid numeric tuple when
99
+ // the switch is changed from an invalid draft.
100
+ const next = validStatistics(draft)
101
+ ? { ...draft, enabled: event.target.checked }
102
+ : { ...saved.settings, enabled: event.target.checked }
103
+ setDraft(next)
104
+ commit(next)
105
+ } })),
106
+ h('section', { className: 'ptcPlusStatisticsParameters', 'aria-label': t('capabilities.statisticsSettings') },
107
+ h('h5', null, t('capabilities.statisticsSettings')),
108
+ h('div', { className: 'ptcPlusBindingFields' }, numeric('windowSize', 1, Number.MAX_SAFE_INTEGER),
109
+ numeric('minimumSamples', 1, Number.MAX_SAFE_INTEGER), numeric('fullThreshold', 0, 100, 100))),
110
+ !valid ? h('p', { role: 'status', className: 'ptcPlusMessage' }, t('capabilities.invalidStatistics'))
111
+ : saved.invalid && draft === saved.settings ? h('p', { role: 'status', className: 'ptcPlusMessage' }, t('capabilities.savedStatisticsInvalid')) : null)
112
+ }
113
+
114
+ const { CapabilityWorkbench } = createCapabilitySettingsViews(React, { SearchIcon, Select, Modal })
14
115
 
15
116
  function fieldInput(field, value, disabled, onChange, label) {
16
117
  if (field.type === 'boolean') {
17
118
  return h('input', {
18
119
  type: 'checkbox', role: 'switch', className: 'ptcPlusCheck', checked: value === true,
19
120
  disabled, 'aria-label': label,
121
+ 'data-modal-autofocus': field.key === 'enabled' && !disabled ? true : undefined,
20
122
  onChange: event => onChange(field, event.target.checked),
21
123
  })
22
124
  }
@@ -39,12 +141,28 @@ export function createPtcSettingsView(React, deps) {
39
141
  })
40
142
  }
41
143
 
42
- function PTCPlusSettingsCard({ t, usePtcSettings, updateSetting, callUserBindings, view, onBindingsOpenChange }) {
144
+ function PTCPlusSettingsCard({ t, usePtcSettings, updateSetting, updateCapabilityConfiguration, callUserBindings, view, onBindingsOpenChange }) {
43
145
  const [open, setOpen] = React.useState(false)
44
146
  const [bindingsOpen, setBindingsOpen] = React.useState(false)
45
147
  const bindingsAction = React.useRef(null)
148
+ const settingsRoot = React.useRef(null)
46
149
  const [status, setStatus] = React.useState(null)
150
+ const [capabilityStatus, setCapabilityStatus] = React.useState(null)
47
151
  const [pending, setPending] = React.useState(() => new Set())
152
+ const [plans, setPlans] = React.useState(null)
153
+ const [plansError, setPlansError] = React.useState(null)
154
+ const [plansPending, setPlansPending] = React.useState(false)
155
+ const [plansObservedConfiguration, setPlansObservedConfiguration] = React.useState(null)
156
+ const [catalog, setCatalog] = React.useState(null)
157
+ const [catalogError, setCatalogError] = React.useState(null)
158
+ const [planRevision, setPlanRevision] = React.useState(0)
159
+ const [targets, setTargets] = React.useState(null)
160
+ const [targetsError, setTargetsError] = React.useState(null)
161
+ const [refreshSessionId, setRefreshSessionId] = React.useState('')
162
+ const [allocation, setAllocation] = React.useState(null)
163
+ const [allocationError, setAllocationError] = React.useState(null)
164
+ const [allocationPending, setAllocationPending] = React.useState(false)
165
+ const [activeSection, setActiveSection] = React.useState('syntax')
48
166
  const writeTail = React.useRef(Promise.resolve())
49
167
  const snapshot = usePtcSettings(snapshot => snapshot)
50
168
  const value = snapshot.status === 'ready' ? resolveConfig(snapshot.value ?? {}) : {}
@@ -53,7 +171,108 @@ export function createPtcSettingsView(React, deps) {
53
171
  const legacyMigration = value.legacyBindingSettings === true
54
172
  const enabled = featureEnabled(snapshot, 'plugin')
55
173
  const globalEnabled = featureEnabled(snapshot, 'bindings')
174
+ const plansEnabled = enabled
175
+ const planConfiguration = JSON.stringify(CONFIG_FIELDS.filter(field => CAPABILITY_CONFIGURATION_KEYS.includes(field.key))
176
+ .map(field => value[field.key]))
177
+ const allocationConfiguration = JSON.stringify(Object.fromEntries(CAPABILITY_CONFIGURATION_KEYS.map(key => [key, value[key]])))
178
+ React.useEffect(() => {
179
+ setAllocation(null); setAllocationError(null); setAllocationPending(false)
180
+ if (!enabled || !refreshSessionId || !inspectCapabilityAllocation || !(open || view === 'page' || view === 'dialog')) return
181
+ const controller = new AbortController()
182
+ setAllocationPending(true)
183
+ Promise.resolve().then(() => inspectCapabilityAllocation({ sessionId: refreshSessionId,
184
+ configuration: JSON.parse(allocationConfiguration) }, controller.signal)).then(result => {
185
+ if (!controller.signal.aborted) { setAllocation(result); setAllocationPending(false) }
186
+ }, error => {
187
+ if (!controller.signal.aborted) { setAllocationError(String(error?.message ?? error)); setAllocationPending(false) }
188
+ })
189
+ return () => controller.abort()
190
+ }, [enabled, open, view, refreshSessionId, allocationConfiguration, planRevision])
191
+ React.useEffect(() => {
192
+ if (!enabled) { setTargets(null); setTargetsError(null); setRefreshSessionId(''); return }
193
+ if (!(open || view === 'page' || view === 'dialog') || !inspectCapabilityTargets) return
194
+ const controller = new AbortController()
195
+ setTargetsError(null)
196
+ Promise.resolve().then(() => inspectCapabilityTargets(controller.signal)).then(next => {
197
+ if (controller.signal.aborted) return
198
+ setTargets(next)
199
+ setRefreshSessionId(current => next.some(target => target.sessionId === current)
200
+ || plans?.some(plan => plan.sessionId === current) ? current : '')
201
+ }, error => {
202
+ if (!controller.signal.aborted) setTargetsError(String(error?.message ?? error))
203
+ })
204
+ return () => controller.abort()
205
+ }, [enabled, open, view, planRevision])
206
+ React.useEffect(() => {
207
+ if (!enabled) {
208
+ setCatalog(null)
209
+ setCatalogError(null)
210
+ return
211
+ }
212
+ if (!(open || view === 'page' || view === 'dialog') || !inspectCapabilityCatalog) return
213
+ const controller = new AbortController()
214
+ setCatalogError(null)
215
+ Promise.resolve().then(() => inspectCapabilityCatalog(controller.signal)).then(available => {
216
+ if (controller.signal.aborted) return
217
+ setCatalog(available)
218
+ }, error => {
219
+ if (controller.signal.aborted) return
220
+ setCatalogError(String(error?.message ?? error))
221
+ })
222
+ return () => controller.abort()
223
+ }, [enabled, open, view, planConfiguration, planRevision])
224
+ React.useEffect(() => {
225
+ if (!plansEnabled) {
226
+ setPlans(null)
227
+ setPlansError(null)
228
+ setPlansPending(false)
229
+ setPlansObservedConfiguration(null)
230
+ return
231
+ }
232
+ if (!(open || view === 'page' || view === 'dialog') || !inspectCapabilityPlans) return
233
+ const controller = new AbortController()
234
+ setPlansError(null)
235
+ setPlansPending(true)
236
+ Promise.resolve().then(() => inspectCapabilityPlans(controller.signal)).then(next => {
237
+ if (!controller.signal.aborted) {
238
+ setPlans(next); setPlansPending(false); setPlansObservedConfiguration(planConfiguration)
239
+ }
240
+ }, error => {
241
+ if (!controller.signal.aborted) { setPlansError(String(error?.message ?? error)); setPlansPending(false) }
242
+ })
243
+ return () => controller.abort()
244
+ }, [plansEnabled, open, view, planConfiguration, planRevision])
56
245
  const groupIdPrefix = view === 'dialog' ? 'ptc-plus-settings-dialog-group' : 'ptc-plus-settings-group'
246
+ const trackSection = React.useCallback((preserveFocus = false) => {
247
+ const scroller = settingsRoot.current?.querySelector('.ptcPlusFields')
248
+ if (!scroller || scroller.clientHeight === 0) return
249
+ const { scale, top, bottom } = settingsScrollGeometry(scroller)
250
+ const sections = CONFIG_GROUPS.slice(1).map(group => ({ key: group.key,
251
+ bounds: scroller.querySelector(`[aria-labelledby="${groupIdPrefix}-${group.key}"]`)?.getBoundingClientRect() }))
252
+ .filter(section => section.bounds)
253
+ const visible = sections.filter(section => section.bounds.bottom > top && section.bounds.top < bottom)
254
+ const atEnd = scroller.scrollHeight > scroller.clientHeight && scroller.scrollTop + scroller.clientHeight >= scroller.scrollHeight - 1
255
+ const focusedControl = preserveFocus === true ? scroller.ownerDocument.activeElement : null
256
+ const focusedBounds = focusedControl?.getBoundingClientRect()
257
+ const focusedGroup = focusedBounds && focusedBounds.bottom > top && focusedBounds.top < bottom
258
+ ? focusedControl.closest('.ptcPlusGroup')?.getAttribute('aria-labelledby') : null
259
+ const focused = visible.find(section => focusedGroup === `${groupIdPrefix}-${section.key}`)
260
+ const next = focused ?? (atEnd ? visible.at(-1) : visible.find(section => section.bounds.bottom > top + 20 * scale))
261
+ if (next) setActiveSection(next.key)
262
+ }, [groupIdPrefix])
263
+ React.useLayoutEffect(() => {
264
+ if (!(view === 'page' || view === 'dialog') || !settingsRoot.current) return
265
+ const scroller = settingsRoot.current.querySelector('.ptcPlusFields')
266
+ if (!scroller) return
267
+ trackSection()
268
+ const resize = typeof ResizeObserver === 'function' ? new ResizeObserver(() => trackSection(true)) : null
269
+ resize?.observe(scroller)
270
+ for (const section of scroller.querySelectorAll('.ptcPlusSettingsModule,.ptcPlusGroup')) resize?.observe(section)
271
+ const changes = typeof MutationObserver === 'function' ? new MutationObserver(() => trackSection(true)) : null
272
+ changes?.observe(scroller, { subtree: true, childList: true, attributes: true, attributeFilter: ['hidden', 'open'] })
273
+ globalThis.addEventListener?.('resize', trackSection)
274
+ return () => { resize?.disconnect(); changes?.disconnect(); globalThis.removeEventListener?.('resize', trackSection) }
275
+ }, [view, open, snapshot.status, trackSection])
57
276
  // The open dialog owns the management session: closing it releases the draft
58
277
  // and drops every response the host has not answered yet.
59
278
  const workbench = useWorkbenchController({ enabled: globalEnabled && bindingsOpen, callUserBindings })
@@ -65,19 +284,29 @@ export function createPtcSettingsView(React, deps) {
65
284
  if (!globalEnabled) setBindingsDialogOpen(false)
66
285
  }, [globalEnabled])
67
286
  const unavailable = snapshot.status !== 'ready' || snapshot.writable !== true
287
+ const isCapabilityField = key => CAPABILITY_CONFIGURATION_KEYS.includes(key)
288
+ || ['capabilityProfiles', 'capabilityConfiguration'].includes(key)
289
+ const recordStatus = next => {
290
+ setStatus(next)
291
+ if (isCapabilityField(next.fieldKey)) setCapabilityStatus(next)
292
+ }
68
293
  const persist = (field, nextValue) => {
69
294
  if (unavailable || pending.has(field.key)) return
70
295
  const operation = writeTail.current.then(async () => {
71
296
  setPending(current => new Set(current).add(field.key))
72
297
  setStatus(null)
298
+ if (isCapabilityField(field.key)) setCapabilityStatus(null)
73
299
  try {
74
300
  const key = await updateSetting(field.key, nextValue)
75
- if (key !== null) setStatus({ key })
301
+ if (key !== null) recordStatus({ key, fieldKey: field.key })
302
+ return key === 'status.applied' || (key === null && value[field.key] === nextValue)
76
303
  } catch (error) {
77
- setStatus({
304
+ recordStatus({
78
305
  key: 'status.failed',
306
+ fieldKey: field.key,
79
307
  params: { error: error instanceof Error ? error.message : String(error) },
80
308
  })
309
+ return false
81
310
  } finally {
82
311
  setPending(current => {
83
312
  const next = new Set(current)
@@ -87,26 +316,38 @@ export function createPtcSettingsView(React, deps) {
87
316
  }
88
317
  })
89
318
  writeTail.current = operation.catch(() => {})
319
+ return operation
320
+ }
321
+ const persistConfiguration = configuration => {
322
+ if (unavailable || !enabled || !updateCapabilityConfiguration) return Promise.reject(new Error(t('capabilities.configurationUnavailable')))
323
+ const validated = capabilityProfileConfiguration(configuration)
324
+ const operation = writeTail.current.then(async () => {
325
+ setPending(current => new Set([...current, ...CAPABILITY_CONFIGURATION_KEYS]))
326
+ setCapabilityStatus(null)
327
+ try {
328
+ const key = await updateCapabilityConfiguration(validated)
329
+ recordStatus({ key: key ?? 'status.applied', fieldKey: 'capabilityConfiguration' })
330
+ setPlanRevision(current => current + 1)
331
+ } catch (error) {
332
+ recordStatus({ key: 'status.failed', fieldKey: 'capabilityConfiguration',
333
+ params: { error: error instanceof Error ? error.message : String(error) } })
334
+ throw error
335
+ } finally { setPending(current => new Set([...current].filter(key => !CAPABILITY_CONFIGURATION_KEYS.includes(key)))) }
336
+ })
337
+ writeTail.current = operation.catch(() => {})
338
+ return operation
90
339
  }
91
340
  const fieldDisabled = field => unavailable
92
341
  || pending.has(field.key)
93
342
  || (field.key !== 'enabled' && !enabled)
94
- const settingsFields = () => CONFIG_GROUPS.map(group => {
95
- const fields = group.key === 'syntax' && legacyMigration
96
- ? [...group.fields, ...legacyKeys]
97
- : group.fields
98
- return h('section', {
99
- key: group.key,
100
- className: 'ptcPlusGroup',
101
- 'aria-labelledby': `${groupIdPrefix}-${group.key}`,
102
- },
103
- h('h3', {
104
- id: `${groupIdPrefix}-${group.key}`,
105
- className: 'ptcPlusGroupTitle',
106
- }, t(`group.${group.key}`)),
107
- ...fields.map(key => {
343
+ const renderField = key => {
108
344
  const field = CONFIG_FIELDS.find(candidate => candidate.key === key)
109
345
  if (field === undefined) return null
346
+ if (['capabilityOverrides', 'capabilityPolicy'].includes(field.key)) return null
347
+ if (field.key === 'capabilityBaseline') return null
348
+ if (field.key === 'capabilityProfiles') return null
349
+ if (field.key === 'capabilityStatistics') return h('div', { key },
350
+ h(StatisticsSetting, { value: value[key] ?? CONFIG_DEFAULTS[key], disabled: fieldDisabled(field), onChange: persist, t }))
110
351
  const migratingPolicy = field.key === 'bindingUpdates' && legacyMigration
111
352
  return h(React.Fragment, { key: field.key },
112
353
  h('div', { className: 'ptcPlusRow' },
@@ -115,35 +356,162 @@ export function createPtcSettingsView(React, deps) {
115
356
  field.description === '' ? null : h('div', { className: 'ptcPlusDetail' },
116
357
  t(migratingPolicy ? 'bindingPolicy.migrationDescription' : `${field.key}.description`))),
117
358
  migratingPolicy
118
- ? h('select', {
119
- className: 'ptcPlusSelect', value: 'legacy', disabled: fieldDisabled(field),
120
- 'aria-label': t(`${field.key}.label`),
121
- onChange: event => persist(field, event.target.value),
122
- },
123
- h('option', { value: 'legacy', disabled: true }, t('bindingPolicy.legacy')),
124
- ...field.options.map(policy => h('option', { key: policy, value: policy }, t(`bindingPolicy.${policy}`))))
359
+ ? h(Select, { value: 'legacy', disabled: fieldDisabled(field), label: t(`${field.key}.label`),
360
+ options: [{ value: 'legacy', label: t('bindingPolicy.legacy'), disabled: true },
361
+ ...field.options.map(policy => ({ value: policy, label: t(`bindingPolicy.${policy}`) }))],
362
+ onChange: next => persist(field, next) })
125
363
  : fieldInput(field, value[field.key], fieldDisabled(field), persist, t(`${field.key}.label`))),
126
364
  field.key === 'userBindingsEnabled' && globalEnabled
127
365
  ? h('div', { className: 'ptcPlusSettingAction', ref: bindingsAction },
128
366
  h(ActionButton, { type: 'button', className: 'ptcPlusButton', onClick: () => setBindingsDialogOpen(true) }, t('bindings.manage')))
129
367
  : null)
130
- }))
131
- })
132
- const settingsBody = (expanded, id) => h('div', {
133
- id, className: 'ptcPlusBody', 'data-open': expanded, hidden: !expanded,
368
+ }
369
+ const renderPlans = () => plansEnabled && inspectCapabilityPlans ? h('section', { className: 'ptcPlusCapabilityPlans',
370
+ 'aria-labelledby': `${groupIdPrefix}-plans` },
371
+ h('h4', { id: `${groupIdPrefix}-plans`, className: 'ptcPlusGroupTitle' }, t('capabilities.sessions')),
372
+ h(ActionButton, { type: 'button', disabled: !enabled, onClick: () => setPlanRevision(current => current + 1) }, t('capabilities.reload')),
373
+ h('p', { className: 'ptcPlusDetail' }, t('capabilities.refreshHint')),
374
+ plansError ? h('p', { role: 'status' }, t('capabilities.unavailable', { error: plansError }))
375
+ : plans === null ? h('p', null, t('state.syncing'))
376
+ : plans.length === 0 ? h('p', null, t('capabilities.empty'))
377
+ : plans.filter(plan => plan.sessionId === refreshSessionId).map(plan => h('details', { key: plan.sessionId },
378
+ h('summary', { tabIndex: 0 }, `${plan.sessionId} · ${t('capabilities.combination.lastRead')}`),
379
+ h('p', { className: 'ptcPlusDetail' }, t('capabilities.frozenHint')),
380
+ h('p', null, t('capabilities.budget', { bytes: plan.effective.budgets.contractBytes, overage: plan.effective.budgets.overageBytes })),
381
+ h('table', null, h('thead', null, h('tr', null,
382
+ ...['member', 'selection', 'reason', 'coverage'].map(key => h('th', { key }, t(`capabilities.${key}`))))),
383
+ h('tbody', null, ...plan.effective.entries.map(entry => h('tr', { key: entry.id },
384
+ h('td', null, entry.id), h('td', null, t(`capabilities.${entry.selection}`)),
385
+ h('td', null, capabilityPlanReason(t, entry)),
386
+ h('td', null, entry.evidence ? `${entry.evidence.used}/${entry.evidence.eligible} (${entry.evidence.scope})` : '—')))))))) : null
387
+
388
+ const comparisonTargets = targets === null && inspectCapabilityTargets ? null
389
+ : [...new Set([...(targets ?? []).map(target => target.sessionId), ...(plans ?? []).map(plan => plan.sessionId)])]
390
+ const renderRefresh = ({ observation = false } = {}) => CapabilityRefreshButton || inspectCapabilityPlans ? h('section', { className: 'ptcPlusCapabilityApply',
391
+ 'aria-label': t(observation ? 'capabilities.observationGroup' : 'capabilities.applyGroup') },
392
+ h('label', { className: `ptcPlusCapabilityFilter${observation ? ' ptcPlusStatisticsObservation' : ''}` },
393
+ observation ? h('span', { className: 'ptcPlusStatisticsObservationHeading' },
394
+ h('span', null, t('capabilities.observationSession')),
395
+ h('span', { className: 'ptcPlusStatisticsObservationHint' }, t('capabilities.selectObservation')))
396
+ : h('span', null, t('capabilities.targetSession')),
397
+ h(Select, { value: refreshSessionId, label: t(observation ? 'capabilities.observationSession' : 'capabilities.targetSession'),
398
+ disabled: !enabled || comparisonTargets === null || Boolean(targetsError), onChange: setRefreshSessionId,
399
+ options: [{ value: '', label: t('capabilities.chooseSession') },
400
+ ...(comparisonTargets ?? []).map(sessionId => ({ value: sessionId, label: sessionId }))] })),
401
+ observation || !CapabilityRefreshButton ? null : h(CapabilityRefreshButton, { sessionId: refreshSessionId, t,
402
+ enabled: enabled && !unavailable && pending.size === 0 && !targetsError
403
+ && (targets ?? []).some(target => target.sessionId === refreshSessionId),
404
+ onInvalidated: () => setPlansObservedConfiguration(null),
405
+ onApplied: () => setPlanRevision(current => current + 1) }),
406
+ targetsError ? h('p', { role: 'status', className: 'ptcPlusDetail' }, t('capabilities.unavailable', { error: targetsError })) : null) : null
407
+
408
+ const renderGroup = group => {
409
+ const fields = group.key === 'syntax' && legacyMigration
410
+ ? [...group.fields, ...legacyKeys]
411
+ : group.fields
412
+ const heading = h('h3', {
413
+ id: `${groupIdPrefix}-${group.key}`,
414
+ className: 'ptcPlusGroupTitle',
415
+ 'data-experimental-label': group.key === 'capabilities' ? t('settings.experimental') : undefined,
416
+ }, t(`group.${group.key}`))
417
+ return h('section', {
418
+ key: group.key,
419
+ className: 'ptcPlusGroup',
420
+ 'data-experimental': group.key === 'capabilities' ? 'true' : undefined,
421
+ 'aria-labelledby': `${groupIdPrefix}-${group.key}`,
422
+ },
423
+ group.key === 'capabilities' ? h('div', { className: 'ptcPlusCapabilityHead' }, heading,
424
+ h('label', { className: 'ptcPlusCapabilityManagement' },
425
+ h('span', { role: 'status' }, t(value.capabilityContextEnabled === false ? 'capabilities.groupPaused' : 'capabilities.serverOn')),
426
+ h('input', { type: 'checkbox', role: 'switch', className: 'ptcPlusCheck', checked: value.capabilityContextEnabled !== false,
427
+ disabled: fieldDisabled({ key: 'capabilityContextEnabled' }), 'aria-label': t('group.capabilities'),
428
+ onChange: event => persist(CONFIG_FIELDS.find(field => field.key === 'capabilityContextEnabled'), event.target.checked) }))) : heading,
429
+ ...(group.key === 'capabilities' ? [
430
+ h(CapabilityWorkbench, { key: 'capability-workbench', id: `${groupIdPrefix}-capability-workbench`,
431
+ value, configurationReady: snapshot.status === 'ready', catalog, catalogError, plans: plansEnabled ? plans : null, plansError, plansPending,
432
+ plansStale: plansObservedConfiguration !== planConfiguration,
433
+ disabled: key => fieldDisabled({ key }), onChange: persist,
434
+ onReload: () => setPlanRevision(current => current + 1), t, renderField, renderPlans, renderRefresh,
435
+ selectedSessionId: refreshSessionId, onSessionSelect: setRefreshSessionId,
436
+ allocation, allocationError, allocationPending, saving: [...pending].some(key => CAPABILITY_CONFIGURATION_KEYS.includes(key) || key === 'capabilityProfiles'), saveStatus: capabilityStatus, configurationWritable: !unavailable && enabled, onApplyConfiguration: updateCapabilityConfiguration ? persistConfiguration : undefined })]
437
+ : fields.map(renderField)))
438
+ }
439
+
440
+ const settingsFields = navigable => [
441
+ navigable ? null : renderGroup(CONFIG_GROUPS[0]),
442
+ ...CONFIG_MODULES.map(module => h('section', {
443
+ key: module.key, className: 'ptcPlusSettingsModule',
444
+ 'aria-labelledby': `${groupIdPrefix}-module-${module.key}`,
445
+ }, h('h2', { id: `${groupIdPrefix}-module-${module.key}`, className: 'ptcPlusSettingsModuleTitle' }, t(`module.${module.key}`)),
446
+ ...module.groups.map(key => renderGroup(CONFIG_GROUPS.find(group => group.key === key))))),
447
+ ]
448
+
449
+ const settingsBody = (expanded, id) => {
450
+ const navigable = view === 'page' || view === 'dialog'
451
+ const revealSection = key => {
452
+ const id = `${groupIdPrefix}-${key}`
453
+ const root = settingsRoot.current
454
+ const heading = root?.querySelector(`[id="${id}"]`)
455
+ const scroller = root?.querySelector('.ptcPlusFields')
456
+ if (!heading || !scroller) return
457
+ const headingBounds = heading.getBoundingClientRect()
458
+ const { bounds: scrollerBounds, scale, borderTop } = settingsScrollGeometry(scroller)
459
+ const inset = Math.min(16, Math.max(0, scroller.clientHeight - headingBounds.height / scale))
460
+ scroller.scrollTop += (headingBounds.top - scrollerBounds.top) / scale - borderTop - inset
461
+ setActiveSection(key)
462
+ trackSection()
463
+ }
464
+ const revealNavigationItem = element => {
465
+ const scroller = element.closest('.ptcPlusSettingsNavList')
466
+ if (!scroller || scroller.scrollWidth <= scroller.clientWidth) return
467
+ const item = element.getBoundingClientRect()
468
+ const clip = scroller.getBoundingClientRect()
469
+ if (item.right > clip.right) scroller.scrollLeft += item.right - clip.right
470
+ else if (item.left < clip.left) scroller.scrollLeft += item.left - clip.left
471
+ }
472
+ const navigation = navigable ? h('nav', {
473
+ className: 'ptcPlusSettingsNav',
474
+ 'aria-label': t('settings.navigation'),
475
+ },
476
+ h('div', { className: 'ptcPlusSettingsNavIntro' },
477
+ h('label', { className: 'ptcPlusSettingsTitleSwitch', 'data-disabled': fieldDisabled(enabledField) },
478
+ h('strong', null, t('settings.navigationTitle')),
479
+ fieldInput(enabledField, enabled, fieldDisabled(enabledField), persist, t('enabled.label'))),
480
+ h('p', null, t('settings.navigationHint'))),
481
+ h('div', { className: 'ptcPlusSettingsNavList' }, ...CONFIG_MODULES.map(module => h('div', {
482
+ key: module.key, className: 'ptcPlusSettingsNavModule',
483
+ }, h('span', { className: 'ptcPlusSettingsNavModuleTitle' }, t(`module.${module.key}`)),
484
+ h('div', { className: 'ptcPlusSettingsNavModuleItems' }, ...module.groups.map(key => {
485
+ const group = CONFIG_GROUPS.find(group => group.key === key)
486
+ return h('button', {
487
+ key, type: 'button', className: 'ptcPlusSettingsNavItem', 'data-section': key,
488
+ 'data-experimental': key === 'capabilities' ? 'true' : undefined, title: t(`group.${group.key}`),
489
+ 'aria-current': activeSection === key ? 'location' : undefined,
490
+ onClick: () => revealSection(key), onFocus: event => revealNavigationItem(event.currentTarget),
491
+ }, h(SectionIcon, { section: key }),
492
+ h('span', { className: 'ptcPlusSettingsNavLabel' }, t(`group.${group.key}`)),
493
+ key === 'capabilities' ? h('span', { className: 'ptcPlusSettingsNavBadge' }, t('settings.experimental')) : null,
494
+ h('span', { className: 'ptcPlusSettingsNavArrow', 'aria-hidden': true }, h(NavigationArrow)))
495
+ })))))) : null
496
+ return h('div', {
497
+ id, ref: settingsRoot, className: 'ptcPlusBody', 'data-open': expanded, hidden: !expanded,
134
498
  },
135
- h('div', { className: 'ptcPlusBodyInner' }, h('div', { className: 'ptcPlusFields' },
499
+ h('div', { className: 'ptcPlusBodyInner' },
500
+ h('div', { className: navigable ? 'ptcPlusSettingsLayout' : undefined },
501
+ navigation,
502
+ h('div', { className: 'ptcPlusFields', onScroll: navigable ? () => trackSection(true) : undefined },
136
503
  snapshot.status === 'loading'
137
504
  ? h('p', { className: 'ptcPlusMessage' }, t('state.syncing'))
138
505
  : snapshot.status === 'unavailable'
139
506
  ? h('p', { className: 'ptcPlusMessage' }, t('state.unavailable'))
140
507
  : [
141
- ...settingsFields(),
508
+ ...settingsFields(navigable),
142
509
  h('div', { key: 'footer', className: 'ptcPlusFooter' },
143
510
  h('span', { className: 'ptcPlusMessage', role: 'status' }, status === null
144
511
  ? t(snapshot.writable ? 'footer.live' : 'footer.readOnly')
145
512
  : t(status.key, status.params))),
146
- ])))
513
+ ]))))
514
+ }
147
515
  const openBindings = globalEnabled && bindingsOpen
148
516
  ? h(BindingsDialog, {
149
517
  controller: workbench, t, onClose: () => setBindingsDialogOpen(false),
@@ -154,7 +522,7 @@ export function createPtcSettingsView(React, deps) {
154
522
  // with its own save control (the form already writes on change). The legacy
155
523
  // card seat asks for neither and keeps the whole collapsible card.
156
524
  if (view === 'summary') return h('span', { className: 'ptcPlusDescription' }, t('card.description'))
157
- if (view === 'page') return h('div', { className: 'ptcPlusCard' }, settingsBody(true), openBindings)
525
+ if (view === 'page') return h('div', { className: 'ptcPlusCard ptcPlusSettingsPage' }, settingsBody(true), openBindings)
158
526
  if (view === 'dialog') return h(React.Fragment, null, settingsBody(true), openBindings)
159
527
  return h('li', { className: 'ptcPlusCard' },
160
528
  h('button', {
@@ -174,34 +542,7 @@ export function createPtcSettingsView(React, deps) {
174
542
 
175
543
  function PTCPlusSettingsDialog(props) {
176
544
  const { t, onClose } = props
177
- const body = React.useRef(null)
178
545
  const [bindingsOpen, setBindingsOpen] = React.useState(false)
179
- React.useEffect(() => {
180
- const dialog = body.current?.closest('[role=dialog]')
181
- const target = body.current?.querySelector(focusableSelector)
182
- ?? dialog?.querySelector(focusableSelector)
183
- target?.focus({ preventScroll: true })
184
- }, [])
185
- React.useEffect(() => {
186
- const trapFocus = event => {
187
- if (event.key !== 'Tab' || bindingsOpen) return
188
- const dialog = body.current?.closest('[role=dialog]')
189
- if (dialog === undefined || dialog === null) return
190
- const controls = [...dialog.querySelectorAll(focusableSelector)]
191
- .filter(element => !element.closest('[hidden], [inert], [aria-hidden="true"]'))
192
- const first = controls[0]
193
- const last = controls.at(-1)
194
- const active = document.activeElement
195
- if (!dialog.contains(active)
196
- || (event.shiftKey && active === first)
197
- || (!event.shiftKey && active === last)) {
198
- event.preventDefault()
199
- ;(event.shiftKey ? last : first)?.focus({ preventScroll: true })
200
- }
201
- }
202
- document.addEventListener('keydown', trapFocus)
203
- return () => document.removeEventListener('keydown', trapFocus)
204
- }, [bindingsOpen])
205
546
  const close = () => {
206
547
  if (!bindingsOpen) onClose()
207
548
  }
@@ -210,11 +551,13 @@ export function createPtcSettingsView(React, deps) {
210
551
  onClose: close,
211
552
  title: t('settings.dialogTitle'),
212
553
  closeLabel: t('settings.close'),
213
- description: t('card.description'),
554
+ headless: true,
214
555
  className: 'ptcPlusSettingsModal',
215
- contentClassName: 'ptcPlusSettingsDialogContent',
216
- }, h('div', { className: 'ptcPlusSettingsDialog', ref: body },
217
- h(PTCPlusSettingsCard, { ...props, view: 'dialog', onBindingsOpenChange: setBindingsOpen })))
556
+ }, h('div', { className: 'ptcPlusSettingsDialogHead' },
557
+ h('div', null, h('h2', null, t('settings.dialogTitle')), h('p', null, t('card.description'))),
558
+ h('button', { type: 'button', className: 'ptcPlusDialogClose', 'aria-label': t('settings.close'), onClick: close }, '\u00d7')),
559
+ h('div', { className: 'ptcPlusSettingsDialogContent' }, h('div', { className: 'ptcPlusSettingsDialog' },
560
+ h(PTCPlusSettingsCard, { ...props, view: 'dialog', onBindingsOpenChange: setBindingsOpen }))))
218
561
  }
219
562
 
220
563
  return { PTCPlusSettingsCard, PTCPlusSettingsDialog }