dsh-ptc-plus 0.4.10 → 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 (122) hide show
  1. package/README.en.md +60 -130
  2. package/README.md +60 -130
  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 +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 +2 -2
  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/README.md +6 -0
  35. package/docs/architecture.md +23 -25
  36. package/docs/capability-planning-components.md +93 -0
  37. package/docs/capability-projection.md +7 -0
  38. package/docs/client-ui.md +70 -23
  39. package/docs/client-visual-contract.md +26 -9
  40. package/docs/context-management.md +22 -0
  41. package/docs/context-optimization-plan.md +353 -0
  42. package/docs/context-public-interface-coverage.md +25 -0
  43. package/docs/durability-design.md +20 -15
  44. package/docs/evaluation.md +41 -6
  45. package/docs/evaluations/command-goat-deepseek-v4.1-flash-20261006.md +126 -0
  46. package/docs/evaluations/command-goat-deepseek-v4.1-flash.md +281 -0
  47. package/docs/installation.md +31 -10
  48. package/docs/program-data-plane.md +5 -3
  49. package/docs/publishing.md +5 -5
  50. package/docs/release-coverage.md +73 -0
  51. package/docs/release-notes.md +1 -1
  52. package/docs/review-procedure.md +56 -0
  53. package/docs/runtime-reference.md +48 -16
  54. package/docs/semantic-validation.md +14 -2
  55. package/docs/stateful-binding-updates-proposal.md +2 -2
  56. package/docs/user-bindings.md +1 -1
  57. package/docs/value-wire.md +11 -1
  58. package/docs/verification-optimization-work.md +26 -15
  59. package/docs/verification.md +30 -88
  60. package/index.js +123 -20
  61. package/internal/binding-toggle-receipt.js +24 -0
  62. package/internal/capability-command-receipt.js +29 -0
  63. package/internal/capability-environment.js +1247 -0
  64. package/internal/capability-plan.js +285 -0
  65. package/internal/capability-policy.js +96 -0
  66. package/internal/capability-statistics-store.js +180 -0
  67. package/internal/capability-statistics.js +210 -0
  68. package/internal/config-spec.js +79 -9
  69. package/internal/direct-surface-owner.js +27 -29
  70. package/internal/dynamic-environment-runtime.js +9 -9
  71. package/internal/edit-transport-owner.js +180 -139
  72. package/internal/execution-provider-owner.js +15 -12
  73. package/internal/execution-seam-compat.js +8 -8
  74. package/internal/kernel-worker.js +104 -32
  75. package/internal/native-root-dynamic.js +12 -13
  76. package/internal/program-bindings.js +190 -0
  77. package/internal/recovery-tips.js +3 -2
  78. package/internal/rejected-cell-editor.js +8 -8
  79. package/internal/repl-observation-interest.js +11 -3
  80. package/internal/rpc-contract.js +1 -0
  81. package/internal/runtime-bridge-owner.js +272 -34
  82. package/internal/runtime-config.js +58 -0
  83. package/internal/runtime-contexts.js +74 -5
  84. package/internal/runtime-intrinsics.js +1 -0
  85. package/internal/runtime-messages.js +41 -0
  86. package/internal/sdk-projection.js +10 -82
  87. package/internal/session-cell-executor.js +42 -19
  88. package/internal/session-events.js +34 -0
  89. package/internal/session-journal-recovery.js +234 -11
  90. package/internal/session-journal-schema.js +6 -3
  91. package/internal/session-journal.js +31 -2
  92. package/internal/session-log-view.js +1 -1
  93. package/internal/session-runtime.js +119 -15
  94. package/internal/settings-compat.js +12 -5
  95. package/internal/stateful-module-compiler.js +3 -2
  96. package/internal/stateful-module-runtime.js +7 -3
  97. package/internal/stateful-root-runtime.js +9 -4
  98. package/internal/user-binding-authoring.js +2 -7
  99. package/internal/user-bindings-owner.js +41 -2
  100. package/internal/validated-parse-repair.js +1 -1
  101. package/internal/value-wire.js +68 -21
  102. package/internal/worker-cwd-virtualization.js +20 -18
  103. package/internal/worker-realm-surfaces.js +3 -3
  104. package/package.json +47 -29
  105. package/src/client-authoring-view.js +67 -30
  106. package/src/client-binding-review.js +2 -1
  107. package/src/client-binding-toggle.js +18 -0
  108. package/src/client-capabilities-view.js +1341 -0
  109. package/src/client-capability-sort.js +29 -0
  110. package/src/client-copy.js +871 -8
  111. package/src/client-feature-gates.js +3 -0
  112. package/src/client-indicator-view.js +4 -6
  113. package/src/client-primitives.js +33 -2
  114. package/src/client-repl-view.js +20 -13
  115. package/src/client-settings-view.js +411 -41
  116. package/src/client-styles.js +108 -15
  117. package/src/client-tool-hover.js +185 -0
  118. package/src/client-tool-view.js +37 -8
  119. package/src/client-transport.js +44 -9
  120. package/src/client-workbench.js +6 -7
  121. package/src/client.js +76 -22
  122. package/client.js +0 -32479
@@ -0,0 +1,1341 @@
1
+ import { capabilityPolicyForConfig, capabilityRule, changeCapabilityRule, effectiveCapabilitySettings, CAPABILITY_DISCLOSURES, CAPABILITY_INVENTORY_CATEGORIES } from '../internal/capability-policy.js'
2
+ import { CONFIG_DEFAULTS, CONFIG_FIELDS } from '../internal/config-spec.js'
3
+ import { CAPABILITY_CONFIGURATION_KEYS, capabilityProfileConfiguration, validateCapabilityProfiles } from '../internal/runtime-config.js'
4
+ import { readCapabilityCommandReceipt } from '../internal/capability-command-receipt.js'
5
+ import { createFallbackModal } from './client-primitives.js'
6
+ import { CAPABILITY_SORT_COLUMNS, capabilitySortDirection, cycleCapabilitySort, sortCapabilityEntries } from './client-capability-sort.js'
7
+
8
+ export function capabilityCommandText(t, text) {
9
+ if (text?.key) return t(text.key, text.params)
10
+ const receipt = readCapabilityCommandReceipt(text)
11
+ return receipt ? t(`capabilities.receipt.${receipt.state}`, { session: receipt.sessionId, error: receipt.error ?? '' }) : text
12
+ }
13
+
14
+ function capabilityChoiceLabel(t, value) {
15
+ return t(value === 'inherit' ? 'capabilities.quick.noOverride'
16
+ : ['enabled', 'pending', 'unknown'].includes(value) ? `capabilities.quick.${value}`
17
+ : [...selections, ...CAPABILITY_DISCLOSURES].includes(value) ? `capabilities.${value}` : 'capabilities.quick.unknown')
18
+ }
19
+
20
+ function capabilityRuleLabel(t, rule, empty = 'capabilities.policy.inheritRule') {
21
+ const dimensions = ['availability', 'disclosure'].filter(dimension => rule?.[dimension] !== undefined)
22
+ return dimensions.length ? dimensions.map(dimension => `${t(`capabilities.policy.${dimension}`)}: ${rule[dimension] === 'automatic'
23
+ ? t('capabilities.policy.inheritRule') : capabilityChoiceLabel(t, rule[dimension])}`).join(' · ') : t(empty)
24
+ }
25
+
26
+ function validCatalogRule(rule, temporary = false) {
27
+ return rule !== null && typeof rule === 'object' && !Array.isArray(rule)
28
+ && Object.entries(rule).every(([dimension, choice]) => ['availability', 'disclosure'].includes(dimension)
29
+ && (temporary && choice === 'automatic' || (dimension === 'availability'
30
+ ? ['enabled', 'closed'].includes(choice) : CAPABILITY_DISCLOSURES.includes(choice))))
31
+ }
32
+
33
+ export function createCapabilityCommandCard(React) {
34
+ const h = React.createElement
35
+ return function CapabilityCommandCard({ node, t }) {
36
+ const [showSource, setShowSource] = React.useState(false)
37
+ const command = `/${node.name ?? 'capabilities'}${node.args ? ` ${String(node.args).trim()}` : ''}`
38
+ const text = node.outcome?.text
39
+ const receipt = readCapabilityCommandReceipt(text)
40
+ const legacyRefresh = text === 'Session capability environment refreshed; the model-visible notification is recorded.'
41
+ if (receipt || String(node.args ?? '').trim() === 'refresh' || legacyRefresh) {
42
+ const outcome = receipt ? t(`capabilities.card.${receipt.state}`, { error: receipt.error ?? '' })
43
+ : legacyRefresh ? t('capabilities.card.recorded')
44
+ : node.outcome ? (typeof text === 'string' && !text.trim() ? undefined : capabilityCommandText(t, text))
45
+ ?? t(node.outcome.kind === 'error' ? 'capabilities.commandFailed' : 'capabilities.refreshAcknowledged')
46
+ : t('capabilities.refreshing')
47
+ return h('p', { className: 'ptcPlusCapabilityCommandNotice',
48
+ role: node.outcome?.kind === 'error' ? 'alert' : 'status',
49
+ 'data-state': node.outcome?.kind ?? 'pending' }, outcome)
50
+ }
51
+ let json
52
+ if (typeof text === 'string') {
53
+ try { json = JSON.parse(text) } catch { /* Plain command outcomes remain readable text. */ }
54
+ }
55
+ const entries = json?.schema === 'ptc-capability-catalog/v1' && Array.isArray(json.entries)
56
+ && json.entries.every(entry => entry && typeof entry.id === 'string'
57
+ && ['name', 'description', 'effective', 'saved', 'temporary']
58
+ .every(key => entry[key] === undefined || typeof entry[key] === 'string')
59
+ && ['savedRule', 'temporaryRule'].every(key => entry[key] === undefined || validCatalogRule(entry[key], key === 'temporaryRule'))) ? json.entries : null
60
+ const outcome = entries ? t('capabilities.card.catalogCount', { count: entries.length })
61
+ : json ? t('capabilities.card.details')
62
+ : node.outcome ? capabilityCommandText(t, text)
63
+ ?? t(node.outcome.kind === 'error' ? 'capabilities.commandFailed' : 'capabilities.refreshAcknowledged')
64
+ : t('capabilities.refreshing')
65
+ const label = (entry, key) => {
66
+ const ruleKey = key === 'saved' ? 'savedRule' : key === 'temporary' ? 'temporaryRule' : 'effectiveDimensions'
67
+ const rule = entry[ruleKey]
68
+ if (rule !== undefined && (key !== 'effective' || validCatalogRule({ availability: rule?.availability, disclosure: rule?.disclosure }))) {
69
+ const dimensions = capabilityRuleLabel(t, rule, key === 'temporary' ? 'capabilities.quick.noOverride' : 'capabilities.policy.inheritRule')
70
+ return key === 'effective' ? `${capabilityChoiceLabel(t, entry.effective)} (${dimensions})` : dimensions
71
+ }
72
+ return capabilityChoiceLabel(t, entry[key])
73
+ }
74
+ return h('section', { className: 'ptcPlusCapabilityReceipt', 'aria-label': t('capabilities.receipt.title') },
75
+ h('div', { className: 'ptcPlusCapabilityReceiptHead' },
76
+ h('code', null, command),
77
+ h('span', { className: 'ptcPlusCapabilityReceiptState', 'data-state': node.outcome?.kind ?? 'pending' },
78
+ t(!node.outcome ? 'capabilities.card.running' : node.outcome.kind === 'error'
79
+ ? 'capabilities.card.failed' : 'capabilities.card.completed'))),
80
+ h('p', { className: 'ptcPlusCapabilityReceiptSummary', role: 'status' }, outcome),
81
+ entries?.length ? h('details', { className: 'ptcPlusCapabilityReceiptCatalog' },
82
+ h('summary', null, t('capabilities.card.browse')),
83
+ h('div', { className: 'ptcPlusCapabilityReceiptTable' }, h('table', null,
84
+ h('thead', null, h('tr', null, ...['member', 'quick.effective', 'quick.globalSaved', 'quick.temporary']
85
+ .map(key => h('th', { key, scope: 'col' }, t(`capabilities.${key}`))))),
86
+ h('tbody', null, ...entries.map((entry, index) => h('tr', { key: `${entry.id}-${index}` },
87
+ h('td', null, h('strong', null, entry.name ?? entry.id),
88
+ entry.description ? h('small', null, entry.description) : null),
89
+ ...['effective', 'saved', 'temporary'].map(key => h('td', { key }, label(entry, key))))))))) : null,
90
+ text ? h('details', { className: 'ptcPlusCapabilityReceiptSource',
91
+ onToggle: event => setShowSource(event.currentTarget.open) },
92
+ h('summary', null, t('capabilities.card.source')),
93
+ showSource ? h('pre', null, text) : null) : null)
94
+ }
95
+ }
96
+
97
+ const selections = ['automatic', 'full', 'on-demand', 'closed']
98
+ const kinds = ['all', 'tool', 'skill', 'resource', 'other']
99
+ const compare = (a, b) => a < b ? -1 : a > b ? 1 : 0
100
+ const configurationId = () => Array.from(globalThis.crypto.getRandomValues(new Uint8Array(16)), byte => byte.toString(16).padStart(2, '0')).join('')
101
+
102
+ /** Catalog facts and saved preferences stay separate from frozen session results. */
103
+ export function capabilitySettingsEntries(value, catalog, plans, uncertain = false) {
104
+ const baseline = JSON.parse(value.capabilityBaseline ?? CONFIG_DEFAULTS.capabilityBaseline)
105
+ const entries = new Map((catalog ?? []).map(entry => [entry.id, { ...entry, available: true }]))
106
+ for (const plan of plans ?? []) for (const entry of plan.effective.entries) {
107
+ if (!entries.has(entry.id)) entries.set(entry.id, { id: entry.id, category: entry.category })
108
+ }
109
+ const policy = capabilityPolicyForConfig(value)
110
+ for (const id of [...baseline, ...Object.keys(policy.items)].filter(id => !id.startsWith('mcpServers.'))) if (!entries.has(id)) entries.set(id, { id })
111
+ return [...entries.values()].map(entry => ({ ...entry,
112
+ name: entry.name ?? entry.id,
113
+ kind: ['tool', 'skill'].includes(entry.category) ? entry.category
114
+ : ['resource', 'resource-template'].includes(entry.category) ? 'resource' : 'other',
115
+ availability: catalog === null || uncertain ? 'unknown' : entry.available ? 'available' : 'unavailable',
116
+ common: baseline.includes(entry.id), rule: policy.items[entry.id] ?? {}, resolvedRule: capabilityRule(policy, entry),
117
+ }))
118
+ }
119
+
120
+ export function capabilityPlanReason(t, entry) {
121
+ const known = ['manual-override', 'baseline-not-selected', 'baseline', 'coverage-below-threshold', 'statistical-coverage',
122
+ 'mcp-server-disabled', 'contract-budget', 'management-disabled', 'statistics-disabled', 'statistics-settings-invalid', 'statistics-data-invalid',
123
+ 'statistics-unavailable', 'workspace', 'global', 'host-disclosure', 'availability-rule', 'disclosure-rule']
124
+ return [entry.reason, entry.statisticsReason].filter(Boolean)
125
+ .map(reason => known.includes(reason) ? t(`capabilities.reason.${reason}`) : reason).join(' · ')
126
+ }
127
+
128
+ export function createCapabilitySettingsViews(React, { SearchIcon, Select, Modal = createFallbackModal(React) }) {
129
+ const h = React.createElement
130
+ const field = key => CONFIG_FIELDS.find(item => item.key === key)
131
+ const responsibilityGroups = [
132
+ { tab: 'manual', keys: ['rules'] },
133
+ { tab: 'budgets', keys: ['description', 'recovery', 'preview'] },
134
+ { tab: 'statistics', keys: ['statistics'] },
135
+ ]
136
+ const responsibilities = responsibilityGroups.flatMap(group => group.keys)
137
+ const choicesFor = (configuration, plan) => {
138
+ const historical = plan && ['ptc-capability-plan/v1', 'ptc-capability-plan/v3'].includes(plan.schema)
139
+ && configuration && configuration.capabilityPolicy === undefined
140
+ let statistics
141
+ try { statistics = JSON.parse(configuration?.capabilityStatistics ?? 'null') } catch { statistics = null }
142
+ return { historical, contextEnabled: plan ? plan.contextEnabled : configuration?.capabilityContextEnabled,
143
+ enabled: configuration?.capabilityManagementEnabled ?? plan?.enabled,
144
+ statistics,
145
+ recoveryEnabled: configuration?.capabilityRecoveryEnabled ?? (historical ? plan.enabled : undefined),
146
+ previewEnabled: configuration?.modelPreviewEnabled ?? (historical ? plan.enabled : undefined) }
147
+ }
148
+ const flagsFor = settings => ({ rules: settings.enabled, description: settings.enabled, statistics: settings.statistics?.enabled,
149
+ recovery: settings.recoveryEnabled, preview: settings.previewEnabled })
150
+ const choiceState = (retained, applied, key) => typeof retained[key] !== 'boolean' ? 'unknown'
151
+ : retained[key] ? typeof applied[key] !== 'boolean' ? 'unknown' : applied[key] === false ? 'paused' : 'selected'
152
+ : key === 'rules' || key === 'description' ? 'host' : 'unselected'
153
+
154
+ function CombinationSummary({ value, configurationReady, plans, plansError, plansPending, plansStale, selectedSessionId,
155
+ feedback, onNavigate, onReload, renderRefresh, renderPlans, t, id }) {
156
+ const saved = choicesFor(configurationReady ? value : undefined)
157
+ const retained = flagsFor(saved)
158
+ const applied = flagsFor(effectiveCapabilitySettings(saved))
159
+ const selected = responsibilities.filter(key => retained[key] === true)
160
+ const effect = !configurationReady ? t('capabilities.combination.unknownSettings')
161
+ : saved.contextEnabled === false ? t('capabilities.combination.pausedEffect')
162
+ : selected.length === 0 ? t('capabilities.combination.hostEffect')
163
+ : selected.length === 2 && selected.includes('rules') && selected.includes('description') ? t('capabilities.combination.rulesOnly')
164
+ : t('capabilities.combination.selectedEffect', { features: selected.map(key => t(`capabilities.combination.${key}`)).join(t('capabilities.combination.separator')) })
165
+ const plan = selectedSessionId ? plans?.find(item => item.sessionId === selectedSessionId) : undefined
166
+ const frozen = plan && choicesFor(plan.configuration, plan.effective)
167
+ const frozenRetained = frozen && flagsFor(frozen)
168
+ const frozenApplied = frozen && flagsFor(effectiveCapabilitySettings(frozen))
169
+ if (frozenApplied) {
170
+ frozenApplied.rules = plan.effective.enabled
171
+ frozenApplied.description = plan.effective.enabled
172
+ if (frozen.historical && plan.effective.enabled === false) frozenApplied.statistics = false
173
+ }
174
+ const publication = ['accepted', 'pending', 'not-required'].includes(plan?.publication) ? plan.publication : 'unknown'
175
+ const stale = !configurationReady || plansStale || Boolean(plansError) || plansPending
176
+ return h('section', { className: 'ptcPlusCombination', 'aria-label': t('capabilities.combination.saved') },
177
+ h('div', { className: 'ptcPlusTaskHead' }, h('h4', null, t('capabilities.combination.saved')),
178
+ h('span', { className: 'ptcPlusCapabilitySaveState', role: 'status' }, feedback)),
179
+ h('div', { className: 'ptcPlusCombinationChoices' }, ...responsibilityGroups.map(group =>
180
+ h('div', { key: group.tab, className: 'ptcPlusCombinationGroup', role: 'group',
181
+ 'aria-label': t(`capabilities.tab.${group.tab}`) },
182
+ h('strong', { className: 'ptcPlusCombinationGroupTitle' }, t(`capabilities.tab.${group.tab}`)),
183
+ h('div', { className: 'ptcPlusCombinationGroupChoices' }, ...group.keys.map(key => {
184
+ const state = choiceState(retained, applied, key)
185
+ return h('button', { key, type: 'button', className: 'ptcPlusCombinationChoice', 'data-state': state,
186
+ 'aria-controls': key === 'rules' || key === 'statistics' ? `${id}-pane-${group.tab}` : `${id}-context-pane-${key}`,
187
+ onClick: () => onNavigate(key) }, h('span', null, t(`capabilities.combination.${key}`)),
188
+ h('small', null, t(`capabilities.combination.state.${state}`)))
189
+ }))))),
190
+ h('p', { className: 'ptcPlusDetail', role: 'status' }, effect),
191
+ h('details', { className: 'ptcPlusCapabilitySessionTools' }, h('summary', { tabIndex: 0 }, t('capabilities.existingSessions')),
192
+ renderRefresh?.(),
193
+ h('button', { type: 'button', className: 'ptcPlusCapabilityLink', onClick: onReload }, t('capabilities.reloadObservation')),
194
+ h('div', { className: 'ptcPlusCombinationSession', 'aria-label': t('capabilities.combination.observed'), 'data-stale': stale },
195
+ !selectedSessionId ? h('p', { className: 'ptcPlusDetail' }, t('capabilities.combination.chooseTarget'))
196
+ : h(React.Fragment, null,
197
+ h('p', { className: 'ptcPlusDetail' }, t('capabilities.combination.target', { session: selectedSessionId })),
198
+ plansPending ? h('p', { role: 'status' }, t('state.syncing')) : null,
199
+ plansError ? h('p', { role: 'status' }, t('capabilities.combination.readFailed', { error: plansError })) : null,
200
+ plan ? h(React.Fragment, null,
201
+ h('p', { role: 'status', className: 'ptcPlusDetail' }, t('capabilities.combination.lastRead'), ' · ', t(`capabilities.combination.publication.${publication}`)),
202
+ frozen.contextEnabled === undefined ? h('p', { className: 'ptcPlusDetail' }, t('capabilities.combination.historical')) : null,
203
+ h('div', { className: 'ptcPlusCombinationFacts' }, ...responsibilities.map(key => h('span', { key },
204
+ t(`capabilities.combination.${key}`), ' · ', t(`capabilities.combination.state.${choiceState(frozenRetained, frozenApplied, key)}`)))),
205
+ h('p', { role: 'status', className: 'ptcPlusDetail' }, t(stale ? 'capabilities.combination.stale'
206
+ : typeof plan.pendingRefresh !== 'boolean' ? 'capabilities.combination.differenceUnknown'
207
+ : plan.pendingRefresh ? 'capabilities.combination.different' : 'capabilities.combination.same')),
208
+ Object.keys(plan.sessionOverrides ?? {}).length ? h('p', { className: 'ptcPlusDetail' }, t('capabilities.combination.overrides')) : null,
209
+ h('details', null, h('summary', { tabIndex: 0 }, t('capabilities.combination.inspectPlan')), renderPlans?.()))
210
+ : !plansPending ? h('p', { role: 'status', className: 'ptcPlusDetail' }, t('capabilities.combination.noPlan')) : null)))
211
+ )
212
+ }
213
+
214
+ function PolicyRules({ value, catalog, disabled, onChange, t }) {
215
+ const policy = capabilityPolicyForConfig(value)
216
+ const write = next => onChange(field('capabilityPolicy'), JSON.stringify(next))
217
+ const ruleControls = (rule, update, label, defaults = false) => h('div', { className: 'ptcPlusPolicyRule', key: label },
218
+ h('strong', null, label), ...['availability', 'disclosure'].map(dimension => h('div', { className: 'ptcPlusCapabilityFilter', key: dimension },
219
+ h('span', null, t(`capabilities.policy.${dimension}`)), h(Select, {
220
+ label: `${label} · ${t(`capabilities.policy.${dimension}`)}`, disabled: disabled('capabilityPolicy'),
221
+ value: rule[dimension] ?? 'automatic', onChange: choice => {
222
+ const next = { ...rule }; if (choice === 'automatic') delete next[dimension]; else next[dimension] = choice; update(next)
223
+ }, options: [...(defaults ? [] : ['automatic']), ...(dimension === 'availability' ? ['enabled', 'closed'] : CAPABILITY_DISCLOSURES)]
224
+ .map(choice => ({ value: choice, label: t(choice === 'automatic' ? 'capabilities.policy.inheritRule' : `capabilities.${choice === 'enabled' ? 'quick.enabled' : choice}`) })) }))))
225
+ return h('section', { className: 'ptcPlusPolicyRules', 'aria-label': t('capabilities.policy.rules') },
226
+ h('h4', null, t('capabilities.inheritedRules')), h('p', { className: 'ptcPlusDetail' }, t('capabilities.inheritedRulesHelp')),
227
+ h('p', { className: 'ptcPlusDetail' }, t('capabilities.policy.precedence')),
228
+ ruleControls(policy.defaults, defaults => write({ ...policy, defaults }), t('capabilities.policy.default'), true),
229
+ h('details', null, h('summary', { tabIndex: 0 }, t('capabilities.policy.categories')),
230
+ ...Object.values(CAPABILITY_INVENTORY_CATEGORIES).map(category => ruleControls(policy.categories[category] ?? {}, rule =>
231
+ write({ ...policy, categories: { ...policy.categories, [category]: rule } }), t(`capabilities.policy.category.${category}`)))),
232
+ h('details', null, h('summary', { tabIndex: 0 }, t('capabilities.sourceRules')),
233
+ ...[...new Set([...Object.keys(policy.mcpServers), ...(catalog ?? []).map(item => item.mcpServer).filter(Boolean)])].sort(compare)
234
+ .map(server => ruleControls(policy.mcpServers[server] ?? {}, rule =>
235
+ write(changeCapabilityRule(changeCapabilityRule(policy, `mcpServers.${server}`, 'availability', rule.availability ?? 'automatic'),
236
+ `mcpServers.${server}`, 'disclosure', rule.disclosure ?? 'automatic')), server))))
237
+
238
+ }
239
+
240
+ /** Capability-group edits open their own window; the group never expands controls into the settings page. */
241
+ function CapabilityDialog({ title, onClose, t, className, children, footer }) {
242
+ return h(Modal, { open: true, onClose, title, closeLabel: t('capabilities.closeDialog'), headless: true,
243
+ className: `ptcPlusCapabilityDialog ${className ?? ''}`.trim() },
244
+ h('div', { className: 'ptcPlusCapabilityDialogHead' }, h('h2', null, title),
245
+ h('button', { type: 'button', className: 'ptcPlusDialogClose', 'aria-label': t('capabilities.closeDialog'),
246
+ onClick: onClose }, '\u00d7')),
247
+ h('div', { className: 'ptcPlusCapabilityDialogBody' }, children),
248
+ footer ? h('div', { className: 'ptcPlusCapabilityDialogFooter' }, footer) : null)
249
+ }
250
+
251
+ function CapabilityBatchDialog({ operation, value, entries, disabled, onChange, onClose, t }) {
252
+ const [choices, setChoices] = React.useState({ availability: 'keep', disclosure: 'keep' })
253
+ const [pending, setPending] = React.useState(false)
254
+ const [failed, setFailed] = React.useState(false)
255
+ const saving = React.useRef(false)
256
+ const mounted = React.useRef(true)
257
+ React.useEffect(() => { mounted.current = true; return () => { mounted.current = false } }, [])
258
+ const targetIds = new Set(entries.filter(entry => entry.availability === 'available').map(entry => entry.id))
259
+ const missing = operation.targets.filter(entry => !targetIds.has(entry.id)).length
260
+ const blocked = disabled('capabilityPolicy') || pending
261
+ const changed = Object.values(choices).some(choice => choice !== 'keep')
262
+ const close = () => { if (!saving.current) onClose() }
263
+ const confirm = async () => {
264
+ if (saving.current || blocked || missing || !changed || !operation.targets.length) return
265
+ let next = capabilityPolicyForConfig(value)
266
+ for (const target of operation.targets) for (const dimension of ['availability', 'disclosure']) {
267
+ if (choices[dimension] !== 'keep') next = changeCapabilityRule(next, target.id, dimension, choices[dimension])
268
+ }
269
+ saving.current = true; setPending(true); setFailed(false)
270
+ try {
271
+ const saved = await onChange(field('capabilityPolicy'), JSON.stringify(next))
272
+ if (!mounted.current) return
273
+ if (saved === true) onClose()
274
+ else setFailed(true)
275
+ } catch {
276
+ if (mounted.current) setFailed(true)
277
+ } finally {
278
+ saving.current = false
279
+ if (mounted.current) setPending(false)
280
+ }
281
+ }
282
+ return h(Modal, { open: true, onClose: close, title: t('capabilities.batchSettings'),
283
+ closeLabel: t('capabilities.batch.cancel'), headless: true, className: 'ptcPlusCapabilityBatchModal' },
284
+ h('div', { className: 'ptcPlusCapabilityBatchHead' }, h('h2', null, t('capabilities.batchSettings')),
285
+ h('button', { type: 'button', className: 'ptcPlusDialogClose', disabled: pending,
286
+ 'aria-label': t('capabilities.batch.cancel'), onClick: close }, '×')),
287
+ h('div', { className: 'ptcPlusCapabilityBatchBody' },
288
+ h('p', { className: 'ptcPlusDetail', role: 'status' }, t('capabilities.batch.targets', { count: operation.targets.length })),
289
+ h('p', { className: 'ptcPlusDetail' }, t('capabilities.batch.scope', { scope: operation.scope })),
290
+ operation.excluded ? h('p', { className: 'ptcPlusDetail' }, t('capabilities.batch.excluded', { count: operation.excluded })) : null,
291
+ operation.targets.length ? h('ul', { className: 'ptcPlusCapabilityBatchTargets', 'aria-label': t('capabilities.batch.targetList') },
292
+ ...operation.targets.map(target => h('li', { key: target.id }, h('span', null, target.name), h('code', null, target.id))))
293
+ : h('p', { className: 'ptcPlusDetail' }, t('capabilities.batch.empty')),
294
+ h('div', { className: 'ptcPlusCapabilityBatchFields' }, ...['availability', 'disclosure'].map(dimension =>
295
+ h('div', { key: dimension, className: 'ptcPlusCapabilityFilter' }, h('span', null, t(`capabilities.policy.${dimension}`)),
296
+ h(Select, { value: choices[dimension], disabled: blocked || !operation.targets.length,
297
+ label: t(`capabilities.batch.${dimension}`), onChange: choice => setChoices(current => ({ ...current, [dimension]: choice })),
298
+ options: ['keep', 'automatic', ...(dimension === 'availability' ? ['enabled', 'closed'] : CAPABILITY_DISCLOSURES)]
299
+ .map(choice => ({ value: choice, label: t(choice === 'keep' ? 'capabilities.batch.keep'
300
+ : choice === 'automatic' ? 'capabilities.policy.inheritRule'
301
+ : choice === 'enabled' ? 'capabilities.quick.enabled' : `capabilities.${choice}`) })) })))),
302
+ h('p', { className: 'ptcPlusDetail' }, t('capabilities.batch.effect')),
303
+ h('p', { className: 'ptcPlusDetail' }, t('capabilities.batch.sourceClosure')),
304
+ missing ? h('p', { className: 'ptcPlusMessage', role: 'alert' }, t('capabilities.batch.changedTargets', { count: missing })) : null,
305
+ failed ? h('p', { className: 'ptcPlusMessage', role: 'alert' }, t('capabilities.batch.failed')) : null),
306
+ h('div', { className: 'ptcPlusCapabilityBatchFooter' },
307
+ h('button', { type: 'button', className: 'ptcPlusButton', disabled: pending, onClick: close }, t('capabilities.batch.cancel')),
308
+ h('button', { type: 'button', className: 'ptcPlusButton', 'data-kind': 'primary',
309
+ disabled: blocked || Boolean(missing) || !changed || !operation.targets.length, onClick: confirm },
310
+ t(pending ? 'state.syncing' : 'capabilities.batch.confirm', { count: operation.targets.length }))))
311
+ }
312
+
313
+ function CapabilityChoices({ value, catalog, catalogError, plans, plansError, disabled, onChange, onReload, t, selectedSessionId, onSessionSelect }) {
314
+ const root = React.useRef(null)
315
+ const [query, setQuery] = React.useState('')
316
+ const [kind, setKind] = React.useState('all')
317
+ const [source, setSource] = React.useState('all')
318
+ const [strategy, setStrategy] = React.useState('all')
319
+ const [availabilityRule, setAvailabilityRule] = React.useState('all')
320
+ const [onlyCommon, setOnlyCommon] = React.useState(false)
321
+ const [availability, setAvailability] = React.useState('all')
322
+ const [sort, setSort] = React.useState(null)
323
+ const [selected, setSelected] = React.useState(null)
324
+ const [filtersOpen, setFiltersOpen] = React.useState(false)
325
+ const [batchOperation, setBatchOperation] = React.useState(null)
326
+ const [inheritedRulesOpen, setInheritedRulesOpen] = React.useState(false)
327
+ const [localSessionId, setLocalSessionId] = React.useState('')
328
+ const sessionId = onSessionSelect ? selectedSessionId : localSessionId
329
+ const setSessionId = next => { setLocalSessionId(next); onSessionSelect?.(next) }
330
+ const baseline = JSON.parse(value.capabilityBaseline ?? CONFIG_DEFAULTS.capabilityBaseline)
331
+ const policy = capabilityPolicyForConfig(value)
332
+ const entries = capabilitySettingsEntries(value, catalog, plans, Boolean(catalogError))
333
+ const sourceKey = entry => JSON.stringify([entry.source?.kind ?? 'unknown', entry.source?.server ?? null,
334
+ entry.source?.bucket ?? null, entry.source?.provider ?? null])
335
+ const sourceLabel = entry => {
336
+ if (entry.source?.kind === 'skill') {
337
+ const bucket = entry.source.bucket
338
+ const known = ['project-dsh', 'project-agents', 'user-dsh', 'user-agents', 'runtime', 'bundled', 'custom']
339
+ const label = known.includes(bucket) ? t(`capabilities.source.${bucket}`)
340
+ : bucket ? `${t('capabilities.source.other-skill')} · ${bucket}` : t('capabilities.source.unknown')
341
+ return `${label}${entry.source.provider ? ` · ${entry.source.provider}` : ''}`
342
+ }
343
+ if (entry.source?.kind === 'mcp') return `MCP${entry.source.server ? ` · ${entry.source.server}` : ''}`
344
+ return t(`capabilities.source.${entry.source?.kind === 'host' ? 'host' : 'unknown'}`)
345
+ }
346
+ const matchesKind = (entry, type) => type === 'all' || (type === 'resource' ? entry.source?.kind === 'mcp'
347
+ : type === 'tool' ? entry.kind === 'tool' && entry.source?.kind !== 'mcp' : type === entry.kind)
348
+ const sources = new Map(entries.filter(entry => matchesKind(entry, kind)).map(entry => [sourceKey(entry), sourceLabel(entry)]))
349
+ const setType = next => { setKind(next); if (!entries.some(entry => matchesKind(entry, next) && sourceKey(entry) === source)) setSource('all') }
350
+ const setCompatibleSource = next => {
351
+ setSource(next)
352
+ if (next !== 'all' && !entries.some(entry => matchesKind(entry, kind) && sourceKey(entry) === next)) setKind('all')
353
+ }
354
+ const normalizedQuery = query.trim().toLowerCase()
355
+ const filtered = entries.filter(entry => matchesKind(entry, kind)
356
+ && (source === 'all' || sourceKey(entry) === source)
357
+ && (strategy === 'all' || (entry.rule.disclosure ?? 'automatic') === strategy)
358
+ && (availabilityRule === 'all' || (entry.rule?.availability ?? 'automatic') === availabilityRule)
359
+ && (!onlyCommon || entry.common)
360
+ && (availability === 'all' || entry.availability === availability)
361
+ && `${entry.name} ${entry.id} ${entry.description ?? ''} ${sourceLabel(entry)}`.toLowerCase().includes(normalizedQuery))
362
+ const defaultOrder = (a, b) => compare(a.name.toLowerCase(), b.name.toLowerCase()) || compare(a.id, b.id)
363
+ const state = item => item.availability === 'unknown' ? 'unknown' : item.availability === 'unavailable' ? 'unavailable'
364
+ : value.capabilityContextEnabled === false || value.capabilityManagementEnabled !== true ? 'unapplied'
365
+ : item.resolvedRule.availabilitySource.startsWith('mcp:') && item.resolvedRule.availability === 'closed' ? 'source-closed' : item.resolvedRule.availability
366
+ const type = item => item.source?.kind === 'mcp' ? 'resource' : item.kind
367
+ const displayedSource = item => item.source?.kind === 'mcp' ? item.source.server ?? sourceLabel(item) : sourceLabel(item)
368
+ const groups = new Map()
369
+ for (const entry of filtered) {
370
+ const group = entry.source?.kind === 'mcp' && entry.source.server ? `mcp:${entry.source.server}`
371
+ : entry.availability === 'unavailable' ? 'unavailable' : entry.kind
372
+ if (!groups.has(group)) groups.set(group, [])
373
+ groups.get(group).push(entry)
374
+ }
375
+ const groupOrder = ['tool', ...[...groups.keys()].filter(key => key.startsWith('mcp:')).sort(compare), 'skill', 'resource', 'other', 'unavailable']
376
+ const defaultEntries = groupOrder.flatMap(group => (groups.get(group) ?? []).sort(defaultOrder))
377
+ const visible = sortCapabilityEntries(defaultEntries, sort, (item, column) => column === 'name' ? item.name
378
+ : column === 'type' ? t(`capabilities.type.${type(item)}`)
379
+ : column === 'source' ? displayedSource(item)
380
+ : column === 'availability' ? t(`capabilities.rowState.${state(item)}`)
381
+ : t(`capabilities.${item.resolvedRule.disclosure}`))
382
+ const entry = visible.find(item => item.id === selected)
383
+ const plan = sessionId ? plans?.find(item => item.sessionId === sessionId) : undefined
384
+ const effective = plan?.effective.entries.find(item => item.id === entry?.id)
385
+ const change = (key, next) => onChange(field(key), JSON.stringify(next))
386
+ const setManual = (id, selection) => change('capabilityPolicy', changeCapabilityRule(policy, id, 'disclosure', selection))
387
+ const clear = () => { setQuery(''); setKind('all'); setSource('all'); setStrategy('all'); setAvailabilityRule('all'); setOnlyCommon(false); setAvailability('all') }
388
+ const activeFilters = Number(kind !== 'all') + Number(source !== 'all') + Number(strategy !== 'all') + Number(availabilityRule !== 'all') + Number(onlyCommon) + Number(availability !== 'all')
389
+ const select = (label, current, update, options, blocked = false) => h('div', { className: 'ptcPlusCapabilityFilter' },
390
+ h('span', null, label), h(Select, { label, value: current, disabled: blocked, onChange: update,
391
+ options: options.map(([key, text]) => ({ value: key, label: text })) }))
392
+ const editorField = (label, control) => h('div', { className: 'ptcPlusCapabilityField' }, h('span', null, label), control)
393
+ const manualSelect = item => h('div', { className: 'ptcPlusCapabilityEditorFields' },
394
+ editorField(t('capabilities.enabledColumn'), h(Select, { value: item.rule.availability ?? 'automatic', disabled: disabled('capabilityPolicy'),
395
+ label: t('capabilities.policy.availabilityFor', { name: item.id }),
396
+ onChange: next => change('capabilityPolicy', changeCapabilityRule(policy, item.id, 'availability', next)),
397
+ options: ['automatic', 'enabled', 'closed'].map(choice => ({ value: choice, label: t(choice === 'automatic' ? 'capabilities.policy.inheritRule' : choice === 'enabled' ? 'capabilities.quick.enabled' : `capabilities.${choice}`) })) })),
398
+ editorField(t('capabilities.contextColumn'), h(Select, { value: item.rule.disclosure ?? 'automatic', disabled: disabled('capabilityPolicy'),
399
+ label: t('capabilities.disclosureFor', { name: item.id }),
400
+ onChange: next => setManual(item.id, next), options: ['automatic', ...CAPABILITY_DISCLOSURES]
401
+ .map(choice => ({ value: choice, label: t(choice === 'automatic' ? 'capabilities.policy.inheritRule' : `capabilities.${choice}`) })) })))
402
+ const commonSwitch = item => h('div', { className: 'ptcPlusCapabilitySwitchRow' },
403
+ h('div', { className: 'ptcPlusCapabilitySwitchText' },
404
+ h('span', { className: 'ptcPlusCapabilitySwitchLabel' }, t('capabilities.commonBaseline')),
405
+ h('span', { className: 'ptcPlusCapabilitySwitchHint' }, t(item.common ? 'capabilities.inBaseline' : 'capabilities.notInBaseline'))),
406
+ h('input', { type: 'checkbox', role: 'switch', className: 'ptcPlusCheck', checked: item.common,
407
+ disabled: disabled('capabilityBaseline'), 'aria-label': t('capabilities.baselineFor', { name: item.id }),
408
+ onChange: event => change('capabilityBaseline', event.target.checked
409
+ ? [...baseline.filter(id => id !== item.id), item.id] : baseline.filter(id => id !== item.id)) }))
410
+ const filterLabels = [kind !== 'all' ? t(`capabilities.type.${kind}`) : null,
411
+ source !== 'all' ? sources.get(source) ?? t('capabilities.previousSource') : null,
412
+ strategy !== 'all' ? t(strategy === 'automatic' ? 'capabilities.policy.inheritRule' : `capabilities.${strategy}`) : null,
413
+ availabilityRule !== 'all' ? t(availabilityRule === 'automatic' ? 'capabilities.policy.inheritRule'
414
+ : availabilityRule === 'enabled' ? 'capabilities.quick.enabled' : 'capabilities.closed') : null,
415
+ onlyCommon ? t('capabilities.onlyCommon') : null,
416
+ availability !== 'all' ? t(`capabilities.availability.${availability}`) : null].filter(Boolean)
417
+ const closeEditor = () => {
418
+ const button = [...(root.current?.querySelectorAll('[data-capability-edit-id]') ?? [])].find(item => item.dataset.capabilityEditId === selected)
419
+ setSelected(null); button?.focus({ preventScroll: true })
420
+ }
421
+ return h('div', { className: 'ptcPlusCapabilityChoices', ref: root },
422
+ h('div', { className: 'ptcPlusCapabilityToolbar' },
423
+ h('label', { className: 'ptcPlusSearch' }, h(SearchIcon),
424
+ h('input', { type: 'search', value: query, 'aria-label': t('capabilities.search'), placeholder: t('capabilities.search'),
425
+ onChange: event => setQuery(event.target.value) })),
426
+ h('div', { className: 'ptcPlusCapabilityTypes', role: 'group', 'aria-label': t('capabilities.types') },
427
+ ...kinds.filter(key => key !== 'other' || entries.some(item => item.kind === 'other')).map(key => h('button', {
428
+ key, type: 'button', 'aria-pressed': kind === key, onClick: () => setType(key),
429
+ }, t(`capabilities.type.${key}`)))),
430
+ h('button', { type: 'button', className: 'ptcPlusButton', 'aria-haspopup': 'dialog',
431
+ onClick: () => setFiltersOpen(true) }, t('capabilities.filterAction')),
432
+ h('button', { type: 'button', className: 'ptcPlusButton', 'aria-haspopup': 'dialog',
433
+ disabled: disabled('capabilityPolicy'), onClick: () => setBatchOperation({
434
+ targets: visible.filter(item => item.availability === 'available').map(item => ({ id: item.id, name: item.name })),
435
+ excluded: visible.filter(item => item.availability !== 'available').length,
436
+ scope: [...filterLabels, ...(query.trim() ? [t('capabilities.batch.search', { query: query.trim() })] : [])].join(' · ') || t('capabilities.batch.all'),
437
+ }) }, t('capabilities.batchSettings')),
438
+ h('button', { type: 'button', className: 'ptcPlusButton', 'aria-haspopup': 'dialog',
439
+ onClick: () => setInheritedRulesOpen(true) }, t('capabilities.inheritedRules'))),
440
+ filtersOpen ? h(CapabilityDialog, { title: t('capabilities.filterAction'), onClose: () => setFiltersOpen(false), t,
441
+ className: 'ptcPlusCapabilityFilterDialog', footer: h('button', { type: 'button', className: 'ptcPlusButton',
442
+ 'data-kind': 'primary', onClick: () => setFiltersOpen(false) }, t('capabilities.dialogDone')) },
443
+ h('div', { className: 'ptcPlusCapabilityFilterFields' },
444
+ select(t('capabilities.sourceFilter'), source, setCompatibleSource, [['all', t('capabilities.allSources')], ...sources,
445
+ ...(source !== 'all' && !sources.has(source) ? [[source, t('capabilities.previousSource')]] : [])]),
446
+ select(t('capabilities.policy.disclosureFilter'), strategy, setStrategy, [['all', t('capabilities.allStrategies')],
447
+ ...['automatic', ...CAPABILITY_DISCLOSURES].map(key => [key, t(key === 'automatic' ? 'capabilities.policy.inheritRule' : `capabilities.${key}`)])]),
448
+ select(t('capabilities.policy.availabilityFilter'), availabilityRule, setAvailabilityRule, [['all', t('capabilities.allStrategies')],
449
+ ...['automatic', 'enabled', 'closed'].map(key => [key, t(key === 'automatic' ? 'capabilities.policy.inheritRule' : key === 'enabled' ? 'capabilities.quick.enabled' : 'capabilities.closed')])]),
450
+ h('label', { className: 'ptcPlusCapabilityOnlyCommon' }, h('input', { type: 'checkbox', checked: onlyCommon,
451
+ onChange: event => setOnlyCommon(event.target.checked) }), t('capabilities.onlyCommon')),
452
+ select(t('capabilities.availabilityFilter'), availability, setAvailability,
453
+ ['all', 'available', 'unavailable', 'unknown'].map(key => [key, t(`capabilities.availability.${key}`)])))) : null,
454
+ inheritedRulesOpen ? h(CapabilityDialog, { title: t('capabilities.inheritedRules'), onClose: () => setInheritedRulesOpen(false), t,
455
+ footer: h('button', { type: 'button', className: 'ptcPlusButton', 'data-kind': 'primary',
456
+ onClick: () => setInheritedRulesOpen(false) }, t('capabilities.dialogDone')) },
457
+ h(PolicyRules, { value, catalog, disabled, onChange, t })) : null,
458
+ batchOperation ? h(CapabilityBatchDialog, { operation: batchOperation, value, entries, disabled, onChange,
459
+ onClose: () => setBatchOperation(null), t }) : null,
460
+ activeFilters ? h('div', { className: 'ptcPlusActiveFilters' }, h('span', null, filterLabels.join(' · ')),
461
+ h('button', { type: 'button', className: 'ptcPlusCapabilityLink', onClick: clear }, t('capabilities.clear'))) : null,
462
+ h('div', { className: 'ptcPlusCapabilityResultLine' }, h('span', { role: 'status' }, t('capabilities.results', { count: visible.length, total: entries.length })),
463
+ h('button', { type: 'button', className: 'ptcPlusCapabilityLink', onClick: onReload }, t('capabilities.reloadCatalog'))),
464
+ catalogError ? h('p', { role: 'status', className: 'ptcPlusMessage' }, t('capabilities.catalogUnavailable', { error: catalogError })) : null,
465
+ entries.length === 0 ? h('p', { className: 'ptcPlusDetail' }, t('capabilities.catalogEmpty'))
466
+ : visible.length === 0 ? h('div', { className: 'ptcPlusCapabilityEmpty' }, h('p', null, t('capabilities.noMatches')),
467
+ h('button', { type: 'button', className: 'ptcPlusCapabilityLink', onClick: clear }, t('capabilities.clear'))) :
468
+ h('div', { className: 'ptcPlusCapabilityTable', role: 'table', 'aria-label': t('capabilities.management') },
469
+ h('div', { className: 'ptcPlusCapabilityColumns', role: 'row' },
470
+ ...CAPABILITY_SORT_COLUMNS.map(column => h('span', { key: column.key, role: 'columnheader',
471
+ 'aria-sort': capabilitySortDirection(sort, column.key) },
472
+ h('button', { type: 'button', className: 'ptcPlusCapabilitySort', title: t('capabilities.sortCycle'),
473
+ onClick: () => setSort(current => cycleCapabilitySort(current, column.key)) },
474
+ t(`capabilities.${column.label}`), h('span', { 'aria-hidden': true },
475
+ capabilitySortDirection(sort, column.key) === 'ascending' ? '↑' : capabilitySortDirection(sort, column.key) === 'descending' ? '↓' : '↕'))))),
476
+ h('div', { className: 'ptcPlusCapabilityList', role: 'rowgroup' }, ...visible.map(item => h('div', { key: item.id, className: 'ptcPlusCapabilityItem', role: 'row',
477
+ 'data-selected': entry?.id === item.id, 'data-availability': item.availability,
478
+ onClick: () => setSelected(item.id) },
479
+ h('div', { className: 'ptcPlusCapabilityIdentity', role: 'cell' }, h('span', { className: 'ptcPlusCapabilityName' }, item.name), h('code', null, item.id)),
480
+ h('span', { className: 'ptcPlusCapabilityBadge', role: 'cell', 'data-kind': type(item) }, t(`capabilities.type.${type(item)}`)),
481
+ h('span', { className: 'ptcPlusCapabilitySource', role: 'cell' }, displayedSource(item)),
482
+ // The cell stays a plain cell so its empty area selects the row; only the switch and
483
+ // its own label swallow the click, keeping a toggle from also opening the row editor.
484
+ h('div', { className: 'ptcPlusCapabilityAvailability', role: 'cell', 'data-state': state(item) },
485
+ h('input', { type: 'checkbox', role: 'switch', className: 'ptcPlusCheck', id: `ptcPlusCapabilityAvailability-${item.id}`,
486
+ checked: item.resolvedRule.availability === 'enabled',
487
+ disabled: disabled('capabilityPolicy') || state(item) === 'source-closed',
488
+ 'aria-label': t('capabilities.policy.availabilityFor', { name: item.id }),
489
+ onClick: event => event.stopPropagation(),
490
+ onChange: event => change('capabilityPolicy', changeCapabilityRule(policy, item.id, 'availability', event.target.checked ? 'enabled' : 'closed')) }),
491
+ h('label', { htmlFor: `ptcPlusCapabilityAvailability-${item.id}`,
492
+ onClick: event => event.stopPropagation() }, t(`capabilities.rowState.${state(item)}`))),
493
+ h('div', { role: 'cell', className: 'ptcPlusCapabilityContextCell' }, h('button', { type: 'button', className: 'ptcPlusCapabilityContext', 'data-capability-edit-id': item.id,
494
+ 'aria-label': t('capabilities.editContextFor', { name: item.id }), 'aria-haspopup': 'dialog',
495
+ onClick: event => { event.stopPropagation(); setSelected(item.id) } },
496
+ h('span', null, t(`capabilities.${item.resolvedRule.disclosure}`)),
497
+ h('svg', { width: 16, height: 16, viewBox: '0 0 24 24', fill: 'none', stroke: 'currentColor', strokeWidth: 1.5, 'aria-hidden': true },
498
+ h('path', { d: 'm15 5 4 4M4 20l4-1L20 7l-4-4L4 15Z' })))))))),
499
+ entry ? h(CapabilityDialog, { title: entry.name, onClose: closeEditor, t, className: 'ptcPlusCapabilityEditorDialog',
500
+ footer: h('button', { type: 'button', className: 'ptcPlusButton', onClick: closeEditor }, t('capabilities.closeDetails')) },
501
+ h('code', null, entry.id), h('p', { className: 'ptcPlusDetail' }, t('capabilities.contextHelp')),
502
+ manualSelect(entry), commonSwitch(entry),
503
+ h('p', { className: 'ptcPlusDetail' }, t(value.capabilityContextEnabled === false ? 'capabilities.effect.paused'
504
+ : value.capabilityManagementEnabled !== true ? 'capabilities.effect.rulesOff'
505
+ : entry.resolvedRule.availabilitySource.startsWith('mcp:') && entry.resolvedRule.availability === 'closed' ? 'capabilities.effect.sourceClosed'
506
+ : entry.resolvedRule.availability === 'closed' ? 'capabilities.effect.closed'
507
+ : `capabilities.effect.disclosure.${entry.resolvedRule.disclosure}`)),
508
+ h('p', { className: 'ptcPlusDetail' }, t('capabilities.policy.resolved', {
509
+ availability: t(entry.resolvedRule.availability === 'enabled' ? 'capabilities.quick.enabled' : 'capabilities.closed'),
510
+ disclosure: t(`capabilities.${entry.resolvedRule.disclosure}`),
511
+ source: `${entry.resolvedRule.availabilitySource} · ${entry.resolvedRule.disclosureSource}` })),
512
+ entry.rule.disclosure !== undefined ? h('button', { type: 'button', className: 'ptcPlusCapabilityLink', disabled: disabled('capabilityPolicy'),
513
+ onClick: () => setManual(entry.id, 'automatic') }, t('capabilities.restoreAutomatic')) : null,
514
+ entry.source?.kind === 'mcp' && entry.mcpServer ? h('section', { className: 'ptcPlusCapabilityEditorSection' },
515
+ h('h5', null, t('capabilities.sourceRules')),
516
+ h('p', { className: 'ptcPlusDetail' }, t('capabilities.serverHint')),
517
+ h('div', { className: 'ptcPlusCapabilityEditorFields' },
518
+ ...['availability', 'disclosure'].map(dimension => editorField(t(`capabilities.policy.${dimension}`),
519
+ h(Select, { value: policy.mcpServers[entry.mcpServer]?.[dimension] ?? 'automatic', disabled: disabled('capabilityPolicy'),
520
+ label: `${entry.mcpServer} · ${t(`capabilities.policy.${dimension}`)}`,
521
+ onChange: next => change('capabilityPolicy', changeCapabilityRule(policy, `mcpServers.${entry.mcpServer}`, dimension, next)),
522
+ options: ['automatic', ...(dimension === 'availability' ? ['enabled', 'closed'] : CAPABILITY_DISCLOSURES)].map(key => ({ value: key,
523
+ label: t(key === 'automatic' ? 'capabilities.policy.inheritRule' : key === 'enabled' ? 'capabilities.quick.enabled' : `capabilities.${key}`) })) }))))) : null,
524
+ entry.kind === 'skill' ? h('p', { className: 'ptcPlusDetail' }, t('capabilities.skillDisclosure')) : null,
525
+ entry.kind === 'skill' && entry.invocationConflict ? h('p', { className: 'ptcPlusDetail' }, t('capabilities.invocation.varies')) : null,
526
+ entry.kind === 'skill' && entry.invocation ? h('p', { className: 'ptcPlusDetail' },
527
+ t(entry.invocation.modelInvocable ? entry.invocation.userInvocable ? 'capabilities.invocation.both'
528
+ : 'capabilities.invocation.model' : 'capabilities.invocation.user', { name: entry.name })) : null,
529
+ entry.description ? h('section', { className: 'ptcPlusCapabilityEditorSection' },
530
+ h('h5', null, t('capabilities.catalogDescription')), h('p', { className: 'ptcPlusCapabilityProse' }, entry.description)) : null,
531
+ entry.availability === 'unavailable' ? h('button', { type: 'button', className: 'ptcPlusCapabilityLink',
532
+ disabled: disabled('capabilityBaseline') || disabled('capabilityPolicy'), onClick: () => {
533
+ const next = { ...policy, items: { ...policy.items } }; delete next.items[entry.id]; change('capabilityPolicy', next)
534
+ if (baseline.includes(entry.id)) change('capabilityBaseline', baseline.filter(id => id !== entry.id))
535
+ } }, t('capabilities.removeSaved')) : null,
536
+ value.capabilityContextEnabled !== false && value.capabilityManagementEnabled === true ? h('section', { className: 'ptcPlusCapabilityEditorSection' },
537
+ h('h5', null, t('capabilities.currentSession')),
538
+ editorField(t('capabilities.sessionSelector'), h(Select, { value: sessionId ?? '', label: t('capabilities.sessionSelector'), onChange: setSessionId,
539
+ options: [{ value: '', label: t('capabilities.chooseSession') }, ...(plans ?? []).map(item => ({ value: item.sessionId, label: item.sessionId }))] })),
540
+ plansError ? h('p', { role: 'status', className: 'ptcPlusDetail' }, t('capabilities.unavailable', { error: plansError }))
541
+ : !plan ? h('p', { className: 'ptcPlusDetail' }, t('capabilities.selectObservation')) : h(React.Fragment, null,
542
+ h('p', null, effective ? t(`capabilities.${effective.selection}`) : t('capabilities.notInSession')),
543
+ effective ? h('p', { className: 'ptcPlusDetail' }, capabilityPlanReason(t, effective)) : null,
544
+ effective?.evidence ? h('p', { className: 'ptcPlusDetail' }, `${effective.evidence.used}/${effective.evidence.eligible} (${effective.evidence.scope})`) : null,
545
+ plan.pendingRefresh ? h('p', { className: 'ptcPlusDetail' }, t('capabilities.pending')) : null)) : null) : null)
546
+
547
+ }
548
+
549
+ function AllocationEvidence({ allocation, allocationError, allocationPending, selectedSessionId, statistics = false, onReload, t }) {
550
+ const [statisticsScope, setStatisticsScope] = React.useState('workspace')
551
+ if (allocationPending) return h('p', { role: 'status' }, t('state.syncing'))
552
+ if (allocationError) return h('p', { role: 'status', className: 'ptcPlusMessage' }, t('capabilities.unavailable', { error: allocationError }))
553
+ if (!allocation) return selectedSessionId ? h('p', { className: 'ptcPlusCapabilityNotice' }, t('capabilities.observationUnavailable')) : null
554
+ const { plan, coverage, configuration } = allocation
555
+ const count = selection => plan.entries.filter(entry => entry.selection === selection).length
556
+ const evidenceFor = item => coverage?.[item.id]?.[statisticsScope]
557
+ const usable = evidence => evidence?.status === 'usable' && Number.isFinite(evidence.used) && Number.isFinite(evidence.eligible)
558
+ const coverageText = evidence => usable(evidence) && Number.isFinite(evidence.coverage) ? `${Math.round(evidence.coverage * 100)}%`
559
+ : usable(evidence) ? t('capabilities.statStatus.no-samples')
560
+ : t(`capabilities.statStatus.${evidence?.status ?? 'unavailable'}`)
561
+ return h('section', { className: 'ptcPlusAllocationEvidence', 'aria-label': t(statistics ? 'capabilities.observedUsage' : 'capabilities.budgetPreview') },
562
+ h('div', { className: 'ptcPlusCapabilityResultLine' }, h('h4', null, t(statistics ? 'capabilities.observedUsage' : 'capabilities.budgetPreview')),
563
+ h('button', { type: 'button', className: 'ptcPlusCapabilityLink', onClick: onReload }, t('capabilities.reloadObservation'))),
564
+ h('p', { className: 'ptcPlusDetail' }, t('capabilities.observationScope', { workspace: allocation.workspace, session: allocation.sessionId })),
565
+ h('p', { className: 'ptcPlusDetail' }, t(statistics ? 'capabilities.familyHint' : 'capabilities.previewHint')),
566
+ statistics ? h(Select, { value: statisticsScope, label: t('capabilities.coverageScope'), onChange: setStatisticsScope,
567
+ options: ['workspace', 'global'].map(key => ({ value: key, label: t(`capabilities.scope.${key}`) })) }) : null,
568
+ !statistics ? h('p', { className: 'ptcPlusCapabilityNotice' }, t('capabilities.coldStartHint')) : null,
569
+ !statistics ? h(React.Fragment, null,
570
+ h('div', { className: 'ptcPlusAllocationMetrics' },
571
+ ...['full', 'on-demand', 'closed'].map(selection => h('div', { key: selection }, h('strong', null, count(selection)), h('span', null, t(`capabilities.${selection}`)))),
572
+ h('div', null, h('strong', null, `${plan.budgets.contractBytes} / ${configuration.capabilityBudgetBytes}`), h('span', null, 'UTF-8 bytes'))),
573
+ h('progress', { max: configuration.capabilityBudgetBytes, value: Math.min(configuration.capabilityBudgetBytes, plan.budgets.contractBytes),
574
+ 'aria-label': t('capabilities.budgetPreview') }),
575
+ h('p', { className: 'ptcPlusDetail' }, t('capabilities.budget', { bytes: plan.budgets.contractBytes, overage: plan.budgets.overageBytes })),
576
+ h('p', { className: 'ptcPlusDetail' }, t('capabilities.directoryUsage', { bytes: plan.budgets.directoryBytes, budget: configuration.capabilityDirectoryBytes })),
577
+ !plan.enabled ? h('p', { className: 'ptcPlusCapabilityNotice' }, t('capabilities.managementOff')) : null)
578
+ : coverage === null ? h('p', { role: 'status' }, t('capabilities.statStatus.unavailable')) : null,
579
+ h('div', { className: 'ptcPlusCapabilityEvidenceTable' }, h('table', null,
580
+ h('thead', null, h('tr', null, ...[t('capabilities.member'), ...(statistics
581
+ ? [t('capabilities.usedFamilies'), t('capabilities.eligibleFamilies'), t('capabilities.coverageColumn')]
582
+ : [t('capabilities.selection'), t('capabilities.reason'), t('capabilities.contractSize')])].map((label, key) => h('th', { key }, label)))),
583
+ h('tbody', null, ...allocation.capabilities.map(item => {
584
+ const entry = plan.entries.find(entry => entry.id === item.id)
585
+ return h('tr', { key: item.id }, h('td', null, item.id), ...(statistics
586
+ ? [h('td', { key: 'used' }, usable(evidenceFor(item)) ? evidenceFor(item).used : '—'),
587
+ h('td', { key: 'eligible' }, usable(evidenceFor(item)) ? evidenceFor(item).eligible : '—'),
588
+ h('td', { key: 'coverage' }, coverageText(evidenceFor(item)), usable(evidenceFor(item)) && Number.isFinite(evidenceFor(item).coverage)
589
+ ? h('progress', { max: 1, value: evidenceFor(item).coverage, 'aria-label': t('capabilities.coverageFor', { name: item.id }) }) : null)]
590
+ : [h('td', { key: 'selection' }, t(`capabilities.${entry?.selection ?? 'host'}`)), h('td', { key: 'reason' }, entry ? capabilityPlanReason(t, entry) : '—'), h('td', { key: 'size' }, `${item.contractBytes} B`)]))
591
+ })))))
592
+ }
593
+
594
+ function ConfigurationProfiles({ value, plans, disabled, writable, onChange, onApplyConfiguration, t }) {
595
+ const library = validateCapabilityProfiles(JSON.parse(value.capabilityProfiles ?? CONFIG_DEFAULTS.capabilityProfiles))
596
+ const [selectedId, setSelectedId] = React.useState('')
597
+ const [action, setAction] = React.useState(null)
598
+ const [name, setName] = React.useState('')
599
+ const [rename, setRename] = React.useState('')
600
+ const [copyName, setCopyName] = React.useState('')
601
+ const [status, setStatus] = React.useState(null)
602
+ const [busy, setBusy] = React.useState(false)
603
+ const [sessionId, setSessionId] = React.useState('')
604
+ const fileInput = React.useRef(null)
605
+ const saveAction = React.useRef(null)
606
+ const returnToLibrary = React.useRef(false)
607
+ const live = React.useRef(null)
608
+ const lifecycle = React.useRef(0)
609
+ const mounted = React.useRef(true)
610
+ const running = React.useRef(null)
611
+ const unavailable = disabled('capabilityProfiles') || busy
612
+ live.current = { library, writable }
613
+ React.useEffect(() => { mounted.current = true; return () => { lifecycle.current++; mounted.current = false } }, [])
614
+ React.useEffect(() => {
615
+ if (!writable) { lifecycle.current++; running.current = null; setBusy(false) }
616
+ }, [writable])
617
+ React.useLayoutEffect(() => {
618
+ if (!busy && returnToLibrary.current) {
619
+ returnToLibrary.current = false
620
+ saveAction.current?.focus({ preventScroll: true })
621
+ }
622
+ }, [busy])
623
+ const finishAction = () => { returnToLibrary.current = true; setAction(null) }
624
+ const selected = library.profiles.find(profile => profile.id === selectedId) ?? library.profiles[0]
625
+ const current = () => capabilityProfileConfiguration(Object.fromEntries(CAPABILITY_CONFIGURATION_KEYS.map(key => [key, value[key]])))
626
+ const perform = async (area, operation) => {
627
+ if (!mounted.current || !live.current?.writable || running.current) return
628
+ const token = { area }
629
+ running.current = token
630
+ const generation = lifecycle.current
631
+ setBusy(true); setStatus(null)
632
+ const assertCurrent = () => {
633
+ if (!mounted.current || !live.current?.writable || generation !== lifecycle.current) throw new Error(t('capabilities.configurationUnavailable'))
634
+ }
635
+ try { await operation(assertCurrent); if (generation === lifecycle.current) setStatus({ area, key: 'capabilities.configurationSaved' }) }
636
+ catch (error) { if (generation === lifecycle.current) setStatus({ area, error: String(error?.message ?? error) }) }
637
+ finally { if (running.current === token) running.current = null; if (generation === lifecycle.current) setBusy(false) }
638
+ }
639
+ const saveLibrary = async profiles => {
640
+ if (!mounted.current || !live.current?.writable) throw new Error(t('capabilities.configurationUnavailable'))
641
+ const next = validateCapabilityProfiles({ schema: library.schema, profiles })
642
+ if (!await onChange(field('capabilityProfiles'), JSON.stringify(next))) throw new Error(t('capabilities.configurationUnavailable'))
643
+ }
644
+ const save = async (configuration, requestedName = name) => {
645
+ const existing = library.profiles.find(profile => profile.name === requestedName.trim())
646
+ const profile = { id: existing?.id ?? configurationId(), name: requestedName.trim(), configuration }
647
+ await saveLibrary([...library.profiles.filter(item => item.id !== profile.id), profile])
648
+ setSelectedId(profile.id)
649
+ setRename(profile.name)
650
+ finishAction()
651
+ }
652
+ const exportProfiles = (scope = 'all') => {
653
+ const document = { schema: library.schema, profiles: scope === 'selected' && selected ? [selected] : library.profiles }
654
+ const url = URL.createObjectURL(new Blob([JSON.stringify(document, null, 2) + '\n'], { type: 'application/json' }))
655
+ const link = globalThis.document.createElement('a')
656
+ link.href = url; link.download = 'ptc-plus-capability-configurations.json'; link.click()
657
+ const revoke = URL.revokeObjectURL.bind(URL)
658
+ setTimeout(() => revoke(url), 0)
659
+ }
660
+ const importProfiles = event => {
661
+ const file = event.target.files?.[0]; event.target.value = ''
662
+ if (!file) return
663
+ void perform('transfer', async assertCurrent => {
664
+ if (file.size > 2_000_000) throw new Error(t('capabilities.configurationTooLarge'))
665
+ const imported = validateCapabilityProfiles(JSON.parse(await file.text()))
666
+ assertCurrent()
667
+ const currentLibrary = live.current.library
668
+ const names = new Set(currentLibrary.profiles.map(profile => profile.name))
669
+ const merged = [...currentLibrary.profiles]
670
+ for (const profile of imported.profiles) {
671
+ let name = profile.name; let suffix = 2
672
+ while (names.has(name)) name = `${profile.name.slice(0, 68)} (${suffix++})`
673
+ names.add(name); merged.push({ ...profile, id: configurationId(), name })
674
+ }
675
+ await saveLibrary(merged)
676
+ })
677
+ }
678
+ const feedback = status || busy ? h('p', { className: 'ptcPlusTaskFeedback', role: status?.error ? 'alert' : 'status' },
679
+ status?.error ?? (busy ? t('state.syncing') : t(status.key))) : null
680
+ const frozen = plans?.find(plan => plan.sessionId === sessionId)
681
+ let matches = false
682
+ try { matches = selected && JSON.stringify(selected.configuration) === JSON.stringify(current()) } catch {}
683
+ const input = (label, text, update) => h('label', { className: 'ptcPlusCapabilityFilter' }, h('span', null, t(label)),
684
+ h('input', { className: 'ptcPlusInput', value: text, maxLength: 80, disabled: unavailable,
685
+ onChange: event => update(event.target.value), 'aria-label': t(label) }))
686
+ const button = (label, click, blocked = false, extra = {}) => h('button', { type: 'button', className: 'ptcPlusButton',
687
+ disabled: blocked, onClick: click, ...extra }, t(label))
688
+ const closeAction = () => setAction(null)
689
+ const setting = (label, text) => h('div', { className: 'ptcPlusConfigurationSummaryRow', key: label }, h('span', null, t(label)), h('span', null, text))
690
+ const configuration = selected?.configuration
691
+ const policy = configuration ? capabilityPolicyForConfig(configuration) : null
692
+ const enabled = key => t(configuration[key] ? 'capabilities.serverOn' : 'capabilities.serverOff')
693
+ return h('section', { className: 'ptcPlusConfigurationProfiles', 'aria-label': t('capabilities.savedConfigurations') },
694
+ h('div', { className: 'ptcPlusConfigurationLibrary' }, h('h4', null, t('capabilities.savedConfigurations')),
695
+ h('div', { className: 'ptcPlusConfigurationList', role: 'group', 'aria-label': t('capabilities.chooseConfiguration') },
696
+ ...library.profiles.map(profile => h('button', { type: 'button', key: profile.id,
697
+ className: 'ptcPlusConfigurationChoice', 'aria-pressed': selected?.id === profile.id,
698
+ onClick: () => { setSelectedId(profile.id); setRename(profile.name); setAction(null) } }, profile.name)),
699
+ !library.profiles.length ? h('p', { className: 'ptcPlusDetail' }, t('capabilities.noConfigurations')) : null),
700
+ button('capabilities.saveConfiguration', () => { setAction('save'); setStatus(null) }, unavailable,
701
+ { ref: saveAction, className: 'ptcPlusButton ptcPlusConfigurationSave', 'aria-label': t('capabilities.saveConfiguration'), 'aria-haspopup': 'dialog' }),
702
+ h('div', { className: 'ptcPlusConfigurationLibraryActions' },
703
+ button('capabilities.importConfigurations', () => fileInput.current.click(), unavailable),
704
+ h('button', { type: 'button', className: 'ptcPlusButton ptcPlusConfigurationMore', 'aria-label': t('capabilities.libraryMore'),
705
+ 'aria-haspopup': 'dialog', onClick: () => { setAction('library'); setStatus(null) } }, '···')),
706
+ h('input', { ref: fileInput, type: 'file', accept: '.json,application/json', hidden: true, disabled: unavailable,
707
+ onChange: importProfiles, 'aria-label': t('capabilities.importConfigurations') }),
708
+ action === 'library' ? h(CapabilityDialog, { title: t('capabilities.libraryActions'), onClose: closeAction, t,
709
+ className: 'ptcPlusCapabilityConfigurationDialog' },
710
+ h('div', { className: 'ptcPlusCapabilityDialogActions' },
711
+ button('capabilities.exportConfigurations', () => { exportProfiles('all'); closeAction() }, !library.profiles.length),
712
+ button('capabilities.copyFromSession', () => { setAction('copy'); setStatus(null) }, unavailable))) : null,
713
+ action === 'save' ? h(CapabilityDialog, { title: t('capabilities.saveConfiguration'), onClose: closeAction, t,
714
+ className: 'ptcPlusCapabilityConfigurationDialog' },
715
+ h('form', { className: 'ptcPlusConfigurationAction', onSubmit: event => {
716
+ event.preventDefault(); if (!unavailable && name.trim()) void perform('save', () => save(current()))
717
+ } }, input('capabilities.configurationName', name, setName),
718
+ h('p', { className: 'ptcPlusDetail' }, t('capabilities.sameNameHint')),
719
+ h('div', { className: 'ptcPlusCapabilityDialogActions' },
720
+ h('button', { type: 'button', className: 'ptcPlusButton', onClick: closeAction }, t('capabilities.batch.cancel')),
721
+ button('capabilities.confirmSave', undefined, unavailable || !name.trim(), { type: 'submit' })))) : null,
722
+ action === 'copy' ? h(CapabilityDialog, { title: t('capabilities.copyFromSession'), onClose: closeAction, t,
723
+ className: 'ptcPlusCapabilityConfigurationDialog' },
724
+ h('div', { className: 'ptcPlusConfigurationAction' }, h('p', { className: 'ptcPlusDetail' }, t('capabilities.copySessionHint')),
725
+ h(Select, { value: sessionId, disabled: unavailable, label: t('capabilities.copyFromSession'), onChange: setSessionId,
726
+ options: [{ value: '', label: t('capabilities.chooseSession') }, ...(plans ?? []).map(plan => ({ value: plan.sessionId, label: plan.sessionId }))] }),
727
+ input('capabilities.copyName', copyName, setCopyName),
728
+ h('div', { className: 'ptcPlusCapabilityDialogActions' },
729
+ h('button', { type: 'button', className: 'ptcPlusButton', onClick: closeAction }, t('capabilities.batch.cancel')),
730
+ button('capabilities.saveSessionConfiguration', () => perform('copy', () => save(capabilityProfileConfiguration(frozen.configuration), copyName)),
731
+ unavailable || !copyName.trim() || !frozen?.configuration)))) : null),
732
+ h('div', { className: 'ptcPlusConfigurationDetail' }, selected ? h(React.Fragment, null,
733
+ h('div', { className: 'ptcPlusTaskHead' }, h('h4', null, selected.name),
734
+ button('capabilities.applyConfiguration', () => perform('library', () => onApplyConfiguration(selected.configuration)), unavailable || !onApplyConfiguration,
735
+ { 'data-kind': 'primary' }),
736
+ h('button', { type: 'button', className: 'ptcPlusButton ptcPlusConfigurationMore', 'aria-label': t('capabilities.profileMore'),
737
+ 'aria-haspopup': 'dialog', onClick: () => { setRename(selected.name); setAction('profile') } }, '···')),
738
+ action === 'profile' ? h(CapabilityDialog, { title: t('capabilities.profileActions'), onClose: closeAction, t,
739
+ className: 'ptcPlusCapabilityConfigurationDialog' },
740
+ h('div', { className: 'ptcPlusCapabilityDialogActions' },
741
+ button('capabilities.renameConfiguration', () => { setRename(selected.name); setAction('rename') }, unavailable),
742
+ button('capabilities.exportConfiguration', () => { exportProfiles('selected'); closeAction() }),
743
+ button('capabilities.deleteConfiguration', () => perform('library', async () => {
744
+ await saveLibrary(library.profiles.filter(profile => profile.id !== selected.id)); setSelectedId(''); finishAction()
745
+ }), unavailable, { 'data-kind': 'danger' }))) : null,
746
+ h('div', { className: 'ptcPlusConfigurationSummary' },
747
+ setting('group.capabilities', enabled('capabilityContextEnabled')),
748
+ setting('capabilities.management', enabled('capabilityManagementEnabled')),
749
+ setting('capabilities.statisticsEnabled', t(JSON.parse(configuration.capabilityStatistics).enabled ? 'capabilities.serverOn' : 'capabilities.serverOff')),
750
+ setting('capabilities.defaultDisclosure', t(`capabilities.${policy.defaults.disclosure}`)),
751
+ setting('capabilities.allocationTitle', ['baseline', 'statistics', 'budget'].filter(key => policy.allocation[key]).map(key => t(`capabilities.allocation.${key}`)).join(' · ') || '—'),
752
+ setting('capabilities.recoveryEnabled', enabled('capabilityRecoveryEnabled')),
753
+ setting('capabilities.previewEnabled', `${enabled('modelPreviewEnabled')}${configuration.modelPreviewEnabled ? ` · ${t('capabilities.estimatedTokens', { count: configuration.modelPreviewTokens })}` : ''}`),
754
+ setting('capabilities.savedRules', t('capabilities.ruleCount', { count: Object.keys(policy.items).length + Object.keys(policy.categories).length + Object.keys(policy.mcpServers).length }))),
755
+ h('details', { className: 'ptcPlusConfigurationRules' }, h('summary', null, t('capabilities.profileDetails')),
756
+ h('div', { className: 'ptcPlusConfigurationSummary' },
757
+ setting('capabilities.commonBaseline', JSON.parse(configuration.capabilityBaseline).join(' · ') || '—'),
758
+ ...['categories', 'mcpServers', 'items'].flatMap(scope => Object.entries(policy[scope]).map(([name, rule]) =>
759
+ setting(scope === 'categories' ? `capabilities.policy.category.${name}` : name, capabilityRuleLabel(t, rule)))),
760
+ ...['capabilityBudgetBytes', 'capabilityDirectoryBytes', 'capabilityRecoveryBytes', 'modelPreviewTokens'].map(key => setting(`${key}.label`, configuration[key])),
761
+ ...['windowSize', 'minimumSamples', 'fullThreshold'].map(key => setting(`capabilities.${key}`,
762
+ key === 'fullThreshold' ? `${Math.round(JSON.parse(configuration.capabilityStatistics)[key] * 100)}%` : JSON.parse(configuration.capabilityStatistics)[key])))),
763
+ h('p', { className: 'ptcPlusDetail' }, t(matches ? 'capabilities.configurationMatches' : 'capabilities.configurationDifferent')),
764
+ h('p', { className: 'ptcPlusDetail' }, t('capabilities.configurationsHint')),
765
+ action === 'rename' ? h(CapabilityDialog, { title: t('capabilities.renameConfiguration'), onClose: closeAction, t,
766
+ className: 'ptcPlusCapabilityConfigurationDialog' },
767
+ h('form', { className: 'ptcPlusConfigurationAction', onSubmit: event => {
768
+ event.preventDefault(); if (!unavailable && rename.trim()) void perform('library', async () => {
769
+ await saveLibrary(library.profiles.map(profile => profile.id === selected.id ? { ...profile, name: rename.trim() } : profile)); finishAction()
770
+ })
771
+ } }, input('capabilities.renameTo', rename, setRename),
772
+ h('div', { className: 'ptcPlusCapabilityDialogActions' },
773
+ h('button', { type: 'button', className: 'ptcPlusButton', onClick: closeAction }, t('capabilities.batch.cancel')),
774
+ button('capabilities.confirmRename', undefined, unavailable || !rename.trim(), { type: 'submit' })))) : null)
775
+ : h('p', { className: 'ptcPlusDetail' }, t('capabilities.chooseConfiguration'))), feedback)
776
+ }
777
+
778
+ function ContextSettings({ value, configurationReady, disabled, onChange, t, renderField, allocation, allocationError, allocationPending, selectedSessionId, onReload, renderRefresh, id, section, setSection }) {
779
+ const sections = ['description', 'recovery', 'preview']
780
+ const saved = choicesFor(configurationReady ? value : undefined)
781
+ const retained = flagsFor(saved)
782
+ const applied = flagsFor(effectiveCapabilitySettings(saved))
783
+ const policy = capabilityPolicyForConfig(value)
784
+ const write = next => onChange(field('capabilityPolicy'), JSON.stringify(next))
785
+ const adaptive = [policy.defaults, ...Object.values(policy.categories), ...Object.values(policy.mcpServers), ...Object.values(policy.items)]
786
+ .some(rule => rule.disclosure === 'adaptive')
787
+ const summary = key => {
788
+ if (choiceState(retained, applied, key) === 'unknown') return t('capabilities.combination.unknownSettings')
789
+ if (key === 'description') {
790
+ const description = t('capabilities.context.summary.description', { disclosure: t(`capabilities.${policy.defaults.disclosure}`) })
791
+ if (policy.defaults.disclosure !== 'adaptive') return description
792
+ const criteria = ['baseline', 'statistics', 'budget'].filter(key => policy.allocation[key])
793
+ .map(key => t(`capabilities.allocation.${key}`)).join(t('capabilities.combination.separator'))
794
+ return `${description} · ${criteria || t('capabilities.context.summary.noCriteria')}`
795
+ }
796
+ return t(`capabilities.context.summary.${key}`, key === 'recovery'
797
+ ? { bytes: value.capabilityRecoveryBytes } : { tokens: value.modelPreviewTokens })
798
+ }
799
+ const effect = key => t(!configurationReady ? 'capabilities.combination.unknownSettings'
800
+ : value.capabilityContextEnabled === false ? 'capabilities.effect.paused' : key)
801
+ const criteria = h('section', { className: 'ptcPlusPolicyAllocation', 'aria-label': t('capabilities.allocationTitle') },
802
+ h('h5', null, t('capabilities.allocationTitle')),
803
+ h('div', { className: 'ptcPlusAllocationChoices' }, ...['baseline', 'statistics', 'budget'].map(key => h('label', { key },
804
+ h('input', { type: 'checkbox', role: 'switch', disabled: disabled('capabilityPolicy'), checked: policy.allocation[key],
805
+ 'aria-label': t(`capabilities.policy.allocate.${key}`),
806
+ onChange: event => write({ ...policy, allocation: { ...policy.allocation, [key]: event.target.checked } }) }),
807
+ h('span', null, t(`capabilities.allocation.${key}`))))),
808
+ h('p', { className: 'ptcPlusDetail' }, effect(value.capabilityManagementEnabled !== true ? 'capabilities.effect.rulesOff'
809
+ : adaptive ? 'capabilities.effect.allocationActive' : 'capabilities.effect.allocationRetained')),
810
+ policy.allocation.statistics ? h('p', { className: 'ptcPlusDetail' }, t('capabilities.effect.historicalStatistics')) : null,
811
+ h('p', { className: 'ptcPlusDetail' }, t('capabilities.effect.allocationLimits')))
812
+ const feature = (key, label) => h('label', { className: 'ptcPlusRow' }, h('span', { className: 'ptcPlusMain' }, t(label)),
813
+ h('input', { type: 'checkbox', role: 'switch', className: 'ptcPlusCheck', checked: value[key] === true,
814
+ disabled: disabled(key), 'aria-label': t(label), onChange: event => onChange(field(key), event.target.checked) }))
815
+ return h('div', { className: 'ptcPlusContextLayout' },
816
+ h('div', { className: 'ptcPlusContextNavigation', role: 'tablist', 'aria-orientation': 'vertical', 'aria-label': t('capabilities.contextNavigation') },
817
+ ...sections.map((key, index) => {
818
+ const state = choiceState(retained, applied, key)
819
+ const paused = state === 'paused'
820
+ return h('button', { key, type: 'button', role: 'tab', id: `${id}-context-tab-${key}`,
821
+ 'aria-label': t(`capabilities.context.${key}`), 'aria-describedby': `${id}-context-state-${key} ${id}-context-summary-${key}`,
822
+ 'aria-controls': `${id}-context-pane-${key}`, 'aria-selected': section === key, tabIndex: section === key ? 0 : -1,
823
+ onClick: () => setSection(key), onKeyDown: event => navigateTabs(event, sections, index, setSection),
824
+ }, h('span', null, t(`capabilities.context.${key}`)),
825
+ h('span', { className: 'ptcPlusContextState', id: `${id}-context-state-${key}`, 'data-enabled': state === 'selected',
826
+ 'data-paused': paused }, t(`capabilities.combination.state.${state}`)),
827
+ h('span', { className: 'ptcPlusContextSummary', id: `${id}-context-summary-${key}` }, summary(key)))
828
+ })),
829
+ h('div', { className: 'ptcPlusContextContent' }, ...sections.map(key => h('section', { key, role: 'tabpanel',
830
+ id: `${id}-context-pane-${key}`, 'aria-labelledby': `${id}-context-tab-${key}`, hidden: section !== key },
831
+ h('h4', null, t(`capabilities.context.${key}`)),
832
+ key === 'description' ? h(React.Fragment, null,
833
+ h('p', { className: 'ptcPlusDetail' }, t('capabilities.descriptionHelp')),
834
+ h('p', { className: 'ptcPlusDetail' }, t('capabilities.descriptionDependency')),
835
+ h('div', { className: 'ptcPlusCapabilityFilter' }, h('span', null, t('capabilities.defaultDisclosure')),
836
+ h(Select, { value: policy.defaults.disclosure, disabled: disabled('capabilityPolicy'), label: t('capabilities.defaultDisclosure'),
837
+ options: CAPABILITY_DISCLOSURES.map(choice => ({ value: choice, label: t(`capabilities.${choice}`) })),
838
+ onChange: disclosure => write({ ...policy, defaults: { ...policy.defaults, disclosure } }) })),
839
+ h('p', { className: 'ptcPlusDetail' }, effect(value.capabilityManagementEnabled !== true ? 'capabilities.effect.rulesOff' : `capabilities.effect.disclosure.${policy.defaults.disclosure}`)),
840
+ criteria,
841
+ h('section', { className: 'ptcPlusContextQuota', 'aria-label': t('capabilities.quotaSettings') }, h('h5', null, t('capabilities.quotaSettings')),
842
+ ...['capabilityBudgetBytes', 'capabilityDirectoryBytes'].map(renderField)),
843
+ value.capabilityManagementEnabled !== true ? h('p', { className: 'ptcPlusDetail' }, t('capabilities.descriptionInactive')) : null,
844
+ h('section', { className: 'ptcPlusAllocationPreview' },
845
+ !allocation || allocationPending || allocationError ? h('h5', null, t('capabilities.budgetPreview')) : null,
846
+ renderRefresh?.({ observation: true }), h(AllocationEvidence, { allocation, allocationError, allocationPending, selectedSessionId, onReload, t })))
847
+ : key === 'recovery' ? h(React.Fragment, null,
848
+ h('p', { className: 'ptcPlusDetail' }, t('capabilities.recoveryHelp')),
849
+ feature('capabilityRecoveryEnabled', 'capabilities.recoveryEnabled'),
850
+ h('p', { className: 'ptcPlusDetail' }, !configurationReady ? t('capabilities.combination.unknownSettings')
851
+ : value.capabilityRecoveryEnabled === true ? effect('capabilities.effect.recoveryOn') : t('capabilities.effect.recoveryOff')),
852
+ h('section', { className: 'ptcPlusContextQuota', 'aria-label': t('capabilities.quotaSettings') }, h('h5', null, t('capabilities.quotaSettings')), renderField('capabilityRecoveryBytes')))
853
+ : h(React.Fragment, null, h('p', { className: 'ptcPlusDetail' }, t('capabilities.previewHelp')),
854
+ feature('modelPreviewEnabled', 'capabilities.previewEnabled'),
855
+ h('p', { className: 'ptcPlusDetail' }, !configurationReady ? t('capabilities.combination.unknownSettings')
856
+ : value.modelPreviewEnabled === true ? effect('capabilities.effect.previewOn') : t('capabilities.effect.previewOff')),
857
+ h('section', { className: 'ptcPlusContextQuota', 'aria-label': t('capabilities.quotaSettings') }, h('h5', null, t('capabilities.quotaSettings')), renderField('modelPreviewTokens')))))))
858
+ }
859
+
860
+ function navigateTabs(event, tabs, index, update) {
861
+ const offset = ['ArrowRight', 'ArrowDown'].includes(event.key) ? 1 : ['ArrowLeft', 'ArrowUp'].includes(event.key) ? -1 : 0
862
+ const next = offset ? (index + offset + tabs.length) % tabs.length : event.key === 'Home' ? 0 : event.key === 'End' ? tabs.length - 1 : null
863
+ if (next === null) return
864
+ event.preventDefault(); update(tabs[next])
865
+ event.currentTarget.parentElement.querySelectorAll('[role=tab]')[next].focus({ preventScroll: true })
866
+ }
867
+
868
+ function CapabilityWorkbench({ value, configurationReady, catalog, catalogError, plans, plansError, plansPending, plansStale, disabled, onChange, onReload, t, renderField, renderPlans, renderRefresh, id, selectedSessionId, onSessionSelect,
869
+ allocation, allocationError, allocationPending, onApplyConfiguration, configurationWritable, saving, saveStatus }) {
870
+ const [tab, setTab] = React.useState('manual')
871
+ const [contextSection, setContextSection] = React.useState('description')
872
+ const tabs = ['manual', 'budgets', 'statistics', 'plans']
873
+ const feedback = saving ? t('state.syncing') : saveStatus && saveStatus.key !== 'status.applied' ? t(saveStatus.key, saveStatus.params)
874
+ : t(configurationWritable ? 'capabilities.savedForNew' : 'footer.readOnly')
875
+ return h('div', { className: 'ptcPlusCapabilityWorkbench' },
876
+ h(CombinationSummary, { value, configurationReady, plans, plansError, plansPending, plansStale, selectedSessionId,
877
+ feedback, onReload, renderRefresh, renderPlans, t, id, onNavigate: key => {
878
+ setTab(key === 'rules' ? 'manual' : key === 'statistics' ? 'statistics' : 'budgets')
879
+ if (['description', 'recovery', 'preview'].includes(key)) setContextSection(key)
880
+ } }),
881
+ h('div', { className: 'ptcPlusCapabilityTabs', role: 'tablist', 'aria-label': t('capabilities.settingsTabs') },
882
+ ...tabs.map((key, index) => h('button', { key, type: 'button', role: 'tab', id: `${id}-tab-${key}`,
883
+ 'aria-controls': `${id}-pane-${key}`, 'aria-selected': tab === key, tabIndex: tab === key ? 0 : -1,
884
+ onClick: () => setTab(key), onKeyDown: event => navigateTabs(event, tabs, index, setTab),
885
+ }, t(`capabilities.tab.${key}`)))),
886
+ ...tabs.map(key => h('div', { key, className: 'ptcPlusCapabilityPane', role: 'tabpanel', id: `${id}-pane-${key}`,
887
+ 'aria-labelledby': `${id}-tab-${key}`, hidden: tab !== key },
888
+ h('div', { className: 'ptcPlusTaskHead' }, h('h4', null, t(`capabilities.title.${key}`)),
889
+ key === 'manual' ? h('label', { className: 'ptcPlusCapabilityManagement' }, h('span', null, t('capabilities.management')),
890
+ h('input', { type: 'checkbox', role: 'switch', className: 'ptcPlusCheck', checked: value.capabilityManagementEnabled === true,
891
+ disabled: disabled('capabilityManagementEnabled'), 'aria-label': t('capabilities.management'),
892
+ onChange: event => onChange(field('capabilityManagementEnabled'), event.target.checked) })) : null),
893
+ key === 'manual' ? h(React.Fragment, null, h('p', { className: 'ptcPlusDetail' }, t(value.capabilityContextEnabled === false ? 'capabilities.effect.paused'
894
+ : value.capabilityManagementEnabled === true ? 'capabilities.managementHelp' : 'capabilities.effect.rulesOff')),
895
+ h(CapabilityChoices, { value, catalog, catalogError, plans, plansError, disabled, onChange, onReload, t, selectedSessionId, onSessionSelect }))
896
+ : key === 'statistics' ? h(React.Fragment, null, renderField('capabilityStatistics'),
897
+ h('p', { className: 'ptcPlusDetail' }, t(value.capabilityContextEnabled === false ? 'capabilities.effect.paused'
898
+ : choicesFor(value).statistics?.enabled === true ? 'capabilities.effect.collectionOn'
899
+ : choicesFor(value).statistics?.enabled === false ? 'capabilities.effect.collectionOff' : 'capabilities.combination.unknownSettings')),
900
+ h('p', { className: 'ptcPlusDetail' }, t('capabilities.effect.statisticsIndependent')),
901
+ renderRefresh?.({ observation: true }),
902
+ h(AllocationEvidence, { allocation, allocationError, allocationPending, selectedSessionId, statistics: true, onReload, t }))
903
+ : key === 'budgets' ? h(ContextSettings, { value, configurationReady, disabled, onChange, t, renderField, allocation, allocationError, allocationPending,
904
+ selectedSessionId, onReload, renderRefresh, id, section: contextSection, setSection: setContextSection })
905
+ : h(ConfigurationProfiles, { value, plans, disabled, writable: configurationWritable, onChange, onApplyConfiguration, t }))))
906
+ }
907
+
908
+ return { CapabilityWorkbench }
909
+ }
910
+
911
+ /** Execute a human command through DSH; a receipt never stands in for model publication. */
912
+ export function createCapabilityRefreshView(React, commands) {
913
+ const h = React.createElement
914
+ const absent = Object.freeze({ getSnapshot: () => false, subscribe: () => () => {} })
915
+ return function CapabilityRefreshButton({ sessionId, enabled, t, onApplied, onInvalidated }) {
916
+ const source = React.useMemo(() => sessionId ? commands.source(sessionId) : absent, [sessionId])
917
+ const available = React.useSyncExternalStore(source.subscribe, source.getSnapshot)
918
+ const [pending, setPending] = React.useState(false)
919
+ const [status, setStatus] = React.useState(null)
920
+ const request = React.useRef(null)
921
+ React.useEffect(() => {
922
+ setStatus(null)
923
+ setPending(false)
924
+ return () => { request.current?.abort(); request.current = null }
925
+ }, [sessionId, enabled, available])
926
+ const cancel = event => {
927
+ event.stopPropagation()
928
+ request.current?.abort(); request.current = null; setPending(false)
929
+ setStatus({ key: 'capabilities.refreshCancelled' })
930
+ onApplied?.()
931
+ }
932
+ const execute = async event => {
933
+ event.stopPropagation()
934
+ if (!enabled || !available || request.current) return
935
+ const controller = new AbortController()
936
+ request.current = controller
937
+ setPending(true)
938
+ setStatus(null)
939
+ onInvalidated?.()
940
+ try {
941
+ const result = await commands.run(sessionId, 'refresh', controller.signal)
942
+ if (controller.signal.aborted || request.current !== controller) return
943
+ setStatus(result.text ?? { key: 'capabilities.refreshAcknowledged' })
944
+ onApplied?.()
945
+ } catch (error) {
946
+ if (!controller.signal.aborted && request.current === controller) {
947
+ const text = String(error?.message ?? error)
948
+ if (!readCapabilityCommandReceipt(text)) {
949
+ setStatus({ key: 'capabilities.refreshFailed', params: { error: text } })
950
+ }
951
+ onApplied?.()
952
+ }
953
+ } finally {
954
+ if (request.current === controller) { request.current = null; setPending(false) }
955
+ }
956
+ }
957
+ return h('div', { className: 'ptcPlusCapabilityRefresh' },
958
+ h('button', { type: 'button', className: 'ptcPlusButton', disabled: !enabled || !available || pending,
959
+ title: !available ? t(sessionId ? 'capabilities.refreshUnavailable' : 'capabilities.chooseSession') : undefined,
960
+ onClick: execute }, t(pending ? 'capabilities.refreshing' : 'capabilities.applySession')),
961
+ pending ? h('button', { type: 'button', className: 'ptcPlusButton', onClick: cancel }, t('capabilities.cancelRefresh')) : null,
962
+ status ? h('p', { className: 'ptcPlusDetail', role: 'status' }, capabilityCommandText(t, status))
963
+ : !available ? h('p', { className: 'ptcPlusDetail' }, t(sessionId ? 'capabilities.refreshUnavailable' : 'capabilities.chooseSession')) : null)
964
+ }
965
+ }
966
+
967
+ /** Session capability choices use the real command transaction and a read-only catalog observation. */
968
+ export function createSessionCapabilityControlsView(React, { commands, Modal, Select, inspectSessionCatalog }) {
969
+ const h = React.createElement
970
+ const emptyDrafts = () => ({})
971
+ return function SessionCapabilityControls({ sessionId, open, enabled, t, onClose }) {
972
+ const source = React.useMemo(() => commands.source(sessionId), [sessionId])
973
+ const available = React.useSyncExternalStore(source.subscribe, source.getSnapshot)
974
+ const [catalog, setCatalog] = React.useState([])
975
+ const [management, setManagement] = React.useState('unknown')
976
+ const [loading, setLoading] = React.useState(false)
977
+ const [error, setError] = React.useState(null)
978
+ const [drafts, setDrafts] = React.useState(emptyDrafts)
979
+ const [selectedId, setSelectedId] = React.useState(null)
980
+ const [pane, setPane] = React.useState('list')
981
+ const [query, setQuery] = React.useState('')
982
+ const [kind, setKind] = React.useState('all')
983
+ const [sourceFilter, setSourceFilter] = React.useState('all')
984
+ const [stateFilter, setStateFilter] = React.useState('all')
985
+ const [sort, setSort] = React.useState(null)
986
+ const [focusDimension, setFocusDimension] = React.useState(null)
987
+ const [status, setStatus] = React.useState(null)
988
+ const [pending, setPending] = React.useState(false)
989
+ const request = React.useRef(null)
990
+ const applying = React.useRef(false)
991
+ const mounted = React.useRef(true)
992
+ const detail = React.useRef(null)
993
+ const list = React.useRef(null)
994
+ React.useEffect(() => {
995
+ if (!focusDimension) return
996
+ const field = focusDimension === 'list' ? list.current?.querySelector('[data-selected="true"] .ptcPlusSessionCapabilityName')
997
+ : detail.current?.querySelector(`[data-capability-dimension="${focusDimension}"] [role="combobox"]`)
998
+ ?? detail.current?.querySelector('[role="combobox"]')
999
+ field?.focus({ preventScroll: true })
1000
+ setFocusDimension(null)
1001
+ }, [focusDimension, selectedId, pane])
1002
+ React.useEffect(() => {
1003
+ mounted.current = true
1004
+ return () => { mounted.current = false; request.current?.abort(); request.current = null }
1005
+ }, [])
1006
+ React.useEffect(() => {
1007
+ request.current?.abort()
1008
+ request.current = null
1009
+ applying.current = false
1010
+ setCatalog([])
1011
+ setManagement('unknown')
1012
+ setDrafts(emptyDrafts())
1013
+ setSelectedId(null)
1014
+ setPane('list')
1015
+ setFocusDimension(null)
1016
+ setStatus(null)
1017
+ setPending(false)
1018
+ setError(null)
1019
+ }, [sessionId])
1020
+ const load = React.useCallback(async signal => {
1021
+ setLoading(true)
1022
+ try {
1023
+ const payload = await inspectSessionCatalog(sessionId, signal)
1024
+ if (signal.aborted || !mounted.current || request.current?.signal !== signal) return
1025
+ if (payload?.schema !== 'ptc-capability-session-catalog/v1' || !Array.isArray(payload.entries)) {
1026
+ throw new Error('Invalid capability catalog')
1027
+ }
1028
+ setCatalog(payload.entries)
1029
+ setManagement(['managed', 'paused', 'off', 'unknown'].includes(payload.management)
1030
+ ? payload.management : 'unknown')
1031
+ setError(null)
1032
+ return payload.entries
1033
+ } catch (failure) {
1034
+ if (!signal.aborted && mounted.current && request.current?.signal === signal) setError(String(failure?.message ?? failure))
1035
+ } finally {
1036
+ if (!signal.aborted && mounted.current && request.current?.signal === signal) setLoading(false)
1037
+ }
1038
+ }, [sessionId])
1039
+ React.useEffect(() => {
1040
+ if (!open || !enabled || !available) {
1041
+ request.current?.abort(); request.current = null; setLoading(false); return
1042
+ }
1043
+ const controller = new AbortController()
1044
+ request.current = controller
1045
+ void load(controller.signal)
1046
+ return () => { controller.abort(); if (request.current === controller) request.current = null }
1047
+ }, [open, enabled, available, load])
1048
+ React.useEffect(() => {
1049
+ if (!enabled || !available) {
1050
+ request.current?.abort()
1051
+ request.current = null
1052
+ applying.current = false
1053
+ setPending(false)
1054
+ }
1055
+ }, [enabled, available])
1056
+ const choices = drafts
1057
+ const choiceFor = entry => choices[entry.id] ?? entry.temporary ?? 'inherit'
1058
+ const writesBlocked = management === 'paused' || management === 'off'
1059
+ const sourceKey = entry => entry.source?.kind === 'mcp' ? 'mcp:' + (entry.source.server ?? '')
1060
+ : entry.source?.kind === 'skill' ? 'skill:' + (entry.source.bucket ?? '')
1061
+ : entry.source?.kind ?? 'unknown'
1062
+ const sourceLabel = entry => entry.source?.kind === 'mcp' ? `MCP · ${entry.source.server ?? ''}`
1063
+ : entry.source?.kind === 'skill' ? `Skill · ${entry.source.bucket
1064
+ ? t(`capabilities.source.${entry.source.bucket}`) : t('capabilities.source.unknown')}`
1065
+ : t(`capabilities.source.${entry.source?.kind === 'host' ? 'host' : 'unknown'}`)
1066
+ const category = entry => entry.category === 'skill' ? 'skills'
1067
+ : ['server', 'resource'].includes(entry.category) ? 'mcp'
1068
+ : entry.source?.kind === 'mcp' ? 'mcp_tools' : 'tools'
1069
+ const type = entry => entry.category === 'server' ? 'server'
1070
+ : entry.source?.kind === 'mcp' ? 'resource'
1071
+ : entry.category === 'skill' ? 'skill'
1072
+ : entry.category === 'resource' ? 'resource'
1073
+ : entry.category === 'tool' ? 'tool' : 'other'
1074
+ const displayedSource = entry => entry.source?.kind === 'mcp' ? entry.source.server ?? sourceLabel(entry) : sourceLabel(entry)
1075
+ const effectiveAvailability = entry => entry.effective === 'pending' ? 'pending'
1076
+ : entry.enclosingClosure || entry.effective === 'closed' ? 'closed'
1077
+ : entry.effectiveDimensions?.availability
1078
+ ?? (['full', 'on-demand', 'host', 'enabled'].includes(entry.effective) ? 'enabled' : 'unknown')
1079
+ const effectiveContext = entry => entry.effective === 'pending' ? 'pending'
1080
+ : effectiveAvailability(entry) === 'closed' ? 'closed'
1081
+ : entry.effectiveDimensions?.disclosure
1082
+ ?? (['full', 'on-demand', 'host'].includes(entry.effective) ? entry.effective : 'unknown')
1083
+ const sources = [...new Set(catalog.filter(entry => kind === 'all' || category(entry) === kind).map(sourceKey))].sort()
1084
+ const setCompatibleKind = next => {
1085
+ setKind(next)
1086
+ if (sourceFilter !== 'all' && !catalog.some(entry => (next === 'all' || category(entry) === next)
1087
+ && sourceKey(entry) === sourceFilter)) setSourceFilter('all')
1088
+ }
1089
+ const setCompatibleSource = next => {
1090
+ setSourceFilter(next)
1091
+ if (next !== 'all' && !catalog.some(entry => (kind === 'all' || category(entry) === kind)
1092
+ && sourceKey(entry) === next)) setKind('all')
1093
+ }
1094
+ const filtered = catalog.filter(entry => {
1095
+ const text = [entry.name, entry.id, entry.description, entry.source?.server, entry.source?.bucket].filter(Boolean).join(' ').toLocaleLowerCase()
1096
+ return (kind === 'all' || category(entry) === kind)
1097
+ && (sourceFilter === 'all' || sourceKey(entry) === sourceFilter)
1098
+ && (stateFilter === 'all' || (stateFilter === 'changed' ? Object.hasOwn(choices, entry.id) : entry.effective === stateFilter))
1099
+ && text.includes(query.trim().toLocaleLowerCase())
1100
+ }).sort((a, b) => (a.name ?? a.id).localeCompare(b.name ?? b.id) || a.id.localeCompare(b.id))
1101
+ const groups = ['tools', 'skills', 'mcp', 'mcp_tools']
1102
+ const defaultOrder = groups.flatMap(group => filtered.filter(entry => category(entry) === group))
1103
+ const matches = sortCapabilityEntries(defaultOrder, sort, (entry, column) => column === 'name' ? entry.name ?? entry.id
1104
+ : column === 'type' ? t(`capabilities.type.${type(entry)}`)
1105
+ : column === 'source' ? displayedSource(entry)
1106
+ : column === 'availability' ? capabilityChoiceLabel(t, effectiveAvailability(entry))
1107
+ : capabilityChoiceLabel(t, effectiveContext(entry)))
1108
+ const selected = matches.find(entry => entry.id === selectedId)
1109
+ ?? ['tools', 'skills', 'mcp', 'mcp_tools'].map(group => matches.find(entry => category(entry) === group)).find(Boolean)
1110
+ const staged = Object.entries(choices).flatMap(([id, choice]) => typeof choice === 'object'
1111
+ ? Object.entries(choice).map(([dimension, selection]) => [id, selection, dimension]) : [[id, choice]])
1112
+ const chooseDimension = (id, dimension, choice) => {
1113
+ setDrafts(current => {
1114
+ const patch = typeof current[id] === 'object' ? { ...current[id] } : {}
1115
+ const original = catalog.find(entry => entry.id === id)?.temporaryRule?.[dimension] ?? 'inherit'
1116
+ if (choice === original && choice === 'inherit') delete patch[dimension]
1117
+ else patch[dimension] = choice
1118
+ const next = { ...current }; if (Object.keys(patch).length) next[id] = patch; else delete next[id]; return next
1119
+ })
1120
+ }
1121
+ const choose = (id, next) => {
1122
+ const entry = catalog.find(item => item.id === id)
1123
+ if (!entry) return
1124
+ const original = entry.temporary ?? 'inherit'
1125
+ setDrafts(current => {
1126
+ const nextChoices = { ...current }
1127
+ if (next === original && next === 'inherit') delete nextChoices[id]
1128
+ else nextChoices[id] = next
1129
+ return nextChoices
1130
+ })
1131
+ }
1132
+ const hasInheritanceReset = staged.some(([, selection]) => selection === 'inherit')
1133
+ const apply = async targetScope => {
1134
+ if (applying.current || !enabled || !available || writesBlocked || staged.length === 0) return
1135
+ if (targetScope === 'global' && hasInheritanceReset) return
1136
+ const submitted = structuredClone(choices)
1137
+ applying.current = true
1138
+ setPending(true)
1139
+ setStatus(null)
1140
+ const controller = new AbortController()
1141
+ request.current?.abort()
1142
+ request.current = controller
1143
+ let completed = 0
1144
+ let pendingCount = 0
1145
+ let failure = null
1146
+ let savedOnly = null
1147
+ for (const [id, selection, dimension] of staged) {
1148
+ if (controller.signal.aborted) break
1149
+ try {
1150
+ const result = await commands.run(sessionId, `set ${targetScope} ${encodeURIComponent(id)} ${dimension ? dimension + ' ' : ''}${selection}`, controller.signal)
1151
+ const receipt = readCapabilityCommandReceipt(result.text)
1152
+ if (!receipt || receipt.sessionId !== sessionId || !['recorded', 'pending'].includes(receipt.state)) {
1153
+ throw new Error(receipt ? capabilityCommandText(t, result.text) : t('capabilities.quick.unknownReceipt'))
1154
+ }
1155
+ if (receipt?.state === 'pending') pendingCount += 1
1156
+ completed += 1
1157
+ } catch (cause) {
1158
+ failure = String(cause?.message ?? cause)
1159
+ savedOnly = readCapabilityCommandReceipt(failure)?.state === 'saved-only'
1160
+ ? readCapabilityCommandReceipt(failure) : null
1161
+ break
1162
+ }
1163
+ }
1164
+ if (request.current !== controller || controller.signal.aborted || !mounted.current) return
1165
+ const refreshed = await load(controller.signal)
1166
+ if (request.current !== controller || controller.signal.aborted || !mounted.current) return
1167
+ const confirmed = refreshed ? staged.slice(0, completed).filter(([id, selection, dimension]) => {
1168
+ const entry = refreshed.find(item => item.id === id)
1169
+ if (dimension) return entry && (targetScope === 'session'
1170
+ ? (entry.temporaryRule?.[dimension] ?? 'inherit') === selection
1171
+ : (entry.savedRule?.[dimension] ?? 'automatic') === selection && entry.temporaryRule?.[dimension] === undefined)
1172
+ return entry && (targetScope === 'session' ? entry.temporary === selection
1173
+ : entry.saved === selection && entry.temporary === 'inherit')
1174
+ }) : []
1175
+ setDrafts(current => {
1176
+ const nextChoices = { ...current }
1177
+ for (const [id, selection, dimension] of confirmed) {
1178
+ if (savedOnly && id === staged[completed]?.[0] && dimension === staged[completed]?.[2]) continue
1179
+ if (dimension) {
1180
+ if (nextChoices[id]?.[dimension] !== submitted[id]?.[dimension]) continue
1181
+ const patch = { ...nextChoices[id] }; delete patch[dimension]
1182
+ if (Object.keys(patch).length) nextChoices[id] = patch; else delete nextChoices[id]
1183
+ } else if (nextChoices[id] === submitted[id]) delete nextChoices[id]
1184
+ }
1185
+ return nextChoices
1186
+ })
1187
+ if (savedOnly) setStatus({ key: 'capabilities.quick.savedOnly', params: { count: completed, error: savedOnly.error ?? '' } })
1188
+ else if (failure || !refreshed || confirmed.length < completed) setStatus({ key: 'capabilities.quick.partial',
1189
+ params: { count: confirmed.length, error: failure ?? t('capabilities.quick.confirmationUnavailable') } })
1190
+ else setStatus({ key: pendingCount ? 'capabilities.quick.pendingApplied' : 'capabilities.quick.applied', params: { count: completed } })
1191
+ if (request.current === controller) { request.current = null; applying.current = false; setPending(false) }
1192
+ }
1193
+ const discard = () => { setDrafts(emptyDrafts()); setStatus(null) }
1194
+ const close = () => {
1195
+ request.current?.abort()
1196
+ request.current = null
1197
+ applying.current = false
1198
+ setPending(false)
1199
+ setDrafts(emptyDrafts())
1200
+ setStatus(null)
1201
+ onClose()
1202
+ }
1203
+ const filter = (label, value, onChange, options, disabled = false) => h('div', { className: 'ptcPlusCapabilityFilter' },
1204
+ h('span', null, label), h(Select, { label, value, onChange, disabled,
1205
+ options: options.map(([key, text]) => ({ value: key, label: text })) }))
1206
+ const choiceOptions = selected ? [
1207
+ 'inherit', 'automatic',
1208
+ ...(selected.category === 'server' ? [] : ['full', 'on-demand']), 'closed',
1209
+ ].map(choice => ({ value: choice, label: t(choice === 'inherit' ? 'capabilities.quick.inherit' : `capabilities.${choice}`) })) : []
1210
+ const ruleLabel = (rule, empty) => capabilityRuleLabel(t, rule, empty)
1211
+ const dimensionFacts = dimensions => ['availability', 'disclosure'].map(dimension =>
1212
+ `${t(`capabilities.policy.${dimension}`)}: ${statusLabel(dimensions[dimension])} (${dimensions[`${dimension}Source`] ?? t('capabilities.quick.unknown')})`).join(' · ')
1213
+ const statusLabel = value => value && typeof value === 'object' ? ruleLabel(value) : capabilityChoiceLabel(t, value)
1214
+ const openEditor = (entry, dimension = 'availability') => { setSelectedId(entry.id); setPane('detail'); setFocusDimension(dimension) }
1215
+ const renderRow = entry => h('div', { key: entry.id, role: 'row',
1216
+ className: 'ptcPlusSessionCapabilityRow ptcPlusCapabilityItem', 'data-capability-id': entry.id, 'data-selected': selected?.id === entry.id,
1217
+ onClick: () => openEditor(entry) },
1218
+ h('div', { role: 'cell', className: 'ptcPlusCapabilityIdentity' },
1219
+ h('button', { type: 'button', className: 'ptcPlusSessionCapabilityName ptcPlusCapabilityName',
1220
+ 'aria-current': selected?.id === entry.id ? 'true' : undefined,
1221
+ onClick: event => { event.stopPropagation(); openEditor(entry) } }, entry.name ?? entry.id),
1222
+ entry.name && entry.name !== entry.id ? h('code', null, entry.id) : null),
1223
+ h('span', { role: 'cell', className: 'ptcPlusCapabilityBadge', 'data-kind': type(entry) }, t(`capabilities.type.${type(entry)}`)),
1224
+ h('span', { role: 'cell', className: 'ptcPlusCapabilitySource' }, displayedSource(entry)),
1225
+ h('div', { role: 'cell', className: 'ptcPlusCapabilityAvailability', 'data-state': effectiveAvailability(entry) },
1226
+ h('button', { type: 'button', className: 'ptcPlusCapabilityContext',
1227
+ 'aria-label': t('capabilities.policy.availabilityFor', { name: entry.id }),
1228
+ title: t('capabilities.quick.effective'),
1229
+ onClick: event => { event.stopPropagation(); openEditor(entry, 'availability') } }, statusLabel(effectiveAvailability(entry)))),
1230
+ h('div', { role: 'cell', className: 'ptcPlusCapabilityContextCell' },
1231
+ h('button', { type: 'button', className: 'ptcPlusCapabilityContext',
1232
+ 'aria-label': t('capabilities.editContextFor', { name: entry.id }),
1233
+ title: t('capabilities.quick.effective'),
1234
+ onClick: event => { event.stopPropagation(); openEditor(entry, 'disclosure') } }, statusLabel(effectiveContext(entry)))),
1235
+ h('span', { role: 'cell', className: 'ptcPlusSessionCapabilityPending', 'data-changed': Object.hasOwn(choices, entry.id),
1236
+ 'data-label': t('capabilities.quick.changed') },
1237
+ Object.hasOwn(choices, entry.id) ? statusLabel(choices[entry.id]) : '—'))
1238
+ return h(Modal, { open, onClose: close, title: t('capabilities.quick.modalTitle'),
1239
+ closeLabel: t('settings.close'), headless: true, className: 'ptcPlusSessionCapabilityModal' },
1240
+ h('div', { className: 'ptcPlusSessionCapabilityHead' },
1241
+ h('div', null, h('h2', null, t('capabilities.quick.modalTitle')),
1242
+ h('p', null, t('capabilities.quick.sessionIdentity', { session: sessionId }))),
1243
+ h('button', { type: 'button', className: 'ptcPlusDialogClose', 'aria-label': t('settings.close'), onClick: close }, '×')),
1244
+ h('div', { className: 'ptcPlusSessionCapabilityToolbar' },
1245
+ h('label', { className: 'ptcPlusSearch' },
1246
+ h('svg', { viewBox: '0 0 24 24', width: 16, height: 16, 'aria-hidden': true },
1247
+ h('circle', { cx: 10, cy: 10, r: 6, fill: 'none', stroke: 'currentColor', strokeWidth: 2 }),
1248
+ h('path', { d: 'm15 15 6 6', fill: 'none', stroke: 'currentColor', strokeWidth: 2 })),
1249
+ h('input', { type: 'search', value: query, 'aria-label': t('capabilities.search'),
1250
+ placeholder: t('capabilities.search'), onChange: event => setQuery(event.target.value) })),
1251
+ filter(t('capabilities.quick.kind'), kind, setCompatibleKind,
1252
+ ['all', 'tools', 'skills', 'mcp', 'mcp_tools'].map(key => [key, t(`capabilities.quick.${key}`)])),
1253
+ filter(t('capabilities.sourceFilter'), sourceFilter, setCompatibleSource,
1254
+ [['all', t('capabilities.allSources')], ...sources.map(key => [key,
1255
+ sourceLabel(catalog.find(entry => sourceKey(entry) === key))])]),
1256
+ filter(t('capabilities.quick.state'), stateFilter, setStateFilter,
1257
+ [['all', t('capabilities.quick.allStates')], ['changed', t('capabilities.quick.changed')],
1258
+ ...['full', 'on-demand', 'closed', 'pending'].map(key => [key, statusLabel(key)])])),
1259
+ writesBlocked ? h('p', { className: 'ptcPlusCapabilityNotice', role: 'status' },
1260
+ t(management === 'paused' ? 'capabilities.quick.sessionPaused' : 'capabilities.quick.sessionManagementOff')) : null,
1261
+ h('div', { className: 'ptcPlusSessionCapabilityWorkspace', 'data-pane': pane },
1262
+ h('div', { className: 'ptcPlusSessionCapabilityList', role: 'table', 'aria-label': t('capabilities.quick.members'), ref: list },
1263
+ h('div', { className: 'ptcPlusSessionCapabilityColumns ptcPlusCapabilityColumns', role: 'row' },
1264
+ ...CAPABILITY_SORT_COLUMNS.map(column => h('span', { key: column.key, role: 'columnheader',
1265
+ 'aria-sort': capabilitySortDirection(sort, column.key) },
1266
+ h('button', { type: 'button', className: 'ptcPlusCapabilitySort', title: t('capabilities.sortCycle'),
1267
+ onClick: () => setSort(current => cycleCapabilitySort(current, column.key)) },
1268
+ t(`capabilities.${column.label}`), h('span', { 'aria-hidden': true },
1269
+ capabilitySortDirection(sort, column.key) === 'ascending' ? '↑' : capabilitySortDirection(sort, column.key) === 'descending' ? '↓' : '↕')))),
1270
+ h('span', { role: 'columnheader' }, t('capabilities.quick.changed'))),
1271
+ loading ? h('p', { role: 'status' }, t('state.syncing')) : null,
1272
+ error ? h('p', { role: 'alert' }, t('capabilities.quick.catalogError', { error })) : null,
1273
+ !loading && !error && matches.length === 0 ? h('p', null, t('capabilities.noMatches')) : null,
1274
+ ...(sort ? [h('div', { key: 'sorted', role: 'rowgroup' }, ...matches.map(renderRow))]
1275
+ : groups.filter(group => matches.some(entry => category(entry) === group))
1276
+ .map(group => h('section', { key: group, className: 'ptcPlusSessionCapabilityGroup' },
1277
+ h('h3', null, t(`capabilities.quick.${group}`)),
1278
+ h('div', { role: 'rowgroup' }, ...matches.filter(entry => category(entry) === group).map(renderRow)))))),
1279
+ h('aside', { className: 'ptcPlusSessionCapabilityDetail', 'aria-label': t('capabilities.details'), ref: detail },
1280
+ h('button', { type: 'button', className: 'ptcPlusSessionCapabilityBack', onClick: () => { setPane('list'); setFocusDimension('list') } },
1281
+ t('capabilities.quick.backToList')),
1282
+ selected ? h(React.Fragment, null,
1283
+ h('h3', null, selected.name ?? selected.id), h('code', null, selected.id),
1284
+ selected.description ? h('p', null, selected.description) : null,
1285
+ h('dl', null,
1286
+ h('dt', null, t('capabilities.sourceFilter')), h('dd', null, sourceLabel(selected)),
1287
+ h('dt', null, t('capabilities.quick.globalSaved')), h('dd', null, selected.savedRule ? ruleLabel(selected.savedRule) : statusLabel(selected.saved)),
1288
+ h('dt', null, t('capabilities.quick.sessionSource')), h('dd', null, selected.sourceRule ? ruleLabel(selected.sourceRule) : statusLabel(selected.sessionSourceSelection ?? 'unknown')),
1289
+ h('dt', null, t('capabilities.quick.temporary')), h('dd', null, selected.temporaryRule ? ruleLabel(selected.temporaryRule, 'capabilities.quick.noOverride') : statusLabel(selected.temporary)),
1290
+ h('dt', null, t('capabilities.quick.effective')), h('dd', null, statusLabel(selected.effective)),
1291
+ selected.sourceDimensions ? h(React.Fragment, null, h('dt', null, t('capabilities.policy.frozenSource')), h('dd', null, dimensionFacts(selected.sourceDimensions))) : null,
1292
+ selected.effectiveDimensions ? h(React.Fragment, null, h('dt', null, t('capabilities.policy.effectiveDimensions')), h('dd', null, dimensionFacts(selected.effectiveDimensions))) : null),
1293
+ selected.source?.kind === 'mcp' && selected.category !== 'server'
1294
+ ? (() => {
1295
+ const server = catalog.find(entry => entry.category === 'server' && entry.source?.server === selected.source.server)
1296
+ if (!server) return null
1297
+ const proposed = choiceFor(server)
1298
+ const availability = server.savedRule ? choices[server.id]?.availability ?? server.temporaryRule?.availability ?? 'inherit' : proposed
1299
+ const sourceAvailability = server.sourceRule !== undefined ? server.sourceRule.availability : server.sessionSourceSelection
1300
+ const pendingClose = server.managementEnabled !== false && (availability === 'inherit' ? sourceAvailability : availability) === 'closed'
1301
+ const currentlyClosed = server.enclosingClosure ?? server.effective === 'closed'
1302
+ const key = currentlyClosed ? 'capabilities.quick.disabledByServer'
1303
+ : pendingClose ? 'capabilities.quick.pendingServerClose' : null
1304
+ return key ? h('p', { className: 'ptcPlusCapabilityNotice' }, t(key, { server: selected.source.server })) : null
1305
+ })() : null,
1306
+ selected.category === 'server' ? h('div', { className: 'ptcPlusSessionCapabilityChildren' },
1307
+ h('h4', null, t('capabilities.quick.serverTools')),
1308
+ h('ul', null, ...catalog.filter(entry => entry.id !== selected.id
1309
+ && entry.source?.kind === 'mcp' && entry.source.server === selected.source?.server)
1310
+ .map(entry => h('li', { key: entry.id }, entry.name ?? entry.id)))) : null,
1311
+ selected.savedRule ? h(React.Fragment, null, ...['availability', 'disclosure'].map(dimension =>
1312
+ h('div', { className: 'ptcPlusCapabilityFilter', key: dimension, 'data-capability-dimension': dimension },
1313
+ h('span', null, t(`capabilities.policy.${dimension}`)),
1314
+ h(Select, { label: t(`capabilities.policy.${dimension}`), disabled: !enabled || !available || pending || writesBlocked,
1315
+ value: choices[selected.id]?.[dimension] ?? selected.temporaryRule?.[dimension] ?? 'inherit',
1316
+ onChange: next => chooseDimension(selected.id, dimension, next),
1317
+ options: ['inherit', 'automatic', ...(dimension === 'availability' ? ['enabled', 'closed'] : CAPABILITY_DISCLOSURES)]
1318
+ .map(choice => ({ value: choice, label: t(choice === 'inherit' ? 'capabilities.quick.inherit'
1319
+ : choice === 'automatic' ? 'capabilities.policy.inheritRule' : choice === 'enabled' ? 'capabilities.quick.enabled' : `capabilities.${choice}`) })) }))))
1320
+ : h('div', { className: 'ptcPlusCapabilityFilter' },
1321
+ h('span', null, t('capabilities.quick.choice')),
1322
+ h(Select, { label: t('capabilities.quick.choice'), value: choiceFor(selected),
1323
+ disabled: !enabled || !available || pending || writesBlocked, options: choiceOptions,
1324
+ onChange: next => choose(selected.id, next) })),
1325
+ choices[selected.id] === 'inherit' ? h('p', { className: 'ptcPlusDetail' },
1326
+ t('capabilities.quick.inheritSessionOnly')) : null) : h('p', null, t('capabilities.noMatches')))),
1327
+ h('div', { className: 'ptcPlusSessionCapabilityFooter' },
1328
+ h('span', { role: 'status' }, pending ? t('capabilities.quick.applying') : status ? t(status.key, status.params)
1329
+ : t('capabilities.quick.changes', { count: staged.length })),
1330
+ h('button', { type: 'button', className: 'ptcPlusButton', disabled: pending || staged.length === 0, onClick: discard },
1331
+ t('capabilities.quick.discard')),
1332
+ h('button', { type: 'button', className: 'ptcPlusButton', title: t('capabilities.quick.sessionHint'),
1333
+ disabled: pending || !enabled || !available || writesBlocked || staged.length === 0, onClick: () => void apply('session') },
1334
+ t('capabilities.quick.applySession')),
1335
+ h('button', { type: 'button', className: 'ptcPlusButton', 'data-kind': 'primary',
1336
+ title: t(hasInheritanceReset ? 'capabilities.quick.inheritSessionOnly' : 'capabilities.quick.globalHint'),
1337
+ disabled: pending || !enabled || !available || writesBlocked || staged.length === 0 || hasInheritanceReset,
1338
+ onClick: () => void apply('global') },
1339
+ t('capabilities.quick.applyGlobal'))))
1340
+ }
1341
+ }