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.
- package/README.en.md +65 -127
- package/README.md +65 -127
- package/assets/dsh-ptc-plus-banner-en.webp +0 -0
- package/assets/dsh-ptc-plus-banner-zh.webp +0 -0
- package/assets/ptc-plus-repl-workspace-en.png +0 -0
- package/assets/ptc-plus-repl-workspace-zh.png +0 -0
- package/assets/ptc-plus-settings-en.png +0 -0
- package/assets/ptc-plus-settings-zh.png +0 -0
- package/client.cjs +42702 -0
- package/cordis.patch.yml +1 -0
- package/docs/README.md +6 -2
- package/docs/adr/0005-temporary-rejected-cell-edit-transport.md +31 -198
- package/docs/adr/0008-define-agent-operating-contract.md +5 -1
- package/docs/adr/0009-bound-expensive-acceptance-default.md +2 -2
- package/docs/adr/0016-archive-resolved-review-findings.md +1 -1
- package/docs/adr/0017-track-the-latest-dsh-public-surface.md +4 -4
- package/docs/adr/0019-plugin-settings-and-kill-switch.md +1 -1
- package/docs/adr/0020-optional-cordis-tools-in-ptc-mode.md +1 -1
- package/docs/adr/0022-stage-npm-releases-with-oidc.md +4 -2
- package/docs/adr/0023-global-user-bindings.md +2 -0
- package/docs/adr/0026-persist-the-delivery-verdict-against-the-verified-candidate.md +2 -0
- package/docs/adr/0028-attach-to-the-host-ptc-execution-seam.md +25 -23
- package/docs/adr/0031-close-semantic-obligations-by-owner.md +1 -1
- package/docs/adr/0034-keep-program-values-separate-from-output-projections.md +29 -0
- package/docs/adr/0035-run-coverage-only-before-a-new-release.md +27 -0
- package/docs/adr/0036-separate-focused-development-from-delivery-verification.md +29 -0
- package/docs/adr/0037-keep-tool-hover-previews-passive.md +52 -0
- package/docs/adr/0038-freeze-session-capability-plans.md +71 -0
- package/docs/adr/0039-verify-documentation-without-runtime-suites.md +29 -0
- package/docs/adr/0040-separate-capability-configurations-from-session-observations.md +39 -0
- package/docs/adr/0041-record-cell-execution-directories.md +65 -0
- package/docs/adr/0042-persist-provider-availability-and-handback.md +41 -0
- package/docs/adr/0043-compose-capability-and-context-policies.md +37 -0
- package/docs/adr/0044-coexist-with-the-official-persistent-repl.md +35 -0
- package/docs/adr/README.md +8 -0
- package/docs/architecture.md +23 -25
- package/docs/capability-planning-components.md +93 -0
- package/docs/capability-projection.md +7 -0
- package/docs/client-ui.md +70 -23
- package/docs/client-visual-contract.md +26 -9
- package/docs/context-management.md +22 -0
- package/docs/context-optimization-plan.md +353 -0
- package/docs/context-public-interface-coverage.md +25 -0
- package/docs/durability-design.md +20 -15
- package/docs/evaluation.md +41 -6
- package/docs/evaluations/command-goat-deepseek-v4.1-flash-20261006.md +126 -0
- package/docs/evaluations/command-goat-deepseek-v4.1-flash.md +281 -0
- package/docs/installation.md +33 -10
- package/docs/program-data-plane.md +5 -3
- package/docs/publishing.md +7 -7
- package/docs/release-coverage.md +73 -0
- package/docs/release-notes.md +1 -1
- package/docs/review-procedure.md +56 -0
- package/docs/runtime-reference.md +53 -16
- package/docs/semantic-validation.md +14 -2
- package/docs/stateful-binding-updates-proposal.md +2 -2
- package/docs/user-bindings.md +1 -1
- package/docs/value-wire.md +11 -1
- package/docs/verification-optimization-work.md +26 -15
- package/docs/verification.md +30 -88
- package/index.js +146 -20
- package/internal/binding-toggle-receipt.js +24 -0
- package/internal/capability-command-receipt.js +31 -0
- package/internal/capability-environment.js +1253 -0
- package/internal/capability-plan.js +285 -0
- package/internal/capability-policy.js +96 -0
- package/internal/capability-statistics-store.js +180 -0
- package/internal/capability-statistics.js +214 -0
- package/internal/config-spec.js +89 -10
- package/internal/direct-surface-owner.js +27 -29
- package/internal/dynamic-environment-runtime.js +9 -9
- package/internal/edit-transport-owner.js +195 -140
- package/internal/execution-provider-owner.js +40 -12
- package/internal/execution-seam-compat.js +26 -8
- package/internal/kernel-worker.js +104 -32
- package/internal/native-root-dynamic.js +12 -13
- package/internal/official-repl-priority.js +51 -0
- package/internal/program-bindings.js +190 -0
- package/internal/recovery-tips.js +3 -2
- package/internal/rejected-cell-editor.js +8 -8
- package/internal/repl-observation-interest.js +11 -3
- package/internal/rpc-contract.js +1 -0
- package/internal/runtime-bridge-owner.js +275 -34
- package/internal/runtime-config.js +58 -0
- package/internal/runtime-contexts.js +74 -5
- package/internal/runtime-intrinsics.js +1 -0
- package/internal/runtime-messages.js +41 -0
- package/internal/sdk-projection.js +10 -82
- package/internal/session-cell-executor.js +42 -19
- package/internal/session-events.js +34 -0
- package/internal/session-journal-recovery.js +234 -11
- package/internal/session-journal-schema.js +6 -3
- package/internal/session-journal.js +31 -2
- package/internal/session-log-view.js +1 -1
- package/internal/session-runtime.js +119 -15
- package/internal/settings-compat.js +12 -5
- package/internal/stateful-module-compiler.js +3 -2
- package/internal/stateful-module-runtime.js +7 -3
- package/internal/stateful-root-runtime.js +9 -4
- package/internal/user-binding-authoring.js +2 -7
- package/internal/user-bindings-owner.js +19 -2
- package/internal/validated-parse-repair.js +1 -1
- package/internal/value-wire.js +68 -21
- package/internal/worker-cwd-virtualization.js +20 -18
- package/internal/worker-realm-surfaces.js +3 -3
- package/package.json +49 -30
- package/src/client-activity.js +3 -2
- package/src/client-authoring-view.js +67 -30
- package/src/client-binding-review.js +2 -1
- package/src/client-binding-toggle.js +18 -0
- package/src/client-capabilities-view.js +1555 -0
- package/src/client-capability-sort.js +29 -0
- package/src/client-copy.js +923 -8
- package/src/client-feature-gates.js +3 -0
- package/src/client-indicator-view.js +4 -6
- package/src/client-primitives.js +33 -2
- package/src/client-repl-view.js +20 -13
- package/src/client-settings-view.js +411 -41
- package/src/client-styles.js +112 -15
- package/src/client-tool-hover.js +185 -0
- package/src/client-tool-view.js +45 -9
- package/src/client-transport.js +44 -9
- package/src/client-workbench.js +6 -7
- package/src/client.js +76 -22
- package/client.js +0 -32479
|
@@ -1,15 +1,117 @@
|
|
|
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
|
|
7
|
-
*
|
|
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
|
|
27
|
+
const { ActionButton, BindingsDialog, Modal, Select, useWorkbenchController, icons, inspectCapabilityPlans, inspectCapabilityCatalog,
|
|
28
|
+
inspectCapabilityTargets, inspectCapabilityAllocation, CapabilityRefreshButton } = deps
|
|
12
29
|
const h = React.createElement
|
|
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 })
|
|
13
115
|
|
|
14
116
|
function fieldInput(field, value, disabled, onChange, label) {
|
|
15
117
|
if (field.type === 'boolean') {
|
|
@@ -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)
|
|
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
|
-
|
|
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
|
|
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('
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
onChange:
|
|
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
|
-
|
|
133
|
-
|
|
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' },
|
|
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', {
|
|
@@ -183,11 +551,13 @@ export function createPtcSettingsView(React, deps) {
|
|
|
183
551
|
onClose: close,
|
|
184
552
|
title: t('settings.dialogTitle'),
|
|
185
553
|
closeLabel: t('settings.close'),
|
|
186
|
-
|
|
554
|
+
headless: true,
|
|
187
555
|
className: 'ptcPlusSettingsModal',
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
h(
|
|
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 }))))
|
|
191
561
|
}
|
|
192
562
|
|
|
193
563
|
return { PTCPlusSettingsCard, PTCPlusSettingsDialog }
|