dsh-ptc-plus 0.5.0 → 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 +9 -1
- package/README.md +9 -1
- package/client.cjs +517 -151
- package/docs/adr/0017-track-the-latest-dsh-public-surface.md +2 -2
- package/docs/adr/0023-global-user-bindings.md +1 -1
- package/docs/adr/0038-freeze-session-capability-plans.md +1 -1
- package/docs/adr/0044-coexist-with-the-official-persistent-repl.md +35 -0
- package/docs/adr/README.md +2 -0
- package/docs/capability-planning-components.md +2 -2
- package/docs/client-ui.md +8 -8
- package/docs/client-visual-contract.md +1 -1
- package/docs/installation.md +5 -3
- package/docs/publishing.md +2 -2
- package/docs/runtime-reference.md +6 -1
- package/index.js +27 -4
- package/internal/capability-command-receipt.js +3 -1
- package/internal/capability-environment.js +9 -3
- package/internal/capability-statistics.js +6 -2
- package/internal/config-spec.js +10 -1
- package/internal/edit-transport-owner.js +17 -3
- package/internal/execution-provider-owner.js +25 -0
- package/internal/execution-seam-compat.js +18 -0
- package/internal/official-repl-priority.js +51 -0
- package/internal/runtime-bridge-owner.js +10 -7
- package/internal/sdk-projection.js +2 -2
- package/internal/user-bindings-owner.js +5 -27
- package/package.json +3 -2
- package/src/client-activity.js +3 -2
- package/src/client-capabilities-view.js +315 -101
- package/src/client-copy.js +68 -16
- package/src/client-styles.js +4 -0
- package/src/client-tool-view.js +9 -2
|
@@ -286,7 +286,6 @@ export function createCapabilitySettingsViews(React, { SearchIcon, Select, Modal
|
|
|
286
286
|
'aria-label': t('capabilities.batch.cancel'), onClick: close }, '×')),
|
|
287
287
|
h('div', { className: 'ptcPlusCapabilityBatchBody' },
|
|
288
288
|
h('p', { className: 'ptcPlusDetail', role: 'status' }, t('capabilities.batch.targets', { count: operation.targets.length })),
|
|
289
|
-
h('p', { className: 'ptcPlusDetail' }, t('capabilities.batch.scope', { scope: operation.scope })),
|
|
290
289
|
operation.excluded ? h('p', { className: 'ptcPlusDetail' }, t('capabilities.batch.excluded', { count: operation.excluded })) : null,
|
|
291
290
|
operation.targets.length ? h('ul', { className: 'ptcPlusCapabilityBatchTargets', 'aria-label': t('capabilities.batch.targetList') },
|
|
292
291
|
...operation.targets.map(target => h('li', { key: target.id }, h('span', null, target.name), h('code', null, target.id))))
|
|
@@ -298,7 +297,8 @@ export function createCapabilitySettingsViews(React, { SearchIcon, Select, Modal
|
|
|
298
297
|
options: ['keep', 'automatic', ...(dimension === 'availability' ? ['enabled', 'closed'] : CAPABILITY_DISCLOSURES)]
|
|
299
298
|
.map(choice => ({ value: choice, label: t(choice === 'keep' ? 'capabilities.batch.keep'
|
|
300
299
|
: choice === 'automatic' ? 'capabilities.policy.inheritRule'
|
|
301
|
-
: choice === 'enabled' ? 'capabilities.
|
|
300
|
+
: choice === 'enabled' ? 'capabilities.action.enable'
|
|
301
|
+
: choice === 'closed' ? 'capabilities.action.close' : `capabilities.${choice}`) })) })))),
|
|
302
302
|
h('p', { className: 'ptcPlusDetail' }, t('capabilities.batch.effect')),
|
|
303
303
|
h('p', { className: 'ptcPlusDetail' }, t('capabilities.batch.sourceClosure')),
|
|
304
304
|
missing ? h('p', { className: 'ptcPlusMessage', role: 'alert' }, t('capabilities.batch.changedTargets', { count: missing })) : null,
|
|
@@ -321,6 +321,7 @@ export function createCapabilitySettingsViews(React, { SearchIcon, Select, Modal
|
|
|
321
321
|
const [availability, setAvailability] = React.useState('all')
|
|
322
322
|
const [sort, setSort] = React.useState(null)
|
|
323
323
|
const [selected, setSelected] = React.useState(null)
|
|
324
|
+
const [picked, setPicked] = React.useState([])
|
|
324
325
|
const [filtersOpen, setFiltersOpen] = React.useState(false)
|
|
325
326
|
const [batchOperation, setBatchOperation] = React.useState(null)
|
|
326
327
|
const [inheritedRulesOpen, setInheritedRulesOpen] = React.useState(false)
|
|
@@ -379,6 +380,15 @@ export function createCapabilitySettingsViews(React, { SearchIcon, Select, Modal
|
|
|
379
380
|
: column === 'source' ? displayedSource(item)
|
|
380
381
|
: column === 'availability' ? t(`capabilities.rowState.${state(item)}`)
|
|
381
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)])])
|
|
382
392
|
const entry = visible.find(item => item.id === selected)
|
|
383
393
|
const plan = sessionId ? plans?.find(item => item.sessionId === sessionId) : undefined
|
|
384
394
|
const effective = plan?.effective.entries.find(item => item.id === entry?.id)
|
|
@@ -430,11 +440,15 @@ export function createCapabilitySettingsViews(React, { SearchIcon, Select, Modal
|
|
|
430
440
|
h('button', { type: 'button', className: 'ptcPlusButton', 'aria-haspopup': 'dialog',
|
|
431
441
|
onClick: () => setFiltersOpen(true) }, t('capabilities.filterAction')),
|
|
432
442
|
h('button', { type: 'button', className: 'ptcPlusButton', 'aria-haspopup': 'dialog',
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
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),
|
|
438
452
|
h('button', { type: 'button', className: 'ptcPlusButton', 'aria-haspopup': 'dialog',
|
|
439
453
|
onClick: () => setInheritedRulesOpen(true) }, t('capabilities.inheritedRules'))),
|
|
440
454
|
filtersOpen ? h(CapabilityDialog, { title: t('capabilities.filterAction'), onClose: () => setFiltersOpen(false), t,
|
|
@@ -468,7 +482,12 @@ export function createCapabilitySettingsViews(React, { SearchIcon, Select, Modal
|
|
|
468
482
|
h('div', { className: 'ptcPlusCapabilityTable', role: 'table', 'aria-label': t('capabilities.management') },
|
|
469
483
|
h('div', { className: 'ptcPlusCapabilityColumns', role: 'row' },
|
|
470
484
|
...CAPABILITY_SORT_COLUMNS.map(column => h('span', { key: column.key, role: 'columnheader',
|
|
471
|
-
'aria-sort': capabilitySortDirection(sort, column.key)
|
|
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 })] : []),
|
|
472
491
|
h('button', { type: 'button', className: 'ptcPlusCapabilitySort', title: t('capabilities.sortCycle'),
|
|
473
492
|
onClick: () => setSort(current => cycleCapabilitySort(current, column.key)) },
|
|
474
493
|
t(`capabilities.${column.label}`), h('span', { 'aria-hidden': true },
|
|
@@ -476,7 +495,12 @@ export function createCapabilitySettingsViews(React, { SearchIcon, Select, Modal
|
|
|
476
495
|
h('div', { className: 'ptcPlusCapabilityList', role: 'rowgroup' }, ...visible.map(item => h('div', { key: item.id, className: 'ptcPlusCapabilityItem', role: 'row',
|
|
477
496
|
'data-selected': entry?.id === item.id, 'data-availability': item.availability,
|
|
478
497
|
onClick: () => setSelected(item.id) },
|
|
479
|
-
h('div', { className: 'ptcPlusCapabilityIdentity', role: 'cell' },
|
|
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))),
|
|
480
504
|
h('span', { className: 'ptcPlusCapabilityBadge', role: 'cell', 'data-kind': type(item) }, t(`capabilities.type.${type(item)}`)),
|
|
481
505
|
h('span', { className: 'ptcPlusCapabilitySource', role: 'cell' }, displayedSource(item)),
|
|
482
506
|
// The cell stays a plain cell so its empty area selects the row; only the switch and
|
|
@@ -973,9 +997,13 @@ export function createSessionCapabilityControlsView(React, { commands, Modal, Se
|
|
|
973
997
|
const available = React.useSyncExternalStore(source.subscribe, source.getSnapshot)
|
|
974
998
|
const [catalog, setCatalog] = React.useState([])
|
|
975
999
|
const [management, setManagement] = React.useState('unknown')
|
|
1000
|
+
const [savedSettings, setSavedSettings] = React.useState(null)
|
|
976
1001
|
const [loading, setLoading] = React.useState(false)
|
|
977
1002
|
const [error, setError] = React.useState(null)
|
|
978
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' })
|
|
979
1007
|
const [selectedId, setSelectedId] = React.useState(null)
|
|
980
1008
|
const [pane, setPane] = React.useState('list')
|
|
981
1009
|
const [query, setQuery] = React.useState('')
|
|
@@ -1009,7 +1037,10 @@ export function createSessionCapabilityControlsView(React, { commands, Modal, Se
|
|
|
1009
1037
|
applying.current = false
|
|
1010
1038
|
setCatalog([])
|
|
1011
1039
|
setManagement('unknown')
|
|
1040
|
+
setSavedSettings(null)
|
|
1012
1041
|
setDrafts(emptyDrafts())
|
|
1042
|
+
setPicked([])
|
|
1043
|
+
setBatchOpen(false)
|
|
1013
1044
|
setSelectedId(null)
|
|
1014
1045
|
setPane('list')
|
|
1015
1046
|
setFocusDimension(null)
|
|
@@ -1026,8 +1057,11 @@ export function createSessionCapabilityControlsView(React, { commands, Modal, Se
|
|
|
1026
1057
|
throw new Error('Invalid capability catalog')
|
|
1027
1058
|
}
|
|
1028
1059
|
setCatalog(payload.entries)
|
|
1060
|
+
setPicked(current => current.filter(id => payload.entries.some(entry => entry.id === id)))
|
|
1029
1061
|
setManagement(['managed', 'paused', 'off', 'unknown'].includes(payload.management)
|
|
1030
1062
|
? payload.management : 'unknown')
|
|
1063
|
+
setSavedSettings(payload.savedSettings !== null && typeof payload.savedSettings === 'object'
|
|
1064
|
+
&& !Array.isArray(payload.savedSettings) ? payload.savedSettings : null)
|
|
1031
1065
|
setError(null)
|
|
1032
1066
|
return payload.entries
|
|
1033
1067
|
} catch (failure) {
|
|
@@ -1038,24 +1072,24 @@ export function createSessionCapabilityControlsView(React, { commands, Modal, Se
|
|
|
1038
1072
|
}, [sessionId])
|
|
1039
1073
|
React.useEffect(() => {
|
|
1040
1074
|
if (!open || !enabled || !available) {
|
|
1041
|
-
request.current?.abort(); request.current = null
|
|
1075
|
+
if (!applying.current) { request.current?.abort(); request.current = null }
|
|
1076
|
+
setLoading(false)
|
|
1077
|
+
return
|
|
1042
1078
|
}
|
|
1079
|
+
// An in-flight apply owns the shared catalog request and reloads it on completion.
|
|
1080
|
+
if (applying.current) return
|
|
1043
1081
|
const controller = new AbortController()
|
|
1044
1082
|
request.current = controller
|
|
1045
1083
|
void load(controller.signal)
|
|
1046
1084
|
return () => { controller.abort(); if (request.current === controller) request.current = null }
|
|
1047
1085
|
}, [open, enabled, available, load])
|
|
1048
|
-
React.useEffect(() => {
|
|
1049
|
-
if (!enabled || !available) {
|
|
1050
|
-
request.current?.abort()
|
|
1051
|
-
request.current = null
|
|
1052
|
-
applying.current = false
|
|
1053
|
-
setPending(false)
|
|
1054
|
-
}
|
|
1055
|
-
}, [enabled, available])
|
|
1056
1086
|
const choices = drafts
|
|
1057
1087
|
const choiceFor = entry => choices[entry.id] ?? entry.temporary ?? 'inherit'
|
|
1058
|
-
|
|
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
|
|
1059
1093
|
const sourceKey = entry => entry.source?.kind === 'mcp' ? 'mcp:' + (entry.source.server ?? '')
|
|
1060
1094
|
: entry.source?.kind === 'skill' ? 'skill:' + (entry.source.bucket ?? '')
|
|
1061
1095
|
: entry.source?.kind ?? 'unknown'
|
|
@@ -1109,26 +1143,51 @@ export function createSessionCapabilityControlsView(React, { commands, Modal, Se
|
|
|
1109
1143
|
?? ['tools', 'skills', 'mcp', 'mcp_tools'].map(group => matches.find(entry => category(entry) === group)).find(Boolean)
|
|
1110
1144
|
const staged = Object.entries(choices).flatMap(([id, choice]) => typeof choice === 'object'
|
|
1111
1145
|
? Object.entries(choice).map(([dimension, selection]) => [id, selection, dimension]) : [[id, choice]])
|
|
1112
|
-
const
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
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]
|
|
1117
1163
|
else patch[dimension] = choice
|
|
1118
|
-
|
|
1119
|
-
|
|
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))
|
|
1120
1171
|
}
|
|
1121
1172
|
const choose = (id, next) => {
|
|
1122
1173
|
const entry = catalog.find(item => item.id === id)
|
|
1123
1174
|
if (!entry) return
|
|
1124
1175
|
const original = entry.temporary ?? 'inherit'
|
|
1176
|
+
if (next === (choices[id] ?? original)) return
|
|
1125
1177
|
setDrafts(current => {
|
|
1126
1178
|
const nextChoices = { ...current }
|
|
1127
|
-
if (next === original
|
|
1179
|
+
if (next === original) delete nextChoices[id]
|
|
1128
1180
|
else nextChoices[id] = next
|
|
1129
1181
|
return nextChoices
|
|
1130
1182
|
})
|
|
1131
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))
|
|
1132
1191
|
const hasInheritanceReset = staged.some(([, selection]) => selection === 'inherit')
|
|
1133
1192
|
const apply = async targetScope => {
|
|
1134
1193
|
if (applying.current || !enabled || !available || writesBlocked || staged.length === 0) return
|
|
@@ -1140,58 +1199,71 @@ export function createSessionCapabilityControlsView(React, { commands, Modal, Se
|
|
|
1140
1199
|
const controller = new AbortController()
|
|
1141
1200
|
request.current?.abort()
|
|
1142
1201
|
request.current = controller
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
if (
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
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
|
|
1154
1229
|
}
|
|
1155
|
-
if (receipt?.state === 'pending') pendingCount += 1
|
|
1156
|
-
completed += 1
|
|
1157
|
-
} catch (cause) {
|
|
1158
|
-
failure = String(cause?.message ?? cause)
|
|
1159
|
-
savedOnly = readCapabilityCommandReceipt(failure)?.state === 'saved-only'
|
|
1160
|
-
? readCapabilityCommandReceipt(failure) : null
|
|
1161
|
-
break
|
|
1162
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()
|
|
1163
1260
|
}
|
|
1164
|
-
if (request.current !== controller || controller.signal.aborted || !mounted.current) return
|
|
1165
|
-
const refreshed = await load(controller.signal)
|
|
1166
|
-
if (request.current !== controller || controller.signal.aborted || !mounted.current) return
|
|
1167
|
-
const confirmed = refreshed ? staged.slice(0, completed).filter(([id, selection, dimension]) => {
|
|
1168
|
-
const entry = refreshed.find(item => item.id === id)
|
|
1169
|
-
if (dimension) return entry && (targetScope === 'session'
|
|
1170
|
-
? (entry.temporaryRule?.[dimension] ?? 'inherit') === selection
|
|
1171
|
-
: (entry.savedRule?.[dimension] ?? 'automatic') === selection && entry.temporaryRule?.[dimension] === undefined)
|
|
1172
|
-
return entry && (targetScope === 'session' ? entry.temporary === selection
|
|
1173
|
-
: entry.saved === selection && entry.temporary === 'inherit')
|
|
1174
|
-
}) : []
|
|
1175
|
-
setDrafts(current => {
|
|
1176
|
-
const nextChoices = { ...current }
|
|
1177
|
-
for (const [id, selection, dimension] of confirmed) {
|
|
1178
|
-
if (savedOnly && id === staged[completed]?.[0] && dimension === staged[completed]?.[2]) continue
|
|
1179
|
-
if (dimension) {
|
|
1180
|
-
if (nextChoices[id]?.[dimension] !== submitted[id]?.[dimension]) continue
|
|
1181
|
-
const patch = { ...nextChoices[id] }; delete patch[dimension]
|
|
1182
|
-
if (Object.keys(patch).length) nextChoices[id] = patch; else delete nextChoices[id]
|
|
1183
|
-
} else if (nextChoices[id] === submitted[id]) delete nextChoices[id]
|
|
1184
|
-
}
|
|
1185
|
-
return nextChoices
|
|
1186
|
-
})
|
|
1187
|
-
if (savedOnly) setStatus({ key: 'capabilities.quick.savedOnly', params: { count: completed, error: savedOnly.error ?? '' } })
|
|
1188
|
-
else if (failure || !refreshed || confirmed.length < completed) setStatus({ key: 'capabilities.quick.partial',
|
|
1189
|
-
params: { count: confirmed.length, error: failure ?? t('capabilities.quick.confirmationUnavailable') } })
|
|
1190
|
-
else setStatus({ key: pendingCount ? 'capabilities.quick.pendingApplied' : 'capabilities.quick.applied', params: { count: completed } })
|
|
1191
|
-
if (request.current === controller) { request.current = null; applying.current = false; setPending(false) }
|
|
1192
1261
|
}
|
|
1193
1262
|
const discard = () => { setDrafts(emptyDrafts()); setStatus(null) }
|
|
1194
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
|
|
1195
1267
|
request.current?.abort()
|
|
1196
1268
|
request.current = null
|
|
1197
1269
|
applying.current = false
|
|
@@ -1203,35 +1275,124 @@ export function createSessionCapabilityControlsView(React, { commands, Modal, Se
|
|
|
1203
1275
|
const filter = (label, value, onChange, options, disabled = false) => h('div', { className: 'ptcPlusCapabilityFilter' },
|
|
1204
1276
|
h('span', null, label), h(Select, { label, value, onChange, disabled,
|
|
1205
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
|
+
}
|
|
1206
1348
|
const choiceOptions = selected ? [
|
|
1207
1349
|
'inherit', 'automatic',
|
|
1208
1350
|
...(selected.category === 'server' ? [] : ['full', 'on-demand']), 'closed',
|
|
1209
|
-
].map(choice => ({ value: choice, label:
|
|
1210
|
-
|
|
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}`) })) : []
|
|
1211
1355
|
const dimensionFacts = dimensions => ['availability', 'disclosure'].map(dimension =>
|
|
1212
1356
|
`${t(`capabilities.policy.${dimension}`)}: ${statusLabel(dimensions[dimension])} (${dimensions[`${dimension}Source`] ?? t('capabilities.quick.unknown')})`).join(' · ')
|
|
1213
|
-
|
|
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)
|
|
1214
1371
|
const openEditor = (entry, dimension = 'availability') => { setSelectedId(entry.id); setPane('detail'); setFocusDimension(dimension) }
|
|
1215
1372
|
const renderRow = entry => h('div', { key: entry.id, role: 'row',
|
|
1216
1373
|
className: 'ptcPlusSessionCapabilityRow ptcPlusCapabilityItem', 'data-capability-id': entry.id, 'data-selected': selected?.id === entry.id,
|
|
1217
1374
|
onClick: () => openEditor(entry) },
|
|
1218
1375
|
h('div', { role: 'cell', className: 'ptcPlusCapabilityIdentity' },
|
|
1219
|
-
h('
|
|
1220
|
-
'aria-
|
|
1221
|
-
onClick: event =>
|
|
1222
|
-
|
|
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)),
|
|
1223
1384
|
h('span', { role: 'cell', className: 'ptcPlusCapabilityBadge', 'data-kind': type(entry) }, t(`capabilities.type.${type(entry)}`)),
|
|
1224
1385
|
h('span', { role: 'cell', className: 'ptcPlusCapabilitySource' }, displayedSource(entry)),
|
|
1225
1386
|
h('div', { role: 'cell', className: 'ptcPlusCapabilityAvailability', 'data-state': effectiveAvailability(entry) },
|
|
1226
1387
|
h('button', { type: 'button', className: 'ptcPlusCapabilityContext',
|
|
1227
1388
|
'aria-label': t('capabilities.policy.availabilityFor', { name: entry.id }),
|
|
1228
1389
|
title: t('capabilities.quick.effective'),
|
|
1229
|
-
onClick: event => { event.stopPropagation(); openEditor(entry, 'availability') } },
|
|
1390
|
+
onClick: event => { event.stopPropagation(); openEditor(entry, 'availability') } }, dimensionCell(entry, 'availability', effectiveAvailability(entry)))),
|
|
1230
1391
|
h('div', { role: 'cell', className: 'ptcPlusCapabilityContextCell' },
|
|
1231
1392
|
h('button', { type: 'button', className: 'ptcPlusCapabilityContext',
|
|
1232
1393
|
'aria-label': t('capabilities.editContextFor', { name: entry.id }),
|
|
1233
1394
|
title: t('capabilities.quick.effective'),
|
|
1234
|
-
onClick: event => { event.stopPropagation(); openEditor(entry, 'disclosure') } },
|
|
1395
|
+
onClick: event => { event.stopPropagation(); openEditor(entry, 'disclosure') } }, dimensionCell(entry, 'disclosure', effectiveContext(entry)))),
|
|
1235
1396
|
h('span', { role: 'cell', className: 'ptcPlusSessionCapabilityPending', 'data-changed': Object.hasOwn(choices, entry.id),
|
|
1236
1397
|
'data-label': t('capabilities.quick.changed') },
|
|
1237
1398
|
Object.hasOwn(choices, entry.id) ? statusLabel(choices[entry.id]) : '—'))
|
|
@@ -1240,7 +1401,7 @@ export function createSessionCapabilityControlsView(React, { commands, Modal, Se
|
|
|
1240
1401
|
h('div', { className: 'ptcPlusSessionCapabilityHead' },
|
|
1241
1402
|
h('div', null, h('h2', null, t('capabilities.quick.modalTitle')),
|
|
1242
1403
|
h('p', null, t('capabilities.quick.sessionIdentity', { session: sessionId }))),
|
|
1243
|
-
h('button', { type: 'button', className: 'ptcPlusDialogClose', 'aria-label': t('settings.close'), onClick: close }, '×')),
|
|
1404
|
+
h('button', { type: 'button', className: 'ptcPlusDialogClose', 'aria-label': t('settings.close'), disabled: pending, onClick: close }, '×')),
|
|
1244
1405
|
h('div', { className: 'ptcPlusSessionCapabilityToolbar' },
|
|
1245
1406
|
h('label', { className: 'ptcPlusSearch' },
|
|
1246
1407
|
h('svg', { viewBox: '0 0 24 24', width: 16, height: 16, 'aria-hidden': true },
|
|
@@ -1256,13 +1417,27 @@ export function createSessionCapabilityControlsView(React, { commands, Modal, Se
|
|
|
1256
1417
|
filter(t('capabilities.quick.state'), stateFilter, setStateFilter,
|
|
1257
1418
|
[['all', t('capabilities.quick.allStates')], ['changed', t('capabilities.quick.changed')],
|
|
1258
1419
|
...['full', 'on-demand', 'closed', 'pending'].map(key => [key, statusLabel(key)])])),
|
|
1259
|
-
|
|
1260
|
-
|
|
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,
|
|
1261
1431
|
h('div', { className: 'ptcPlusSessionCapabilityWorkspace', 'data-pane': pane },
|
|
1262
1432
|
h('div', { className: 'ptcPlusSessionCapabilityList', role: 'table', 'aria-label': t('capabilities.quick.members'), ref: list },
|
|
1263
1433
|
h('div', { className: 'ptcPlusSessionCapabilityColumns ptcPlusCapabilityColumns', role: 'row' },
|
|
1264
1434
|
...CAPABILITY_SORT_COLUMNS.map(column => h('span', { key: column.key, role: 'columnheader',
|
|
1265
|
-
'aria-sort': capabilitySortDirection(sort, column.key)
|
|
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 })] : []),
|
|
1266
1441
|
h('button', { type: 'button', className: 'ptcPlusCapabilitySort', title: t('capabilities.sortCycle'),
|
|
1267
1442
|
onClick: () => setSort(current => cycleCapabilitySort(current, column.key)) },
|
|
1268
1443
|
t(`capabilities.${column.label}`), h('span', { 'aria-hidden': true },
|
|
@@ -1308,34 +1483,73 @@ export function createSessionCapabilityControlsView(React, { commands, Modal, Se
|
|
|
1308
1483
|
h('ul', null, ...catalog.filter(entry => entry.id !== selected.id
|
|
1309
1484
|
&& entry.source?.kind === 'mcp' && entry.source.server === selected.source?.server)
|
|
1310
1485
|
.map(entry => h('li', { key: entry.id }, entry.name ?? entry.id)))) : null,
|
|
1311
|
-
selected.savedRule ? h(React.Fragment, null, ...['availability', 'disclosure'].map(dimension =>
|
|
1312
|
-
h('div', { className: 'ptcPlusCapabilityFilter',
|
|
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 },
|
|
1313
1488
|
h('span', null, t(`capabilities.policy.${dimension}`)),
|
|
1314
1489
|
h(Select, { label: t(`capabilities.policy.${dimension}`), disabled: !enabled || !available || pending || writesBlocked,
|
|
1315
|
-
value:
|
|
1490
|
+
value: dimensionCurrent(selected, dimension),
|
|
1316
1491
|
onChange: next => chooseDimension(selected.id, dimension, next),
|
|
1317
|
-
options:
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
: h(
|
|
1321
|
-
h('
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
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' },
|
|
1326
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,
|
|
1327
1531
|
h('div', { className: 'ptcPlusSessionCapabilityFooter' },
|
|
1328
1532
|
h('span', { role: 'status' }, pending ? t('capabilities.quick.applying') : status ? t(status.key, status.params)
|
|
1329
1533
|
: t('capabilities.quick.changes', { count: staged.length })),
|
|
1330
1534
|
h('button', { type: 'button', className: 'ptcPlusButton', disabled: pending || staged.length === 0, onClick: discard },
|
|
1331
1535
|
t('capabilities.quick.discard')),
|
|
1332
|
-
|
|
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'),
|
|
1333
1539
|
disabled: pending || !enabled || !available || writesBlocked || staged.length === 0, onClick: () => void apply('session') },
|
|
1334
1540
|
t('capabilities.quick.applySession')),
|
|
1335
|
-
h('button', { type: 'button', className: 'ptcPlusButton',
|
|
1541
|
+
h('button', { type: 'button', className: 'ptcPlusButton',
|
|
1336
1542
|
title: t(hasInheritanceReset ? 'capabilities.quick.inheritSessionOnly' : 'capabilities.quick.globalHint'),
|
|
1337
1543
|
disabled: pending || !enabled || !available || writesBlocked || staged.length === 0 || hasInheritanceReset,
|
|
1338
1544
|
onClick: () => void apply('global') },
|
|
1339
|
-
t('capabilities.quick.applyGlobal'))
|
|
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)))
|
|
1340
1554
|
}
|
|
1341
1555
|
}
|