dsh-ptc-plus 0.5.0 → 0.5.2
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 +15 -3
- package/README.md +15 -3
- package/client.cjs +1983 -234
- package/compiler-core.cjs +132 -132
- package/compiler-core.cjs.map +3 -3
- package/docs/adr/0005-temporary-rejected-cell-edit-transport.md +2 -0
- 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/0027-run-the-session-kernel-in-a-killable-helper-process.md +11 -1
- package/docs/adr/0032-own-session-log-facts-in-a-host-only-projection.md +31 -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/0045-customize-model-visible-prompt-text.md +38 -0
- package/docs/adr/0046-gate-the-commit-with-the-semantic-evidence-tier.md +35 -0
- package/docs/adr/0047-store-journal-argument-digests-in-the-projection.md +88 -0
- package/docs/adr/README.md +2 -0
- package/docs/architecture.md +2 -2
- package/docs/capability-planning-components.md +2 -2
- package/docs/client-ui.md +13 -10
- package/docs/client-visual-contract.md +1 -1
- package/docs/durability-design.md +7 -1
- package/docs/installation.md +5 -3
- package/docs/publishing.md +2 -2
- package/docs/releases/v0.5.2.md +79 -0
- package/docs/runtime-reference.md +42 -3
- package/docs/unmeasured-cell-recovery-repair-proposal.md +387 -0
- package/docs/verification.md +6 -2
- package/index.js +63 -19
- package/internal/bounded-literal.js +85 -0
- package/internal/capability-command-receipt.js +3 -1
- package/internal/capability-environment.js +171 -13
- package/internal/capability-plan.js +11 -8
- package/internal/capability-statistics.js +6 -2
- package/internal/cell-error.js +13 -0
- package/internal/config-spec.js +122 -2
- package/internal/direct-surface-owner.js +14 -10
- package/internal/edit-transport-owner.js +96 -17
- package/internal/execution-provider-owner.js +25 -0
- package/internal/execution-seam-compat.js +18 -0
- package/internal/isolated-worker.js +134 -17
- package/internal/kernel-worker.js +167 -3
- package/internal/legacy-cell-analysis.js +14 -2
- package/internal/legacy-repl-convenience.js +16 -3
- package/internal/official-repl-priority.js +51 -0
- package/internal/projection-retention.js +238 -0
- package/internal/prompt-text.js +858 -0
- package/internal/recovery-tips.js +50 -11
- package/internal/rejected-cell-editor.js +3 -2
- package/internal/runtime-bridge-owner.js +11 -8
- package/internal/runtime-config.js +2 -0
- package/internal/runtime-contexts.js +14 -5
- package/internal/runtime-messages.js +43 -15
- package/internal/sdk-projection.js +2 -2
- package/internal/session-cell-executor.js +132 -11
- package/internal/session-journal-recovery.js +1009 -99
- package/internal/session-journal-schema.js +7 -5
- package/internal/session-journal.js +177 -14
- package/internal/session-log-view.js +111 -24
- package/internal/session-runtime.js +674 -33
- package/internal/session-state.js +27 -0
- package/internal/stateful-root-compiler.js +11 -0
- package/internal/stateful-root-runtime.js +17 -0
- package/internal/user-binding-console.js +7 -0
- package/internal/user-bindings-owner.js +8 -27
- package/internal/user-bindings.js +2 -2
- package/internal/validated-parse-repair.js +6 -1
- package/internal/worker-client.js +27 -4
- package/package.json +5 -2
- package/src/client-activity.js +27 -6
- package/src/client-capabilities-view.js +774 -125
- package/src/client-copy.js +231 -18
- package/src/client-repl-view.js +6 -1
- package/src/client-styles.js +68 -2
- package/src/client-tool-view.js +9 -2
|
@@ -4,6 +4,10 @@ import { CAPABILITY_CONFIGURATION_KEYS, capabilityProfileConfiguration, validate
|
|
|
4
4
|
import { readCapabilityCommandReceipt } from '../internal/capability-command-receipt.js'
|
|
5
5
|
import { createFallbackModal } from './client-primitives.js'
|
|
6
6
|
import { CAPABILITY_SORT_COLUMNS, capabilitySortDirection, cycleCapabilitySort, sortCapabilityEntries } from './client-capability-sort.js'
|
|
7
|
+
import {
|
|
8
|
+
PROMPT_ASSETS, PROMPT_TEXT_MAX_SLOT_BYTES, PROMPT_TEXT_SCHEMA,
|
|
9
|
+
accountPromptText, parsePromptTextDocument, resolvePromptText,
|
|
10
|
+
} from '../internal/prompt-text.js'
|
|
7
11
|
|
|
8
12
|
export function capabilityCommandText(t, text) {
|
|
9
13
|
if (text?.key) return t(text.key, text.params)
|
|
@@ -11,6 +15,20 @@ export function capabilityCommandText(t, text) {
|
|
|
11
15
|
return receipt ? t(`capabilities.receipt.${receipt.state}`, { session: receipt.sessionId, error: receipt.error ?? '' }) : text
|
|
12
16
|
}
|
|
13
17
|
|
|
18
|
+
// Host-owned context the model sees in PTC mode. The plugin never receives the host's
|
|
19
|
+
// own instruction text and cannot edit these bytes; it can only name the section it
|
|
20
|
+
// appends into or replaces. Listing them keeps the pane answering "what does the model
|
|
21
|
+
// actually see" instead of silently showing plugin-owned anchors only.
|
|
22
|
+
const HOST_CONTEXT_SECTIONS = Object.freeze([
|
|
23
|
+
Object.freeze({ id: 'host:system-prompt', presence: 'capabilities.prompt.hostPresenceEvery' }),
|
|
24
|
+
Object.freeze({ id: 'host:tools:sdk', presence: 'capabilities.prompt.hostPresenceCapabilities' }),
|
|
25
|
+
])
|
|
26
|
+
// The prompt table is one column model shared by the colgroup, the header and every
|
|
27
|
+
// data row. Slot chips do not get a column: they render in their own full-width
|
|
28
|
+
// sub-row, because a narrow chip column stacked a five-slot anchor into a tall stack.
|
|
29
|
+
const PROMPT_TABLE_COLUMN_KEYS = Object.freeze(['channel', 'anchor', 'presence', 'bytes', 'reason', 'kind'])
|
|
30
|
+
const PROMPT_TABLE_COLUMNS = PROMPT_TABLE_COLUMN_KEYS.length
|
|
31
|
+
|
|
14
32
|
function capabilityChoiceLabel(t, value) {
|
|
15
33
|
return t(value === 'inherit' ? 'capabilities.quick.noOverride'
|
|
16
34
|
: ['enabled', 'pending', 'unknown'].includes(value) ? `capabilities.quick.${value}`
|
|
@@ -30,13 +48,66 @@ function validCatalogRule(rule, temporary = false) {
|
|
|
30
48
|
? ['enabled', 'closed'].includes(choice) : CAPABILITY_DISCLOSURES.includes(choice))))
|
|
31
49
|
}
|
|
32
50
|
|
|
51
|
+
/** A Chat snapshot without a node store cannot order a receipt against the conversation. */
|
|
52
|
+
const noSnapshot = () => undefined
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* The first human message is the conversation's opening position. DSH may reuse
|
|
56
|
+
* a blank Session, so capability commands recorded before that message are
|
|
57
|
+
* history from an earlier attempt, not part of this conversation; they must not
|
|
58
|
+
* appear above the user's first sentence.
|
|
59
|
+
*/
|
|
60
|
+
export function firstHumanMessageSeq(nodes) {
|
|
61
|
+
if (nodes === null || nodes === undefined || typeof nodes.values !== 'function') return null
|
|
62
|
+
let first = null
|
|
63
|
+
for (const node of nodes.values()) {
|
|
64
|
+
if (node?.kind !== 'user' || node.data?.source?.kind !== 'user') continue
|
|
65
|
+
const seq = node.anchorSeq ?? node.data?.seq
|
|
66
|
+
if (!Number.isSafeInteger(seq)) continue
|
|
67
|
+
if (first === null || seq < first) first = seq
|
|
68
|
+
}
|
|
69
|
+
return first
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* A capability receipt belongs to chat only after the conversation has opened.
|
|
74
|
+
* A blank Session has nothing to announce, and a receipt recorded before the
|
|
75
|
+
* first human message describes a previous attempt's publication. A truncated
|
|
76
|
+
* Chat window cannot prove where the conversation opened, so its earliest
|
|
77
|
+
* loaded message must not hide a receipt the session may still own.
|
|
78
|
+
*/
|
|
79
|
+
export function capabilityReceiptVisible(node, { blank = false, firstUserSeq = null, windowComplete = true } = {}) {
|
|
80
|
+
if (blank === true) return false
|
|
81
|
+
if (windowComplete !== true) return true
|
|
82
|
+
return !(Number.isSafeInteger(node?.seq) && Number.isSafeInteger(firstUserSeq) && node.seq < firstUserSeq)
|
|
83
|
+
}
|
|
84
|
+
|
|
33
85
|
export function createCapabilityCommandCard(React) {
|
|
34
86
|
const h = React.createElement
|
|
35
|
-
return function CapabilityCommandCard({ node, t }) {
|
|
87
|
+
return function CapabilityCommandCard({ node, t, useChat, useSession }) {
|
|
36
88
|
const [showSource, setShowSource] = React.useState(false)
|
|
89
|
+
// Both hooks are always called so the seat keeps one hook order whether or
|
|
90
|
+
// not the surrounding Chat or Session supplies its store.
|
|
91
|
+
const readSession = typeof useSession === 'function' ? useSession : noSnapshot
|
|
92
|
+
const blank = readSession(snapshot => snapshot?.blank === true) === true
|
|
93
|
+
// `hasMore` marks a window that does not reach the session start; only a
|
|
94
|
+
// complete window may claim its earliest human message is the first one.
|
|
95
|
+
const windowComplete = readSession(snapshot => snapshot?.hasMore !== true)
|
|
96
|
+
const readChat = typeof useChat === 'function' ? useChat : noSnapshot
|
|
97
|
+
const firstUserSeq = readChat(snapshot => firstHumanMessageSeq(snapshot?.nodes))
|
|
98
|
+
// A capability notice is the outcome of a `/capabilities` command the user
|
|
99
|
+
// submitted. What a new session publishes is a model-visible runtime context,
|
|
100
|
+
// not chat, so a node without that submitted input is not a user command and
|
|
101
|
+
// renders no notice: a fresh session has no change to announce.
|
|
102
|
+
if (typeof node.args !== 'string') return null
|
|
103
|
+
if (!capabilityReceiptVisible(node, { blank, firstUserSeq, windowComplete })) return null
|
|
37
104
|
const command = `/${node.name ?? 'capabilities'}${node.args ? ` ${String(node.args).trim()}` : ''}`
|
|
38
105
|
const text = node.outcome?.text
|
|
39
106
|
const receipt = readCapabilityCommandReceipt(text)
|
|
107
|
+
// Pending is the plan's publication state, not an outcome: it becomes true
|
|
108
|
+
// only at the next accepted model step, so a durable notice would state a
|
|
109
|
+
// future that a historical row can no longer verify.
|
|
110
|
+
if (receipt?.state === 'pending') return null
|
|
40
111
|
const legacyRefresh = text === 'Session capability environment refreshed; the model-visible notification is recorded.'
|
|
41
112
|
if (receipt || String(node.args ?? '').trim() === 'refresh' || legacyRefresh) {
|
|
42
113
|
const outcome = receipt ? t(`capabilities.card.${receipt.state}`, { error: receipt.error ?? '' })
|
|
@@ -128,12 +199,13 @@ export function capabilityPlanReason(t, entry) {
|
|
|
128
199
|
export function createCapabilitySettingsViews(React, { SearchIcon, Select, Modal = createFallbackModal(React) }) {
|
|
129
200
|
const h = React.createElement
|
|
130
201
|
const field = key => CONFIG_FIELDS.find(item => item.key === key)
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
202
|
+
// Each group owns one settings tab; the prompt text switch lives in the same
|
|
203
|
+
// capabilities section, so 新会话设置 must list it or a saved choice would be
|
|
204
|
+
// invisible there.
|
|
205
|
+
// 新会话设置 is one row of equally sized choices. Groups only existed to give each
|
|
206
|
+
// tab a caption; that produced a second row as soon as 提示文本 was added, so the
|
|
207
|
+
// choices stay flat and the navigation target is decided by `onNavigate` instead.
|
|
208
|
+
const responsibilities = ['rules', 'description', 'recovery', 'preview', 'statistics', 'promptText']
|
|
137
209
|
const choicesFor = (configuration, plan) => {
|
|
138
210
|
const historical = plan && ['ptc-capability-plan/v1', 'ptc-capability-plan/v3'].includes(plan.schema)
|
|
139
211
|
&& configuration && configuration.capabilityPolicy === undefined
|
|
@@ -143,10 +215,13 @@ export function createCapabilitySettingsViews(React, { SearchIcon, Select, Modal
|
|
|
143
215
|
enabled: configuration?.capabilityManagementEnabled ?? plan?.enabled,
|
|
144
216
|
statistics,
|
|
145
217
|
recoveryEnabled: configuration?.capabilityRecoveryEnabled ?? (historical ? plan.enabled : undefined),
|
|
146
|
-
previewEnabled: configuration?.modelPreviewEnabled ?? (historical ? plan.enabled : undefined)
|
|
218
|
+
previewEnabled: configuration?.modelPreviewEnabled ?? (historical ? plan.enabled : undefined),
|
|
219
|
+
// The switch is new: a stored configuration without it means "on", the
|
|
220
|
+
// documented default, while an unreadable configuration stays unknown.
|
|
221
|
+
promptTextEnabled: configuration === undefined ? undefined : configuration.capabilityPromptTextEnabled !== false }
|
|
147
222
|
}
|
|
148
223
|
const flagsFor = settings => ({ rules: settings.enabled, description: settings.enabled, statistics: settings.statistics?.enabled,
|
|
149
|
-
recovery: settings.recoveryEnabled, preview: settings.previewEnabled })
|
|
224
|
+
recovery: settings.recoveryEnabled, preview: settings.previewEnabled, promptText: settings.promptTextEnabled })
|
|
150
225
|
const choiceState = (retained, applied, key) => typeof retained[key] !== 'boolean' ? 'unknown'
|
|
151
226
|
: retained[key] ? typeof applied[key] !== 'boolean' ? 'unknown' : applied[key] === false ? 'paused' : 'selected'
|
|
152
227
|
: key === 'rules' || key === 'description' ? 'host' : 'unselected'
|
|
@@ -176,17 +251,16 @@ export function createCapabilitySettingsViews(React, { SearchIcon, Select, Modal
|
|
|
176
251
|
return h('section', { className: 'ptcPlusCombination', 'aria-label': t('capabilities.combination.saved') },
|
|
177
252
|
h('div', { className: 'ptcPlusTaskHead' }, h('h4', null, t('capabilities.combination.saved')),
|
|
178
253
|
h('span', { className: 'ptcPlusCapabilitySaveState', role: 'status' }, feedback)),
|
|
179
|
-
h('div', { className: 'ptcPlusCombinationChoices' }, ...
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
}))))),
|
|
254
|
+
h('div', { className: 'ptcPlusCombinationChoices' }, ...responsibilities.map(key => {
|
|
255
|
+
const state = choiceState(retained, applied, key)
|
|
256
|
+
return h('button', { key, type: 'button', className: 'ptcPlusCombinationChoice', 'data-state': state,
|
|
257
|
+
'data-choice': key,
|
|
258
|
+
'aria-controls': key === 'promptText' ? `${id}-pane-prompt`
|
|
259
|
+
: key === 'rules' || key === 'statistics' ? `${id}-pane-${key === 'rules' ? 'manual' : 'statistics'}`
|
|
260
|
+
: `${id}-context-pane-${key}`,
|
|
261
|
+
onClick: () => onNavigate(key) }, h('span', null, t(`capabilities.combination.${key}`)),
|
|
262
|
+
h('small', null, t(`capabilities.combination.state.${state}`)))
|
|
263
|
+
})),
|
|
190
264
|
h('p', { className: 'ptcPlusDetail', role: 'status' }, effect),
|
|
191
265
|
h('details', { className: 'ptcPlusCapabilitySessionTools' }, h('summary', { tabIndex: 0 }, t('capabilities.existingSessions')),
|
|
192
266
|
renderRefresh?.(),
|
|
@@ -286,7 +360,6 @@ export function createCapabilitySettingsViews(React, { SearchIcon, Select, Modal
|
|
|
286
360
|
'aria-label': t('capabilities.batch.cancel'), onClick: close }, '×')),
|
|
287
361
|
h('div', { className: 'ptcPlusCapabilityBatchBody' },
|
|
288
362
|
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
363
|
operation.excluded ? h('p', { className: 'ptcPlusDetail' }, t('capabilities.batch.excluded', { count: operation.excluded })) : null,
|
|
291
364
|
operation.targets.length ? h('ul', { className: 'ptcPlusCapabilityBatchTargets', 'aria-label': t('capabilities.batch.targetList') },
|
|
292
365
|
...operation.targets.map(target => h('li', { key: target.id }, h('span', null, target.name), h('code', null, target.id))))
|
|
@@ -298,7 +371,8 @@ export function createCapabilitySettingsViews(React, { SearchIcon, Select, Modal
|
|
|
298
371
|
options: ['keep', 'automatic', ...(dimension === 'availability' ? ['enabled', 'closed'] : CAPABILITY_DISCLOSURES)]
|
|
299
372
|
.map(choice => ({ value: choice, label: t(choice === 'keep' ? 'capabilities.batch.keep'
|
|
300
373
|
: choice === 'automatic' ? 'capabilities.policy.inheritRule'
|
|
301
|
-
: choice === 'enabled' ? 'capabilities.
|
|
374
|
+
: choice === 'enabled' ? 'capabilities.action.enable'
|
|
375
|
+
: choice === 'closed' ? 'capabilities.action.close' : `capabilities.${choice}`) })) })))),
|
|
302
376
|
h('p', { className: 'ptcPlusDetail' }, t('capabilities.batch.effect')),
|
|
303
377
|
h('p', { className: 'ptcPlusDetail' }, t('capabilities.batch.sourceClosure')),
|
|
304
378
|
missing ? h('p', { className: 'ptcPlusMessage', role: 'alert' }, t('capabilities.batch.changedTargets', { count: missing })) : null,
|
|
@@ -321,6 +395,7 @@ export function createCapabilitySettingsViews(React, { SearchIcon, Select, Modal
|
|
|
321
395
|
const [availability, setAvailability] = React.useState('all')
|
|
322
396
|
const [sort, setSort] = React.useState(null)
|
|
323
397
|
const [selected, setSelected] = React.useState(null)
|
|
398
|
+
const [picked, setPicked] = React.useState([])
|
|
324
399
|
const [filtersOpen, setFiltersOpen] = React.useState(false)
|
|
325
400
|
const [batchOperation, setBatchOperation] = React.useState(null)
|
|
326
401
|
const [inheritedRulesOpen, setInheritedRulesOpen] = React.useState(false)
|
|
@@ -379,6 +454,15 @@ export function createCapabilitySettingsViews(React, { SearchIcon, Select, Modal
|
|
|
379
454
|
: column === 'source' ? displayedSource(item)
|
|
380
455
|
: column === 'availability' ? t(`capabilities.rowState.${state(item)}`)
|
|
381
456
|
: t(`capabilities.${item.resolvedRule.disclosure}`))
|
|
457
|
+
const pickedIds = picked.filter(id => entries.some(item => item.id === id))
|
|
458
|
+
const pickedEntries = pickedIds.map(id => entries.find(item => item.id === id)).filter(Boolean)
|
|
459
|
+
const pickedTargets = pickedEntries.filter(item => item.availability === 'available')
|
|
460
|
+
const allVisiblePicked = visible.length > 0 && visible.every(item => pickedIds.includes(item.id))
|
|
461
|
+
const someVisiblePicked = visible.some(item => pickedIds.includes(item.id))
|
|
462
|
+
const togglePick = id => setPicked(current => current.includes(id) ? current.filter(item => item !== id) : [...current, id])
|
|
463
|
+
const toggleAllVisible = () => setPicked(current => allVisiblePicked
|
|
464
|
+
? current.filter(id => !visible.some(item => item.id === id))
|
|
465
|
+
: [...new Set([...current, ...visible.map(item => item.id)])])
|
|
382
466
|
const entry = visible.find(item => item.id === selected)
|
|
383
467
|
const plan = sessionId ? plans?.find(item => item.sessionId === sessionId) : undefined
|
|
384
468
|
const effective = plan?.effective.entries.find(item => item.id === entry?.id)
|
|
@@ -430,11 +514,15 @@ export function createCapabilitySettingsViews(React, { SearchIcon, Select, Modal
|
|
|
430
514
|
h('button', { type: 'button', className: 'ptcPlusButton', 'aria-haspopup': 'dialog',
|
|
431
515
|
onClick: () => setFiltersOpen(true) }, t('capabilities.filterAction')),
|
|
432
516
|
h('button', { type: 'button', className: 'ptcPlusButton', 'aria-haspopup': 'dialog',
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
517
|
+
'aria-label': t('capabilities.batchSettings'),
|
|
518
|
+
disabled: disabled('capabilityPolicy') || pickedTargets.length === 0,
|
|
519
|
+
onClick: () => setBatchOperation({
|
|
520
|
+
targets: pickedTargets.map(item => ({ id: item.id, name: item.name })),
|
|
521
|
+
excluded: pickedIds.length - pickedTargets.length,
|
|
522
|
+
}) }, t('capabilities.batchSettings'),
|
|
523
|
+
pickedIds.length ? h('span', { className: 'ptcPlusCapabilityBatchCount', 'aria-hidden': true,
|
|
524
|
+
title: t('capabilities.selectionCount', { count: pickedIds.length }) },
|
|
525
|
+
String(pickedIds.length)) : null),
|
|
438
526
|
h('button', { type: 'button', className: 'ptcPlusButton', 'aria-haspopup': 'dialog',
|
|
439
527
|
onClick: () => setInheritedRulesOpen(true) }, t('capabilities.inheritedRules'))),
|
|
440
528
|
filtersOpen ? h(CapabilityDialog, { title: t('capabilities.filterAction'), onClose: () => setFiltersOpen(false), t,
|
|
@@ -468,7 +556,12 @@ export function createCapabilitySettingsViews(React, { SearchIcon, Select, Modal
|
|
|
468
556
|
h('div', { className: 'ptcPlusCapabilityTable', role: 'table', 'aria-label': t('capabilities.management') },
|
|
469
557
|
h('div', { className: 'ptcPlusCapabilityColumns', role: 'row' },
|
|
470
558
|
...CAPABILITY_SORT_COLUMNS.map(column => h('span', { key: column.key, role: 'columnheader',
|
|
471
|
-
'aria-sort': capabilitySortDirection(sort, column.key)
|
|
559
|
+
'aria-sort': capabilitySortDirection(sort, column.key),
|
|
560
|
+
...(column.key === 'name' ? { 'aria-label': t('capabilities.member'), className: 'ptcPlusCapabilityPickHead' } : {}) },
|
|
561
|
+
...(column.key === 'name' ? [h('input', { type: 'checkbox', className: 'ptcPlusPick', checked: allVisiblePicked,
|
|
562
|
+
ref: node => { if (node) node.indeterminate = someVisiblePicked && !allVisiblePicked },
|
|
563
|
+
disabled: visible.length === 0, 'aria-label': t('capabilities.selectAllVisible'),
|
|
564
|
+
onChange: toggleAllVisible })] : []),
|
|
472
565
|
h('button', { type: 'button', className: 'ptcPlusCapabilitySort', title: t('capabilities.sortCycle'),
|
|
473
566
|
onClick: () => setSort(current => cycleCapabilitySort(current, column.key)) },
|
|
474
567
|
t(`capabilities.${column.label}`), h('span', { 'aria-hidden': true },
|
|
@@ -476,7 +569,12 @@ export function createCapabilitySettingsViews(React, { SearchIcon, Select, Modal
|
|
|
476
569
|
h('div', { className: 'ptcPlusCapabilityList', role: 'rowgroup' }, ...visible.map(item => h('div', { key: item.id, className: 'ptcPlusCapabilityItem', role: 'row',
|
|
477
570
|
'data-selected': entry?.id === item.id, 'data-availability': item.availability,
|
|
478
571
|
onClick: () => setSelected(item.id) },
|
|
479
|
-
h('div', { className: 'ptcPlusCapabilityIdentity', role: 'cell' },
|
|
572
|
+
h('div', { className: 'ptcPlusCapabilityIdentity', role: 'cell' },
|
|
573
|
+
h('input', { type: 'checkbox', className: 'ptcPlusPick', checked: pickedIds.includes(item.id),
|
|
574
|
+
'aria-label': t('capabilities.selectForBatch', { name: item.name ?? item.id }),
|
|
575
|
+
onClick: event => event.stopPropagation(), onChange: () => togglePick(item.id) }),
|
|
576
|
+
h('div', { className: 'ptcPlusCapabilityIdentityText' },
|
|
577
|
+
h('span', { className: 'ptcPlusCapabilityName' }, item.name), h('code', null, item.id))),
|
|
480
578
|
h('span', { className: 'ptcPlusCapabilityBadge', role: 'cell', 'data-kind': type(item) }, t(`capabilities.type.${type(item)}`)),
|
|
481
579
|
h('span', { className: 'ptcPlusCapabilitySource', role: 'cell' }, displayedSource(item)),
|
|
482
580
|
// The cell stays a plain cell so its empty area selects the row; only the switch and
|
|
@@ -865,17 +963,375 @@ export function createCapabilitySettingsViews(React, { SearchIcon, Select, Modal
|
|
|
865
963
|
event.currentTarget.parentElement.querySelectorAll('[role=tab]')[next].focus({ preventScroll: true })
|
|
866
964
|
}
|
|
867
965
|
|
|
966
|
+
/**
|
|
967
|
+
* Per-asset editor for the fixed prompt text the plugin writes.
|
|
968
|
+
*
|
|
969
|
+
* One row per registered asset: the built-in sentence stays visible as the
|
|
970
|
+
* placeholder, so a user compares against it instead of a blank box. The
|
|
971
|
+
* document is written through the normal settings path, which validates it;
|
|
972
|
+
* a document the panel cannot read is reported here instead of blanking the
|
|
973
|
+
* whole workbench.
|
|
974
|
+
*/
|
|
975
|
+
// Registry codes are protocol identifiers. The pane shows a localized label and
|
|
976
|
+
// keeps the raw code in the element title, so a Chinese UI never reads like a
|
|
977
|
+
// dump of `fact-derived` / `protocol-exact-text` constants.
|
|
978
|
+
const PROMPT_REASON_KEYS = Object.freeze({
|
|
979
|
+
'fact-derived': 'factDerived',
|
|
980
|
+
'aggregate-frozen': 'aggregateFrozen',
|
|
981
|
+
'protocol-exact-text': 'protocolExactText',
|
|
982
|
+
'protocol-structural': 'protocolStructural',
|
|
983
|
+
'shadows-host': 'shadowsHost',
|
|
984
|
+
'other-owner': 'otherOwner',
|
|
985
|
+
})
|
|
986
|
+
|
|
987
|
+
function promptReasonLabel(t, reason) {
|
|
988
|
+
const key = PROMPT_REASON_KEYS[reason]
|
|
989
|
+
return key === undefined ? reason : t(`capabilities.prompt.reason.${key}`)
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
function promptKindLabel(t, kind) {
|
|
993
|
+
return kind === undefined ? '' : t(`capabilities.prompt.kind.${kind}`)
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
function PromptTextSettings({ value, disabled, onChange, t, id }) {
|
|
997
|
+
let document
|
|
998
|
+
let readError
|
|
999
|
+
try {
|
|
1000
|
+
document = parsePromptTextDocument(value.promptText)
|
|
1001
|
+
} catch (error) {
|
|
1002
|
+
readError = error.message
|
|
1003
|
+
document = { schema: PROMPT_TEXT_SCHEMA, overrides: {} }
|
|
1004
|
+
}
|
|
1005
|
+
// The workbench receives `disabled` as a per-field predicate, not a boolean.
|
|
1006
|
+
// The feature's own pause switch is separate from the settings lock: off means
|
|
1007
|
+
// every channel falls back to the built-in text while the saved document stays.
|
|
1008
|
+
const paused = value.capabilityPromptTextEnabled === false
|
|
1009
|
+
const blocked = paused || disabled === true
|
|
1010
|
+
|| (typeof disabled === 'function' && disabled('promptText') === true)
|
|
1011
|
+
const resolved = resolvePromptText(value.promptText)
|
|
1012
|
+
const account = accountPromptText(resolved)
|
|
1013
|
+
// The pane stages edits in a draft and writes the document once, so the fields
|
|
1014
|
+
// and the preview always describe the same pending bytes. 保存 writes the draft;
|
|
1015
|
+
// 取消 drops it; a saved document still differs from the built-ins, so reopening
|
|
1016
|
+
// the pane shows that difference without a draft.
|
|
1017
|
+
const [draft, setDraft] = React.useState(null)
|
|
1018
|
+
const [selectedAnchorId, setSelectedAnchorId] = React.useState(null)
|
|
1019
|
+
const anchorPanelRef = React.useRef(null)
|
|
1020
|
+
const [openSlots, setOpenSlots] = React.useState({})
|
|
1021
|
+
const overrides = draft ?? document.overrides
|
|
1022
|
+
const byteCount = text => new TextEncoder().encode(text).length
|
|
1023
|
+
const group = number => String(Math.round(number)).replace(/\B(?=(\d{3})+(?!\d))/gu, ' ')
|
|
1024
|
+
const bytePair = bytes => t('capabilities.prompt.bytesPair', {
|
|
1025
|
+
bytes: group(bytes), kb: (bytes / 1024).toFixed(1),
|
|
1026
|
+
})
|
|
1027
|
+
const write = next => onChange(CONFIG_FIELDS.find(field => field.key === 'promptText'),
|
|
1028
|
+
JSON.stringify({ schema: PROMPT_TEXT_SCHEMA, overrides: next }))
|
|
1029
|
+
const update = (slotId, next) => {
|
|
1030
|
+
const base = { ...overrides }
|
|
1031
|
+
if (next === undefined) delete base[slotId]
|
|
1032
|
+
else base[slotId] = next
|
|
1033
|
+
setDraft(base)
|
|
1034
|
+
}
|
|
1035
|
+
const save = () => {
|
|
1036
|
+
if (draft === null) return
|
|
1037
|
+
write(draft)
|
|
1038
|
+
setDraft(null)
|
|
1039
|
+
}
|
|
1040
|
+
const cancel = () => setDraft(null)
|
|
1041
|
+
const pending = draft !== null
|
|
1042
|
+
const unmeasuredSlots = account.counts.unmeasuredSlots
|
|
1043
|
+
const dynamicSlots = account.counts.dynamicSlots
|
|
1044
|
+
const unwiredAnchors = account.anchors.filter(anchor => anchor.wired !== true)
|
|
1045
|
+
const measuredBytes = account.owners.A + account.owners.B + account.owners.C
|
|
1046
|
+
const ownerOf = anchor => (anchor.owner === 'mixed' ? 'mixed' : anchor.owner)
|
|
1047
|
+
const ownerAnchors = key => account.anchors.filter(anchor => ownerOf(anchor) === key).length
|
|
1048
|
+
// Icons are monochrome inline SVG: the mockup's cards carry a glyph, and an
|
|
1049
|
+
// icon font or an emoji would bring its own colour into the settings surface.
|
|
1050
|
+
const icon = path => h('svg', {
|
|
1051
|
+
viewBox: '0 0 20 20', width: 16, height: 16, fill: 'none', stroke: 'currentColor',
|
|
1052
|
+
'stroke-width': 1.6, 'stroke-linecap': 'round', 'stroke-linejoin': 'round', 'aria-hidden': 'true',
|
|
1053
|
+
}, ...path.map((d, index) => h('path', { key: index, d })))
|
|
1054
|
+
const card = (key, labelKey, glyph) => {
|
|
1055
|
+
const mixed = key === 'mixed'
|
|
1056
|
+
const bytes = account.owners[key] ?? 0
|
|
1057
|
+
const value_ = mixed
|
|
1058
|
+
? t('capabilities.prompt.cardMixed', { count: ownerAnchors('mixed') })
|
|
1059
|
+
: bytes > 0
|
|
1060
|
+
? `${bytePair(bytes)} · ${t('capabilities.prompt.cardMetaAnchors', { count: ownerAnchors(key) })}`
|
|
1061
|
+
: t('capabilities.prompt.cardMetaAnchors', { count: ownerAnchors(key) })
|
|
1062
|
+
return h('div', { key, className: 'ptcPlusPromptCard', 'data-owner': key },
|
|
1063
|
+
h('span', { className: 'ptcPlusPromptCardIcon' }, glyph),
|
|
1064
|
+
h('span', { className: 'ptcPlusPromptCardBody' },
|
|
1065
|
+
h('span', { className: 'ptcPlusPromptCardLabel' }, t(labelKey)),
|
|
1066
|
+
mixed
|
|
1067
|
+
? h('span', { className: 'ptcPlusPromptCardMeta' }, value_)
|
|
1068
|
+
: h('span', { className: 'ptcPlusPromptCardValue', 'data-owner-bytes': String(account.owners[key] ?? 0) }, value_)))
|
|
1069
|
+
}
|
|
1070
|
+
const cards = h('div', { className: 'ptcPlusPromptCards' },
|
|
1071
|
+
card('A', 'capabilities.prompt.ownerEditable', icon(['M4 16h3l9-9-3-3-9 9v3Z', 'M12.5 4.5 15.5 7.5'])),
|
|
1072
|
+
card('B', 'capabilities.prompt.ownerReadOnly', icon(['M5.5 9V6.5a4.5 4.5 0 0 1 9 0V9', 'M4.5 9h11v7h-11Z'])),
|
|
1073
|
+
card('C', 'capabilities.prompt.ownerOther', icon(['M10 10a3 3 0 1 0 0-6 3 3 0 0 0 0 6Z', 'M4 17c0-3 2.7-5 6-5s6 2 6 5'])),
|
|
1074
|
+
card('mixed', 'capabilities.prompt.ownerMixed', icon(['M4 6h12', 'M4 10h12', 'M4 14h12', 'M13 4v4', 'M7 8v4'])))
|
|
1075
|
+
const strip = h('p', { className: 'ptcPlusPromptStrip', 'data-prompt-strip': String(unwiredAnchors.length) },
|
|
1076
|
+
t('capabilities.prompt.strip', {
|
|
1077
|
+
anchors: unwiredAnchors.length, bytes: group(measuredBytes),
|
|
1078
|
+
kb: (measuredBytes / 1024).toFixed(1), total: account.counts.total,
|
|
1079
|
+
}))
|
|
1080
|
+
const reasonsOf = anchor => {
|
|
1081
|
+
const reasons = [...new Set(anchor.slots.map(slot => slot.reason).filter(Boolean))]
|
|
1082
|
+
return reasons.length === 0 ? '—' : reasons.map(reason => promptReasonLabel(t, reason)).join(' + ')
|
|
1083
|
+
}
|
|
1084
|
+
const kindsOf = anchor => [...new Set(anchor.slots.map(slot => slot.kind))]
|
|
1085
|
+
.map(kind => promptKindLabel(t, kind)).join(' + ')
|
|
1086
|
+
const splitOf = anchor => {
|
|
1087
|
+
const measured = anchor.slots.filter(slot => slot.measured === true && slot.bytes > 0)
|
|
1088
|
+
return measured.length > 1
|
|
1089
|
+
? t('capabilities.prompt.anchorBytesSplit', {
|
|
1090
|
+
parts: measured.map(slot => `${slot.slotId.split('.').pop()} ${slot.bytes}`).join(' / '),
|
|
1091
|
+
})
|
|
1092
|
+
: null
|
|
1093
|
+
}
|
|
1094
|
+
const select = anchorId => setSelectedAnchorId(anchorId)
|
|
1095
|
+
// The expanded panel sits below the table, so on a short viewport selecting a
|
|
1096
|
+
// lower row would otherwise leave the editor it opened off-screen.
|
|
1097
|
+
React.useEffect(() => {
|
|
1098
|
+
if (selectedAnchorId === null) return
|
|
1099
|
+
const node = anchorPanelRef.current
|
|
1100
|
+
if (node === null || typeof node.scrollIntoView !== 'function') return
|
|
1101
|
+
node.scrollIntoView({ block: 'nearest', behavior: 'smooth' })
|
|
1102
|
+
}, [selectedAnchorId])
|
|
1103
|
+
const rowKey = (event, anchorId) => {
|
|
1104
|
+
if (event.key !== 'Enter' && event.key !== ' ') return
|
|
1105
|
+
event.preventDefault()
|
|
1106
|
+
select(anchorId)
|
|
1107
|
+
}
|
|
1108
|
+
const anchorRow = (anchor, index) => {
|
|
1109
|
+
const split = splitOf(anchor)
|
|
1110
|
+
return h('tr', {
|
|
1111
|
+
key: anchor.anchorId,
|
|
1112
|
+
className: 'ptcPlusPromptRow',
|
|
1113
|
+
'data-anchor-id': anchor.anchorId,
|
|
1114
|
+
'data-wired': anchor.wired === true ? 'true' : 'false',
|
|
1115
|
+
// A plain <table> row cannot carry aria-selected (that needs role="grid"),
|
|
1116
|
+
// so the expanded row is announced with aria-current instead.
|
|
1117
|
+
'data-selected': selectedAnchorId === anchor.anchorId ? 'true' : 'false',
|
|
1118
|
+
'aria-current': selectedAnchorId === anchor.anchorId ? 'true' : undefined,
|
|
1119
|
+
tabIndex: 0,
|
|
1120
|
+
onClick: () => select(anchor.anchorId),
|
|
1121
|
+
onKeyDown: event => rowKey(event, anchor.anchorId),
|
|
1122
|
+
},
|
|
1123
|
+
h('td', { className: 'ptcPlusPromptIndex' }, String(index)),
|
|
1124
|
+
h('td', null, h('span', { className: 'ptcPlusPromptChannel', 'data-channel': anchor.channel }, anchor.channel)),
|
|
1125
|
+
h('td', null, h('code', { className: 'ptcPlusPromptAnchorId', title: anchor.anchorId }, anchor.anchorId)),
|
|
1126
|
+
h('td', null, h('span', { className: 'ptcPlusPromptPresence', title: t(anchor.presence) }, t(anchor.presence))),
|
|
1127
|
+
h('td', { className: 'ptcPlusPromptBytesCell' },
|
|
1128
|
+
anchor.wired !== true
|
|
1129
|
+
? h('span', { 'data-anchor-bytes': 'unwired' }, t('capabilities.prompt.unwiredRow', { slots: anchor.unmeasuredSlots }))
|
|
1130
|
+
: h('span', { 'data-anchor-bytes': String(anchor.bytes) },
|
|
1131
|
+
paused ? t('capabilities.prompt.pausedBytes', { bytes: anchor.bytes }) : t('capabilities.prompt.cardBytes', { bytes: anchor.bytes }),
|
|
1132
|
+
split === null ? null : h('small', { className: 'ptcPlusPromptSplit', 'data-anchor-split': 'true' }, split))),
|
|
1133
|
+
h('td', { className: 'ptcPlusPromptReasonCell' }, reasonsOf(anchor)),
|
|
1134
|
+
h('td', { className: 'ptcPlusPromptKindCell' }, kindsOf(anchor),
|
|
1135
|
+
anchor.wired !== true ? h('span', { className: 'ptcPlusPromptTag', 'data-prompt-unwired-tag': 'true' },
|
|
1136
|
+
t('capabilities.prompt.unwiredTag')) : null))
|
|
1137
|
+
}
|
|
1138
|
+
const hostRow = (section, index) => h('tr', {
|
|
1139
|
+
key: section.id, className: 'ptcPlusPromptRow ptcPlusPromptHostRow', 'data-host-row': section.id,
|
|
1140
|
+
},
|
|
1141
|
+
h('td', { className: 'ptcPlusPromptIndex' }, String(index)),
|
|
1142
|
+
h('td', null, h('span', { className: 'ptcPlusPromptChannel' }, 'prompt')),
|
|
1143
|
+
h('td', null, h('code', { className: 'ptcPlusPromptAnchorId', title: section.id }, section.id)),
|
|
1144
|
+
h('td', null, h('span', { className: 'ptcPlusPromptPresence' }, t(section.presence))),
|
|
1145
|
+
h('td', { className: 'ptcPlusPromptBytesCell' }, t('capabilities.prompt.hostUnavailable')),
|
|
1146
|
+
h('td', { className: 'ptcPlusPromptReasonCell' }, '—'),
|
|
1147
|
+
h('td', { className: 'ptcPlusPromptKindCell' }, t('capabilities.prompt.ownerHost')))
|
|
1148
|
+
const table = h('table', { className: 'ptcPlusPromptTable' },
|
|
1149
|
+
h('colgroup', null,
|
|
1150
|
+
h('col', { className: 'ptcPlusPromptColIndex' }),
|
|
1151
|
+
h('col', { className: 'ptcPlusPromptColChannel' }),
|
|
1152
|
+
h('col', { className: 'ptcPlusPromptColAnchor' }),
|
|
1153
|
+
h('col', { className: 'ptcPlusPromptColPresence' }),
|
|
1154
|
+
h('col', { className: 'ptcPlusPromptColBytes' }),
|
|
1155
|
+
h('col', { className: 'ptcPlusPromptColReason' }),
|
|
1156
|
+
h('col', { className: 'ptcPlusPromptColKind' })),
|
|
1157
|
+
h('thead', null, h('tr', null,
|
|
1158
|
+
h('th', { scope: 'col', className: 'ptcPlusPromptIndex' }, ''),
|
|
1159
|
+
h('th', { scope: 'col' }, t('capabilities.prompt.columnChannel')),
|
|
1160
|
+
h('th', { scope: 'col' }, t('capabilities.prompt.columnAnchor')),
|
|
1161
|
+
h('th', { scope: 'col' }, t('capabilities.prompt.columnPresence')),
|
|
1162
|
+
h('th', { scope: 'col' }, t('capabilities.prompt.columnBytesSplit')),
|
|
1163
|
+
h('th', { scope: 'col' }, t('capabilities.prompt.columnReason')),
|
|
1164
|
+
h('th', { scope: 'col' }, t('capabilities.prompt.columnKind')))),
|
|
1165
|
+
h('tbody', null,
|
|
1166
|
+
...account.anchors.map((anchor, index) => anchorRow(anchor, index + 1)),
|
|
1167
|
+
...HOST_CONTEXT_SECTIONS.map((section, index) => hostRow(section, account.anchors.length + index + 1))))
|
|
1168
|
+
const chip = (anchor, slot) => {
|
|
1169
|
+
const templateBytes = slot.measured === true && slot.kind === 'projected'
|
|
1170
|
+
const aggregateSet = slot.kind === 'aggregate'
|
|
1171
|
+
const shared = {
|
|
1172
|
+
className: 'ptcPlusPromptChip',
|
|
1173
|
+
'data-slot-id': slot.slotId,
|
|
1174
|
+
'data-slot-kind': slot.kind,
|
|
1175
|
+
...(templateBytes ? { 'data-prompt-template-bytes': 'true' } : {}),
|
|
1176
|
+
...(aggregateSet ? { 'data-prompt-aggregate-set': 'true' } : {}),
|
|
1177
|
+
title: templateBytes
|
|
1178
|
+
? `${slot.slotId} · ${t('capabilities.prompt.templateBytes')}`
|
|
1179
|
+
: aggregateSet ? `${slot.slotId} · ${t('capabilities.prompt.aggregateFrozen')}` : slot.slotId,
|
|
1180
|
+
}
|
|
1181
|
+
if (slot.owner !== 'A') {
|
|
1182
|
+
return h('span', { key: slot.slotId, ...shared, 'data-readonly': 'true' },
|
|
1183
|
+
h('span', { className: 'ptcPlusPromptChipLabel' }, slot.slotId),
|
|
1184
|
+
h('span', { className: 'ptcPlusPromptChipNote' },
|
|
1185
|
+
t('capabilities.prompt.readonlyReason', { reason: promptReasonLabel(t, slot.reason), kind: promptKindLabel(t, slot.kind) })))
|
|
1186
|
+
}
|
|
1187
|
+
const selected = (openSlots[anchor.anchorId] ?? editableSlots(anchor)[0]?.slotId) === slot.slotId
|
|
1188
|
+
return h('button', {
|
|
1189
|
+
key: slot.slotId,
|
|
1190
|
+
...shared,
|
|
1191
|
+
type: 'button',
|
|
1192
|
+
disabled: blocked,
|
|
1193
|
+
'aria-pressed': selected,
|
|
1194
|
+
onClick: () => setOpenSlots(previous => ({ ...previous, [anchor.anchorId]: slot.slotId })),
|
|
1195
|
+
}, h('span', { className: 'ptcPlusPromptChipLabel' }, slot.slotId))
|
|
1196
|
+
}
|
|
1197
|
+
// One editor per anchor: the chips choose which editable slot is open, so the
|
|
1198
|
+
// run_code anchor keeps its three descriptions behind one field.
|
|
1199
|
+
const editableSlots = anchor => anchor.slots.filter(slot => slot.owner === 'A'
|
|
1200
|
+
&& PROMPT_ASSETS.find(candidate => candidate.id === slot.slotId)?.defaultText !== null)
|
|
1201
|
+
const editorFor = anchor => {
|
|
1202
|
+
const slots = editableSlots(anchor)
|
|
1203
|
+
if (anchor.wired !== true || slots.length === 0) return null
|
|
1204
|
+
const selectedId = openSlots[anchor.anchorId] ?? slots[0].slotId
|
|
1205
|
+
const slot = slots.find(candidate => candidate.slotId === selectedId) ?? slots[0]
|
|
1206
|
+
const asset = PROMPT_ASSETS.find(candidate => candidate.id === slot.slotId)
|
|
1207
|
+
const override = Object.hasOwn(overrides, slot.slotId) ? overrides[slot.slotId] : undefined
|
|
1208
|
+
const current = override ?? asset.defaultText
|
|
1209
|
+
// The built-in sentence is the field's value, not a placeholder: the user must be
|
|
1210
|
+
// able to tweak the shipped wording without retyping it.
|
|
1211
|
+
return h('div', { key: slot.slotId, className: 'ptcPlusPromptEditor', 'data-editor-slot': slot.slotId },
|
|
1212
|
+
h('textarea', {
|
|
1213
|
+
id: `${id}-prompt-${slot.slotId}`, rows: 3, disabled: blocked, spellCheck: false,
|
|
1214
|
+
className: 'ptcPlusPromptTextarea', 'aria-label': slot.slotId, value: current,
|
|
1215
|
+
onChange: event => update(slot.slotId, event.target.value === '' ? undefined : event.target.value),
|
|
1216
|
+
}),
|
|
1217
|
+
h('div', { className: 'ptcPlusPromptEditorFoot' },
|
|
1218
|
+
h('span', { className: 'ptcPlusPromptHint' },
|
|
1219
|
+
t('capabilities.prompt.bytesOfLimit', { used: byteCount(current), max: PROMPT_TEXT_MAX_SLOT_BYTES })),
|
|
1220
|
+
h('span', { className: 'ptcPlusPromptEditorActions' },
|
|
1221
|
+
asset.omittable ? h('button', {
|
|
1222
|
+
type: 'button', className: 'ptcPlusButton', disabled: blocked,
|
|
1223
|
+
onClick: () => update(slot.slotId, ''),
|
|
1224
|
+
}, t('capabilities.prompt.omit')) : null,
|
|
1225
|
+
h('button', {
|
|
1226
|
+
type: 'button', className: 'ptcPlusButton', disabled: blocked || override === undefined,
|
|
1227
|
+
onClick: () => update(slot.slotId, undefined),
|
|
1228
|
+
}, t('capabilities.prompt.reset')))))
|
|
1229
|
+
}
|
|
1230
|
+
const defaultAnchor = account.anchors.find(anchor => anchor.wired === true && editableSlots(anchor).length > 0)
|
|
1231
|
+
const selectedAnchor = account.anchors.find(anchor => anchor.anchorId === selectedAnchorId) ?? defaultAnchor
|
|
1232
|
+
const selectedIndex = selectedAnchor === undefined ? 0 : account.anchors.indexOf(selectedAnchor) + 1
|
|
1233
|
+
const anchorPanel = h('section', { className: 'ptcPlusPromptPanel', 'data-prompt-panel': 'anchor', ref: anchorPanelRef },
|
|
1234
|
+
h('h5', { className: 'ptcPlusPromptPanelTitle' },
|
|
1235
|
+
t('capabilities.prompt.rowExpand', { index: selectedIndex })),
|
|
1236
|
+
selectedAnchor === undefined
|
|
1237
|
+
? h('p', { className: 'ptcPlusPromptHint' }, t('capabilities.prompt.rowExpandHint'))
|
|
1238
|
+
: h('div', { className: 'ptcPlusPromptPanelBody' },
|
|
1239
|
+
h('div', { className: 'ptcPlusPromptChips', 'data-anchor-chips': selectedAnchor.anchorId },
|
|
1240
|
+
...selectedAnchor.slots.map(slot => chip(selectedAnchor, slot))),
|
|
1241
|
+
selectedAnchor.wired !== true
|
|
1242
|
+
? h('p', { className: 'ptcPlusPromptHint' }, t('capabilities.prompt.unwiredRow', { slots: selectedAnchor.unmeasuredSlots }))
|
|
1243
|
+
: editorFor(selectedAnchor)))
|
|
1244
|
+
// Saving shows what will change. The preview is derived from the same draft the
|
|
1245
|
+
// editors write, so it cannot disagree with them; a saved document that already
|
|
1246
|
+
// differs from the built-ins shows here too, because saving would rewrite it.
|
|
1247
|
+
const changes = account.anchors.flatMap(anchor => anchor.slots
|
|
1248
|
+
.map(slot => ({ slot, builtin: PROMPT_ASSETS.find(asset => asset.id === slot.slotId)?.defaultText }))
|
|
1249
|
+
.filter(entry => typeof entry.builtin === 'string'
|
|
1250
|
+
&& Object.hasOwn(overrides, entry.slot.slotId)
|
|
1251
|
+
&& overrides[entry.slot.slotId] !== entry.builtin)
|
|
1252
|
+
.map(entry => {
|
|
1253
|
+
const effective = overrides[entry.slot.slotId]
|
|
1254
|
+
const effect = anchor.channel === 'plan' ? t('capabilities.prompt.effectPlan')
|
|
1255
|
+
: anchor.channel === 'install' ? t('capabilities.prompt.effectInstall')
|
|
1256
|
+
: anchor.channel === 'prompt' ? t('capabilities.prompt.effectPrompt')
|
|
1257
|
+
: t('capabilities.prompt.effectTrigger')
|
|
1258
|
+
return {
|
|
1259
|
+
anchor, slot: entry.slot, effect,
|
|
1260
|
+
before: byteCount(entry.builtin), after: byteCount(effective), delta: byteCount(effective) - byteCount(entry.builtin),
|
|
1261
|
+
}
|
|
1262
|
+
}))
|
|
1263
|
+
const restoreSlot = slotId => update(slotId, undefined)
|
|
1264
|
+
const diffPanel = h('section', {
|
|
1265
|
+
className: 'ptcPlusPromptPanel',
|
|
1266
|
+
...(changes.length === 0 ? {} : { 'data-prompt-diff': 'true' }),
|
|
1267
|
+
'data-prompt-diff-panel': 'true',
|
|
1268
|
+
},
|
|
1269
|
+
h('h5', { className: 'ptcPlusPromptPanelTitle' }, t('capabilities.prompt.diffTitle')),
|
|
1270
|
+
changes.length === 0
|
|
1271
|
+
? h('p', { className: 'ptcPlusPromptHint' }, t('capabilities.prompt.diffEmpty'))
|
|
1272
|
+
: h('ul', { className: 'ptcPlusPromptDiffList' },
|
|
1273
|
+
...changes.map((entry, index) => h('li', {
|
|
1274
|
+
key: entry.slot.slotId, className: 'ptcPlusPromptDiffRow',
|
|
1275
|
+
'data-diff-anchor': entry.anchor.anchorId, 'data-diff-slot': entry.slot.slotId,
|
|
1276
|
+
},
|
|
1277
|
+
h('span', { className: 'ptcPlusPromptDot', 'data-tone': String(index % 3) }),
|
|
1278
|
+
h('span', { className: 'ptcPlusPromptDiffBody' },
|
|
1279
|
+
h('span', { className: 'ptcPlusPromptDiffHead' },
|
|
1280
|
+
h('code', { className: 'ptcPlusPromptDiffSlot' }, entry.slot.slotId),
|
|
1281
|
+
h('span', { className: 'ptcPlusPromptHint' }, entry.effect)),
|
|
1282
|
+
h('span', { className: 'ptcPlusPromptDiffDelta' },
|
|
1283
|
+
t('capabilities.prompt.diffBytes', {
|
|
1284
|
+
before: group(entry.before), after: group(entry.after),
|
|
1285
|
+
delta: entry.delta > 0 ? `+${group(entry.delta)}` : group(entry.delta),
|
|
1286
|
+
}),
|
|
1287
|
+
h('button', {
|
|
1288
|
+
type: 'button', className: 'ptcPlusButton', disabled: blocked,
|
|
1289
|
+
onClick: () => restoreSlot(entry.slot.slotId),
|
|
1290
|
+
}, t('capabilities.prompt.reset'))))))),
|
|
1291
|
+
h('div', { className: 'ptcPlusPromptActions' },
|
|
1292
|
+
h('button', {
|
|
1293
|
+
type: 'button', className: 'ptcPlusButton', 'data-kind': 'primary', disabled: blocked || !pending,
|
|
1294
|
+
onClick: save,
|
|
1295
|
+
}, t('capabilities.prompt.save')),
|
|
1296
|
+
h('button', { type: 'button', className: 'ptcPlusButton', disabled: blocked || !pending, onClick: cancel },
|
|
1297
|
+
t('capabilities.prompt.cancel'))))
|
|
1298
|
+
return h('section', {
|
|
1299
|
+
className: 'ptcPlusPromptText', 'data-prompt-paused': paused ? 'true' : 'false',
|
|
1300
|
+
},
|
|
1301
|
+
h('header', { className: 'ptcPlusPromptHeader' },
|
|
1302
|
+
h('span', { className: 'ptcPlusPromptHeaderIcon' }, icon(['M10 2.5 12 8l5.5 2-5.5 2-2 5.5-2-5.5L2.5 10 8 8Z'])),
|
|
1303
|
+
h('span', { className: 'ptcPlusPromptHeaderBody' },
|
|
1304
|
+
h('h4', { className: 'ptcPlusPromptTitle' }, t('capabilities.prompt.title')),
|
|
1305
|
+
h('p', { className: 'ptcPlusPromptSubtitle' }, t('capabilities.prompt.subtitle')))),
|
|
1306
|
+
paused
|
|
1307
|
+
? h('p', { className: 'ptcPlusPromptPaused', role: 'status', 'data-prompt-paused-notice': 'true' },
|
|
1308
|
+
t('capabilities.prompt.paused'))
|
|
1309
|
+
: null,
|
|
1310
|
+
readError === undefined ? null : h('p', { className: 'ptcPlusPromptHint' }, t('capabilities.prompt.unreadable', { error: readError })),
|
|
1311
|
+
...resolved.reported.map((entry, index) => h('p', { key: `report-${index}`, className: 'ptcPlusPromptHint' },
|
|
1312
|
+
t('capabilities.prompt.reported', {
|
|
1313
|
+
asset: entry.slotId ?? entry.anchorId ?? 'document', reason: entry.reason,
|
|
1314
|
+
}))),
|
|
1315
|
+
cards,
|
|
1316
|
+
strip,
|
|
1317
|
+
h('p', { className: 'ptcPlusPromptHint', 'data-prompt-slot-counts': 'true' },
|
|
1318
|
+
t('capabilities.prompt.slotCounts', { bytes: group(measuredBytes), unmeasured: unmeasuredSlots, dynamic: dynamicSlots })),
|
|
1319
|
+
table,
|
|
1320
|
+
h('div', { className: 'ptcPlusPromptPanels' }, anchorPanel, diffPanel),
|
|
1321
|
+
h('p', { className: 'ptcPlusPromptIntro' }, t('capabilities.prompt.intro')))
|
|
1322
|
+
}
|
|
1323
|
+
|
|
868
1324
|
function CapabilityWorkbench({ value, configurationReady, catalog, catalogError, plans, plansError, plansPending, plansStale, disabled, onChange, onReload, t, renderField, renderPlans, renderRefresh, id, selectedSessionId, onSessionSelect,
|
|
869
1325
|
allocation, allocationError, allocationPending, onApplyConfiguration, configurationWritable, saving, saveStatus }) {
|
|
870
1326
|
const [tab, setTab] = React.useState('manual')
|
|
871
1327
|
const [contextSection, setContextSection] = React.useState('description')
|
|
872
|
-
const tabs = ['manual', 'budgets', 'statistics', 'plans']
|
|
1328
|
+
const tabs = ['manual', 'budgets', 'statistics', 'prompt', 'plans']
|
|
873
1329
|
const feedback = saving ? t('state.syncing') : saveStatus && saveStatus.key !== 'status.applied' ? t(saveStatus.key, saveStatus.params)
|
|
874
1330
|
: t(configurationWritable ? 'capabilities.savedForNew' : 'footer.readOnly')
|
|
875
1331
|
return h('div', { className: 'ptcPlusCapabilityWorkbench' },
|
|
876
1332
|
h(CombinationSummary, { value, configurationReady, plans, plansError, plansPending, plansStale, selectedSessionId,
|
|
877
1333
|
feedback, onReload, renderRefresh, renderPlans, t, id, onNavigate: key => {
|
|
878
|
-
setTab(key === 'rules' ? 'manual' : key === 'statistics' ? 'statistics' : 'budgets')
|
|
1334
|
+
setTab(key === 'rules' ? 'manual' : key === 'statistics' ? 'statistics' : key === 'promptText' ? 'prompt' : 'budgets')
|
|
879
1335
|
if (['description', 'recovery', 'preview'].includes(key)) setContextSection(key)
|
|
880
1336
|
} }),
|
|
881
1337
|
h('div', { className: 'ptcPlusCapabilityTabs', role: 'tablist', 'aria-label': t('capabilities.settingsTabs') },
|
|
@@ -885,7 +1341,7 @@ export function createCapabilitySettingsViews(React, { SearchIcon, Select, Modal
|
|
|
885
1341
|
}, t(`capabilities.tab.${key}`)))),
|
|
886
1342
|
...tabs.map(key => h('div', { key, className: 'ptcPlusCapabilityPane', role: 'tabpanel', id: `${id}-pane-${key}`,
|
|
887
1343
|
'aria-labelledby': `${id}-tab-${key}`, hidden: tab !== key },
|
|
888
|
-
h('div', { className: 'ptcPlusTaskHead' }, h('h4', null, t(`capabilities.title.${key}`)),
|
|
1344
|
+
key === 'prompt' ? null : h('div', { className: 'ptcPlusTaskHead' }, h('h4', null, t(`capabilities.title.${key}`)),
|
|
889
1345
|
key === 'manual' ? h('label', { className: 'ptcPlusCapabilityManagement' }, h('span', null, t('capabilities.management')),
|
|
890
1346
|
h('input', { type: 'checkbox', role: 'switch', className: 'ptcPlusCheck', checked: value.capabilityManagementEnabled === true,
|
|
891
1347
|
disabled: disabled('capabilityManagementEnabled'), 'aria-label': t('capabilities.management'),
|
|
@@ -902,7 +1358,10 @@ export function createCapabilitySettingsViews(React, { SearchIcon, Select, Modal
|
|
|
902
1358
|
h(AllocationEvidence, { allocation, allocationError, allocationPending, selectedSessionId, statistics: true, onReload, t }))
|
|
903
1359
|
: key === 'budgets' ? h(ContextSettings, { value, configurationReady, disabled, onChange, t, renderField, allocation, allocationError, allocationPending,
|
|
904
1360
|
selectedSessionId, onReload, renderRefresh, id, section: contextSection, setSection: setContextSection })
|
|
905
|
-
|
|
1361
|
+
// Rendered only while its tab is active: eight idle textareas in a hidden
|
|
1362
|
+
// pane would change every generic textbox query in the settings card.
|
|
1363
|
+
: key === 'prompt' ? (tab === 'prompt' ? h(PromptTextSettings, { value, disabled, onChange, t, id }) : null)
|
|
1364
|
+
: h(ConfigurationProfiles, { value, plans, disabled, writable: configurationWritable, onChange, onApplyConfiguration, t }))))
|
|
906
1365
|
}
|
|
907
1366
|
|
|
908
1367
|
return { CapabilityWorkbench }
|
|
@@ -973,9 +1432,13 @@ export function createSessionCapabilityControlsView(React, { commands, Modal, Se
|
|
|
973
1432
|
const available = React.useSyncExternalStore(source.subscribe, source.getSnapshot)
|
|
974
1433
|
const [catalog, setCatalog] = React.useState([])
|
|
975
1434
|
const [management, setManagement] = React.useState('unknown')
|
|
1435
|
+
const [savedSettings, setSavedSettings] = React.useState(null)
|
|
976
1436
|
const [loading, setLoading] = React.useState(false)
|
|
977
1437
|
const [error, setError] = React.useState(null)
|
|
978
1438
|
const [drafts, setDrafts] = React.useState(emptyDrafts)
|
|
1439
|
+
const [picked, setPicked] = React.useState([])
|
|
1440
|
+
const [batchOpen, setBatchOpen] = React.useState(false)
|
|
1441
|
+
const [batchChoices, setBatchChoices] = React.useState({ availability: 'keep', disclosure: 'keep' })
|
|
979
1442
|
const [selectedId, setSelectedId] = React.useState(null)
|
|
980
1443
|
const [pane, setPane] = React.useState('list')
|
|
981
1444
|
const [query, setQuery] = React.useState('')
|
|
@@ -1009,7 +1472,10 @@ export function createSessionCapabilityControlsView(React, { commands, Modal, Se
|
|
|
1009
1472
|
applying.current = false
|
|
1010
1473
|
setCatalog([])
|
|
1011
1474
|
setManagement('unknown')
|
|
1475
|
+
setSavedSettings(null)
|
|
1012
1476
|
setDrafts(emptyDrafts())
|
|
1477
|
+
setPicked([])
|
|
1478
|
+
setBatchOpen(false)
|
|
1013
1479
|
setSelectedId(null)
|
|
1014
1480
|
setPane('list')
|
|
1015
1481
|
setFocusDimension(null)
|
|
@@ -1026,8 +1492,11 @@ export function createSessionCapabilityControlsView(React, { commands, Modal, Se
|
|
|
1026
1492
|
throw new Error('Invalid capability catalog')
|
|
1027
1493
|
}
|
|
1028
1494
|
setCatalog(payload.entries)
|
|
1495
|
+
setPicked(current => current.filter(id => payload.entries.some(entry => entry.id === id)))
|
|
1029
1496
|
setManagement(['managed', 'paused', 'off', 'unknown'].includes(payload.management)
|
|
1030
1497
|
? payload.management : 'unknown')
|
|
1498
|
+
setSavedSettings(payload.savedSettings !== null && typeof payload.savedSettings === 'object'
|
|
1499
|
+
&& !Array.isArray(payload.savedSettings) ? payload.savedSettings : null)
|
|
1031
1500
|
setError(null)
|
|
1032
1501
|
return payload.entries
|
|
1033
1502
|
} catch (failure) {
|
|
@@ -1038,24 +1507,24 @@ export function createSessionCapabilityControlsView(React, { commands, Modal, Se
|
|
|
1038
1507
|
}, [sessionId])
|
|
1039
1508
|
React.useEffect(() => {
|
|
1040
1509
|
if (!open || !enabled || !available) {
|
|
1041
|
-
request.current?.abort(); request.current = null
|
|
1510
|
+
if (!applying.current) { request.current?.abort(); request.current = null }
|
|
1511
|
+
setLoading(false)
|
|
1512
|
+
return
|
|
1042
1513
|
}
|
|
1514
|
+
// An in-flight apply owns the shared catalog request and reloads it on completion.
|
|
1515
|
+
if (applying.current) return
|
|
1043
1516
|
const controller = new AbortController()
|
|
1044
1517
|
request.current = controller
|
|
1045
1518
|
void load(controller.signal)
|
|
1046
1519
|
return () => { controller.abort(); if (request.current === controller) request.current = null }
|
|
1047
1520
|
}, [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
1521
|
const choices = drafts
|
|
1057
1522
|
const choiceFor = entry => choices[entry.id] ?? entry.temporary ?? 'inherit'
|
|
1058
|
-
|
|
1523
|
+
// Session writes also require the saved configuration to keep capability
|
|
1524
|
+
// management and context on; a frozen managed session cannot be edited
|
|
1525
|
+
// while the current settings have them off.
|
|
1526
|
+
const savedBlocks = savedSettings !== null && (savedSettings.enabled === false || savedSettings.contextEnabled === false)
|
|
1527
|
+
const writesBlocked = management === 'paused' || management === 'off' || savedBlocks
|
|
1059
1528
|
const sourceKey = entry => entry.source?.kind === 'mcp' ? 'mcp:' + (entry.source.server ?? '')
|
|
1060
1529
|
: entry.source?.kind === 'skill' ? 'skill:' + (entry.source.bucket ?? '')
|
|
1061
1530
|
: entry.source?.kind ?? 'unknown'
|
|
@@ -1109,26 +1578,51 @@ export function createSessionCapabilityControlsView(React, { commands, Modal, Se
|
|
|
1109
1578
|
?? ['tools', 'skills', 'mcp', 'mcp_tools'].map(group => matches.find(entry => category(entry) === group)).find(Boolean)
|
|
1110
1579
|
const staged = Object.entries(choices).flatMap(([id, choice]) => typeof choice === 'object'
|
|
1111
1580
|
? Object.entries(choice).map(([dimension, selection]) => [id, selection, dimension]) : [[id, choice]])
|
|
1112
|
-
const
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1581
|
+
const pickedRows = picked.map(id => catalog.find(entry => entry.id === id)).filter(Boolean)
|
|
1582
|
+
const allMatchesPicked = matches.length > 0 && matches.every(entry => picked.includes(entry.id))
|
|
1583
|
+
const someMatchesPicked = matches.some(entry => picked.includes(entry.id))
|
|
1584
|
+
const togglePick = id => setPicked(current => current.includes(id) ? current.filter(item => item !== id) : [...current, id])
|
|
1585
|
+
const toggleAllMatches = () => setPicked(current => allMatchesPicked
|
|
1586
|
+
? current.filter(id => !matches.some(entry => entry.id === id))
|
|
1587
|
+
: [...new Set([...current, ...matches.map(entry => entry.id)])])
|
|
1588
|
+
// One staging rule for single and batch edits: an item keeps one temporary
|
|
1589
|
+
// patch, "inherit" removes the dimension and reselecting the stored choice
|
|
1590
|
+
// never dirties the draft.
|
|
1591
|
+
const stageDimension = (current, entry, dimension, choice) => {
|
|
1592
|
+
const next = { ...current }
|
|
1593
|
+
const patch = typeof next[entry.id] === 'object' && next[entry.id] !== null ? { ...next[entry.id] } : {}
|
|
1594
|
+
const original = storedDimension(entry, dimension) ?? 'inherit'
|
|
1595
|
+
const selectedChoice = patch[dimension] ?? original
|
|
1596
|
+
if (choice !== selectedChoice) {
|
|
1597
|
+
if (choice === original) delete patch[dimension]
|
|
1117
1598
|
else patch[dimension] = choice
|
|
1118
|
-
|
|
1119
|
-
|
|
1599
|
+
}
|
|
1600
|
+
if (Object.keys(patch).length) next[entry.id] = patch; else delete next[entry.id]
|
|
1601
|
+
return next
|
|
1602
|
+
}
|
|
1603
|
+
const chooseDimension = (id, dimension, choice) => {
|
|
1604
|
+
const entry = catalog.find(item => item.id === id)
|
|
1605
|
+
if (entry) setDrafts(current => stageDimension(current, entry, dimension, choice))
|
|
1120
1606
|
}
|
|
1121
1607
|
const choose = (id, next) => {
|
|
1122
1608
|
const entry = catalog.find(item => item.id === id)
|
|
1123
1609
|
if (!entry) return
|
|
1124
1610
|
const original = entry.temporary ?? 'inherit'
|
|
1611
|
+
if (next === (choices[id] ?? original)) return
|
|
1125
1612
|
setDrafts(current => {
|
|
1126
1613
|
const nextChoices = { ...current }
|
|
1127
|
-
if (next === original
|
|
1614
|
+
if (next === original) delete nextChoices[id]
|
|
1128
1615
|
else nextChoices[id] = next
|
|
1129
1616
|
return nextChoices
|
|
1130
1617
|
})
|
|
1131
1618
|
}
|
|
1619
|
+
const stageBatch = (ids, selections) => setDrafts(current => ids.reduce((draft, id) => {
|
|
1620
|
+
const entry = catalog.find(item => item.id === id)
|
|
1621
|
+
if (!entry) return draft
|
|
1622
|
+
return ['availability', 'disclosure'].reduce((next, dimension) =>
|
|
1623
|
+
selections[dimension] === undefined || selections[dimension] === 'keep'
|
|
1624
|
+
? next : stageDimension(next, entry, dimension, selections[dimension]), draft)
|
|
1625
|
+
}, current))
|
|
1132
1626
|
const hasInheritanceReset = staged.some(([, selection]) => selection === 'inherit')
|
|
1133
1627
|
const apply = async targetScope => {
|
|
1134
1628
|
if (applying.current || !enabled || !available || writesBlocked || staged.length === 0) return
|
|
@@ -1140,58 +1634,71 @@ export function createSessionCapabilityControlsView(React, { commands, Modal, Se
|
|
|
1140
1634
|
const controller = new AbortController()
|
|
1141
1635
|
request.current?.abort()
|
|
1142
1636
|
request.current = controller
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
if (
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1637
|
+
// One owner releases the shared request slot and the pending state on every exit path, so a
|
|
1638
|
+
// closed, superseded or failed apply cannot strand the workbench in "applying".
|
|
1639
|
+
const finish = () => {
|
|
1640
|
+
if (request.current === controller) request.current = null
|
|
1641
|
+
applying.current = false
|
|
1642
|
+
if (mounted.current) setPending(false)
|
|
1643
|
+
}
|
|
1644
|
+
try {
|
|
1645
|
+
let completed = 0
|
|
1646
|
+
let pendingCount = 0
|
|
1647
|
+
let failure = null
|
|
1648
|
+
let savedOnly = null
|
|
1649
|
+
for (const [id, selection, dimension] of staged) {
|
|
1650
|
+
if (controller.signal.aborted) break
|
|
1651
|
+
try {
|
|
1652
|
+
const result = await commands.run(sessionId, `set ${targetScope} ${encodeURIComponent(id)} ${dimension ? dimension + ' ' : ''}${selection}`, controller.signal)
|
|
1653
|
+
const receipt = readCapabilityCommandReceipt(result.text)
|
|
1654
|
+
if (!receipt || receipt.sessionId !== sessionId || !['recorded', 'pending'].includes(receipt.state)) {
|
|
1655
|
+
throw new Error(receipt ? capabilityCommandText(t, result.text) : t('capabilities.quick.unknownReceipt'))
|
|
1656
|
+
}
|
|
1657
|
+
if (receipt?.state === 'pending') pendingCount += 1
|
|
1658
|
+
completed += 1
|
|
1659
|
+
} catch (cause) {
|
|
1660
|
+
failure = String(cause?.message ?? cause)
|
|
1661
|
+
savedOnly = readCapabilityCommandReceipt(failure)?.state === 'saved-only'
|
|
1662
|
+
? readCapabilityCommandReceipt(failure) : null
|
|
1663
|
+
break
|
|
1154
1664
|
}
|
|
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
1665
|
}
|
|
1666
|
+
if (request.current !== controller || controller.signal.aborted || !mounted.current) return
|
|
1667
|
+
const refreshed = await load(controller.signal)
|
|
1668
|
+
if (request.current !== controller || controller.signal.aborted || !mounted.current) return
|
|
1669
|
+
const confirmed = refreshed ? staged.slice(0, completed).filter(([id, selection, dimension]) => {
|
|
1670
|
+
const entry = refreshed.find(item => item.id === id)
|
|
1671
|
+
if (dimension) return entry && (targetScope === 'session'
|
|
1672
|
+
? (entry.temporaryRule?.[dimension] ?? 'inherit') === selection
|
|
1673
|
+
: (entry.savedRule?.[dimension] ?? 'automatic') === selection && entry.temporaryRule?.[dimension] === undefined)
|
|
1674
|
+
return entry && (targetScope === 'session' ? entry.temporary === selection
|
|
1675
|
+
: entry.saved === selection && entry.temporary === 'inherit')
|
|
1676
|
+
}) : []
|
|
1677
|
+
setDrafts(current => {
|
|
1678
|
+
const nextChoices = { ...current }
|
|
1679
|
+
for (const [id, selection, dimension] of confirmed) {
|
|
1680
|
+
if (savedOnly && id === staged[completed]?.[0] && dimension === staged[completed]?.[2]) continue
|
|
1681
|
+
if (dimension) {
|
|
1682
|
+
if (nextChoices[id]?.[dimension] !== submitted[id]?.[dimension]) continue
|
|
1683
|
+
const patch = { ...nextChoices[id] }; delete patch[dimension]
|
|
1684
|
+
if (Object.keys(patch).length) nextChoices[id] = patch; else delete nextChoices[id]
|
|
1685
|
+
} else if (nextChoices[id] === submitted[id]) delete nextChoices[id]
|
|
1686
|
+
}
|
|
1687
|
+
return nextChoices
|
|
1688
|
+
})
|
|
1689
|
+
if (savedOnly) setStatus({ key: 'capabilities.quick.savedOnly', params: { count: completed, error: savedOnly.error ?? '' } })
|
|
1690
|
+
else if (failure || !refreshed || confirmed.length < completed) setStatus({ key: 'capabilities.quick.partial',
|
|
1691
|
+
params: { count: confirmed.length, error: failure ?? t('capabilities.quick.confirmationUnavailable') } })
|
|
1692
|
+
else setStatus({ key: pendingCount ? 'capabilities.quick.pendingApplied' : 'capabilities.quick.applied', params: { count: completed } })
|
|
1693
|
+
} finally {
|
|
1694
|
+
finish()
|
|
1163
1695
|
}
|
|
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
1696
|
}
|
|
1193
1697
|
const discard = () => { setDrafts(emptyDrafts()); setStatus(null) }
|
|
1194
1698
|
const close = () => {
|
|
1699
|
+
// A confirmed apply keeps its own lifecycle: refusing the close keeps an accidental
|
|
1700
|
+
// Escape or backdrop click from aborting host commands the user already submitted.
|
|
1701
|
+
if (applying.current) return
|
|
1195
1702
|
request.current?.abort()
|
|
1196
1703
|
request.current = null
|
|
1197
1704
|
applying.current = false
|
|
@@ -1203,35 +1710,124 @@ export function createSessionCapabilityControlsView(React, { commands, Modal, Se
|
|
|
1203
1710
|
const filter = (label, value, onChange, options, disabled = false) => h('div', { className: 'ptcPlusCapabilityFilter' },
|
|
1204
1711
|
h('span', null, label), h(Select, { label, value, onChange, disabled,
|
|
1205
1712
|
options: options.map(([key, text]) => ({ value: key, label: text })) }))
|
|
1713
|
+
const ruleLabel = (rule, empty) => capabilityRuleLabel(t, rule, empty)
|
|
1714
|
+
const statusLabel = value => value && typeof value === 'object' ? ruleLabel(value) : capabilityChoiceLabel(t, value)
|
|
1715
|
+
// The stored temporary value for one dimension, including the legacy scalar
|
|
1716
|
+
// form where a single selection covered both dimensions.
|
|
1717
|
+
const storedDimension = (entry, dimension) => {
|
|
1718
|
+
const rule = entry.temporaryRule !== null && typeof entry.temporaryRule === 'object' && !Array.isArray(entry.temporaryRule)
|
|
1719
|
+
? entry.temporaryRule : undefined
|
|
1720
|
+
const override = rule?.[dimension]
|
|
1721
|
+
if (override !== undefined) return override
|
|
1722
|
+
if (entry.temporary === 'automatic') return 'automatic'
|
|
1723
|
+
if (typeof entry.temporary === 'string' && !['inherit', 'automatic', 'unknown', 'pending'].includes(entry.temporary)) {
|
|
1724
|
+
return dimension === 'availability' ? (entry.temporary === 'closed' ? 'closed' : 'enabled')
|
|
1725
|
+
: (CAPABILITY_DISCLOSURES.includes(entry.temporary) ? entry.temporary : undefined)
|
|
1726
|
+
}
|
|
1727
|
+
return undefined
|
|
1728
|
+
}
|
|
1729
|
+
// The value a dimension falls back to when this session keeps no temporary
|
|
1730
|
+
// change; the frozen plan already resolved the saved layers.
|
|
1731
|
+
const frozenDimension = (entry, dimension) => {
|
|
1732
|
+
const source = entry.sourceDimensions?.[dimension]
|
|
1733
|
+
if (typeof source === 'string') return source
|
|
1734
|
+
const effective = entry.effectiveDimensions?.[dimension]
|
|
1735
|
+
if (typeof effective === 'string' && storedDimension(entry, dimension) === undefined) return effective
|
|
1736
|
+
if (dimension === 'availability') return entry.effective === 'closed' ? 'closed'
|
|
1737
|
+
: ['full', 'on-demand', 'host', 'enabled'].includes(entry.effective) ? 'enabled' : undefined
|
|
1738
|
+
return ['full', 'on-demand', 'host'].includes(entry.effective) ? entry.effective : undefined
|
|
1739
|
+
}
|
|
1740
|
+
const stateText = value => value === undefined ? t('capabilities.quick.unknown') : statusLabel(value)
|
|
1741
|
+
const dimensionCurrent = (entry, dimension) => choices[entry.id]?.[dimension] ?? storedDimension(entry, dimension) ?? 'inherit'
|
|
1742
|
+
const dimensionImpact = (entry, dimension) => {
|
|
1743
|
+
const draft = choices[entry.id]?.[dimension]
|
|
1744
|
+
const choice = draft ?? storedDimension(entry, dimension) ?? 'inherit'
|
|
1745
|
+
const stagedChange = draft !== undefined
|
|
1746
|
+
if (choice === 'inherit') return { state: frozenDimension(entry, dimension), origin: 'capabilities.quick.originPlan', staged: stagedChange }
|
|
1747
|
+
if (choice === 'automatic') return { upper: true, staged: stagedChange }
|
|
1748
|
+
return { state: choice, origin: 'capabilities.quick.originSession', staged: stagedChange }
|
|
1749
|
+
}
|
|
1750
|
+
const impactLine = impact => impact.upper
|
|
1751
|
+
? t(impact.staged ? 'capabilities.quick.impactUpperPending' : 'capabilities.quick.impactUpper')
|
|
1752
|
+
: t(impact.staged ? 'capabilities.quick.impactPending' : 'capabilities.quick.impact',
|
|
1753
|
+
{ state: stateText(impact.state), source: t(impact.origin) })
|
|
1754
|
+
const dimensionOptions = (entry, dimension) => {
|
|
1755
|
+
const restore = stateText(frozenDimension(entry, dimension))
|
|
1756
|
+
const inherit = storedDimension(entry, dimension) === undefined
|
|
1757
|
+
? t('capabilities.quick.noChange', { state: restore })
|
|
1758
|
+
: t('capabilities.quick.undoTemp', { state: restore })
|
|
1759
|
+
// "Let the level above decide" only differs from "no change" while the
|
|
1760
|
+
// session's frozen source still carries an explicit rule for the dimension.
|
|
1761
|
+
const sourceDimension = entry.sourceRule?.[dimension]
|
|
1762
|
+
const distinctRule = sourceDimension !== undefined && sourceDimension !== 'automatic'
|
|
1763
|
+
return ['inherit', ...(distinctRule || dimensionCurrent(entry, dimension) === 'automatic' ? ['automatic'] : []),
|
|
1764
|
+
...(dimension === 'availability' ? ['enabled', 'closed'] : CAPABILITY_DISCLOSURES)]
|
|
1765
|
+
.map(choice => ({ value: choice, label: choice === 'inherit' ? inherit
|
|
1766
|
+
: choice === 'automatic' ? t('capabilities.quick.upperRule')
|
|
1767
|
+
: t(choice === 'enabled' ? 'capabilities.action.enable' : choice === 'closed' ? 'capabilities.action.close' : `capabilities.${choice}`) }))
|
|
1768
|
+
}
|
|
1769
|
+
const legacyFrozenState = entry => {
|
|
1770
|
+
const stored = typeof entry.temporary === 'string' && !['inherit', 'automatic', 'unknown', 'pending'].includes(entry.temporary)
|
|
1771
|
+
if (stored) return ['full', 'on-demand', 'closed'].includes(entry.sessionSourceSelection) ? entry.sessionSourceSelection : undefined
|
|
1772
|
+
return ['full', 'on-demand', 'closed', 'host'].includes(entry.effective) ? entry.effective : undefined
|
|
1773
|
+
}
|
|
1774
|
+
const choiceCurrent = entry => choices[entry.id] ?? entry.temporary ?? 'inherit'
|
|
1775
|
+
const choiceImpact = entry => {
|
|
1776
|
+
const draft = choices[entry.id]
|
|
1777
|
+
const choice = draft ?? entry.temporary ?? 'inherit'
|
|
1778
|
+
const stagedChange = draft !== undefined
|
|
1779
|
+
if (choice === 'inherit') return { state: legacyFrozenState(entry), origin: 'capabilities.quick.originPlan', staged: stagedChange }
|
|
1780
|
+
if (choice === 'automatic') return { upper: true, staged: stagedChange }
|
|
1781
|
+
return { state: choice, origin: 'capabilities.quick.originSession', staged: stagedChange }
|
|
1782
|
+
}
|
|
1206
1783
|
const choiceOptions = selected ? [
|
|
1207
1784
|
'inherit', 'automatic',
|
|
1208
1785
|
...(selected.category === 'server' ? [] : ['full', 'on-demand']), 'closed',
|
|
1209
|
-
].map(choice => ({ value: choice, label:
|
|
1210
|
-
|
|
1786
|
+
].map(choice => ({ value: choice, label: choice === 'inherit'
|
|
1787
|
+
? t(selected.temporary !== undefined && selected.temporary !== 'inherit' ? 'capabilities.quick.undoChange' : 'capabilities.quick.keepUnchanged')
|
|
1788
|
+
: choice === 'automatic' ? t('capabilities.quick.upperRule')
|
|
1789
|
+
: t(`capabilities.${choice}`) })) : []
|
|
1211
1790
|
const dimensionFacts = dimensions => ['availability', 'disclosure'].map(dimension =>
|
|
1212
1791
|
`${t(`capabilities.policy.${dimension}`)}: ${statusLabel(dimensions[dimension])} (${dimensions[`${dimension}Source`] ?? t('capabilities.quick.unknown')})`).join(' · ')
|
|
1213
|
-
|
|
1792
|
+
// A dimension whose effective value comes from the session-only temporary
|
|
1793
|
+
// choice is marked beside the frozen inheritance it replaces.
|
|
1794
|
+
const temporaryDimension = (entry, dimension) => {
|
|
1795
|
+
const rule = entry.temporaryRule !== null && typeof entry.temporaryRule === 'object' && !Array.isArray(entry.temporaryRule)
|
|
1796
|
+
? entry.temporaryRule : undefined
|
|
1797
|
+
const override = rule?.[dimension]
|
|
1798
|
+
if (override !== undefined && override !== 'automatic') return override
|
|
1799
|
+
if (typeof entry.temporary === 'string' && !['inherit', 'automatic'].includes(entry.temporary)) {
|
|
1800
|
+
return dimension === 'availability' ? (entry.temporary === 'closed' ? 'closed' : 'enabled') : entry.temporary
|
|
1801
|
+
}
|
|
1802
|
+
return undefined
|
|
1803
|
+
}
|
|
1804
|
+
const dimensionCell = (entry, dimension, value) => temporaryDimension(entry, dimension) === value
|
|
1805
|
+
? t('capabilities.quick.temporaryState', { state: statusLabel(value) }) : statusLabel(value)
|
|
1214
1806
|
const openEditor = (entry, dimension = 'availability') => { setSelectedId(entry.id); setPane('detail'); setFocusDimension(dimension) }
|
|
1215
1807
|
const renderRow = entry => h('div', { key: entry.id, role: 'row',
|
|
1216
1808
|
className: 'ptcPlusSessionCapabilityRow ptcPlusCapabilityItem', 'data-capability-id': entry.id, 'data-selected': selected?.id === entry.id,
|
|
1217
1809
|
onClick: () => openEditor(entry) },
|
|
1218
1810
|
h('div', { role: 'cell', className: 'ptcPlusCapabilityIdentity' },
|
|
1219
|
-
h('
|
|
1220
|
-
'aria-
|
|
1221
|
-
onClick: event =>
|
|
1222
|
-
|
|
1811
|
+
h('input', { type: 'checkbox', className: 'ptcPlusPick', checked: picked.includes(entry.id), disabled: writesBlocked,
|
|
1812
|
+
'aria-label': t('capabilities.selectForBatch', { name: entry.name ?? entry.id }),
|
|
1813
|
+
onClick: event => event.stopPropagation(), onChange: () => togglePick(entry.id) }),
|
|
1814
|
+
h('div', { className: 'ptcPlusCapabilityIdentityText' },
|
|
1815
|
+
h('button', { type: 'button', className: 'ptcPlusSessionCapabilityName ptcPlusCapabilityName',
|
|
1816
|
+
'aria-current': selected?.id === entry.id ? 'true' : undefined,
|
|
1817
|
+
onClick: event => { event.stopPropagation(); openEditor(entry) } }, entry.name ?? entry.id),
|
|
1818
|
+
entry.name && entry.name !== entry.id ? h('code', null, entry.id) : null)),
|
|
1223
1819
|
h('span', { role: 'cell', className: 'ptcPlusCapabilityBadge', 'data-kind': type(entry) }, t(`capabilities.type.${type(entry)}`)),
|
|
1224
1820
|
h('span', { role: 'cell', className: 'ptcPlusCapabilitySource' }, displayedSource(entry)),
|
|
1225
1821
|
h('div', { role: 'cell', className: 'ptcPlusCapabilityAvailability', 'data-state': effectiveAvailability(entry) },
|
|
1226
1822
|
h('button', { type: 'button', className: 'ptcPlusCapabilityContext',
|
|
1227
1823
|
'aria-label': t('capabilities.policy.availabilityFor', { name: entry.id }),
|
|
1228
1824
|
title: t('capabilities.quick.effective'),
|
|
1229
|
-
onClick: event => { event.stopPropagation(); openEditor(entry, 'availability') } },
|
|
1825
|
+
onClick: event => { event.stopPropagation(); openEditor(entry, 'availability') } }, dimensionCell(entry, 'availability', effectiveAvailability(entry)))),
|
|
1230
1826
|
h('div', { role: 'cell', className: 'ptcPlusCapabilityContextCell' },
|
|
1231
1827
|
h('button', { type: 'button', className: 'ptcPlusCapabilityContext',
|
|
1232
1828
|
'aria-label': t('capabilities.editContextFor', { name: entry.id }),
|
|
1233
1829
|
title: t('capabilities.quick.effective'),
|
|
1234
|
-
onClick: event => { event.stopPropagation(); openEditor(entry, 'disclosure') } },
|
|
1830
|
+
onClick: event => { event.stopPropagation(); openEditor(entry, 'disclosure') } }, dimensionCell(entry, 'disclosure', effectiveContext(entry)))),
|
|
1235
1831
|
h('span', { role: 'cell', className: 'ptcPlusSessionCapabilityPending', 'data-changed': Object.hasOwn(choices, entry.id),
|
|
1236
1832
|
'data-label': t('capabilities.quick.changed') },
|
|
1237
1833
|
Object.hasOwn(choices, entry.id) ? statusLabel(choices[entry.id]) : '—'))
|
|
@@ -1240,7 +1836,7 @@ export function createSessionCapabilityControlsView(React, { commands, Modal, Se
|
|
|
1240
1836
|
h('div', { className: 'ptcPlusSessionCapabilityHead' },
|
|
1241
1837
|
h('div', null, h('h2', null, t('capabilities.quick.modalTitle')),
|
|
1242
1838
|
h('p', null, t('capabilities.quick.sessionIdentity', { session: sessionId }))),
|
|
1243
|
-
h('button', { type: 'button', className: 'ptcPlusDialogClose', 'aria-label': t('settings.close'), onClick: close }, '×')),
|
|
1839
|
+
h('button', { type: 'button', className: 'ptcPlusDialogClose', 'aria-label': t('settings.close'), disabled: pending, onClick: close }, '×')),
|
|
1244
1840
|
h('div', { className: 'ptcPlusSessionCapabilityToolbar' },
|
|
1245
1841
|
h('label', { className: 'ptcPlusSearch' },
|
|
1246
1842
|
h('svg', { viewBox: '0 0 24 24', width: 16, height: 16, 'aria-hidden': true },
|
|
@@ -1256,13 +1852,27 @@ export function createSessionCapabilityControlsView(React, { commands, Modal, Se
|
|
|
1256
1852
|
filter(t('capabilities.quick.state'), stateFilter, setStateFilter,
|
|
1257
1853
|
[['all', t('capabilities.quick.allStates')], ['changed', t('capabilities.quick.changed')],
|
|
1258
1854
|
...['full', 'on-demand', 'closed', 'pending'].map(key => [key, statusLabel(key)])])),
|
|
1259
|
-
|
|
1260
|
-
|
|
1855
|
+
// A blocked workbench states why writes are off and how to restore them; the banner styling
|
|
1856
|
+
// keeps that condition from reading as an incidental list message.
|
|
1857
|
+
writesBlocked ? h('p', { className: 'ptcPlusCapabilityNotice ptcPlusCapabilityBlocked', role: 'status' },
|
|
1858
|
+
h('svg', { className: 'ptcPlusCapabilityBlockedIcon', viewBox: '0 0 16 16', width: 14, height: 14,
|
|
1859
|
+
'aria-hidden': true, fill: 'none' },
|
|
1860
|
+
h('path', { d: 'M8 2.3 14.7 13.5H1.3L8 2.3Z', stroke: 'currentColor', strokeWidth: 1.3, strokeLinejoin: 'round' }),
|
|
1861
|
+
h('path', { d: 'M8 6.4v3M8 11.2v.7', stroke: 'currentColor', strokeWidth: 1.3, strokeLinecap: 'round' })),
|
|
1862
|
+
t(management === 'paused' ? 'capabilities.quick.sessionPaused'
|
|
1863
|
+
: management === 'off' ? 'capabilities.quick.sessionManagementOff'
|
|
1864
|
+
: savedSettings?.contextEnabled === false ? 'capabilities.quick.savedPaused'
|
|
1865
|
+
: 'capabilities.quick.savedManagementOff')) : null,
|
|
1261
1866
|
h('div', { className: 'ptcPlusSessionCapabilityWorkspace', 'data-pane': pane },
|
|
1262
1867
|
h('div', { className: 'ptcPlusSessionCapabilityList', role: 'table', 'aria-label': t('capabilities.quick.members'), ref: list },
|
|
1263
1868
|
h('div', { className: 'ptcPlusSessionCapabilityColumns ptcPlusCapabilityColumns', role: 'row' },
|
|
1264
1869
|
...CAPABILITY_SORT_COLUMNS.map(column => h('span', { key: column.key, role: 'columnheader',
|
|
1265
|
-
'aria-sort': capabilitySortDirection(sort, column.key)
|
|
1870
|
+
'aria-sort': capabilitySortDirection(sort, column.key),
|
|
1871
|
+
...(column.key === 'name' ? { 'aria-label': t('capabilities.member'), className: 'ptcPlusCapabilityPickHead' } : {}) },
|
|
1872
|
+
...(column.key === 'name' ? [h('input', { type: 'checkbox', className: 'ptcPlusPick', checked: allMatchesPicked,
|
|
1873
|
+
ref: node => { if (node) node.indeterminate = someMatchesPicked && !allMatchesPicked },
|
|
1874
|
+
disabled: writesBlocked || matches.length === 0, 'aria-label': t('capabilities.selectAllVisible'),
|
|
1875
|
+
onChange: toggleAllMatches })] : []),
|
|
1266
1876
|
h('button', { type: 'button', className: 'ptcPlusCapabilitySort', title: t('capabilities.sortCycle'),
|
|
1267
1877
|
onClick: () => setSort(current => cycleCapabilitySort(current, column.key)) },
|
|
1268
1878
|
t(`capabilities.${column.label}`), h('span', { 'aria-hidden': true },
|
|
@@ -1308,34 +1918,73 @@ export function createSessionCapabilityControlsView(React, { commands, Modal, Se
|
|
|
1308
1918
|
h('ul', null, ...catalog.filter(entry => entry.id !== selected.id
|
|
1309
1919
|
&& entry.source?.kind === 'mcp' && entry.source.server === selected.source?.server)
|
|
1310
1920
|
.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',
|
|
1921
|
+
selected.savedRule ? h(React.Fragment, null, ...['availability', 'disclosure'].map(dimension => h(React.Fragment, { key: dimension },
|
|
1922
|
+
h('div', { className: 'ptcPlusCapabilityFilter', 'data-capability-dimension': dimension },
|
|
1313
1923
|
h('span', null, t(`capabilities.policy.${dimension}`)),
|
|
1314
1924
|
h(Select, { label: t(`capabilities.policy.${dimension}`), disabled: !enabled || !available || pending || writesBlocked,
|
|
1315
|
-
value:
|
|
1925
|
+
value: dimensionCurrent(selected, dimension),
|
|
1316
1926
|
onChange: next => chooseDimension(selected.id, dimension, next),
|
|
1317
|
-
options:
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
: h(
|
|
1321
|
-
h('
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1927
|
+
options: dimensionOptions(selected, dimension) })),
|
|
1928
|
+
h('p', { className: 'ptcPlusDetail ptcPlusCapabilityImpact', 'data-impact-dimension': dimension },
|
|
1929
|
+
impactLine(dimensionImpact(selected, dimension))))))
|
|
1930
|
+
: h(React.Fragment, null,
|
|
1931
|
+
h('div', { className: 'ptcPlusCapabilityFilter' },
|
|
1932
|
+
h('span', null, t('capabilities.quick.choice')),
|
|
1933
|
+
h(Select, { label: t('capabilities.quick.choice'), value: choiceCurrent(selected),
|
|
1934
|
+
disabled: !enabled || !available || pending || writesBlocked, options: choiceOptions,
|
|
1935
|
+
onChange: next => choose(selected.id, next) })),
|
|
1936
|
+
h('p', { className: 'ptcPlusDetail ptcPlusCapabilityImpact', 'data-impact-dimension': 'selection' },
|
|
1937
|
+
impactLine(choiceImpact(selected)))),
|
|
1938
|
+
(typeof choices[selected.id] === 'string' ? choices[selected.id] === 'inherit'
|
|
1939
|
+
: Object.values(choices[selected.id] ?? {}).includes('inherit')) ? h('p', { className: 'ptcPlusDetail' },
|
|
1326
1940
|
t('capabilities.quick.inheritSessionOnly')) : null) : h('p', null, t('capabilities.noMatches')))),
|
|
1941
|
+
batchOpen ? h(Modal, { open: true, onClose: () => setBatchOpen(false), title: t('capabilities.batchSettings'),
|
|
1942
|
+
closeLabel: t('capabilities.batch.cancel'), headless: true, className: 'ptcPlusCapabilityBatchModal' },
|
|
1943
|
+
h('div', { className: 'ptcPlusCapabilityBatchHead' }, h('h2', null, t('capabilities.batchSettings')),
|
|
1944
|
+
h('button', { type: 'button', className: 'ptcPlusDialogClose', 'aria-label': t('capabilities.batch.cancel'),
|
|
1945
|
+
onClick: () => setBatchOpen(false) }, '\u00d7')),
|
|
1946
|
+
h('div', { className: 'ptcPlusCapabilityBatchBody' },
|
|
1947
|
+
h('p', { className: 'ptcPlusDetail', role: 'status' }, t('capabilities.batch.selected', { count: pickedRows.length })),
|
|
1948
|
+
h('ul', { className: 'ptcPlusCapabilityBatchTargets', 'aria-label': t('capabilities.batch.targetList') },
|
|
1949
|
+
...pickedRows.map(entry => h('li', { key: entry.id }, h('span', null, entry.name ?? entry.id), h('code', null, entry.id)))),
|
|
1950
|
+
h('div', { className: 'ptcPlusCapabilityBatchFields' }, ...['availability', 'disclosure'].map(dimension =>
|
|
1951
|
+
h('div', { key: dimension, className: 'ptcPlusCapabilityFilter' }, h('span', null, t(`capabilities.policy.${dimension}`)),
|
|
1952
|
+
h(Select, { value: batchChoices[dimension], label: t(`capabilities.batch.${dimension}`),
|
|
1953
|
+
onChange: choice => setBatchChoices(current => ({ ...current, [dimension]: choice })),
|
|
1954
|
+
options: ['keep', 'inherit', ...(dimension === 'availability' ? ['enabled', 'closed'] : CAPABILITY_DISCLOSURES)]
|
|
1955
|
+
.map(choice => ({ value: choice, label: choice === 'keep' ? t('capabilities.batch.keep')
|
|
1956
|
+
: choice === 'inherit' ? t('capabilities.batch.sessionInherit')
|
|
1957
|
+
: choice === 'enabled' ? t('capabilities.action.enable')
|
|
1958
|
+
: choice === 'closed' ? t('capabilities.action.close') : t(`capabilities.${choice}`) })) })))),
|
|
1959
|
+
h('p', { className: 'ptcPlusDetail' }, t('capabilities.batch.sessionStage'))),
|
|
1960
|
+
h('div', { className: 'ptcPlusCapabilityBatchFooter' },
|
|
1961
|
+
h('button', { type: 'button', className: 'ptcPlusButton', onClick: () => setBatchOpen(false) }, t('capabilities.batch.cancel')),
|
|
1962
|
+
h('button', { type: 'button', className: 'ptcPlusButton', 'data-kind': 'primary',
|
|
1963
|
+
disabled: pickedRows.length === 0 || !['availability', 'disclosure'].some(dimension => batchChoices[dimension] !== 'keep'),
|
|
1964
|
+
onClick: () => { stageBatch(pickedRows.map(entry => entry.id), batchChoices); setBatchOpen(false) } },
|
|
1965
|
+
t('capabilities.batch.stage', { count: pickedRows.length })))) : null,
|
|
1327
1966
|
h('div', { className: 'ptcPlusSessionCapabilityFooter' },
|
|
1328
1967
|
h('span', { role: 'status' }, pending ? t('capabilities.quick.applying') : status ? t(status.key, status.params)
|
|
1329
1968
|
: t('capabilities.quick.changes', { count: staged.length })),
|
|
1330
1969
|
h('button', { type: 'button', className: 'ptcPlusButton', disabled: pending || staged.length === 0, onClick: discard },
|
|
1331
1970
|
t('capabilities.quick.discard')),
|
|
1332
|
-
|
|
1971
|
+
// Session-scoped temporary application stays the footer's primary action; saving global
|
|
1972
|
+
// defaults is an explicit secondary choice, so no default-looking control changes global state.
|
|
1973
|
+
h('button', { type: 'button', className: 'ptcPlusButton', 'data-kind': 'primary', title: t('capabilities.quick.sessionHint'),
|
|
1333
1974
|
disabled: pending || !enabled || !available || writesBlocked || staged.length === 0, onClick: () => void apply('session') },
|
|
1334
1975
|
t('capabilities.quick.applySession')),
|
|
1335
|
-
h('button', { type: 'button', className: 'ptcPlusButton',
|
|
1976
|
+
h('button', { type: 'button', className: 'ptcPlusButton',
|
|
1336
1977
|
title: t(hasInheritanceReset ? 'capabilities.quick.inheritSessionOnly' : 'capabilities.quick.globalHint'),
|
|
1337
1978
|
disabled: pending || !enabled || !available || writesBlocked || staged.length === 0 || hasInheritanceReset,
|
|
1338
1979
|
onClick: () => void apply('global') },
|
|
1339
|
-
t('capabilities.quick.applyGlobal'))
|
|
1980
|
+
t('capabilities.quick.applyGlobal')),
|
|
1981
|
+
h('button', { type: 'button', className: 'ptcPlusButton', 'aria-haspopup': 'dialog',
|
|
1982
|
+
'aria-label': t('capabilities.batchSettings'),
|
|
1983
|
+
disabled: pending || writesBlocked || picked.length === 0,
|
|
1984
|
+
onClick: () => { setBatchChoices({ availability: 'keep', disclosure: 'keep' }); setBatchOpen(true) } },
|
|
1985
|
+
t('capabilities.batchSettings'),
|
|
1986
|
+
picked.length ? h('span', { className: 'ptcPlusCapabilityBatchCount', 'aria-hidden': true,
|
|
1987
|
+
title: t('capabilities.selectionCount', { count: picked.length }) },
|
|
1988
|
+
String(picked.length)) : null)))
|
|
1340
1989
|
}
|
|
1341
1990
|
}
|