dsh-ptc-plus 0.4.10 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (125) hide show
  1. package/README.en.md +65 -127
  2. package/README.md +65 -127
  3. package/assets/dsh-ptc-plus-banner-en.webp +0 -0
  4. package/assets/dsh-ptc-plus-banner-zh.webp +0 -0
  5. package/assets/ptc-plus-repl-workspace-en.png +0 -0
  6. package/assets/ptc-plus-repl-workspace-zh.png +0 -0
  7. package/assets/ptc-plus-settings-en.png +0 -0
  8. package/assets/ptc-plus-settings-zh.png +0 -0
  9. package/client.cjs +42702 -0
  10. package/cordis.patch.yml +1 -0
  11. package/docs/README.md +6 -2
  12. package/docs/adr/0005-temporary-rejected-cell-edit-transport.md +31 -198
  13. package/docs/adr/0008-define-agent-operating-contract.md +5 -1
  14. package/docs/adr/0009-bound-expensive-acceptance-default.md +2 -2
  15. package/docs/adr/0016-archive-resolved-review-findings.md +1 -1
  16. package/docs/adr/0017-track-the-latest-dsh-public-surface.md +4 -4
  17. package/docs/adr/0019-plugin-settings-and-kill-switch.md +1 -1
  18. package/docs/adr/0020-optional-cordis-tools-in-ptc-mode.md +1 -1
  19. package/docs/adr/0022-stage-npm-releases-with-oidc.md +4 -2
  20. package/docs/adr/0023-global-user-bindings.md +2 -0
  21. package/docs/adr/0026-persist-the-delivery-verdict-against-the-verified-candidate.md +2 -0
  22. package/docs/adr/0028-attach-to-the-host-ptc-execution-seam.md +25 -23
  23. package/docs/adr/0031-close-semantic-obligations-by-owner.md +1 -1
  24. package/docs/adr/0034-keep-program-values-separate-from-output-projections.md +29 -0
  25. package/docs/adr/0035-run-coverage-only-before-a-new-release.md +27 -0
  26. package/docs/adr/0036-separate-focused-development-from-delivery-verification.md +29 -0
  27. package/docs/adr/0037-keep-tool-hover-previews-passive.md +52 -0
  28. package/docs/adr/0038-freeze-session-capability-plans.md +71 -0
  29. package/docs/adr/0039-verify-documentation-without-runtime-suites.md +29 -0
  30. package/docs/adr/0040-separate-capability-configurations-from-session-observations.md +39 -0
  31. package/docs/adr/0041-record-cell-execution-directories.md +65 -0
  32. package/docs/adr/0042-persist-provider-availability-and-handback.md +41 -0
  33. package/docs/adr/0043-compose-capability-and-context-policies.md +37 -0
  34. package/docs/adr/0044-coexist-with-the-official-persistent-repl.md +35 -0
  35. package/docs/adr/README.md +8 -0
  36. package/docs/architecture.md +23 -25
  37. package/docs/capability-planning-components.md +93 -0
  38. package/docs/capability-projection.md +7 -0
  39. package/docs/client-ui.md +70 -23
  40. package/docs/client-visual-contract.md +26 -9
  41. package/docs/context-management.md +22 -0
  42. package/docs/context-optimization-plan.md +353 -0
  43. package/docs/context-public-interface-coverage.md +25 -0
  44. package/docs/durability-design.md +20 -15
  45. package/docs/evaluation.md +41 -6
  46. package/docs/evaluations/command-goat-deepseek-v4.1-flash-20261006.md +126 -0
  47. package/docs/evaluations/command-goat-deepseek-v4.1-flash.md +281 -0
  48. package/docs/installation.md +33 -10
  49. package/docs/program-data-plane.md +5 -3
  50. package/docs/publishing.md +7 -7
  51. package/docs/release-coverage.md +73 -0
  52. package/docs/release-notes.md +1 -1
  53. package/docs/review-procedure.md +56 -0
  54. package/docs/runtime-reference.md +53 -16
  55. package/docs/semantic-validation.md +14 -2
  56. package/docs/stateful-binding-updates-proposal.md +2 -2
  57. package/docs/user-bindings.md +1 -1
  58. package/docs/value-wire.md +11 -1
  59. package/docs/verification-optimization-work.md +26 -15
  60. package/docs/verification.md +30 -88
  61. package/index.js +146 -20
  62. package/internal/binding-toggle-receipt.js +24 -0
  63. package/internal/capability-command-receipt.js +31 -0
  64. package/internal/capability-environment.js +1253 -0
  65. package/internal/capability-plan.js +285 -0
  66. package/internal/capability-policy.js +96 -0
  67. package/internal/capability-statistics-store.js +180 -0
  68. package/internal/capability-statistics.js +214 -0
  69. package/internal/config-spec.js +89 -10
  70. package/internal/direct-surface-owner.js +27 -29
  71. package/internal/dynamic-environment-runtime.js +9 -9
  72. package/internal/edit-transport-owner.js +195 -140
  73. package/internal/execution-provider-owner.js +40 -12
  74. package/internal/execution-seam-compat.js +26 -8
  75. package/internal/kernel-worker.js +104 -32
  76. package/internal/native-root-dynamic.js +12 -13
  77. package/internal/official-repl-priority.js +51 -0
  78. package/internal/program-bindings.js +190 -0
  79. package/internal/recovery-tips.js +3 -2
  80. package/internal/rejected-cell-editor.js +8 -8
  81. package/internal/repl-observation-interest.js +11 -3
  82. package/internal/rpc-contract.js +1 -0
  83. package/internal/runtime-bridge-owner.js +275 -34
  84. package/internal/runtime-config.js +58 -0
  85. package/internal/runtime-contexts.js +74 -5
  86. package/internal/runtime-intrinsics.js +1 -0
  87. package/internal/runtime-messages.js +41 -0
  88. package/internal/sdk-projection.js +10 -82
  89. package/internal/session-cell-executor.js +42 -19
  90. package/internal/session-events.js +34 -0
  91. package/internal/session-journal-recovery.js +234 -11
  92. package/internal/session-journal-schema.js +6 -3
  93. package/internal/session-journal.js +31 -2
  94. package/internal/session-log-view.js +1 -1
  95. package/internal/session-runtime.js +119 -15
  96. package/internal/settings-compat.js +12 -5
  97. package/internal/stateful-module-compiler.js +3 -2
  98. package/internal/stateful-module-runtime.js +7 -3
  99. package/internal/stateful-root-runtime.js +9 -4
  100. package/internal/user-binding-authoring.js +2 -7
  101. package/internal/user-bindings-owner.js +19 -2
  102. package/internal/validated-parse-repair.js +1 -1
  103. package/internal/value-wire.js +68 -21
  104. package/internal/worker-cwd-virtualization.js +20 -18
  105. package/internal/worker-realm-surfaces.js +3 -3
  106. package/package.json +49 -30
  107. package/src/client-activity.js +3 -2
  108. package/src/client-authoring-view.js +67 -30
  109. package/src/client-binding-review.js +2 -1
  110. package/src/client-binding-toggle.js +18 -0
  111. package/src/client-capabilities-view.js +1555 -0
  112. package/src/client-capability-sort.js +29 -0
  113. package/src/client-copy.js +923 -8
  114. package/src/client-feature-gates.js +3 -0
  115. package/src/client-indicator-view.js +4 -6
  116. package/src/client-primitives.js +33 -2
  117. package/src/client-repl-view.js +20 -13
  118. package/src/client-settings-view.js +411 -41
  119. package/src/client-styles.js +112 -15
  120. package/src/client-tool-hover.js +185 -0
  121. package/src/client-tool-view.js +45 -9
  122. package/src/client-transport.js +44 -9
  123. package/src/client-workbench.js +6 -7
  124. package/src/client.js +76 -22
  125. package/client.js +0 -32479
@@ -0,0 +1,1555 @@
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
+ operation.excluded ? h('p', { className: 'ptcPlusDetail' }, t('capabilities.batch.excluded', { count: operation.excluded })) : null,
290
+ operation.targets.length ? h('ul', { className: 'ptcPlusCapabilityBatchTargets', 'aria-label': t('capabilities.batch.targetList') },
291
+ ...operation.targets.map(target => h('li', { key: target.id }, h('span', null, target.name), h('code', null, target.id))))
292
+ : h('p', { className: 'ptcPlusDetail' }, t('capabilities.batch.empty')),
293
+ h('div', { className: 'ptcPlusCapabilityBatchFields' }, ...['availability', 'disclosure'].map(dimension =>
294
+ h('div', { key: dimension, className: 'ptcPlusCapabilityFilter' }, h('span', null, t(`capabilities.policy.${dimension}`)),
295
+ h(Select, { value: choices[dimension], disabled: blocked || !operation.targets.length,
296
+ label: t(`capabilities.batch.${dimension}`), onChange: choice => setChoices(current => ({ ...current, [dimension]: choice })),
297
+ options: ['keep', 'automatic', ...(dimension === 'availability' ? ['enabled', 'closed'] : CAPABILITY_DISCLOSURES)]
298
+ .map(choice => ({ value: choice, label: t(choice === 'keep' ? 'capabilities.batch.keep'
299
+ : choice === 'automatic' ? 'capabilities.policy.inheritRule'
300
+ : choice === 'enabled' ? 'capabilities.action.enable'
301
+ : choice === 'closed' ? 'capabilities.action.close' : `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 [picked, setPicked] = React.useState([])
325
+ const [filtersOpen, setFiltersOpen] = React.useState(false)
326
+ const [batchOperation, setBatchOperation] = React.useState(null)
327
+ const [inheritedRulesOpen, setInheritedRulesOpen] = React.useState(false)
328
+ const [localSessionId, setLocalSessionId] = React.useState('')
329
+ const sessionId = onSessionSelect ? selectedSessionId : localSessionId
330
+ const setSessionId = next => { setLocalSessionId(next); onSessionSelect?.(next) }
331
+ const baseline = JSON.parse(value.capabilityBaseline ?? CONFIG_DEFAULTS.capabilityBaseline)
332
+ const policy = capabilityPolicyForConfig(value)
333
+ const entries = capabilitySettingsEntries(value, catalog, plans, Boolean(catalogError))
334
+ const sourceKey = entry => JSON.stringify([entry.source?.kind ?? 'unknown', entry.source?.server ?? null,
335
+ entry.source?.bucket ?? null, entry.source?.provider ?? null])
336
+ const sourceLabel = entry => {
337
+ if (entry.source?.kind === 'skill') {
338
+ const bucket = entry.source.bucket
339
+ const known = ['project-dsh', 'project-agents', 'user-dsh', 'user-agents', 'runtime', 'bundled', 'custom']
340
+ const label = known.includes(bucket) ? t(`capabilities.source.${bucket}`)
341
+ : bucket ? `${t('capabilities.source.other-skill')} · ${bucket}` : t('capabilities.source.unknown')
342
+ return `${label}${entry.source.provider ? ` · ${entry.source.provider}` : ''}`
343
+ }
344
+ if (entry.source?.kind === 'mcp') return `MCP${entry.source.server ? ` · ${entry.source.server}` : ''}`
345
+ return t(`capabilities.source.${entry.source?.kind === 'host' ? 'host' : 'unknown'}`)
346
+ }
347
+ const matchesKind = (entry, type) => type === 'all' || (type === 'resource' ? entry.source?.kind === 'mcp'
348
+ : type === 'tool' ? entry.kind === 'tool' && entry.source?.kind !== 'mcp' : type === entry.kind)
349
+ const sources = new Map(entries.filter(entry => matchesKind(entry, kind)).map(entry => [sourceKey(entry), sourceLabel(entry)]))
350
+ const setType = next => { setKind(next); if (!entries.some(entry => matchesKind(entry, next) && sourceKey(entry) === source)) setSource('all') }
351
+ const setCompatibleSource = next => {
352
+ setSource(next)
353
+ if (next !== 'all' && !entries.some(entry => matchesKind(entry, kind) && sourceKey(entry) === next)) setKind('all')
354
+ }
355
+ const normalizedQuery = query.trim().toLowerCase()
356
+ const filtered = entries.filter(entry => matchesKind(entry, kind)
357
+ && (source === 'all' || sourceKey(entry) === source)
358
+ && (strategy === 'all' || (entry.rule.disclosure ?? 'automatic') === strategy)
359
+ && (availabilityRule === 'all' || (entry.rule?.availability ?? 'automatic') === availabilityRule)
360
+ && (!onlyCommon || entry.common)
361
+ && (availability === 'all' || entry.availability === availability)
362
+ && `${entry.name} ${entry.id} ${entry.description ?? ''} ${sourceLabel(entry)}`.toLowerCase().includes(normalizedQuery))
363
+ const defaultOrder = (a, b) => compare(a.name.toLowerCase(), b.name.toLowerCase()) || compare(a.id, b.id)
364
+ const state = item => item.availability === 'unknown' ? 'unknown' : item.availability === 'unavailable' ? 'unavailable'
365
+ : value.capabilityContextEnabled === false || value.capabilityManagementEnabled !== true ? 'unapplied'
366
+ : item.resolvedRule.availabilitySource.startsWith('mcp:') && item.resolvedRule.availability === 'closed' ? 'source-closed' : item.resolvedRule.availability
367
+ const type = item => item.source?.kind === 'mcp' ? 'resource' : item.kind
368
+ const displayedSource = item => item.source?.kind === 'mcp' ? item.source.server ?? sourceLabel(item) : sourceLabel(item)
369
+ const groups = new Map()
370
+ for (const entry of filtered) {
371
+ const group = entry.source?.kind === 'mcp' && entry.source.server ? `mcp:${entry.source.server}`
372
+ : entry.availability === 'unavailable' ? 'unavailable' : entry.kind
373
+ if (!groups.has(group)) groups.set(group, [])
374
+ groups.get(group).push(entry)
375
+ }
376
+ const groupOrder = ['tool', ...[...groups.keys()].filter(key => key.startsWith('mcp:')).sort(compare), 'skill', 'resource', 'other', 'unavailable']
377
+ const defaultEntries = groupOrder.flatMap(group => (groups.get(group) ?? []).sort(defaultOrder))
378
+ const visible = sortCapabilityEntries(defaultEntries, sort, (item, column) => column === 'name' ? item.name
379
+ : column === 'type' ? t(`capabilities.type.${type(item)}`)
380
+ : column === 'source' ? displayedSource(item)
381
+ : column === 'availability' ? t(`capabilities.rowState.${state(item)}`)
382
+ : t(`capabilities.${item.resolvedRule.disclosure}`))
383
+ const pickedIds = picked.filter(id => entries.some(item => item.id === id))
384
+ const pickedEntries = pickedIds.map(id => entries.find(item => item.id === id)).filter(Boolean)
385
+ const pickedTargets = pickedEntries.filter(item => item.availability === 'available')
386
+ const allVisiblePicked = visible.length > 0 && visible.every(item => pickedIds.includes(item.id))
387
+ const someVisiblePicked = visible.some(item => pickedIds.includes(item.id))
388
+ const togglePick = id => setPicked(current => current.includes(id) ? current.filter(item => item !== id) : [...current, id])
389
+ const toggleAllVisible = () => setPicked(current => allVisiblePicked
390
+ ? current.filter(id => !visible.some(item => item.id === id))
391
+ : [...new Set([...current, ...visible.map(item => item.id)])])
392
+ const entry = visible.find(item => item.id === selected)
393
+ const plan = sessionId ? plans?.find(item => item.sessionId === sessionId) : undefined
394
+ const effective = plan?.effective.entries.find(item => item.id === entry?.id)
395
+ const change = (key, next) => onChange(field(key), JSON.stringify(next))
396
+ const setManual = (id, selection) => change('capabilityPolicy', changeCapabilityRule(policy, id, 'disclosure', selection))
397
+ const clear = () => { setQuery(''); setKind('all'); setSource('all'); setStrategy('all'); setAvailabilityRule('all'); setOnlyCommon(false); setAvailability('all') }
398
+ const activeFilters = Number(kind !== 'all') + Number(source !== 'all') + Number(strategy !== 'all') + Number(availabilityRule !== 'all') + Number(onlyCommon) + Number(availability !== 'all')
399
+ const select = (label, current, update, options, blocked = false) => h('div', { className: 'ptcPlusCapabilityFilter' },
400
+ h('span', null, label), h(Select, { label, value: current, disabled: blocked, onChange: update,
401
+ options: options.map(([key, text]) => ({ value: key, label: text })) }))
402
+ const editorField = (label, control) => h('div', { className: 'ptcPlusCapabilityField' }, h('span', null, label), control)
403
+ const manualSelect = item => h('div', { className: 'ptcPlusCapabilityEditorFields' },
404
+ editorField(t('capabilities.enabledColumn'), h(Select, { value: item.rule.availability ?? 'automatic', disabled: disabled('capabilityPolicy'),
405
+ label: t('capabilities.policy.availabilityFor', { name: item.id }),
406
+ onChange: next => change('capabilityPolicy', changeCapabilityRule(policy, item.id, 'availability', next)),
407
+ options: ['automatic', 'enabled', 'closed'].map(choice => ({ value: choice, label: t(choice === 'automatic' ? 'capabilities.policy.inheritRule' : choice === 'enabled' ? 'capabilities.quick.enabled' : `capabilities.${choice}`) })) })),
408
+ editorField(t('capabilities.contextColumn'), h(Select, { value: item.rule.disclosure ?? 'automatic', disabled: disabled('capabilityPolicy'),
409
+ label: t('capabilities.disclosureFor', { name: item.id }),
410
+ onChange: next => setManual(item.id, next), options: ['automatic', ...CAPABILITY_DISCLOSURES]
411
+ .map(choice => ({ value: choice, label: t(choice === 'automatic' ? 'capabilities.policy.inheritRule' : `capabilities.${choice}`) })) })))
412
+ const commonSwitch = item => h('div', { className: 'ptcPlusCapabilitySwitchRow' },
413
+ h('div', { className: 'ptcPlusCapabilitySwitchText' },
414
+ h('span', { className: 'ptcPlusCapabilitySwitchLabel' }, t('capabilities.commonBaseline')),
415
+ h('span', { className: 'ptcPlusCapabilitySwitchHint' }, t(item.common ? 'capabilities.inBaseline' : 'capabilities.notInBaseline'))),
416
+ h('input', { type: 'checkbox', role: 'switch', className: 'ptcPlusCheck', checked: item.common,
417
+ disabled: disabled('capabilityBaseline'), 'aria-label': t('capabilities.baselineFor', { name: item.id }),
418
+ onChange: event => change('capabilityBaseline', event.target.checked
419
+ ? [...baseline.filter(id => id !== item.id), item.id] : baseline.filter(id => id !== item.id)) }))
420
+ const filterLabels = [kind !== 'all' ? t(`capabilities.type.${kind}`) : null,
421
+ source !== 'all' ? sources.get(source) ?? t('capabilities.previousSource') : null,
422
+ strategy !== 'all' ? t(strategy === 'automatic' ? 'capabilities.policy.inheritRule' : `capabilities.${strategy}`) : null,
423
+ availabilityRule !== 'all' ? t(availabilityRule === 'automatic' ? 'capabilities.policy.inheritRule'
424
+ : availabilityRule === 'enabled' ? 'capabilities.quick.enabled' : 'capabilities.closed') : null,
425
+ onlyCommon ? t('capabilities.onlyCommon') : null,
426
+ availability !== 'all' ? t(`capabilities.availability.${availability}`) : null].filter(Boolean)
427
+ const closeEditor = () => {
428
+ const button = [...(root.current?.querySelectorAll('[data-capability-edit-id]') ?? [])].find(item => item.dataset.capabilityEditId === selected)
429
+ setSelected(null); button?.focus({ preventScroll: true })
430
+ }
431
+ return h('div', { className: 'ptcPlusCapabilityChoices', ref: root },
432
+ h('div', { className: 'ptcPlusCapabilityToolbar' },
433
+ h('label', { className: 'ptcPlusSearch' }, h(SearchIcon),
434
+ h('input', { type: 'search', value: query, 'aria-label': t('capabilities.search'), placeholder: t('capabilities.search'),
435
+ onChange: event => setQuery(event.target.value) })),
436
+ h('div', { className: 'ptcPlusCapabilityTypes', role: 'group', 'aria-label': t('capabilities.types') },
437
+ ...kinds.filter(key => key !== 'other' || entries.some(item => item.kind === 'other')).map(key => h('button', {
438
+ key, type: 'button', 'aria-pressed': kind === key, onClick: () => setType(key),
439
+ }, t(`capabilities.type.${key}`)))),
440
+ h('button', { type: 'button', className: 'ptcPlusButton', 'aria-haspopup': 'dialog',
441
+ onClick: () => setFiltersOpen(true) }, t('capabilities.filterAction')),
442
+ h('button', { type: 'button', className: 'ptcPlusButton', 'aria-haspopup': 'dialog',
443
+ 'aria-label': t('capabilities.batchSettings'),
444
+ disabled: disabled('capabilityPolicy') || pickedTargets.length === 0,
445
+ onClick: () => setBatchOperation({
446
+ targets: pickedTargets.map(item => ({ id: item.id, name: item.name })),
447
+ excluded: pickedIds.length - pickedTargets.length,
448
+ }) }, t('capabilities.batchSettings'),
449
+ pickedIds.length ? h('span', { className: 'ptcPlusCapabilityBatchCount', 'aria-hidden': true,
450
+ title: t('capabilities.selectionCount', { count: pickedIds.length }) },
451
+ String(pickedIds.length)) : null),
452
+ h('button', { type: 'button', className: 'ptcPlusButton', 'aria-haspopup': 'dialog',
453
+ onClick: () => setInheritedRulesOpen(true) }, t('capabilities.inheritedRules'))),
454
+ filtersOpen ? h(CapabilityDialog, { title: t('capabilities.filterAction'), onClose: () => setFiltersOpen(false), t,
455
+ className: 'ptcPlusCapabilityFilterDialog', footer: h('button', { type: 'button', className: 'ptcPlusButton',
456
+ 'data-kind': 'primary', onClick: () => setFiltersOpen(false) }, t('capabilities.dialogDone')) },
457
+ h('div', { className: 'ptcPlusCapabilityFilterFields' },
458
+ select(t('capabilities.sourceFilter'), source, setCompatibleSource, [['all', t('capabilities.allSources')], ...sources,
459
+ ...(source !== 'all' && !sources.has(source) ? [[source, t('capabilities.previousSource')]] : [])]),
460
+ select(t('capabilities.policy.disclosureFilter'), strategy, setStrategy, [['all', t('capabilities.allStrategies')],
461
+ ...['automatic', ...CAPABILITY_DISCLOSURES].map(key => [key, t(key === 'automatic' ? 'capabilities.policy.inheritRule' : `capabilities.${key}`)])]),
462
+ select(t('capabilities.policy.availabilityFilter'), availabilityRule, setAvailabilityRule, [['all', t('capabilities.allStrategies')],
463
+ ...['automatic', 'enabled', 'closed'].map(key => [key, t(key === 'automatic' ? 'capabilities.policy.inheritRule' : key === 'enabled' ? 'capabilities.quick.enabled' : 'capabilities.closed')])]),
464
+ h('label', { className: 'ptcPlusCapabilityOnlyCommon' }, h('input', { type: 'checkbox', checked: onlyCommon,
465
+ onChange: event => setOnlyCommon(event.target.checked) }), t('capabilities.onlyCommon')),
466
+ select(t('capabilities.availabilityFilter'), availability, setAvailability,
467
+ ['all', 'available', 'unavailable', 'unknown'].map(key => [key, t(`capabilities.availability.${key}`)])))) : null,
468
+ inheritedRulesOpen ? h(CapabilityDialog, { title: t('capabilities.inheritedRules'), onClose: () => setInheritedRulesOpen(false), t,
469
+ footer: h('button', { type: 'button', className: 'ptcPlusButton', 'data-kind': 'primary',
470
+ onClick: () => setInheritedRulesOpen(false) }, t('capabilities.dialogDone')) },
471
+ h(PolicyRules, { value, catalog, disabled, onChange, t })) : null,
472
+ batchOperation ? h(CapabilityBatchDialog, { operation: batchOperation, value, entries, disabled, onChange,
473
+ onClose: () => setBatchOperation(null), t }) : null,
474
+ activeFilters ? h('div', { className: 'ptcPlusActiveFilters' }, h('span', null, filterLabels.join(' · ')),
475
+ h('button', { type: 'button', className: 'ptcPlusCapabilityLink', onClick: clear }, t('capabilities.clear'))) : null,
476
+ h('div', { className: 'ptcPlusCapabilityResultLine' }, h('span', { role: 'status' }, t('capabilities.results', { count: visible.length, total: entries.length })),
477
+ h('button', { type: 'button', className: 'ptcPlusCapabilityLink', onClick: onReload }, t('capabilities.reloadCatalog'))),
478
+ catalogError ? h('p', { role: 'status', className: 'ptcPlusMessage' }, t('capabilities.catalogUnavailable', { error: catalogError })) : null,
479
+ entries.length === 0 ? h('p', { className: 'ptcPlusDetail' }, t('capabilities.catalogEmpty'))
480
+ : visible.length === 0 ? h('div', { className: 'ptcPlusCapabilityEmpty' }, h('p', null, t('capabilities.noMatches')),
481
+ h('button', { type: 'button', className: 'ptcPlusCapabilityLink', onClick: clear }, t('capabilities.clear'))) :
482
+ h('div', { className: 'ptcPlusCapabilityTable', role: 'table', 'aria-label': t('capabilities.management') },
483
+ h('div', { className: 'ptcPlusCapabilityColumns', role: 'row' },
484
+ ...CAPABILITY_SORT_COLUMNS.map(column => h('span', { key: column.key, role: 'columnheader',
485
+ 'aria-sort': capabilitySortDirection(sort, column.key),
486
+ ...(column.key === 'name' ? { 'aria-label': t('capabilities.member'), className: 'ptcPlusCapabilityPickHead' } : {}) },
487
+ ...(column.key === 'name' ? [h('input', { type: 'checkbox', className: 'ptcPlusPick', checked: allVisiblePicked,
488
+ ref: node => { if (node) node.indeterminate = someVisiblePicked && !allVisiblePicked },
489
+ disabled: visible.length === 0, 'aria-label': t('capabilities.selectAllVisible'),
490
+ onChange: toggleAllVisible })] : []),
491
+ h('button', { type: 'button', className: 'ptcPlusCapabilitySort', title: t('capabilities.sortCycle'),
492
+ onClick: () => setSort(current => cycleCapabilitySort(current, column.key)) },
493
+ t(`capabilities.${column.label}`), h('span', { 'aria-hidden': true },
494
+ capabilitySortDirection(sort, column.key) === 'ascending' ? '↑' : capabilitySortDirection(sort, column.key) === 'descending' ? '↓' : '↕'))))),
495
+ h('div', { className: 'ptcPlusCapabilityList', role: 'rowgroup' }, ...visible.map(item => h('div', { key: item.id, className: 'ptcPlusCapabilityItem', role: 'row',
496
+ 'data-selected': entry?.id === item.id, 'data-availability': item.availability,
497
+ onClick: () => setSelected(item.id) },
498
+ h('div', { className: 'ptcPlusCapabilityIdentity', role: 'cell' },
499
+ h('input', { type: 'checkbox', className: 'ptcPlusPick', checked: pickedIds.includes(item.id),
500
+ 'aria-label': t('capabilities.selectForBatch', { name: item.name ?? item.id }),
501
+ onClick: event => event.stopPropagation(), onChange: () => togglePick(item.id) }),
502
+ h('div', { className: 'ptcPlusCapabilityIdentityText' },
503
+ h('span', { className: 'ptcPlusCapabilityName' }, item.name), h('code', null, item.id))),
504
+ h('span', { className: 'ptcPlusCapabilityBadge', role: 'cell', 'data-kind': type(item) }, t(`capabilities.type.${type(item)}`)),
505
+ h('span', { className: 'ptcPlusCapabilitySource', role: 'cell' }, displayedSource(item)),
506
+ // The cell stays a plain cell so its empty area selects the row; only the switch and
507
+ // its own label swallow the click, keeping a toggle from also opening the row editor.
508
+ h('div', { className: 'ptcPlusCapabilityAvailability', role: 'cell', 'data-state': state(item) },
509
+ h('input', { type: 'checkbox', role: 'switch', className: 'ptcPlusCheck', id: `ptcPlusCapabilityAvailability-${item.id}`,
510
+ checked: item.resolvedRule.availability === 'enabled',
511
+ disabled: disabled('capabilityPolicy') || state(item) === 'source-closed',
512
+ 'aria-label': t('capabilities.policy.availabilityFor', { name: item.id }),
513
+ onClick: event => event.stopPropagation(),
514
+ onChange: event => change('capabilityPolicy', changeCapabilityRule(policy, item.id, 'availability', event.target.checked ? 'enabled' : 'closed')) }),
515
+ h('label', { htmlFor: `ptcPlusCapabilityAvailability-${item.id}`,
516
+ onClick: event => event.stopPropagation() }, t(`capabilities.rowState.${state(item)}`))),
517
+ h('div', { role: 'cell', className: 'ptcPlusCapabilityContextCell' }, h('button', { type: 'button', className: 'ptcPlusCapabilityContext', 'data-capability-edit-id': item.id,
518
+ 'aria-label': t('capabilities.editContextFor', { name: item.id }), 'aria-haspopup': 'dialog',
519
+ onClick: event => { event.stopPropagation(); setSelected(item.id) } },
520
+ h('span', null, t(`capabilities.${item.resolvedRule.disclosure}`)),
521
+ h('svg', { width: 16, height: 16, viewBox: '0 0 24 24', fill: 'none', stroke: 'currentColor', strokeWidth: 1.5, 'aria-hidden': true },
522
+ h('path', { d: 'm15 5 4 4M4 20l4-1L20 7l-4-4L4 15Z' })))))))),
523
+ entry ? h(CapabilityDialog, { title: entry.name, onClose: closeEditor, t, className: 'ptcPlusCapabilityEditorDialog',
524
+ footer: h('button', { type: 'button', className: 'ptcPlusButton', onClick: closeEditor }, t('capabilities.closeDetails')) },
525
+ h('code', null, entry.id), h('p', { className: 'ptcPlusDetail' }, t('capabilities.contextHelp')),
526
+ manualSelect(entry), commonSwitch(entry),
527
+ h('p', { className: 'ptcPlusDetail' }, t(value.capabilityContextEnabled === false ? 'capabilities.effect.paused'
528
+ : value.capabilityManagementEnabled !== true ? 'capabilities.effect.rulesOff'
529
+ : entry.resolvedRule.availabilitySource.startsWith('mcp:') && entry.resolvedRule.availability === 'closed' ? 'capabilities.effect.sourceClosed'
530
+ : entry.resolvedRule.availability === 'closed' ? 'capabilities.effect.closed'
531
+ : `capabilities.effect.disclosure.${entry.resolvedRule.disclosure}`)),
532
+ h('p', { className: 'ptcPlusDetail' }, t('capabilities.policy.resolved', {
533
+ availability: t(entry.resolvedRule.availability === 'enabled' ? 'capabilities.quick.enabled' : 'capabilities.closed'),
534
+ disclosure: t(`capabilities.${entry.resolvedRule.disclosure}`),
535
+ source: `${entry.resolvedRule.availabilitySource} · ${entry.resolvedRule.disclosureSource}` })),
536
+ entry.rule.disclosure !== undefined ? h('button', { type: 'button', className: 'ptcPlusCapabilityLink', disabled: disabled('capabilityPolicy'),
537
+ onClick: () => setManual(entry.id, 'automatic') }, t('capabilities.restoreAutomatic')) : null,
538
+ entry.source?.kind === 'mcp' && entry.mcpServer ? h('section', { className: 'ptcPlusCapabilityEditorSection' },
539
+ h('h5', null, t('capabilities.sourceRules')),
540
+ h('p', { className: 'ptcPlusDetail' }, t('capabilities.serverHint')),
541
+ h('div', { className: 'ptcPlusCapabilityEditorFields' },
542
+ ...['availability', 'disclosure'].map(dimension => editorField(t(`capabilities.policy.${dimension}`),
543
+ h(Select, { value: policy.mcpServers[entry.mcpServer]?.[dimension] ?? 'automatic', disabled: disabled('capabilityPolicy'),
544
+ label: `${entry.mcpServer} · ${t(`capabilities.policy.${dimension}`)}`,
545
+ onChange: next => change('capabilityPolicy', changeCapabilityRule(policy, `mcpServers.${entry.mcpServer}`, dimension, next)),
546
+ options: ['automatic', ...(dimension === 'availability' ? ['enabled', 'closed'] : CAPABILITY_DISCLOSURES)].map(key => ({ value: key,
547
+ label: t(key === 'automatic' ? 'capabilities.policy.inheritRule' : key === 'enabled' ? 'capabilities.quick.enabled' : `capabilities.${key}`) })) }))))) : null,
548
+ entry.kind === 'skill' ? h('p', { className: 'ptcPlusDetail' }, t('capabilities.skillDisclosure')) : null,
549
+ entry.kind === 'skill' && entry.invocationConflict ? h('p', { className: 'ptcPlusDetail' }, t('capabilities.invocation.varies')) : null,
550
+ entry.kind === 'skill' && entry.invocation ? h('p', { className: 'ptcPlusDetail' },
551
+ t(entry.invocation.modelInvocable ? entry.invocation.userInvocable ? 'capabilities.invocation.both'
552
+ : 'capabilities.invocation.model' : 'capabilities.invocation.user', { name: entry.name })) : null,
553
+ entry.description ? h('section', { className: 'ptcPlusCapabilityEditorSection' },
554
+ h('h5', null, t('capabilities.catalogDescription')), h('p', { className: 'ptcPlusCapabilityProse' }, entry.description)) : null,
555
+ entry.availability === 'unavailable' ? h('button', { type: 'button', className: 'ptcPlusCapabilityLink',
556
+ disabled: disabled('capabilityBaseline') || disabled('capabilityPolicy'), onClick: () => {
557
+ const next = { ...policy, items: { ...policy.items } }; delete next.items[entry.id]; change('capabilityPolicy', next)
558
+ if (baseline.includes(entry.id)) change('capabilityBaseline', baseline.filter(id => id !== entry.id))
559
+ } }, t('capabilities.removeSaved')) : null,
560
+ value.capabilityContextEnabled !== false && value.capabilityManagementEnabled === true ? h('section', { className: 'ptcPlusCapabilityEditorSection' },
561
+ h('h5', null, t('capabilities.currentSession')),
562
+ editorField(t('capabilities.sessionSelector'), h(Select, { value: sessionId ?? '', label: t('capabilities.sessionSelector'), onChange: setSessionId,
563
+ options: [{ value: '', label: t('capabilities.chooseSession') }, ...(plans ?? []).map(item => ({ value: item.sessionId, label: item.sessionId }))] })),
564
+ plansError ? h('p', { role: 'status', className: 'ptcPlusDetail' }, t('capabilities.unavailable', { error: plansError }))
565
+ : !plan ? h('p', { className: 'ptcPlusDetail' }, t('capabilities.selectObservation')) : h(React.Fragment, null,
566
+ h('p', null, effective ? t(`capabilities.${effective.selection}`) : t('capabilities.notInSession')),
567
+ effective ? h('p', { className: 'ptcPlusDetail' }, capabilityPlanReason(t, effective)) : null,
568
+ effective?.evidence ? h('p', { className: 'ptcPlusDetail' }, `${effective.evidence.used}/${effective.evidence.eligible} (${effective.evidence.scope})`) : null,
569
+ plan.pendingRefresh ? h('p', { className: 'ptcPlusDetail' }, t('capabilities.pending')) : null)) : null) : null)
570
+
571
+ }
572
+
573
+ function AllocationEvidence({ allocation, allocationError, allocationPending, selectedSessionId, statistics = false, onReload, t }) {
574
+ const [statisticsScope, setStatisticsScope] = React.useState('workspace')
575
+ if (allocationPending) return h('p', { role: 'status' }, t('state.syncing'))
576
+ if (allocationError) return h('p', { role: 'status', className: 'ptcPlusMessage' }, t('capabilities.unavailable', { error: allocationError }))
577
+ if (!allocation) return selectedSessionId ? h('p', { className: 'ptcPlusCapabilityNotice' }, t('capabilities.observationUnavailable')) : null
578
+ const { plan, coverage, configuration } = allocation
579
+ const count = selection => plan.entries.filter(entry => entry.selection === selection).length
580
+ const evidenceFor = item => coverage?.[item.id]?.[statisticsScope]
581
+ const usable = evidence => evidence?.status === 'usable' && Number.isFinite(evidence.used) && Number.isFinite(evidence.eligible)
582
+ const coverageText = evidence => usable(evidence) && Number.isFinite(evidence.coverage) ? `${Math.round(evidence.coverage * 100)}%`
583
+ : usable(evidence) ? t('capabilities.statStatus.no-samples')
584
+ : t(`capabilities.statStatus.${evidence?.status ?? 'unavailable'}`)
585
+ return h('section', { className: 'ptcPlusAllocationEvidence', 'aria-label': t(statistics ? 'capabilities.observedUsage' : 'capabilities.budgetPreview') },
586
+ h('div', { className: 'ptcPlusCapabilityResultLine' }, h('h4', null, t(statistics ? 'capabilities.observedUsage' : 'capabilities.budgetPreview')),
587
+ h('button', { type: 'button', className: 'ptcPlusCapabilityLink', onClick: onReload }, t('capabilities.reloadObservation'))),
588
+ h('p', { className: 'ptcPlusDetail' }, t('capabilities.observationScope', { workspace: allocation.workspace, session: allocation.sessionId })),
589
+ h('p', { className: 'ptcPlusDetail' }, t(statistics ? 'capabilities.familyHint' : 'capabilities.previewHint')),
590
+ statistics ? h(Select, { value: statisticsScope, label: t('capabilities.coverageScope'), onChange: setStatisticsScope,
591
+ options: ['workspace', 'global'].map(key => ({ value: key, label: t(`capabilities.scope.${key}`) })) }) : null,
592
+ !statistics ? h('p', { className: 'ptcPlusCapabilityNotice' }, t('capabilities.coldStartHint')) : null,
593
+ !statistics ? h(React.Fragment, null,
594
+ h('div', { className: 'ptcPlusAllocationMetrics' },
595
+ ...['full', 'on-demand', 'closed'].map(selection => h('div', { key: selection }, h('strong', null, count(selection)), h('span', null, t(`capabilities.${selection}`)))),
596
+ h('div', null, h('strong', null, `${plan.budgets.contractBytes} / ${configuration.capabilityBudgetBytes}`), h('span', null, 'UTF-8 bytes'))),
597
+ h('progress', { max: configuration.capabilityBudgetBytes, value: Math.min(configuration.capabilityBudgetBytes, plan.budgets.contractBytes),
598
+ 'aria-label': t('capabilities.budgetPreview') }),
599
+ h('p', { className: 'ptcPlusDetail' }, t('capabilities.budget', { bytes: plan.budgets.contractBytes, overage: plan.budgets.overageBytes })),
600
+ h('p', { className: 'ptcPlusDetail' }, t('capabilities.directoryUsage', { bytes: plan.budgets.directoryBytes, budget: configuration.capabilityDirectoryBytes })),
601
+ !plan.enabled ? h('p', { className: 'ptcPlusCapabilityNotice' }, t('capabilities.managementOff')) : null)
602
+ : coverage === null ? h('p', { role: 'status' }, t('capabilities.statStatus.unavailable')) : null,
603
+ h('div', { className: 'ptcPlusCapabilityEvidenceTable' }, h('table', null,
604
+ h('thead', null, h('tr', null, ...[t('capabilities.member'), ...(statistics
605
+ ? [t('capabilities.usedFamilies'), t('capabilities.eligibleFamilies'), t('capabilities.coverageColumn')]
606
+ : [t('capabilities.selection'), t('capabilities.reason'), t('capabilities.contractSize')])].map((label, key) => h('th', { key }, label)))),
607
+ h('tbody', null, ...allocation.capabilities.map(item => {
608
+ const entry = plan.entries.find(entry => entry.id === item.id)
609
+ return h('tr', { key: item.id }, h('td', null, item.id), ...(statistics
610
+ ? [h('td', { key: 'used' }, usable(evidenceFor(item)) ? evidenceFor(item).used : '—'),
611
+ h('td', { key: 'eligible' }, usable(evidenceFor(item)) ? evidenceFor(item).eligible : '—'),
612
+ h('td', { key: 'coverage' }, coverageText(evidenceFor(item)), usable(evidenceFor(item)) && Number.isFinite(evidenceFor(item).coverage)
613
+ ? h('progress', { max: 1, value: evidenceFor(item).coverage, 'aria-label': t('capabilities.coverageFor', { name: item.id }) }) : null)]
614
+ : [h('td', { key: 'selection' }, t(`capabilities.${entry?.selection ?? 'host'}`)), h('td', { key: 'reason' }, entry ? capabilityPlanReason(t, entry) : '—'), h('td', { key: 'size' }, `${item.contractBytes} B`)]))
615
+ })))))
616
+ }
617
+
618
+ function ConfigurationProfiles({ value, plans, disabled, writable, onChange, onApplyConfiguration, t }) {
619
+ const library = validateCapabilityProfiles(JSON.parse(value.capabilityProfiles ?? CONFIG_DEFAULTS.capabilityProfiles))
620
+ const [selectedId, setSelectedId] = React.useState('')
621
+ const [action, setAction] = React.useState(null)
622
+ const [name, setName] = React.useState('')
623
+ const [rename, setRename] = React.useState('')
624
+ const [copyName, setCopyName] = React.useState('')
625
+ const [status, setStatus] = React.useState(null)
626
+ const [busy, setBusy] = React.useState(false)
627
+ const [sessionId, setSessionId] = React.useState('')
628
+ const fileInput = React.useRef(null)
629
+ const saveAction = React.useRef(null)
630
+ const returnToLibrary = React.useRef(false)
631
+ const live = React.useRef(null)
632
+ const lifecycle = React.useRef(0)
633
+ const mounted = React.useRef(true)
634
+ const running = React.useRef(null)
635
+ const unavailable = disabled('capabilityProfiles') || busy
636
+ live.current = { library, writable }
637
+ React.useEffect(() => { mounted.current = true; return () => { lifecycle.current++; mounted.current = false } }, [])
638
+ React.useEffect(() => {
639
+ if (!writable) { lifecycle.current++; running.current = null; setBusy(false) }
640
+ }, [writable])
641
+ React.useLayoutEffect(() => {
642
+ if (!busy && returnToLibrary.current) {
643
+ returnToLibrary.current = false
644
+ saveAction.current?.focus({ preventScroll: true })
645
+ }
646
+ }, [busy])
647
+ const finishAction = () => { returnToLibrary.current = true; setAction(null) }
648
+ const selected = library.profiles.find(profile => profile.id === selectedId) ?? library.profiles[0]
649
+ const current = () => capabilityProfileConfiguration(Object.fromEntries(CAPABILITY_CONFIGURATION_KEYS.map(key => [key, value[key]])))
650
+ const perform = async (area, operation) => {
651
+ if (!mounted.current || !live.current?.writable || running.current) return
652
+ const token = { area }
653
+ running.current = token
654
+ const generation = lifecycle.current
655
+ setBusy(true); setStatus(null)
656
+ const assertCurrent = () => {
657
+ if (!mounted.current || !live.current?.writable || generation !== lifecycle.current) throw new Error(t('capabilities.configurationUnavailable'))
658
+ }
659
+ try { await operation(assertCurrent); if (generation === lifecycle.current) setStatus({ area, key: 'capabilities.configurationSaved' }) }
660
+ catch (error) { if (generation === lifecycle.current) setStatus({ area, error: String(error?.message ?? error) }) }
661
+ finally { if (running.current === token) running.current = null; if (generation === lifecycle.current) setBusy(false) }
662
+ }
663
+ const saveLibrary = async profiles => {
664
+ if (!mounted.current || !live.current?.writable) throw new Error(t('capabilities.configurationUnavailable'))
665
+ const next = validateCapabilityProfiles({ schema: library.schema, profiles })
666
+ if (!await onChange(field('capabilityProfiles'), JSON.stringify(next))) throw new Error(t('capabilities.configurationUnavailable'))
667
+ }
668
+ const save = async (configuration, requestedName = name) => {
669
+ const existing = library.profiles.find(profile => profile.name === requestedName.trim())
670
+ const profile = { id: existing?.id ?? configurationId(), name: requestedName.trim(), configuration }
671
+ await saveLibrary([...library.profiles.filter(item => item.id !== profile.id), profile])
672
+ setSelectedId(profile.id)
673
+ setRename(profile.name)
674
+ finishAction()
675
+ }
676
+ const exportProfiles = (scope = 'all') => {
677
+ const document = { schema: library.schema, profiles: scope === 'selected' && selected ? [selected] : library.profiles }
678
+ const url = URL.createObjectURL(new Blob([JSON.stringify(document, null, 2) + '\n'], { type: 'application/json' }))
679
+ const link = globalThis.document.createElement('a')
680
+ link.href = url; link.download = 'ptc-plus-capability-configurations.json'; link.click()
681
+ const revoke = URL.revokeObjectURL.bind(URL)
682
+ setTimeout(() => revoke(url), 0)
683
+ }
684
+ const importProfiles = event => {
685
+ const file = event.target.files?.[0]; event.target.value = ''
686
+ if (!file) return
687
+ void perform('transfer', async assertCurrent => {
688
+ if (file.size > 2_000_000) throw new Error(t('capabilities.configurationTooLarge'))
689
+ const imported = validateCapabilityProfiles(JSON.parse(await file.text()))
690
+ assertCurrent()
691
+ const currentLibrary = live.current.library
692
+ const names = new Set(currentLibrary.profiles.map(profile => profile.name))
693
+ const merged = [...currentLibrary.profiles]
694
+ for (const profile of imported.profiles) {
695
+ let name = profile.name; let suffix = 2
696
+ while (names.has(name)) name = `${profile.name.slice(0, 68)} (${suffix++})`
697
+ names.add(name); merged.push({ ...profile, id: configurationId(), name })
698
+ }
699
+ await saveLibrary(merged)
700
+ })
701
+ }
702
+ const feedback = status || busy ? h('p', { className: 'ptcPlusTaskFeedback', role: status?.error ? 'alert' : 'status' },
703
+ status?.error ?? (busy ? t('state.syncing') : t(status.key))) : null
704
+ const frozen = plans?.find(plan => plan.sessionId === sessionId)
705
+ let matches = false
706
+ try { matches = selected && JSON.stringify(selected.configuration) === JSON.stringify(current()) } catch {}
707
+ const input = (label, text, update) => h('label', { className: 'ptcPlusCapabilityFilter' }, h('span', null, t(label)),
708
+ h('input', { className: 'ptcPlusInput', value: text, maxLength: 80, disabled: unavailable,
709
+ onChange: event => update(event.target.value), 'aria-label': t(label) }))
710
+ const button = (label, click, blocked = false, extra = {}) => h('button', { type: 'button', className: 'ptcPlusButton',
711
+ disabled: blocked, onClick: click, ...extra }, t(label))
712
+ const closeAction = () => setAction(null)
713
+ const setting = (label, text) => h('div', { className: 'ptcPlusConfigurationSummaryRow', key: label }, h('span', null, t(label)), h('span', null, text))
714
+ const configuration = selected?.configuration
715
+ const policy = configuration ? capabilityPolicyForConfig(configuration) : null
716
+ const enabled = key => t(configuration[key] ? 'capabilities.serverOn' : 'capabilities.serverOff')
717
+ return h('section', { className: 'ptcPlusConfigurationProfiles', 'aria-label': t('capabilities.savedConfigurations') },
718
+ h('div', { className: 'ptcPlusConfigurationLibrary' }, h('h4', null, t('capabilities.savedConfigurations')),
719
+ h('div', { className: 'ptcPlusConfigurationList', role: 'group', 'aria-label': t('capabilities.chooseConfiguration') },
720
+ ...library.profiles.map(profile => h('button', { type: 'button', key: profile.id,
721
+ className: 'ptcPlusConfigurationChoice', 'aria-pressed': selected?.id === profile.id,
722
+ onClick: () => { setSelectedId(profile.id); setRename(profile.name); setAction(null) } }, profile.name)),
723
+ !library.profiles.length ? h('p', { className: 'ptcPlusDetail' }, t('capabilities.noConfigurations')) : null),
724
+ button('capabilities.saveConfiguration', () => { setAction('save'); setStatus(null) }, unavailable,
725
+ { ref: saveAction, className: 'ptcPlusButton ptcPlusConfigurationSave', 'aria-label': t('capabilities.saveConfiguration'), 'aria-haspopup': 'dialog' }),
726
+ h('div', { className: 'ptcPlusConfigurationLibraryActions' },
727
+ button('capabilities.importConfigurations', () => fileInput.current.click(), unavailable),
728
+ h('button', { type: 'button', className: 'ptcPlusButton ptcPlusConfigurationMore', 'aria-label': t('capabilities.libraryMore'),
729
+ 'aria-haspopup': 'dialog', onClick: () => { setAction('library'); setStatus(null) } }, '···')),
730
+ h('input', { ref: fileInput, type: 'file', accept: '.json,application/json', hidden: true, disabled: unavailable,
731
+ onChange: importProfiles, 'aria-label': t('capabilities.importConfigurations') }),
732
+ action === 'library' ? h(CapabilityDialog, { title: t('capabilities.libraryActions'), onClose: closeAction, t,
733
+ className: 'ptcPlusCapabilityConfigurationDialog' },
734
+ h('div', { className: 'ptcPlusCapabilityDialogActions' },
735
+ button('capabilities.exportConfigurations', () => { exportProfiles('all'); closeAction() }, !library.profiles.length),
736
+ button('capabilities.copyFromSession', () => { setAction('copy'); setStatus(null) }, unavailable))) : null,
737
+ action === 'save' ? h(CapabilityDialog, { title: t('capabilities.saveConfiguration'), onClose: closeAction, t,
738
+ className: 'ptcPlusCapabilityConfigurationDialog' },
739
+ h('form', { className: 'ptcPlusConfigurationAction', onSubmit: event => {
740
+ event.preventDefault(); if (!unavailable && name.trim()) void perform('save', () => save(current()))
741
+ } }, input('capabilities.configurationName', name, setName),
742
+ h('p', { className: 'ptcPlusDetail' }, t('capabilities.sameNameHint')),
743
+ h('div', { className: 'ptcPlusCapabilityDialogActions' },
744
+ h('button', { type: 'button', className: 'ptcPlusButton', onClick: closeAction }, t('capabilities.batch.cancel')),
745
+ button('capabilities.confirmSave', undefined, unavailable || !name.trim(), { type: 'submit' })))) : null,
746
+ action === 'copy' ? h(CapabilityDialog, { title: t('capabilities.copyFromSession'), onClose: closeAction, t,
747
+ className: 'ptcPlusCapabilityConfigurationDialog' },
748
+ h('div', { className: 'ptcPlusConfigurationAction' }, h('p', { className: 'ptcPlusDetail' }, t('capabilities.copySessionHint')),
749
+ h(Select, { value: sessionId, disabled: unavailable, label: t('capabilities.copyFromSession'), onChange: setSessionId,
750
+ options: [{ value: '', label: t('capabilities.chooseSession') }, ...(plans ?? []).map(plan => ({ value: plan.sessionId, label: plan.sessionId }))] }),
751
+ input('capabilities.copyName', copyName, setCopyName),
752
+ h('div', { className: 'ptcPlusCapabilityDialogActions' },
753
+ h('button', { type: 'button', className: 'ptcPlusButton', onClick: closeAction }, t('capabilities.batch.cancel')),
754
+ button('capabilities.saveSessionConfiguration', () => perform('copy', () => save(capabilityProfileConfiguration(frozen.configuration), copyName)),
755
+ unavailable || !copyName.trim() || !frozen?.configuration)))) : null),
756
+ h('div', { className: 'ptcPlusConfigurationDetail' }, selected ? h(React.Fragment, null,
757
+ h('div', { className: 'ptcPlusTaskHead' }, h('h4', null, selected.name),
758
+ button('capabilities.applyConfiguration', () => perform('library', () => onApplyConfiguration(selected.configuration)), unavailable || !onApplyConfiguration,
759
+ { 'data-kind': 'primary' }),
760
+ h('button', { type: 'button', className: 'ptcPlusButton ptcPlusConfigurationMore', 'aria-label': t('capabilities.profileMore'),
761
+ 'aria-haspopup': 'dialog', onClick: () => { setRename(selected.name); setAction('profile') } }, '···')),
762
+ action === 'profile' ? h(CapabilityDialog, { title: t('capabilities.profileActions'), onClose: closeAction, t,
763
+ className: 'ptcPlusCapabilityConfigurationDialog' },
764
+ h('div', { className: 'ptcPlusCapabilityDialogActions' },
765
+ button('capabilities.renameConfiguration', () => { setRename(selected.name); setAction('rename') }, unavailable),
766
+ button('capabilities.exportConfiguration', () => { exportProfiles('selected'); closeAction() }),
767
+ button('capabilities.deleteConfiguration', () => perform('library', async () => {
768
+ await saveLibrary(library.profiles.filter(profile => profile.id !== selected.id)); setSelectedId(''); finishAction()
769
+ }), unavailable, { 'data-kind': 'danger' }))) : null,
770
+ h('div', { className: 'ptcPlusConfigurationSummary' },
771
+ setting('group.capabilities', enabled('capabilityContextEnabled')),
772
+ setting('capabilities.management', enabled('capabilityManagementEnabled')),
773
+ setting('capabilities.statisticsEnabled', t(JSON.parse(configuration.capabilityStatistics).enabled ? 'capabilities.serverOn' : 'capabilities.serverOff')),
774
+ setting('capabilities.defaultDisclosure', t(`capabilities.${policy.defaults.disclosure}`)),
775
+ setting('capabilities.allocationTitle', ['baseline', 'statistics', 'budget'].filter(key => policy.allocation[key]).map(key => t(`capabilities.allocation.${key}`)).join(' · ') || '—'),
776
+ setting('capabilities.recoveryEnabled', enabled('capabilityRecoveryEnabled')),
777
+ setting('capabilities.previewEnabled', `${enabled('modelPreviewEnabled')}${configuration.modelPreviewEnabled ? ` · ${t('capabilities.estimatedTokens', { count: configuration.modelPreviewTokens })}` : ''}`),
778
+ setting('capabilities.savedRules', t('capabilities.ruleCount', { count: Object.keys(policy.items).length + Object.keys(policy.categories).length + Object.keys(policy.mcpServers).length }))),
779
+ h('details', { className: 'ptcPlusConfigurationRules' }, h('summary', null, t('capabilities.profileDetails')),
780
+ h('div', { className: 'ptcPlusConfigurationSummary' },
781
+ setting('capabilities.commonBaseline', JSON.parse(configuration.capabilityBaseline).join(' · ') || '—'),
782
+ ...['categories', 'mcpServers', 'items'].flatMap(scope => Object.entries(policy[scope]).map(([name, rule]) =>
783
+ setting(scope === 'categories' ? `capabilities.policy.category.${name}` : name, capabilityRuleLabel(t, rule)))),
784
+ ...['capabilityBudgetBytes', 'capabilityDirectoryBytes', 'capabilityRecoveryBytes', 'modelPreviewTokens'].map(key => setting(`${key}.label`, configuration[key])),
785
+ ...['windowSize', 'minimumSamples', 'fullThreshold'].map(key => setting(`capabilities.${key}`,
786
+ key === 'fullThreshold' ? `${Math.round(JSON.parse(configuration.capabilityStatistics)[key] * 100)}%` : JSON.parse(configuration.capabilityStatistics)[key])))),
787
+ h('p', { className: 'ptcPlusDetail' }, t(matches ? 'capabilities.configurationMatches' : 'capabilities.configurationDifferent')),
788
+ h('p', { className: 'ptcPlusDetail' }, t('capabilities.configurationsHint')),
789
+ action === 'rename' ? h(CapabilityDialog, { title: t('capabilities.renameConfiguration'), onClose: closeAction, t,
790
+ className: 'ptcPlusCapabilityConfigurationDialog' },
791
+ h('form', { className: 'ptcPlusConfigurationAction', onSubmit: event => {
792
+ event.preventDefault(); if (!unavailable && rename.trim()) void perform('library', async () => {
793
+ await saveLibrary(library.profiles.map(profile => profile.id === selected.id ? { ...profile, name: rename.trim() } : profile)); finishAction()
794
+ })
795
+ } }, input('capabilities.renameTo', rename, setRename),
796
+ h('div', { className: 'ptcPlusCapabilityDialogActions' },
797
+ h('button', { type: 'button', className: 'ptcPlusButton', onClick: closeAction }, t('capabilities.batch.cancel')),
798
+ button('capabilities.confirmRename', undefined, unavailable || !rename.trim(), { type: 'submit' })))) : null)
799
+ : h('p', { className: 'ptcPlusDetail' }, t('capabilities.chooseConfiguration'))), feedback)
800
+ }
801
+
802
+ function ContextSettings({ value, configurationReady, disabled, onChange, t, renderField, allocation, allocationError, allocationPending, selectedSessionId, onReload, renderRefresh, id, section, setSection }) {
803
+ const sections = ['description', 'recovery', 'preview']
804
+ const saved = choicesFor(configurationReady ? value : undefined)
805
+ const retained = flagsFor(saved)
806
+ const applied = flagsFor(effectiveCapabilitySettings(saved))
807
+ const policy = capabilityPolicyForConfig(value)
808
+ const write = next => onChange(field('capabilityPolicy'), JSON.stringify(next))
809
+ const adaptive = [policy.defaults, ...Object.values(policy.categories), ...Object.values(policy.mcpServers), ...Object.values(policy.items)]
810
+ .some(rule => rule.disclosure === 'adaptive')
811
+ const summary = key => {
812
+ if (choiceState(retained, applied, key) === 'unknown') return t('capabilities.combination.unknownSettings')
813
+ if (key === 'description') {
814
+ const description = t('capabilities.context.summary.description', { disclosure: t(`capabilities.${policy.defaults.disclosure}`) })
815
+ if (policy.defaults.disclosure !== 'adaptive') return description
816
+ const criteria = ['baseline', 'statistics', 'budget'].filter(key => policy.allocation[key])
817
+ .map(key => t(`capabilities.allocation.${key}`)).join(t('capabilities.combination.separator'))
818
+ return `${description} · ${criteria || t('capabilities.context.summary.noCriteria')}`
819
+ }
820
+ return t(`capabilities.context.summary.${key}`, key === 'recovery'
821
+ ? { bytes: value.capabilityRecoveryBytes } : { tokens: value.modelPreviewTokens })
822
+ }
823
+ const effect = key => t(!configurationReady ? 'capabilities.combination.unknownSettings'
824
+ : value.capabilityContextEnabled === false ? 'capabilities.effect.paused' : key)
825
+ const criteria = h('section', { className: 'ptcPlusPolicyAllocation', 'aria-label': t('capabilities.allocationTitle') },
826
+ h('h5', null, t('capabilities.allocationTitle')),
827
+ h('div', { className: 'ptcPlusAllocationChoices' }, ...['baseline', 'statistics', 'budget'].map(key => h('label', { key },
828
+ h('input', { type: 'checkbox', role: 'switch', disabled: disabled('capabilityPolicy'), checked: policy.allocation[key],
829
+ 'aria-label': t(`capabilities.policy.allocate.${key}`),
830
+ onChange: event => write({ ...policy, allocation: { ...policy.allocation, [key]: event.target.checked } }) }),
831
+ h('span', null, t(`capabilities.allocation.${key}`))))),
832
+ h('p', { className: 'ptcPlusDetail' }, effect(value.capabilityManagementEnabled !== true ? 'capabilities.effect.rulesOff'
833
+ : adaptive ? 'capabilities.effect.allocationActive' : 'capabilities.effect.allocationRetained')),
834
+ policy.allocation.statistics ? h('p', { className: 'ptcPlusDetail' }, t('capabilities.effect.historicalStatistics')) : null,
835
+ h('p', { className: 'ptcPlusDetail' }, t('capabilities.effect.allocationLimits')))
836
+ const feature = (key, label) => h('label', { className: 'ptcPlusRow' }, h('span', { className: 'ptcPlusMain' }, t(label)),
837
+ h('input', { type: 'checkbox', role: 'switch', className: 'ptcPlusCheck', checked: value[key] === true,
838
+ disabled: disabled(key), 'aria-label': t(label), onChange: event => onChange(field(key), event.target.checked) }))
839
+ return h('div', { className: 'ptcPlusContextLayout' },
840
+ h('div', { className: 'ptcPlusContextNavigation', role: 'tablist', 'aria-orientation': 'vertical', 'aria-label': t('capabilities.contextNavigation') },
841
+ ...sections.map((key, index) => {
842
+ const state = choiceState(retained, applied, key)
843
+ const paused = state === 'paused'
844
+ return h('button', { key, type: 'button', role: 'tab', id: `${id}-context-tab-${key}`,
845
+ 'aria-label': t(`capabilities.context.${key}`), 'aria-describedby': `${id}-context-state-${key} ${id}-context-summary-${key}`,
846
+ 'aria-controls': `${id}-context-pane-${key}`, 'aria-selected': section === key, tabIndex: section === key ? 0 : -1,
847
+ onClick: () => setSection(key), onKeyDown: event => navigateTabs(event, sections, index, setSection),
848
+ }, h('span', null, t(`capabilities.context.${key}`)),
849
+ h('span', { className: 'ptcPlusContextState', id: `${id}-context-state-${key}`, 'data-enabled': state === 'selected',
850
+ 'data-paused': paused }, t(`capabilities.combination.state.${state}`)),
851
+ h('span', { className: 'ptcPlusContextSummary', id: `${id}-context-summary-${key}` }, summary(key)))
852
+ })),
853
+ h('div', { className: 'ptcPlusContextContent' }, ...sections.map(key => h('section', { key, role: 'tabpanel',
854
+ id: `${id}-context-pane-${key}`, 'aria-labelledby': `${id}-context-tab-${key}`, hidden: section !== key },
855
+ h('h4', null, t(`capabilities.context.${key}`)),
856
+ key === 'description' ? h(React.Fragment, null,
857
+ h('p', { className: 'ptcPlusDetail' }, t('capabilities.descriptionHelp')),
858
+ h('p', { className: 'ptcPlusDetail' }, t('capabilities.descriptionDependency')),
859
+ h('div', { className: 'ptcPlusCapabilityFilter' }, h('span', null, t('capabilities.defaultDisclosure')),
860
+ h(Select, { value: policy.defaults.disclosure, disabled: disabled('capabilityPolicy'), label: t('capabilities.defaultDisclosure'),
861
+ options: CAPABILITY_DISCLOSURES.map(choice => ({ value: choice, label: t(`capabilities.${choice}`) })),
862
+ onChange: disclosure => write({ ...policy, defaults: { ...policy.defaults, disclosure } }) })),
863
+ h('p', { className: 'ptcPlusDetail' }, effect(value.capabilityManagementEnabled !== true ? 'capabilities.effect.rulesOff' : `capabilities.effect.disclosure.${policy.defaults.disclosure}`)),
864
+ criteria,
865
+ h('section', { className: 'ptcPlusContextQuota', 'aria-label': t('capabilities.quotaSettings') }, h('h5', null, t('capabilities.quotaSettings')),
866
+ ...['capabilityBudgetBytes', 'capabilityDirectoryBytes'].map(renderField)),
867
+ value.capabilityManagementEnabled !== true ? h('p', { className: 'ptcPlusDetail' }, t('capabilities.descriptionInactive')) : null,
868
+ h('section', { className: 'ptcPlusAllocationPreview' },
869
+ !allocation || allocationPending || allocationError ? h('h5', null, t('capabilities.budgetPreview')) : null,
870
+ renderRefresh?.({ observation: true }), h(AllocationEvidence, { allocation, allocationError, allocationPending, selectedSessionId, onReload, t })))
871
+ : key === 'recovery' ? h(React.Fragment, null,
872
+ h('p', { className: 'ptcPlusDetail' }, t('capabilities.recoveryHelp')),
873
+ feature('capabilityRecoveryEnabled', 'capabilities.recoveryEnabled'),
874
+ h('p', { className: 'ptcPlusDetail' }, !configurationReady ? t('capabilities.combination.unknownSettings')
875
+ : value.capabilityRecoveryEnabled === true ? effect('capabilities.effect.recoveryOn') : t('capabilities.effect.recoveryOff')),
876
+ h('section', { className: 'ptcPlusContextQuota', 'aria-label': t('capabilities.quotaSettings') }, h('h5', null, t('capabilities.quotaSettings')), renderField('capabilityRecoveryBytes')))
877
+ : h(React.Fragment, null, h('p', { className: 'ptcPlusDetail' }, t('capabilities.previewHelp')),
878
+ feature('modelPreviewEnabled', 'capabilities.previewEnabled'),
879
+ h('p', { className: 'ptcPlusDetail' }, !configurationReady ? t('capabilities.combination.unknownSettings')
880
+ : value.modelPreviewEnabled === true ? effect('capabilities.effect.previewOn') : t('capabilities.effect.previewOff')),
881
+ h('section', { className: 'ptcPlusContextQuota', 'aria-label': t('capabilities.quotaSettings') }, h('h5', null, t('capabilities.quotaSettings')), renderField('modelPreviewTokens')))))))
882
+ }
883
+
884
+ function navigateTabs(event, tabs, index, update) {
885
+ const offset = ['ArrowRight', 'ArrowDown'].includes(event.key) ? 1 : ['ArrowLeft', 'ArrowUp'].includes(event.key) ? -1 : 0
886
+ const next = offset ? (index + offset + tabs.length) % tabs.length : event.key === 'Home' ? 0 : event.key === 'End' ? tabs.length - 1 : null
887
+ if (next === null) return
888
+ event.preventDefault(); update(tabs[next])
889
+ event.currentTarget.parentElement.querySelectorAll('[role=tab]')[next].focus({ preventScroll: true })
890
+ }
891
+
892
+ function CapabilityWorkbench({ value, configurationReady, catalog, catalogError, plans, plansError, plansPending, plansStale, disabled, onChange, onReload, t, renderField, renderPlans, renderRefresh, id, selectedSessionId, onSessionSelect,
893
+ allocation, allocationError, allocationPending, onApplyConfiguration, configurationWritable, saving, saveStatus }) {
894
+ const [tab, setTab] = React.useState('manual')
895
+ const [contextSection, setContextSection] = React.useState('description')
896
+ const tabs = ['manual', 'budgets', 'statistics', 'plans']
897
+ const feedback = saving ? t('state.syncing') : saveStatus && saveStatus.key !== 'status.applied' ? t(saveStatus.key, saveStatus.params)
898
+ : t(configurationWritable ? 'capabilities.savedForNew' : 'footer.readOnly')
899
+ return h('div', { className: 'ptcPlusCapabilityWorkbench' },
900
+ h(CombinationSummary, { value, configurationReady, plans, plansError, plansPending, plansStale, selectedSessionId,
901
+ feedback, onReload, renderRefresh, renderPlans, t, id, onNavigate: key => {
902
+ setTab(key === 'rules' ? 'manual' : key === 'statistics' ? 'statistics' : 'budgets')
903
+ if (['description', 'recovery', 'preview'].includes(key)) setContextSection(key)
904
+ } }),
905
+ h('div', { className: 'ptcPlusCapabilityTabs', role: 'tablist', 'aria-label': t('capabilities.settingsTabs') },
906
+ ...tabs.map((key, index) => h('button', { key, type: 'button', role: 'tab', id: `${id}-tab-${key}`,
907
+ 'aria-controls': `${id}-pane-${key}`, 'aria-selected': tab === key, tabIndex: tab === key ? 0 : -1,
908
+ onClick: () => setTab(key), onKeyDown: event => navigateTabs(event, tabs, index, setTab),
909
+ }, t(`capabilities.tab.${key}`)))),
910
+ ...tabs.map(key => h('div', { key, className: 'ptcPlusCapabilityPane', role: 'tabpanel', id: `${id}-pane-${key}`,
911
+ 'aria-labelledby': `${id}-tab-${key}`, hidden: tab !== key },
912
+ h('div', { className: 'ptcPlusTaskHead' }, h('h4', null, t(`capabilities.title.${key}`)),
913
+ key === 'manual' ? h('label', { className: 'ptcPlusCapabilityManagement' }, h('span', null, t('capabilities.management')),
914
+ h('input', { type: 'checkbox', role: 'switch', className: 'ptcPlusCheck', checked: value.capabilityManagementEnabled === true,
915
+ disabled: disabled('capabilityManagementEnabled'), 'aria-label': t('capabilities.management'),
916
+ onChange: event => onChange(field('capabilityManagementEnabled'), event.target.checked) })) : null),
917
+ key === 'manual' ? h(React.Fragment, null, h('p', { className: 'ptcPlusDetail' }, t(value.capabilityContextEnabled === false ? 'capabilities.effect.paused'
918
+ : value.capabilityManagementEnabled === true ? 'capabilities.managementHelp' : 'capabilities.effect.rulesOff')),
919
+ h(CapabilityChoices, { value, catalog, catalogError, plans, plansError, disabled, onChange, onReload, t, selectedSessionId, onSessionSelect }))
920
+ : key === 'statistics' ? h(React.Fragment, null, renderField('capabilityStatistics'),
921
+ h('p', { className: 'ptcPlusDetail' }, t(value.capabilityContextEnabled === false ? 'capabilities.effect.paused'
922
+ : choicesFor(value).statistics?.enabled === true ? 'capabilities.effect.collectionOn'
923
+ : choicesFor(value).statistics?.enabled === false ? 'capabilities.effect.collectionOff' : 'capabilities.combination.unknownSettings')),
924
+ h('p', { className: 'ptcPlusDetail' }, t('capabilities.effect.statisticsIndependent')),
925
+ renderRefresh?.({ observation: true }),
926
+ h(AllocationEvidence, { allocation, allocationError, allocationPending, selectedSessionId, statistics: true, onReload, t }))
927
+ : key === 'budgets' ? h(ContextSettings, { value, configurationReady, disabled, onChange, t, renderField, allocation, allocationError, allocationPending,
928
+ selectedSessionId, onReload, renderRefresh, id, section: contextSection, setSection: setContextSection })
929
+ : h(ConfigurationProfiles, { value, plans, disabled, writable: configurationWritable, onChange, onApplyConfiguration, t }))))
930
+ }
931
+
932
+ return { CapabilityWorkbench }
933
+ }
934
+
935
+ /** Execute a human command through DSH; a receipt never stands in for model publication. */
936
+ export function createCapabilityRefreshView(React, commands) {
937
+ const h = React.createElement
938
+ const absent = Object.freeze({ getSnapshot: () => false, subscribe: () => () => {} })
939
+ return function CapabilityRefreshButton({ sessionId, enabled, t, onApplied, onInvalidated }) {
940
+ const source = React.useMemo(() => sessionId ? commands.source(sessionId) : absent, [sessionId])
941
+ const available = React.useSyncExternalStore(source.subscribe, source.getSnapshot)
942
+ const [pending, setPending] = React.useState(false)
943
+ const [status, setStatus] = React.useState(null)
944
+ const request = React.useRef(null)
945
+ React.useEffect(() => {
946
+ setStatus(null)
947
+ setPending(false)
948
+ return () => { request.current?.abort(); request.current = null }
949
+ }, [sessionId, enabled, available])
950
+ const cancel = event => {
951
+ event.stopPropagation()
952
+ request.current?.abort(); request.current = null; setPending(false)
953
+ setStatus({ key: 'capabilities.refreshCancelled' })
954
+ onApplied?.()
955
+ }
956
+ const execute = async event => {
957
+ event.stopPropagation()
958
+ if (!enabled || !available || request.current) return
959
+ const controller = new AbortController()
960
+ request.current = controller
961
+ setPending(true)
962
+ setStatus(null)
963
+ onInvalidated?.()
964
+ try {
965
+ const result = await commands.run(sessionId, 'refresh', controller.signal)
966
+ if (controller.signal.aborted || request.current !== controller) return
967
+ setStatus(result.text ?? { key: 'capabilities.refreshAcknowledged' })
968
+ onApplied?.()
969
+ } catch (error) {
970
+ if (!controller.signal.aborted && request.current === controller) {
971
+ const text = String(error?.message ?? error)
972
+ if (!readCapabilityCommandReceipt(text)) {
973
+ setStatus({ key: 'capabilities.refreshFailed', params: { error: text } })
974
+ }
975
+ onApplied?.()
976
+ }
977
+ } finally {
978
+ if (request.current === controller) { request.current = null; setPending(false) }
979
+ }
980
+ }
981
+ return h('div', { className: 'ptcPlusCapabilityRefresh' },
982
+ h('button', { type: 'button', className: 'ptcPlusButton', disabled: !enabled || !available || pending,
983
+ title: !available ? t(sessionId ? 'capabilities.refreshUnavailable' : 'capabilities.chooseSession') : undefined,
984
+ onClick: execute }, t(pending ? 'capabilities.refreshing' : 'capabilities.applySession')),
985
+ pending ? h('button', { type: 'button', className: 'ptcPlusButton', onClick: cancel }, t('capabilities.cancelRefresh')) : null,
986
+ status ? h('p', { className: 'ptcPlusDetail', role: 'status' }, capabilityCommandText(t, status))
987
+ : !available ? h('p', { className: 'ptcPlusDetail' }, t(sessionId ? 'capabilities.refreshUnavailable' : 'capabilities.chooseSession')) : null)
988
+ }
989
+ }
990
+
991
+ /** Session capability choices use the real command transaction and a read-only catalog observation. */
992
+ export function createSessionCapabilityControlsView(React, { commands, Modal, Select, inspectSessionCatalog }) {
993
+ const h = React.createElement
994
+ const emptyDrafts = () => ({})
995
+ return function SessionCapabilityControls({ sessionId, open, enabled, t, onClose }) {
996
+ const source = React.useMemo(() => commands.source(sessionId), [sessionId])
997
+ const available = React.useSyncExternalStore(source.subscribe, source.getSnapshot)
998
+ const [catalog, setCatalog] = React.useState([])
999
+ const [management, setManagement] = React.useState('unknown')
1000
+ const [savedSettings, setSavedSettings] = React.useState(null)
1001
+ const [loading, setLoading] = React.useState(false)
1002
+ const [error, setError] = React.useState(null)
1003
+ const [drafts, setDrafts] = React.useState(emptyDrafts)
1004
+ const [picked, setPicked] = React.useState([])
1005
+ const [batchOpen, setBatchOpen] = React.useState(false)
1006
+ const [batchChoices, setBatchChoices] = React.useState({ availability: 'keep', disclosure: 'keep' })
1007
+ const [selectedId, setSelectedId] = React.useState(null)
1008
+ const [pane, setPane] = React.useState('list')
1009
+ const [query, setQuery] = React.useState('')
1010
+ const [kind, setKind] = React.useState('all')
1011
+ const [sourceFilter, setSourceFilter] = React.useState('all')
1012
+ const [stateFilter, setStateFilter] = React.useState('all')
1013
+ const [sort, setSort] = React.useState(null)
1014
+ const [focusDimension, setFocusDimension] = React.useState(null)
1015
+ const [status, setStatus] = React.useState(null)
1016
+ const [pending, setPending] = React.useState(false)
1017
+ const request = React.useRef(null)
1018
+ const applying = React.useRef(false)
1019
+ const mounted = React.useRef(true)
1020
+ const detail = React.useRef(null)
1021
+ const list = React.useRef(null)
1022
+ React.useEffect(() => {
1023
+ if (!focusDimension) return
1024
+ const field = focusDimension === 'list' ? list.current?.querySelector('[data-selected="true"] .ptcPlusSessionCapabilityName')
1025
+ : detail.current?.querySelector(`[data-capability-dimension="${focusDimension}"] [role="combobox"]`)
1026
+ ?? detail.current?.querySelector('[role="combobox"]')
1027
+ field?.focus({ preventScroll: true })
1028
+ setFocusDimension(null)
1029
+ }, [focusDimension, selectedId, pane])
1030
+ React.useEffect(() => {
1031
+ mounted.current = true
1032
+ return () => { mounted.current = false; request.current?.abort(); request.current = null }
1033
+ }, [])
1034
+ React.useEffect(() => {
1035
+ request.current?.abort()
1036
+ request.current = null
1037
+ applying.current = false
1038
+ setCatalog([])
1039
+ setManagement('unknown')
1040
+ setSavedSettings(null)
1041
+ setDrafts(emptyDrafts())
1042
+ setPicked([])
1043
+ setBatchOpen(false)
1044
+ setSelectedId(null)
1045
+ setPane('list')
1046
+ setFocusDimension(null)
1047
+ setStatus(null)
1048
+ setPending(false)
1049
+ setError(null)
1050
+ }, [sessionId])
1051
+ const load = React.useCallback(async signal => {
1052
+ setLoading(true)
1053
+ try {
1054
+ const payload = await inspectSessionCatalog(sessionId, signal)
1055
+ if (signal.aborted || !mounted.current || request.current?.signal !== signal) return
1056
+ if (payload?.schema !== 'ptc-capability-session-catalog/v1' || !Array.isArray(payload.entries)) {
1057
+ throw new Error('Invalid capability catalog')
1058
+ }
1059
+ setCatalog(payload.entries)
1060
+ setPicked(current => current.filter(id => payload.entries.some(entry => entry.id === id)))
1061
+ setManagement(['managed', 'paused', 'off', 'unknown'].includes(payload.management)
1062
+ ? payload.management : 'unknown')
1063
+ setSavedSettings(payload.savedSettings !== null && typeof payload.savedSettings === 'object'
1064
+ && !Array.isArray(payload.savedSettings) ? payload.savedSettings : null)
1065
+ setError(null)
1066
+ return payload.entries
1067
+ } catch (failure) {
1068
+ if (!signal.aborted && mounted.current && request.current?.signal === signal) setError(String(failure?.message ?? failure))
1069
+ } finally {
1070
+ if (!signal.aborted && mounted.current && request.current?.signal === signal) setLoading(false)
1071
+ }
1072
+ }, [sessionId])
1073
+ React.useEffect(() => {
1074
+ if (!open || !enabled || !available) {
1075
+ if (!applying.current) { request.current?.abort(); request.current = null }
1076
+ setLoading(false)
1077
+ return
1078
+ }
1079
+ // An in-flight apply owns the shared catalog request and reloads it on completion.
1080
+ if (applying.current) return
1081
+ const controller = new AbortController()
1082
+ request.current = controller
1083
+ void load(controller.signal)
1084
+ return () => { controller.abort(); if (request.current === controller) request.current = null }
1085
+ }, [open, enabled, available, load])
1086
+ const choices = drafts
1087
+ const choiceFor = entry => choices[entry.id] ?? entry.temporary ?? 'inherit'
1088
+ // Session writes also require the saved configuration to keep capability
1089
+ // management and context on; a frozen managed session cannot be edited
1090
+ // while the current settings have them off.
1091
+ const savedBlocks = savedSettings !== null && (savedSettings.enabled === false || savedSettings.contextEnabled === false)
1092
+ const writesBlocked = management === 'paused' || management === 'off' || savedBlocks
1093
+ const sourceKey = entry => entry.source?.kind === 'mcp' ? 'mcp:' + (entry.source.server ?? '')
1094
+ : entry.source?.kind === 'skill' ? 'skill:' + (entry.source.bucket ?? '')
1095
+ : entry.source?.kind ?? 'unknown'
1096
+ const sourceLabel = entry => entry.source?.kind === 'mcp' ? `MCP · ${entry.source.server ?? ''}`
1097
+ : entry.source?.kind === 'skill' ? `Skill · ${entry.source.bucket
1098
+ ? t(`capabilities.source.${entry.source.bucket}`) : t('capabilities.source.unknown')}`
1099
+ : t(`capabilities.source.${entry.source?.kind === 'host' ? 'host' : 'unknown'}`)
1100
+ const category = entry => entry.category === 'skill' ? 'skills'
1101
+ : ['server', 'resource'].includes(entry.category) ? 'mcp'
1102
+ : entry.source?.kind === 'mcp' ? 'mcp_tools' : 'tools'
1103
+ const type = entry => entry.category === 'server' ? 'server'
1104
+ : entry.source?.kind === 'mcp' ? 'resource'
1105
+ : entry.category === 'skill' ? 'skill'
1106
+ : entry.category === 'resource' ? 'resource'
1107
+ : entry.category === 'tool' ? 'tool' : 'other'
1108
+ const displayedSource = entry => entry.source?.kind === 'mcp' ? entry.source.server ?? sourceLabel(entry) : sourceLabel(entry)
1109
+ const effectiveAvailability = entry => entry.effective === 'pending' ? 'pending'
1110
+ : entry.enclosingClosure || entry.effective === 'closed' ? 'closed'
1111
+ : entry.effectiveDimensions?.availability
1112
+ ?? (['full', 'on-demand', 'host', 'enabled'].includes(entry.effective) ? 'enabled' : 'unknown')
1113
+ const effectiveContext = entry => entry.effective === 'pending' ? 'pending'
1114
+ : effectiveAvailability(entry) === 'closed' ? 'closed'
1115
+ : entry.effectiveDimensions?.disclosure
1116
+ ?? (['full', 'on-demand', 'host'].includes(entry.effective) ? entry.effective : 'unknown')
1117
+ const sources = [...new Set(catalog.filter(entry => kind === 'all' || category(entry) === kind).map(sourceKey))].sort()
1118
+ const setCompatibleKind = next => {
1119
+ setKind(next)
1120
+ if (sourceFilter !== 'all' && !catalog.some(entry => (next === 'all' || category(entry) === next)
1121
+ && sourceKey(entry) === sourceFilter)) setSourceFilter('all')
1122
+ }
1123
+ const setCompatibleSource = next => {
1124
+ setSourceFilter(next)
1125
+ if (next !== 'all' && !catalog.some(entry => (kind === 'all' || category(entry) === kind)
1126
+ && sourceKey(entry) === next)) setKind('all')
1127
+ }
1128
+ const filtered = catalog.filter(entry => {
1129
+ const text = [entry.name, entry.id, entry.description, entry.source?.server, entry.source?.bucket].filter(Boolean).join(' ').toLocaleLowerCase()
1130
+ return (kind === 'all' || category(entry) === kind)
1131
+ && (sourceFilter === 'all' || sourceKey(entry) === sourceFilter)
1132
+ && (stateFilter === 'all' || (stateFilter === 'changed' ? Object.hasOwn(choices, entry.id) : entry.effective === stateFilter))
1133
+ && text.includes(query.trim().toLocaleLowerCase())
1134
+ }).sort((a, b) => (a.name ?? a.id).localeCompare(b.name ?? b.id) || a.id.localeCompare(b.id))
1135
+ const groups = ['tools', 'skills', 'mcp', 'mcp_tools']
1136
+ const defaultOrder = groups.flatMap(group => filtered.filter(entry => category(entry) === group))
1137
+ const matches = sortCapabilityEntries(defaultOrder, sort, (entry, column) => column === 'name' ? entry.name ?? entry.id
1138
+ : column === 'type' ? t(`capabilities.type.${type(entry)}`)
1139
+ : column === 'source' ? displayedSource(entry)
1140
+ : column === 'availability' ? capabilityChoiceLabel(t, effectiveAvailability(entry))
1141
+ : capabilityChoiceLabel(t, effectiveContext(entry)))
1142
+ const selected = matches.find(entry => entry.id === selectedId)
1143
+ ?? ['tools', 'skills', 'mcp', 'mcp_tools'].map(group => matches.find(entry => category(entry) === group)).find(Boolean)
1144
+ const staged = Object.entries(choices).flatMap(([id, choice]) => typeof choice === 'object'
1145
+ ? Object.entries(choice).map(([dimension, selection]) => [id, selection, dimension]) : [[id, choice]])
1146
+ const pickedRows = picked.map(id => catalog.find(entry => entry.id === id)).filter(Boolean)
1147
+ const allMatchesPicked = matches.length > 0 && matches.every(entry => picked.includes(entry.id))
1148
+ const someMatchesPicked = matches.some(entry => picked.includes(entry.id))
1149
+ const togglePick = id => setPicked(current => current.includes(id) ? current.filter(item => item !== id) : [...current, id])
1150
+ const toggleAllMatches = () => setPicked(current => allMatchesPicked
1151
+ ? current.filter(id => !matches.some(entry => entry.id === id))
1152
+ : [...new Set([...current, ...matches.map(entry => entry.id)])])
1153
+ // One staging rule for single and batch edits: an item keeps one temporary
1154
+ // patch, "inherit" removes the dimension and reselecting the stored choice
1155
+ // never dirties the draft.
1156
+ const stageDimension = (current, entry, dimension, choice) => {
1157
+ const next = { ...current }
1158
+ const patch = typeof next[entry.id] === 'object' && next[entry.id] !== null ? { ...next[entry.id] } : {}
1159
+ const original = storedDimension(entry, dimension) ?? 'inherit'
1160
+ const selectedChoice = patch[dimension] ?? original
1161
+ if (choice !== selectedChoice) {
1162
+ if (choice === original) delete patch[dimension]
1163
+ else patch[dimension] = choice
1164
+ }
1165
+ if (Object.keys(patch).length) next[entry.id] = patch; else delete next[entry.id]
1166
+ return next
1167
+ }
1168
+ const chooseDimension = (id, dimension, choice) => {
1169
+ const entry = catalog.find(item => item.id === id)
1170
+ if (entry) setDrafts(current => stageDimension(current, entry, dimension, choice))
1171
+ }
1172
+ const choose = (id, next) => {
1173
+ const entry = catalog.find(item => item.id === id)
1174
+ if (!entry) return
1175
+ const original = entry.temporary ?? 'inherit'
1176
+ if (next === (choices[id] ?? original)) return
1177
+ setDrafts(current => {
1178
+ const nextChoices = { ...current }
1179
+ if (next === original) delete nextChoices[id]
1180
+ else nextChoices[id] = next
1181
+ return nextChoices
1182
+ })
1183
+ }
1184
+ const stageBatch = (ids, selections) => setDrafts(current => ids.reduce((draft, id) => {
1185
+ const entry = catalog.find(item => item.id === id)
1186
+ if (!entry) return draft
1187
+ return ['availability', 'disclosure'].reduce((next, dimension) =>
1188
+ selections[dimension] === undefined || selections[dimension] === 'keep'
1189
+ ? next : stageDimension(next, entry, dimension, selections[dimension]), draft)
1190
+ }, current))
1191
+ const hasInheritanceReset = staged.some(([, selection]) => selection === 'inherit')
1192
+ const apply = async targetScope => {
1193
+ if (applying.current || !enabled || !available || writesBlocked || staged.length === 0) return
1194
+ if (targetScope === 'global' && hasInheritanceReset) return
1195
+ const submitted = structuredClone(choices)
1196
+ applying.current = true
1197
+ setPending(true)
1198
+ setStatus(null)
1199
+ const controller = new AbortController()
1200
+ request.current?.abort()
1201
+ request.current = controller
1202
+ // One owner releases the shared request slot and the pending state on every exit path, so a
1203
+ // closed, superseded or failed apply cannot strand the workbench in "applying".
1204
+ const finish = () => {
1205
+ if (request.current === controller) request.current = null
1206
+ applying.current = false
1207
+ if (mounted.current) setPending(false)
1208
+ }
1209
+ try {
1210
+ let completed = 0
1211
+ let pendingCount = 0
1212
+ let failure = null
1213
+ let savedOnly = null
1214
+ for (const [id, selection, dimension] of staged) {
1215
+ if (controller.signal.aborted) break
1216
+ try {
1217
+ const result = await commands.run(sessionId, `set ${targetScope} ${encodeURIComponent(id)} ${dimension ? dimension + ' ' : ''}${selection}`, controller.signal)
1218
+ const receipt = readCapabilityCommandReceipt(result.text)
1219
+ if (!receipt || receipt.sessionId !== sessionId || !['recorded', 'pending'].includes(receipt.state)) {
1220
+ throw new Error(receipt ? capabilityCommandText(t, result.text) : t('capabilities.quick.unknownReceipt'))
1221
+ }
1222
+ if (receipt?.state === 'pending') pendingCount += 1
1223
+ completed += 1
1224
+ } catch (cause) {
1225
+ failure = String(cause?.message ?? cause)
1226
+ savedOnly = readCapabilityCommandReceipt(failure)?.state === 'saved-only'
1227
+ ? readCapabilityCommandReceipt(failure) : null
1228
+ break
1229
+ }
1230
+ }
1231
+ if (request.current !== controller || controller.signal.aborted || !mounted.current) return
1232
+ const refreshed = await load(controller.signal)
1233
+ if (request.current !== controller || controller.signal.aborted || !mounted.current) return
1234
+ const confirmed = refreshed ? staged.slice(0, completed).filter(([id, selection, dimension]) => {
1235
+ const entry = refreshed.find(item => item.id === id)
1236
+ if (dimension) return entry && (targetScope === 'session'
1237
+ ? (entry.temporaryRule?.[dimension] ?? 'inherit') === selection
1238
+ : (entry.savedRule?.[dimension] ?? 'automatic') === selection && entry.temporaryRule?.[dimension] === undefined)
1239
+ return entry && (targetScope === 'session' ? entry.temporary === selection
1240
+ : entry.saved === selection && entry.temporary === 'inherit')
1241
+ }) : []
1242
+ setDrafts(current => {
1243
+ const nextChoices = { ...current }
1244
+ for (const [id, selection, dimension] of confirmed) {
1245
+ if (savedOnly && id === staged[completed]?.[0] && dimension === staged[completed]?.[2]) continue
1246
+ if (dimension) {
1247
+ if (nextChoices[id]?.[dimension] !== submitted[id]?.[dimension]) continue
1248
+ const patch = { ...nextChoices[id] }; delete patch[dimension]
1249
+ if (Object.keys(patch).length) nextChoices[id] = patch; else delete nextChoices[id]
1250
+ } else if (nextChoices[id] === submitted[id]) delete nextChoices[id]
1251
+ }
1252
+ return nextChoices
1253
+ })
1254
+ if (savedOnly) setStatus({ key: 'capabilities.quick.savedOnly', params: { count: completed, error: savedOnly.error ?? '' } })
1255
+ else if (failure || !refreshed || confirmed.length < completed) setStatus({ key: 'capabilities.quick.partial',
1256
+ params: { count: confirmed.length, error: failure ?? t('capabilities.quick.confirmationUnavailable') } })
1257
+ else setStatus({ key: pendingCount ? 'capabilities.quick.pendingApplied' : 'capabilities.quick.applied', params: { count: completed } })
1258
+ } finally {
1259
+ finish()
1260
+ }
1261
+ }
1262
+ const discard = () => { setDrafts(emptyDrafts()); setStatus(null) }
1263
+ const close = () => {
1264
+ // A confirmed apply keeps its own lifecycle: refusing the close keeps an accidental
1265
+ // Escape or backdrop click from aborting host commands the user already submitted.
1266
+ if (applying.current) return
1267
+ request.current?.abort()
1268
+ request.current = null
1269
+ applying.current = false
1270
+ setPending(false)
1271
+ setDrafts(emptyDrafts())
1272
+ setStatus(null)
1273
+ onClose()
1274
+ }
1275
+ const filter = (label, value, onChange, options, disabled = false) => h('div', { className: 'ptcPlusCapabilityFilter' },
1276
+ h('span', null, label), h(Select, { label, value, onChange, disabled,
1277
+ options: options.map(([key, text]) => ({ value: key, label: text })) }))
1278
+ const ruleLabel = (rule, empty) => capabilityRuleLabel(t, rule, empty)
1279
+ const statusLabel = value => value && typeof value === 'object' ? ruleLabel(value) : capabilityChoiceLabel(t, value)
1280
+ // The stored temporary value for one dimension, including the legacy scalar
1281
+ // form where a single selection covered both dimensions.
1282
+ const storedDimension = (entry, dimension) => {
1283
+ const rule = entry.temporaryRule !== null && typeof entry.temporaryRule === 'object' && !Array.isArray(entry.temporaryRule)
1284
+ ? entry.temporaryRule : undefined
1285
+ const override = rule?.[dimension]
1286
+ if (override !== undefined) return override
1287
+ if (entry.temporary === 'automatic') return 'automatic'
1288
+ if (typeof entry.temporary === 'string' && !['inherit', 'automatic', 'unknown', 'pending'].includes(entry.temporary)) {
1289
+ return dimension === 'availability' ? (entry.temporary === 'closed' ? 'closed' : 'enabled')
1290
+ : (CAPABILITY_DISCLOSURES.includes(entry.temporary) ? entry.temporary : undefined)
1291
+ }
1292
+ return undefined
1293
+ }
1294
+ // The value a dimension falls back to when this session keeps no temporary
1295
+ // change; the frozen plan already resolved the saved layers.
1296
+ const frozenDimension = (entry, dimension) => {
1297
+ const source = entry.sourceDimensions?.[dimension]
1298
+ if (typeof source === 'string') return source
1299
+ const effective = entry.effectiveDimensions?.[dimension]
1300
+ if (typeof effective === 'string' && storedDimension(entry, dimension) === undefined) return effective
1301
+ if (dimension === 'availability') return entry.effective === 'closed' ? 'closed'
1302
+ : ['full', 'on-demand', 'host', 'enabled'].includes(entry.effective) ? 'enabled' : undefined
1303
+ return ['full', 'on-demand', 'host'].includes(entry.effective) ? entry.effective : undefined
1304
+ }
1305
+ const stateText = value => value === undefined ? t('capabilities.quick.unknown') : statusLabel(value)
1306
+ const dimensionCurrent = (entry, dimension) => choices[entry.id]?.[dimension] ?? storedDimension(entry, dimension) ?? 'inherit'
1307
+ const dimensionImpact = (entry, dimension) => {
1308
+ const draft = choices[entry.id]?.[dimension]
1309
+ const choice = draft ?? storedDimension(entry, dimension) ?? 'inherit'
1310
+ const stagedChange = draft !== undefined
1311
+ if (choice === 'inherit') return { state: frozenDimension(entry, dimension), origin: 'capabilities.quick.originPlan', staged: stagedChange }
1312
+ if (choice === 'automatic') return { upper: true, staged: stagedChange }
1313
+ return { state: choice, origin: 'capabilities.quick.originSession', staged: stagedChange }
1314
+ }
1315
+ const impactLine = impact => impact.upper
1316
+ ? t(impact.staged ? 'capabilities.quick.impactUpperPending' : 'capabilities.quick.impactUpper')
1317
+ : t(impact.staged ? 'capabilities.quick.impactPending' : 'capabilities.quick.impact',
1318
+ { state: stateText(impact.state), source: t(impact.origin) })
1319
+ const dimensionOptions = (entry, dimension) => {
1320
+ const restore = stateText(frozenDimension(entry, dimension))
1321
+ const inherit = storedDimension(entry, dimension) === undefined
1322
+ ? t('capabilities.quick.noChange', { state: restore })
1323
+ : t('capabilities.quick.undoTemp', { state: restore })
1324
+ // "Let the level above decide" only differs from "no change" while the
1325
+ // session's frozen source still carries an explicit rule for the dimension.
1326
+ const sourceDimension = entry.sourceRule?.[dimension]
1327
+ const distinctRule = sourceDimension !== undefined && sourceDimension !== 'automatic'
1328
+ return ['inherit', ...(distinctRule || dimensionCurrent(entry, dimension) === 'automatic' ? ['automatic'] : []),
1329
+ ...(dimension === 'availability' ? ['enabled', 'closed'] : CAPABILITY_DISCLOSURES)]
1330
+ .map(choice => ({ value: choice, label: choice === 'inherit' ? inherit
1331
+ : choice === 'automatic' ? t('capabilities.quick.upperRule')
1332
+ : t(choice === 'enabled' ? 'capabilities.action.enable' : choice === 'closed' ? 'capabilities.action.close' : `capabilities.${choice}`) }))
1333
+ }
1334
+ const legacyFrozenState = entry => {
1335
+ const stored = typeof entry.temporary === 'string' && !['inherit', 'automatic', 'unknown', 'pending'].includes(entry.temporary)
1336
+ if (stored) return ['full', 'on-demand', 'closed'].includes(entry.sessionSourceSelection) ? entry.sessionSourceSelection : undefined
1337
+ return ['full', 'on-demand', 'closed', 'host'].includes(entry.effective) ? entry.effective : undefined
1338
+ }
1339
+ const choiceCurrent = entry => choices[entry.id] ?? entry.temporary ?? 'inherit'
1340
+ const choiceImpact = entry => {
1341
+ const draft = choices[entry.id]
1342
+ const choice = draft ?? entry.temporary ?? 'inherit'
1343
+ const stagedChange = draft !== undefined
1344
+ if (choice === 'inherit') return { state: legacyFrozenState(entry), origin: 'capabilities.quick.originPlan', staged: stagedChange }
1345
+ if (choice === 'automatic') return { upper: true, staged: stagedChange }
1346
+ return { state: choice, origin: 'capabilities.quick.originSession', staged: stagedChange }
1347
+ }
1348
+ const choiceOptions = selected ? [
1349
+ 'inherit', 'automatic',
1350
+ ...(selected.category === 'server' ? [] : ['full', 'on-demand']), 'closed',
1351
+ ].map(choice => ({ value: choice, label: choice === 'inherit'
1352
+ ? t(selected.temporary !== undefined && selected.temporary !== 'inherit' ? 'capabilities.quick.undoChange' : 'capabilities.quick.keepUnchanged')
1353
+ : choice === 'automatic' ? t('capabilities.quick.upperRule')
1354
+ : t(`capabilities.${choice}`) })) : []
1355
+ const dimensionFacts = dimensions => ['availability', 'disclosure'].map(dimension =>
1356
+ `${t(`capabilities.policy.${dimension}`)}: ${statusLabel(dimensions[dimension])} (${dimensions[`${dimension}Source`] ?? t('capabilities.quick.unknown')})`).join(' · ')
1357
+ // A dimension whose effective value comes from the session-only temporary
1358
+ // choice is marked beside the frozen inheritance it replaces.
1359
+ const temporaryDimension = (entry, dimension) => {
1360
+ const rule = entry.temporaryRule !== null && typeof entry.temporaryRule === 'object' && !Array.isArray(entry.temporaryRule)
1361
+ ? entry.temporaryRule : undefined
1362
+ const override = rule?.[dimension]
1363
+ if (override !== undefined && override !== 'automatic') return override
1364
+ if (typeof entry.temporary === 'string' && !['inherit', 'automatic'].includes(entry.temporary)) {
1365
+ return dimension === 'availability' ? (entry.temporary === 'closed' ? 'closed' : 'enabled') : entry.temporary
1366
+ }
1367
+ return undefined
1368
+ }
1369
+ const dimensionCell = (entry, dimension, value) => temporaryDimension(entry, dimension) === value
1370
+ ? t('capabilities.quick.temporaryState', { state: statusLabel(value) }) : statusLabel(value)
1371
+ const openEditor = (entry, dimension = 'availability') => { setSelectedId(entry.id); setPane('detail'); setFocusDimension(dimension) }
1372
+ const renderRow = entry => h('div', { key: entry.id, role: 'row',
1373
+ className: 'ptcPlusSessionCapabilityRow ptcPlusCapabilityItem', 'data-capability-id': entry.id, 'data-selected': selected?.id === entry.id,
1374
+ onClick: () => openEditor(entry) },
1375
+ h('div', { role: 'cell', className: 'ptcPlusCapabilityIdentity' },
1376
+ h('input', { type: 'checkbox', className: 'ptcPlusPick', checked: picked.includes(entry.id), disabled: writesBlocked,
1377
+ 'aria-label': t('capabilities.selectForBatch', { name: entry.name ?? entry.id }),
1378
+ onClick: event => event.stopPropagation(), onChange: () => togglePick(entry.id) }),
1379
+ h('div', { className: 'ptcPlusCapabilityIdentityText' },
1380
+ h('button', { type: 'button', className: 'ptcPlusSessionCapabilityName ptcPlusCapabilityName',
1381
+ 'aria-current': selected?.id === entry.id ? 'true' : undefined,
1382
+ onClick: event => { event.stopPropagation(); openEditor(entry) } }, entry.name ?? entry.id),
1383
+ entry.name && entry.name !== entry.id ? h('code', null, entry.id) : null)),
1384
+ h('span', { role: 'cell', className: 'ptcPlusCapabilityBadge', 'data-kind': type(entry) }, t(`capabilities.type.${type(entry)}`)),
1385
+ h('span', { role: 'cell', className: 'ptcPlusCapabilitySource' }, displayedSource(entry)),
1386
+ h('div', { role: 'cell', className: 'ptcPlusCapabilityAvailability', 'data-state': effectiveAvailability(entry) },
1387
+ h('button', { type: 'button', className: 'ptcPlusCapabilityContext',
1388
+ 'aria-label': t('capabilities.policy.availabilityFor', { name: entry.id }),
1389
+ title: t('capabilities.quick.effective'),
1390
+ onClick: event => { event.stopPropagation(); openEditor(entry, 'availability') } }, dimensionCell(entry, 'availability', effectiveAvailability(entry)))),
1391
+ h('div', { role: 'cell', className: 'ptcPlusCapabilityContextCell' },
1392
+ h('button', { type: 'button', className: 'ptcPlusCapabilityContext',
1393
+ 'aria-label': t('capabilities.editContextFor', { name: entry.id }),
1394
+ title: t('capabilities.quick.effective'),
1395
+ onClick: event => { event.stopPropagation(); openEditor(entry, 'disclosure') } }, dimensionCell(entry, 'disclosure', effectiveContext(entry)))),
1396
+ h('span', { role: 'cell', className: 'ptcPlusSessionCapabilityPending', 'data-changed': Object.hasOwn(choices, entry.id),
1397
+ 'data-label': t('capabilities.quick.changed') },
1398
+ Object.hasOwn(choices, entry.id) ? statusLabel(choices[entry.id]) : '—'))
1399
+ return h(Modal, { open, onClose: close, title: t('capabilities.quick.modalTitle'),
1400
+ closeLabel: t('settings.close'), headless: true, className: 'ptcPlusSessionCapabilityModal' },
1401
+ h('div', { className: 'ptcPlusSessionCapabilityHead' },
1402
+ h('div', null, h('h2', null, t('capabilities.quick.modalTitle')),
1403
+ h('p', null, t('capabilities.quick.sessionIdentity', { session: sessionId }))),
1404
+ h('button', { type: 'button', className: 'ptcPlusDialogClose', 'aria-label': t('settings.close'), disabled: pending, onClick: close }, '×')),
1405
+ h('div', { className: 'ptcPlusSessionCapabilityToolbar' },
1406
+ h('label', { className: 'ptcPlusSearch' },
1407
+ h('svg', { viewBox: '0 0 24 24', width: 16, height: 16, 'aria-hidden': true },
1408
+ h('circle', { cx: 10, cy: 10, r: 6, fill: 'none', stroke: 'currentColor', strokeWidth: 2 }),
1409
+ h('path', { d: 'm15 15 6 6', fill: 'none', stroke: 'currentColor', strokeWidth: 2 })),
1410
+ h('input', { type: 'search', value: query, 'aria-label': t('capabilities.search'),
1411
+ placeholder: t('capabilities.search'), onChange: event => setQuery(event.target.value) })),
1412
+ filter(t('capabilities.quick.kind'), kind, setCompatibleKind,
1413
+ ['all', 'tools', 'skills', 'mcp', 'mcp_tools'].map(key => [key, t(`capabilities.quick.${key}`)])),
1414
+ filter(t('capabilities.sourceFilter'), sourceFilter, setCompatibleSource,
1415
+ [['all', t('capabilities.allSources')], ...sources.map(key => [key,
1416
+ sourceLabel(catalog.find(entry => sourceKey(entry) === key))])]),
1417
+ filter(t('capabilities.quick.state'), stateFilter, setStateFilter,
1418
+ [['all', t('capabilities.quick.allStates')], ['changed', t('capabilities.quick.changed')],
1419
+ ...['full', 'on-demand', 'closed', 'pending'].map(key => [key, statusLabel(key)])])),
1420
+ // A blocked workbench states why writes are off and how to restore them; the banner styling
1421
+ // keeps that condition from reading as an incidental list message.
1422
+ writesBlocked ? h('p', { className: 'ptcPlusCapabilityNotice ptcPlusCapabilityBlocked', role: 'status' },
1423
+ h('svg', { className: 'ptcPlusCapabilityBlockedIcon', viewBox: '0 0 16 16', width: 14, height: 14,
1424
+ 'aria-hidden': true, fill: 'none' },
1425
+ h('path', { d: 'M8 2.3 14.7 13.5H1.3L8 2.3Z', stroke: 'currentColor', strokeWidth: 1.3, strokeLinejoin: 'round' }),
1426
+ h('path', { d: 'M8 6.4v3M8 11.2v.7', stroke: 'currentColor', strokeWidth: 1.3, strokeLinecap: 'round' })),
1427
+ t(management === 'paused' ? 'capabilities.quick.sessionPaused'
1428
+ : management === 'off' ? 'capabilities.quick.sessionManagementOff'
1429
+ : savedSettings?.contextEnabled === false ? 'capabilities.quick.savedPaused'
1430
+ : 'capabilities.quick.savedManagementOff')) : null,
1431
+ h('div', { className: 'ptcPlusSessionCapabilityWorkspace', 'data-pane': pane },
1432
+ h('div', { className: 'ptcPlusSessionCapabilityList', role: 'table', 'aria-label': t('capabilities.quick.members'), ref: list },
1433
+ h('div', { className: 'ptcPlusSessionCapabilityColumns ptcPlusCapabilityColumns', role: 'row' },
1434
+ ...CAPABILITY_SORT_COLUMNS.map(column => h('span', { key: column.key, role: 'columnheader',
1435
+ 'aria-sort': capabilitySortDirection(sort, column.key),
1436
+ ...(column.key === 'name' ? { 'aria-label': t('capabilities.member'), className: 'ptcPlusCapabilityPickHead' } : {}) },
1437
+ ...(column.key === 'name' ? [h('input', { type: 'checkbox', className: 'ptcPlusPick', checked: allMatchesPicked,
1438
+ ref: node => { if (node) node.indeterminate = someMatchesPicked && !allMatchesPicked },
1439
+ disabled: writesBlocked || matches.length === 0, 'aria-label': t('capabilities.selectAllVisible'),
1440
+ onChange: toggleAllMatches })] : []),
1441
+ h('button', { type: 'button', className: 'ptcPlusCapabilitySort', title: t('capabilities.sortCycle'),
1442
+ onClick: () => setSort(current => cycleCapabilitySort(current, column.key)) },
1443
+ t(`capabilities.${column.label}`), h('span', { 'aria-hidden': true },
1444
+ capabilitySortDirection(sort, column.key) === 'ascending' ? '↑' : capabilitySortDirection(sort, column.key) === 'descending' ? '↓' : '↕')))),
1445
+ h('span', { role: 'columnheader' }, t('capabilities.quick.changed'))),
1446
+ loading ? h('p', { role: 'status' }, t('state.syncing')) : null,
1447
+ error ? h('p', { role: 'alert' }, t('capabilities.quick.catalogError', { error })) : null,
1448
+ !loading && !error && matches.length === 0 ? h('p', null, t('capabilities.noMatches')) : null,
1449
+ ...(sort ? [h('div', { key: 'sorted', role: 'rowgroup' }, ...matches.map(renderRow))]
1450
+ : groups.filter(group => matches.some(entry => category(entry) === group))
1451
+ .map(group => h('section', { key: group, className: 'ptcPlusSessionCapabilityGroup' },
1452
+ h('h3', null, t(`capabilities.quick.${group}`)),
1453
+ h('div', { role: 'rowgroup' }, ...matches.filter(entry => category(entry) === group).map(renderRow)))))),
1454
+ h('aside', { className: 'ptcPlusSessionCapabilityDetail', 'aria-label': t('capabilities.details'), ref: detail },
1455
+ h('button', { type: 'button', className: 'ptcPlusSessionCapabilityBack', onClick: () => { setPane('list'); setFocusDimension('list') } },
1456
+ t('capabilities.quick.backToList')),
1457
+ selected ? h(React.Fragment, null,
1458
+ h('h3', null, selected.name ?? selected.id), h('code', null, selected.id),
1459
+ selected.description ? h('p', null, selected.description) : null,
1460
+ h('dl', null,
1461
+ h('dt', null, t('capabilities.sourceFilter')), h('dd', null, sourceLabel(selected)),
1462
+ h('dt', null, t('capabilities.quick.globalSaved')), h('dd', null, selected.savedRule ? ruleLabel(selected.savedRule) : statusLabel(selected.saved)),
1463
+ h('dt', null, t('capabilities.quick.sessionSource')), h('dd', null, selected.sourceRule ? ruleLabel(selected.sourceRule) : statusLabel(selected.sessionSourceSelection ?? 'unknown')),
1464
+ h('dt', null, t('capabilities.quick.temporary')), h('dd', null, selected.temporaryRule ? ruleLabel(selected.temporaryRule, 'capabilities.quick.noOverride') : statusLabel(selected.temporary)),
1465
+ h('dt', null, t('capabilities.quick.effective')), h('dd', null, statusLabel(selected.effective)),
1466
+ selected.sourceDimensions ? h(React.Fragment, null, h('dt', null, t('capabilities.policy.frozenSource')), h('dd', null, dimensionFacts(selected.sourceDimensions))) : null,
1467
+ selected.effectiveDimensions ? h(React.Fragment, null, h('dt', null, t('capabilities.policy.effectiveDimensions')), h('dd', null, dimensionFacts(selected.effectiveDimensions))) : null),
1468
+ selected.source?.kind === 'mcp' && selected.category !== 'server'
1469
+ ? (() => {
1470
+ const server = catalog.find(entry => entry.category === 'server' && entry.source?.server === selected.source.server)
1471
+ if (!server) return null
1472
+ const proposed = choiceFor(server)
1473
+ const availability = server.savedRule ? choices[server.id]?.availability ?? server.temporaryRule?.availability ?? 'inherit' : proposed
1474
+ const sourceAvailability = server.sourceRule !== undefined ? server.sourceRule.availability : server.sessionSourceSelection
1475
+ const pendingClose = server.managementEnabled !== false && (availability === 'inherit' ? sourceAvailability : availability) === 'closed'
1476
+ const currentlyClosed = server.enclosingClosure ?? server.effective === 'closed'
1477
+ const key = currentlyClosed ? 'capabilities.quick.disabledByServer'
1478
+ : pendingClose ? 'capabilities.quick.pendingServerClose' : null
1479
+ return key ? h('p', { className: 'ptcPlusCapabilityNotice' }, t(key, { server: selected.source.server })) : null
1480
+ })() : null,
1481
+ selected.category === 'server' ? h('div', { className: 'ptcPlusSessionCapabilityChildren' },
1482
+ h('h4', null, t('capabilities.quick.serverTools')),
1483
+ h('ul', null, ...catalog.filter(entry => entry.id !== selected.id
1484
+ && entry.source?.kind === 'mcp' && entry.source.server === selected.source?.server)
1485
+ .map(entry => h('li', { key: entry.id }, entry.name ?? entry.id)))) : null,
1486
+ selected.savedRule ? h(React.Fragment, null, ...['availability', 'disclosure'].map(dimension => h(React.Fragment, { key: dimension },
1487
+ h('div', { className: 'ptcPlusCapabilityFilter', 'data-capability-dimension': dimension },
1488
+ h('span', null, t(`capabilities.policy.${dimension}`)),
1489
+ h(Select, { label: t(`capabilities.policy.${dimension}`), disabled: !enabled || !available || pending || writesBlocked,
1490
+ value: dimensionCurrent(selected, dimension),
1491
+ onChange: next => chooseDimension(selected.id, dimension, next),
1492
+ options: dimensionOptions(selected, dimension) })),
1493
+ h('p', { className: 'ptcPlusDetail ptcPlusCapabilityImpact', 'data-impact-dimension': dimension },
1494
+ impactLine(dimensionImpact(selected, dimension))))))
1495
+ : h(React.Fragment, null,
1496
+ h('div', { className: 'ptcPlusCapabilityFilter' },
1497
+ h('span', null, t('capabilities.quick.choice')),
1498
+ h(Select, { label: t('capabilities.quick.choice'), value: choiceCurrent(selected),
1499
+ disabled: !enabled || !available || pending || writesBlocked, options: choiceOptions,
1500
+ onChange: next => choose(selected.id, next) })),
1501
+ h('p', { className: 'ptcPlusDetail ptcPlusCapabilityImpact', 'data-impact-dimension': 'selection' },
1502
+ impactLine(choiceImpact(selected)))),
1503
+ (typeof choices[selected.id] === 'string' ? choices[selected.id] === 'inherit'
1504
+ : Object.values(choices[selected.id] ?? {}).includes('inherit')) ? h('p', { className: 'ptcPlusDetail' },
1505
+ t('capabilities.quick.inheritSessionOnly')) : null) : h('p', null, t('capabilities.noMatches')))),
1506
+ batchOpen ? h(Modal, { open: true, onClose: () => setBatchOpen(false), title: t('capabilities.batchSettings'),
1507
+ closeLabel: t('capabilities.batch.cancel'), headless: true, className: 'ptcPlusCapabilityBatchModal' },
1508
+ h('div', { className: 'ptcPlusCapabilityBatchHead' }, h('h2', null, t('capabilities.batchSettings')),
1509
+ h('button', { type: 'button', className: 'ptcPlusDialogClose', 'aria-label': t('capabilities.batch.cancel'),
1510
+ onClick: () => setBatchOpen(false) }, '\u00d7')),
1511
+ h('div', { className: 'ptcPlusCapabilityBatchBody' },
1512
+ h('p', { className: 'ptcPlusDetail', role: 'status' }, t('capabilities.batch.selected', { count: pickedRows.length })),
1513
+ h('ul', { className: 'ptcPlusCapabilityBatchTargets', 'aria-label': t('capabilities.batch.targetList') },
1514
+ ...pickedRows.map(entry => h('li', { key: entry.id }, h('span', null, entry.name ?? entry.id), h('code', null, entry.id)))),
1515
+ h('div', { className: 'ptcPlusCapabilityBatchFields' }, ...['availability', 'disclosure'].map(dimension =>
1516
+ h('div', { key: dimension, className: 'ptcPlusCapabilityFilter' }, h('span', null, t(`capabilities.policy.${dimension}`)),
1517
+ h(Select, { value: batchChoices[dimension], label: t(`capabilities.batch.${dimension}`),
1518
+ onChange: choice => setBatchChoices(current => ({ ...current, [dimension]: choice })),
1519
+ options: ['keep', 'inherit', ...(dimension === 'availability' ? ['enabled', 'closed'] : CAPABILITY_DISCLOSURES)]
1520
+ .map(choice => ({ value: choice, label: choice === 'keep' ? t('capabilities.batch.keep')
1521
+ : choice === 'inherit' ? t('capabilities.batch.sessionInherit')
1522
+ : choice === 'enabled' ? t('capabilities.action.enable')
1523
+ : choice === 'closed' ? t('capabilities.action.close') : t(`capabilities.${choice}`) })) })))),
1524
+ h('p', { className: 'ptcPlusDetail' }, t('capabilities.batch.sessionStage'))),
1525
+ h('div', { className: 'ptcPlusCapabilityBatchFooter' },
1526
+ h('button', { type: 'button', className: 'ptcPlusButton', onClick: () => setBatchOpen(false) }, t('capabilities.batch.cancel')),
1527
+ h('button', { type: 'button', className: 'ptcPlusButton', 'data-kind': 'primary',
1528
+ disabled: pickedRows.length === 0 || !['availability', 'disclosure'].some(dimension => batchChoices[dimension] !== 'keep'),
1529
+ onClick: () => { stageBatch(pickedRows.map(entry => entry.id), batchChoices); setBatchOpen(false) } },
1530
+ t('capabilities.batch.stage', { count: pickedRows.length })))) : null,
1531
+ h('div', { className: 'ptcPlusSessionCapabilityFooter' },
1532
+ h('span', { role: 'status' }, pending ? t('capabilities.quick.applying') : status ? t(status.key, status.params)
1533
+ : t('capabilities.quick.changes', { count: staged.length })),
1534
+ h('button', { type: 'button', className: 'ptcPlusButton', disabled: pending || staged.length === 0, onClick: discard },
1535
+ t('capabilities.quick.discard')),
1536
+ // Session-scoped temporary application stays the footer's primary action; saving global
1537
+ // defaults is an explicit secondary choice, so no default-looking control changes global state.
1538
+ h('button', { type: 'button', className: 'ptcPlusButton', 'data-kind': 'primary', title: t('capabilities.quick.sessionHint'),
1539
+ disabled: pending || !enabled || !available || writesBlocked || staged.length === 0, onClick: () => void apply('session') },
1540
+ t('capabilities.quick.applySession')),
1541
+ h('button', { type: 'button', className: 'ptcPlusButton',
1542
+ title: t(hasInheritanceReset ? 'capabilities.quick.inheritSessionOnly' : 'capabilities.quick.globalHint'),
1543
+ disabled: pending || !enabled || !available || writesBlocked || staged.length === 0 || hasInheritanceReset,
1544
+ onClick: () => void apply('global') },
1545
+ t('capabilities.quick.applyGlobal')),
1546
+ h('button', { type: 'button', className: 'ptcPlusButton', 'aria-haspopup': 'dialog',
1547
+ 'aria-label': t('capabilities.batchSettings'),
1548
+ disabled: pending || writesBlocked || picked.length === 0,
1549
+ onClick: () => { setBatchChoices({ availability: 'keep', disclosure: 'keep' }); setBatchOpen(true) } },
1550
+ t('capabilities.batchSettings'),
1551
+ picked.length ? h('span', { className: 'ptcPlusCapabilityBatchCount', 'aria-hidden': true,
1552
+ title: t('capabilities.selectionCount', { count: picked.length }) },
1553
+ String(picked.length)) : null)))
1554
+ }
1555
+ }