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.
@@ -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.quick.enabled' : `capabilities.${choice}`) })) })))),
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
- disabled: disabled('capabilityPolicy'), onClick: () => setBatchOperation({
434
- targets: visible.filter(item => item.availability === 'available').map(item => ({ id: item.id, name: item.name })),
435
- excluded: visible.filter(item => item.availability !== 'available').length,
436
- scope: [...filterLabels, ...(query.trim() ? [t('capabilities.batch.search', { query: query.trim() })] : [])].join(' · ') || t('capabilities.batch.all'),
437
- }) }, t('capabilities.batchSettings')),
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' }, h('span', { className: 'ptcPlusCapabilityName' }, item.name), h('code', null, 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))),
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; setLoading(false); return
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
- const writesBlocked = management === 'paused' || management === 'off'
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 chooseDimension = (id, dimension, choice) => {
1113
- setDrafts(current => {
1114
- const patch = typeof current[id] === 'object' ? { ...current[id] } : {}
1115
- const original = catalog.find(entry => entry.id === id)?.temporaryRule?.[dimension] ?? 'inherit'
1116
- if (choice === original && choice === 'inherit') delete patch[dimension]
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
- const next = { ...current }; if (Object.keys(patch).length) next[id] = patch; else delete next[id]; return next
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 && next === 'inherit') delete nextChoices[id]
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
- let completed = 0
1144
- let pendingCount = 0
1145
- let failure = null
1146
- let savedOnly = null
1147
- for (const [id, selection, dimension] of staged) {
1148
- if (controller.signal.aborted) break
1149
- try {
1150
- const result = await commands.run(sessionId, `set ${targetScope} ${encodeURIComponent(id)} ${dimension ? dimension + ' ' : ''}${selection}`, controller.signal)
1151
- const receipt = readCapabilityCommandReceipt(result.text)
1152
- if (!receipt || receipt.sessionId !== sessionId || !['recorded', 'pending'].includes(receipt.state)) {
1153
- throw new Error(receipt ? capabilityCommandText(t, result.text) : t('capabilities.quick.unknownReceipt'))
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: t(choice === 'inherit' ? 'capabilities.quick.inherit' : `capabilities.${choice}`) })) : []
1210
- const ruleLabel = (rule, empty) => capabilityRuleLabel(t, rule, empty)
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
- const statusLabel = value => value && typeof value === 'object' ? ruleLabel(value) : capabilityChoiceLabel(t, value)
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('button', { type: 'button', className: 'ptcPlusSessionCapabilityName ptcPlusCapabilityName',
1220
- 'aria-current': selected?.id === entry.id ? 'true' : undefined,
1221
- onClick: event => { event.stopPropagation(); openEditor(entry) } }, entry.name ?? entry.id),
1222
- entry.name && entry.name !== entry.id ? h('code', null, entry.id) : null),
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') } }, statusLabel(effectiveAvailability(entry)))),
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') } }, statusLabel(effectiveContext(entry)))),
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
- writesBlocked ? h('p', { className: 'ptcPlusCapabilityNotice', role: 'status' },
1260
- t(management === 'paused' ? 'capabilities.quick.sessionPaused' : 'capabilities.quick.sessionManagementOff')) : null,
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', key: dimension, 'data-capability-dimension': dimension },
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: choices[selected.id]?.[dimension] ?? selected.temporaryRule?.[dimension] ?? 'inherit',
1490
+ value: dimensionCurrent(selected, dimension),
1316
1491
  onChange: next => chooseDimension(selected.id, dimension, next),
1317
- options: ['inherit', 'automatic', ...(dimension === 'availability' ? ['enabled', 'closed'] : CAPABILITY_DISCLOSURES)]
1318
- .map(choice => ({ value: choice, label: t(choice === 'inherit' ? 'capabilities.quick.inherit'
1319
- : choice === 'automatic' ? 'capabilities.policy.inheritRule' : choice === 'enabled' ? 'capabilities.quick.enabled' : `capabilities.${choice}`) })) }))))
1320
- : h('div', { className: 'ptcPlusCapabilityFilter' },
1321
- h('span', null, t('capabilities.quick.choice')),
1322
- h(Select, { label: t('capabilities.quick.choice'), value: choiceFor(selected),
1323
- disabled: !enabled || !available || pending || writesBlocked, options: choiceOptions,
1324
- onChange: next => choose(selected.id, next) })),
1325
- choices[selected.id] === 'inherit' ? h('p', { className: 'ptcPlusDetail' },
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
- h('button', { type: 'button', className: 'ptcPlusButton', title: t('capabilities.quick.sessionHint'),
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', 'data-kind': 'primary',
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
  }