dsh-ptc-plus 0.4.10 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.en.md +65 -127
- package/README.md +65 -127
- package/assets/dsh-ptc-plus-banner-en.webp +0 -0
- package/assets/dsh-ptc-plus-banner-zh.webp +0 -0
- package/assets/ptc-plus-repl-workspace-en.png +0 -0
- package/assets/ptc-plus-repl-workspace-zh.png +0 -0
- package/assets/ptc-plus-settings-en.png +0 -0
- package/assets/ptc-plus-settings-zh.png +0 -0
- package/client.cjs +42702 -0
- package/cordis.patch.yml +1 -0
- package/docs/README.md +6 -2
- package/docs/adr/0005-temporary-rejected-cell-edit-transport.md +31 -198
- package/docs/adr/0008-define-agent-operating-contract.md +5 -1
- package/docs/adr/0009-bound-expensive-acceptance-default.md +2 -2
- package/docs/adr/0016-archive-resolved-review-findings.md +1 -1
- package/docs/adr/0017-track-the-latest-dsh-public-surface.md +4 -4
- package/docs/adr/0019-plugin-settings-and-kill-switch.md +1 -1
- package/docs/adr/0020-optional-cordis-tools-in-ptc-mode.md +1 -1
- package/docs/adr/0022-stage-npm-releases-with-oidc.md +4 -2
- package/docs/adr/0023-global-user-bindings.md +2 -0
- package/docs/adr/0026-persist-the-delivery-verdict-against-the-verified-candidate.md +2 -0
- package/docs/adr/0028-attach-to-the-host-ptc-execution-seam.md +25 -23
- package/docs/adr/0031-close-semantic-obligations-by-owner.md +1 -1
- package/docs/adr/0034-keep-program-values-separate-from-output-projections.md +29 -0
- package/docs/adr/0035-run-coverage-only-before-a-new-release.md +27 -0
- package/docs/adr/0036-separate-focused-development-from-delivery-verification.md +29 -0
- package/docs/adr/0037-keep-tool-hover-previews-passive.md +52 -0
- package/docs/adr/0038-freeze-session-capability-plans.md +71 -0
- package/docs/adr/0039-verify-documentation-without-runtime-suites.md +29 -0
- package/docs/adr/0040-separate-capability-configurations-from-session-observations.md +39 -0
- package/docs/adr/0041-record-cell-execution-directories.md +65 -0
- package/docs/adr/0042-persist-provider-availability-and-handback.md +41 -0
- package/docs/adr/0043-compose-capability-and-context-policies.md +37 -0
- package/docs/adr/0044-coexist-with-the-official-persistent-repl.md +35 -0
- package/docs/adr/README.md +8 -0
- package/docs/architecture.md +23 -25
- package/docs/capability-planning-components.md +93 -0
- package/docs/capability-projection.md +7 -0
- package/docs/client-ui.md +70 -23
- package/docs/client-visual-contract.md +26 -9
- package/docs/context-management.md +22 -0
- package/docs/context-optimization-plan.md +353 -0
- package/docs/context-public-interface-coverage.md +25 -0
- package/docs/durability-design.md +20 -15
- package/docs/evaluation.md +41 -6
- package/docs/evaluations/command-goat-deepseek-v4.1-flash-20261006.md +126 -0
- package/docs/evaluations/command-goat-deepseek-v4.1-flash.md +281 -0
- package/docs/installation.md +33 -10
- package/docs/program-data-plane.md +5 -3
- package/docs/publishing.md +7 -7
- package/docs/release-coverage.md +73 -0
- package/docs/release-notes.md +1 -1
- package/docs/review-procedure.md +56 -0
- package/docs/runtime-reference.md +53 -16
- package/docs/semantic-validation.md +14 -2
- package/docs/stateful-binding-updates-proposal.md +2 -2
- package/docs/user-bindings.md +1 -1
- package/docs/value-wire.md +11 -1
- package/docs/verification-optimization-work.md +26 -15
- package/docs/verification.md +30 -88
- package/index.js +146 -20
- package/internal/binding-toggle-receipt.js +24 -0
- package/internal/capability-command-receipt.js +31 -0
- package/internal/capability-environment.js +1253 -0
- package/internal/capability-plan.js +285 -0
- package/internal/capability-policy.js +96 -0
- package/internal/capability-statistics-store.js +180 -0
- package/internal/capability-statistics.js +214 -0
- package/internal/config-spec.js +89 -10
- package/internal/direct-surface-owner.js +27 -29
- package/internal/dynamic-environment-runtime.js +9 -9
- package/internal/edit-transport-owner.js +195 -140
- package/internal/execution-provider-owner.js +40 -12
- package/internal/execution-seam-compat.js +26 -8
- package/internal/kernel-worker.js +104 -32
- package/internal/native-root-dynamic.js +12 -13
- package/internal/official-repl-priority.js +51 -0
- package/internal/program-bindings.js +190 -0
- package/internal/recovery-tips.js +3 -2
- package/internal/rejected-cell-editor.js +8 -8
- package/internal/repl-observation-interest.js +11 -3
- package/internal/rpc-contract.js +1 -0
- package/internal/runtime-bridge-owner.js +275 -34
- package/internal/runtime-config.js +58 -0
- package/internal/runtime-contexts.js +74 -5
- package/internal/runtime-intrinsics.js +1 -0
- package/internal/runtime-messages.js +41 -0
- package/internal/sdk-projection.js +10 -82
- package/internal/session-cell-executor.js +42 -19
- package/internal/session-events.js +34 -0
- package/internal/session-journal-recovery.js +234 -11
- package/internal/session-journal-schema.js +6 -3
- package/internal/session-journal.js +31 -2
- package/internal/session-log-view.js +1 -1
- package/internal/session-runtime.js +119 -15
- package/internal/settings-compat.js +12 -5
- package/internal/stateful-module-compiler.js +3 -2
- package/internal/stateful-module-runtime.js +7 -3
- package/internal/stateful-root-runtime.js +9 -4
- package/internal/user-binding-authoring.js +2 -7
- package/internal/user-bindings-owner.js +19 -2
- package/internal/validated-parse-repair.js +1 -1
- package/internal/value-wire.js +68 -21
- package/internal/worker-cwd-virtualization.js +20 -18
- package/internal/worker-realm-surfaces.js +3 -3
- package/package.json +49 -30
- package/src/client-activity.js +3 -2
- package/src/client-authoring-view.js +67 -30
- package/src/client-binding-review.js +2 -1
- package/src/client-binding-toggle.js +18 -0
- package/src/client-capabilities-view.js +1555 -0
- package/src/client-capability-sort.js +29 -0
- package/src/client-copy.js +923 -8
- package/src/client-feature-gates.js +3 -0
- package/src/client-indicator-view.js +4 -6
- package/src/client-primitives.js +33 -2
- package/src/client-repl-view.js +20 -13
- package/src/client-settings-view.js +411 -41
- package/src/client-styles.js +112 -15
- package/src/client-tool-hover.js +185 -0
- package/src/client-tool-view.js +45 -9
- package/src/client-transport.js +44 -9
- package/src/client-workbench.js +6 -7
- package/src/client.js +76 -22
- package/client.js +0 -32479
|
@@ -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
|
+
}
|