dsh-ptc-plus 0.5.1 → 0.5.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (85) hide show
  1. package/README.en.md +10 -4
  2. package/README.md +10 -4
  3. package/client.cjs +1623 -104
  4. package/compiler-core.cjs +132 -132
  5. package/compiler-core.cjs.map +3 -3
  6. package/docs/adr/0005-temporary-rejected-cell-edit-transport.md +2 -0
  7. package/docs/adr/0017-track-the-latest-dsh-public-surface.md +4 -2
  8. package/docs/adr/0019-plugin-settings-and-kill-switch.md +2 -2
  9. package/docs/adr/0023-global-user-bindings.md +12 -0
  10. package/docs/adr/0027-run-the-session-kernel-in-a-killable-helper-process.md +11 -1
  11. package/docs/adr/0028-attach-to-the-host-ptc-execution-seam.md +1 -1
  12. package/docs/adr/0030-serve-both-typert-codec-generations-from-one-codec.md +2 -2
  13. package/docs/adr/0032-own-session-log-facts-in-a-host-only-projection.md +31 -1
  14. package/docs/adr/0044-coexist-with-the-official-persistent-repl.md +4 -4
  15. package/docs/adr/0045-customize-model-visible-prompt-text.md +38 -0
  16. package/docs/adr/0046-gate-the-commit-with-the-semantic-evidence-tier.md +35 -0
  17. package/docs/adr/0047-store-journal-argument-digests-in-the-projection.md +88 -0
  18. package/docs/adr/0048-supersede-a-rewritten-cell.md +48 -0
  19. package/docs/adr/0049-prove-a-settlement-against-its-published-record.md +53 -0
  20. package/docs/adr/0050-read-host-surfaces-through-their-published-contract.md +48 -0
  21. package/docs/adr/README.md +7 -1
  22. package/docs/architecture.md +3 -3
  23. package/docs/client-ui.md +5 -2
  24. package/docs/durability-design.md +15 -2
  25. package/docs/installation.md +9 -9
  26. package/docs/releases/v0.5.2.md +91 -0
  27. package/docs/releases/v0.5.3.md +77 -0
  28. package/docs/runtime-reference.md +43 -3
  29. package/docs/unmeasured-cell-recovery-repair-proposal.md +387 -0
  30. package/docs/user-bindings.en.md +15 -0
  31. package/docs/user-bindings.md +15 -0
  32. package/docs/v0.5.3-goals.md +117 -0
  33. package/docs/verification.md +7 -3
  34. package/index.js +100 -31
  35. package/internal/binding-adoption.js +136 -0
  36. package/internal/bounded-literal.js +85 -0
  37. package/internal/capability-command-receipt.js +18 -4
  38. package/internal/capability-delivery-proof.js +70 -0
  39. package/internal/capability-environment.js +200 -11
  40. package/internal/capability-plan.js +11 -8
  41. package/internal/cell-error.js +13 -0
  42. package/internal/config-spec.js +126 -6
  43. package/internal/cordis-tools-owner.js +2 -1
  44. package/internal/direct-surface-owner.js +39 -21
  45. package/internal/edit-transport-owner.js +81 -15
  46. package/internal/execution-provider-owner.js +6 -0
  47. package/internal/execution-seam-compat.js +25 -0
  48. package/internal/host-tool-sdk.js +308 -0
  49. package/internal/isolated-worker.js +134 -17
  50. package/internal/kernel-worker.js +167 -3
  51. package/internal/legacy-cell-analysis.js +14 -2
  52. package/internal/legacy-repl-convenience.js +16 -3
  53. package/internal/official-repl-priority.js +31 -10
  54. package/internal/program-bindings.js +12 -1
  55. package/internal/projection-retention.js +238 -0
  56. package/internal/prompt-text.js +858 -0
  57. package/internal/recovery-tips.js +50 -11
  58. package/internal/rejected-cell-editor.js +3 -2
  59. package/internal/repl-memory-projection.js +21 -2
  60. package/internal/runtime-bridge-owner.js +114 -9
  61. package/internal/runtime-config.js +2 -0
  62. package/internal/runtime-contexts.js +14 -5
  63. package/internal/runtime-messages.js +48 -15
  64. package/internal/session-cell-executor.js +132 -11
  65. package/internal/session-journal-recovery.js +1204 -116
  66. package/internal/session-journal-schema.js +10 -5
  67. package/internal/session-journal.js +214 -14
  68. package/internal/session-log-view.js +133 -26
  69. package/internal/session-runtime.js +746 -38
  70. package/internal/session-state.js +27 -0
  71. package/internal/stateful-root-compiler.js +11 -0
  72. package/internal/stateful-root-runtime.js +17 -0
  73. package/internal/user-binding-console.js +7 -0
  74. package/internal/user-bindings-owner.js +112 -6
  75. package/internal/user-bindings.js +2 -2
  76. package/internal/validated-parse-repair.js +6 -1
  77. package/internal/worker-client.js +27 -4
  78. package/package.json +16 -6
  79. package/src/client-activity.js +24 -4
  80. package/src/client-capabilities-view.js +504 -27
  81. package/src/client-copy.js +187 -2
  82. package/src/client-indicator-view.js +2 -1
  83. package/src/client-repl-view.js +35 -11
  84. package/src/client-styles.js +64 -2
  85. package/src/client.js +3 -1
@@ -4,13 +4,49 @@ 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'
11
+
12
+ /**
13
+ * One capability, three separate facts, in the reader's language. Saved,
14
+ * delivered and usable are different questions, so a reader must be able to see
15
+ * which one failed instead of reading one blanket "applied".
16
+ */
17
+ export function capabilityProofCopy(t, proof) {
18
+ const delivery = ['delivered', 'pending', 'absent'].includes(proof?.delivery) ? proof.delivery : undefined
19
+ const usability = ['proven', 'unproven'].includes(proof?.usability) ? proof.usability : undefined
20
+ if (delivery === undefined || usability === undefined) return t('capabilities.quick.unknown')
21
+ return t('capabilities.proof.line', {
22
+ saved: t(proof.saved === true ? 'capabilities.proof.saved.yes' : 'capabilities.proof.saved.no'),
23
+ delivery: t(`capabilities.proof.delivery.${delivery}`),
24
+ usability: t(`capabilities.proof.usability.${usability}`),
25
+ })
26
+ }
7
27
 
8
28
  export function capabilityCommandText(t, text) {
9
29
  if (text?.key) return t(text.key, text.params)
10
30
  const receipt = readCapabilityCommandReceipt(text)
11
- return receipt ? t(`capabilities.receipt.${receipt.state}`, { session: receipt.sessionId, error: receipt.error ?? '' }) : text
31
+ if (!receipt) return text
32
+ const line = t(`capabilities.receipt.${receipt.state}`, { session: receipt.sessionId, error: receipt.error ?? '' })
33
+ return receipt.proof === undefined ? line : `${line} ${capabilityProofCopy(t, receipt.proof)}`
12
34
  }
13
35
 
36
+ // Host-owned context the model sees in PTC mode. The plugin never receives the host's
37
+ // own instruction text and cannot edit these bytes; it can only name the section it
38
+ // appends into or replaces. Listing them keeps the pane answering "what does the model
39
+ // actually see" instead of silently showing plugin-owned anchors only.
40
+ const HOST_CONTEXT_SECTIONS = Object.freeze([
41
+ Object.freeze({ id: 'host:system-prompt', presence: 'capabilities.prompt.hostPresenceEvery' }),
42
+ Object.freeze({ id: 'host:tools:sdk', presence: 'capabilities.prompt.hostPresenceCapabilities' }),
43
+ ])
44
+ // The prompt table is one column model shared by the colgroup, the header and every
45
+ // data row. Slot chips do not get a column: they render in their own full-width
46
+ // sub-row, because a narrow chip column stacked a five-slot anchor into a tall stack.
47
+ const PROMPT_TABLE_COLUMN_KEYS = Object.freeze(['channel', 'anchor', 'presence', 'bytes', 'reason', 'kind'])
48
+ const PROMPT_TABLE_COLUMNS = PROMPT_TABLE_COLUMN_KEYS.length
49
+
14
50
  function capabilityChoiceLabel(t, value) {
15
51
  return t(value === 'inherit' ? 'capabilities.quick.noOverride'
16
52
  : ['enabled', 'pending', 'unknown'].includes(value) ? `capabilities.quick.${value}`
@@ -30,13 +66,66 @@ function validCatalogRule(rule, temporary = false) {
30
66
  ? ['enabled', 'closed'].includes(choice) : CAPABILITY_DISCLOSURES.includes(choice))))
31
67
  }
32
68
 
69
+ /** A Chat snapshot without a node store cannot order a receipt against the conversation. */
70
+ const noSnapshot = () => undefined
71
+
72
+ /**
73
+ * The first human message is the conversation's opening position. DSH may reuse
74
+ * a blank Session, so capability commands recorded before that message are
75
+ * history from an earlier attempt, not part of this conversation; they must not
76
+ * appear above the user's first sentence.
77
+ */
78
+ export function firstHumanMessageSeq(nodes) {
79
+ if (nodes === null || nodes === undefined || typeof nodes.values !== 'function') return null
80
+ let first = null
81
+ for (const node of nodes.values()) {
82
+ if (node?.kind !== 'user' || node.data?.source?.kind !== 'user') continue
83
+ const seq = node.anchorSeq ?? node.data?.seq
84
+ if (!Number.isSafeInteger(seq)) continue
85
+ if (first === null || seq < first) first = seq
86
+ }
87
+ return first
88
+ }
89
+
90
+ /**
91
+ * A capability receipt belongs to chat only after the conversation has opened.
92
+ * A blank Session has nothing to announce, and a receipt recorded before the
93
+ * first human message describes a previous attempt's publication. A truncated
94
+ * Chat window cannot prove where the conversation opened, so its earliest
95
+ * loaded message must not hide a receipt the session may still own.
96
+ */
97
+ export function capabilityReceiptVisible(node, { blank = false, firstUserSeq = null, windowComplete = true } = {}) {
98
+ if (blank === true) return false
99
+ if (windowComplete !== true) return true
100
+ return !(Number.isSafeInteger(node?.seq) && Number.isSafeInteger(firstUserSeq) && node.seq < firstUserSeq)
101
+ }
102
+
33
103
  export function createCapabilityCommandCard(React) {
34
104
  const h = React.createElement
35
- return function CapabilityCommandCard({ node, t }) {
105
+ return function CapabilityCommandCard({ node, t, useChat, useSession }) {
36
106
  const [showSource, setShowSource] = React.useState(false)
107
+ // Both hooks are always called so the seat keeps one hook order whether or
108
+ // not the surrounding Chat or Session supplies its store.
109
+ const readSession = typeof useSession === 'function' ? useSession : noSnapshot
110
+ const blank = readSession(snapshot => snapshot?.blank === true) === true
111
+ // `hasMore` marks a window that does not reach the session start; only a
112
+ // complete window may claim its earliest human message is the first one.
113
+ const windowComplete = readSession(snapshot => snapshot?.hasMore !== true)
114
+ const readChat = typeof useChat === 'function' ? useChat : noSnapshot
115
+ const firstUserSeq = readChat(snapshot => firstHumanMessageSeq(snapshot?.nodes))
116
+ // A capability notice is the outcome of a `/capabilities` command the user
117
+ // submitted. What a new session publishes is a model-visible runtime context,
118
+ // not chat, so a node without that submitted input is not a user command and
119
+ // renders no notice: a fresh session has no change to announce.
120
+ if (typeof node.args !== 'string') return null
121
+ if (!capabilityReceiptVisible(node, { blank, firstUserSeq, windowComplete })) return null
37
122
  const command = `/${node.name ?? 'capabilities'}${node.args ? ` ${String(node.args).trim()}` : ''}`
38
123
  const text = node.outcome?.text
39
124
  const receipt = readCapabilityCommandReceipt(text)
125
+ // Pending is the plan's publication state, not an outcome: it becomes true
126
+ // only at the next accepted model step, so a durable notice would state a
127
+ // future that a historical row can no longer verify.
128
+ if (receipt?.state === 'pending') return null
40
129
  const legacyRefresh = text === 'Session capability environment refreshed; the model-visible notification is recorded.'
41
130
  if (receipt || String(node.args ?? '').trim() === 'refresh' || legacyRefresh) {
42
131
  const outcome = receipt ? t(`capabilities.card.${receipt.state}`, { error: receipt.error ?? '' })
@@ -44,9 +133,14 @@ export function createCapabilityCommandCard(React) {
44
133
  : node.outcome ? (typeof text === 'string' && !text.trim() ? undefined : capabilityCommandText(t, text))
45
134
  ?? t(node.outcome.kind === 'error' ? 'capabilities.commandFailed' : 'capabilities.refreshAcknowledged')
46
135
  : t('capabilities.refreshing')
136
+ // A command that names one capability carries that capability's three
137
+ // facts; the notice states them beside its session outcome so "saved"
138
+ // cannot be read as "delivered" or "usable".
139
+ const notice = receipt?.proof === undefined || outcome === undefined
140
+ ? outcome : `${outcome} ${capabilityProofCopy(t, receipt.proof)}`
47
141
  return h('p', { className: 'ptcPlusCapabilityCommandNotice',
48
142
  role: node.outcome?.kind === 'error' ? 'alert' : 'status',
49
- 'data-state': node.outcome?.kind ?? 'pending' }, outcome)
143
+ 'data-state': node.outcome?.kind ?? 'pending' }, notice)
50
144
  }
51
145
  let json
52
146
  if (typeof text === 'string') {
@@ -128,12 +222,13 @@ export function capabilityPlanReason(t, entry) {
128
222
  export function createCapabilitySettingsViews(React, { SearchIcon, Select, Modal = createFallbackModal(React) }) {
129
223
  const h = React.createElement
130
224
  const field = key => CONFIG_FIELDS.find(item => item.key === key)
131
- const responsibilityGroups = [
132
- { tab: 'manual', keys: ['rules'] },
133
- { tab: 'budgets', keys: ['description', 'recovery', 'preview'] },
134
- { tab: 'statistics', keys: ['statistics'] },
135
- ]
136
- const responsibilities = responsibilityGroups.flatMap(group => group.keys)
225
+ // Each group owns one settings tab; the prompt text switch lives in the same
226
+ // capabilities section, so 新会话设置 must list it or a saved choice would be
227
+ // invisible there.
228
+ // 新会话设置 is one row of equally sized choices. Groups only existed to give each
229
+ // tab a caption; that produced a second row as soon as 提示文本 was added, so the
230
+ // choices stay flat and the navigation target is decided by `onNavigate` instead.
231
+ const responsibilities = ['rules', 'description', 'recovery', 'preview', 'statistics', 'promptText']
137
232
  const choicesFor = (configuration, plan) => {
138
233
  const historical = plan && ['ptc-capability-plan/v1', 'ptc-capability-plan/v3'].includes(plan.schema)
139
234
  && configuration && configuration.capabilityPolicy === undefined
@@ -143,10 +238,13 @@ export function createCapabilitySettingsViews(React, { SearchIcon, Select, Modal
143
238
  enabled: configuration?.capabilityManagementEnabled ?? plan?.enabled,
144
239
  statistics,
145
240
  recoveryEnabled: configuration?.capabilityRecoveryEnabled ?? (historical ? plan.enabled : undefined),
146
- previewEnabled: configuration?.modelPreviewEnabled ?? (historical ? plan.enabled : undefined) }
241
+ previewEnabled: configuration?.modelPreviewEnabled ?? (historical ? plan.enabled : undefined),
242
+ // The switch is new: a stored configuration without it means "on", the
243
+ // documented default, while an unreadable configuration stays unknown.
244
+ promptTextEnabled: configuration === undefined ? undefined : configuration.capabilityPromptTextEnabled !== false }
147
245
  }
148
246
  const flagsFor = settings => ({ rules: settings.enabled, description: settings.enabled, statistics: settings.statistics?.enabled,
149
- recovery: settings.recoveryEnabled, preview: settings.previewEnabled })
247
+ recovery: settings.recoveryEnabled, preview: settings.previewEnabled, promptText: settings.promptTextEnabled })
150
248
  const choiceState = (retained, applied, key) => typeof retained[key] !== 'boolean' ? 'unknown'
151
249
  : retained[key] ? typeof applied[key] !== 'boolean' ? 'unknown' : applied[key] === false ? 'paused' : 'selected'
152
250
  : key === 'rules' || key === 'description' ? 'host' : 'unselected'
@@ -176,17 +274,16 @@ export function createCapabilitySettingsViews(React, { SearchIcon, Select, Modal
176
274
  return h('section', { className: 'ptcPlusCombination', 'aria-label': t('capabilities.combination.saved') },
177
275
  h('div', { className: 'ptcPlusTaskHead' }, h('h4', null, t('capabilities.combination.saved')),
178
276
  h('span', { className: 'ptcPlusCapabilitySaveState', role: 'status' }, feedback)),
179
- h('div', { className: 'ptcPlusCombinationChoices' }, ...responsibilityGroups.map(group =>
180
- h('div', { key: group.tab, className: 'ptcPlusCombinationGroup', role: 'group',
181
- 'aria-label': t(`capabilities.tab.${group.tab}`) },
182
- h('strong', { className: 'ptcPlusCombinationGroupTitle' }, t(`capabilities.tab.${group.tab}`)),
183
- h('div', { className: 'ptcPlusCombinationGroupChoices' }, ...group.keys.map(key => {
184
- const state = choiceState(retained, applied, key)
185
- return h('button', { key, type: 'button', className: 'ptcPlusCombinationChoice', 'data-state': state,
186
- 'aria-controls': key === 'rules' || key === 'statistics' ? `${id}-pane-${group.tab}` : `${id}-context-pane-${key}`,
187
- onClick: () => onNavigate(key) }, h('span', null, t(`capabilities.combination.${key}`)),
188
- h('small', null, t(`capabilities.combination.state.${state}`)))
189
- }))))),
277
+ h('div', { className: 'ptcPlusCombinationChoices' }, ...responsibilities.map(key => {
278
+ const state = choiceState(retained, applied, key)
279
+ return h('button', { key, type: 'button', className: 'ptcPlusCombinationChoice', 'data-state': state,
280
+ 'data-choice': key,
281
+ 'aria-controls': key === 'promptText' ? `${id}-pane-prompt`
282
+ : key === 'rules' || key === 'statistics' ? `${id}-pane-${key === 'rules' ? 'manual' : 'statistics'}`
283
+ : `${id}-context-pane-${key}`,
284
+ onClick: () => onNavigate(key) }, h('span', null, t(`capabilities.combination.${key}`)),
285
+ h('small', null, t(`capabilities.combination.state.${state}`)))
286
+ })),
190
287
  h('p', { className: 'ptcPlusDetail', role: 'status' }, effect),
191
288
  h('details', { className: 'ptcPlusCapabilitySessionTools' }, h('summary', { tabIndex: 0 }, t('capabilities.existingSessions')),
192
289
  renderRefresh?.(),
@@ -889,17 +986,375 @@ export function createCapabilitySettingsViews(React, { SearchIcon, Select, Modal
889
986
  event.currentTarget.parentElement.querySelectorAll('[role=tab]')[next].focus({ preventScroll: true })
890
987
  }
891
988
 
989
+ /**
990
+ * Per-asset editor for the fixed prompt text the plugin writes.
991
+ *
992
+ * One row per registered asset: the built-in sentence stays visible as the
993
+ * placeholder, so a user compares against it instead of a blank box. The
994
+ * document is written through the normal settings path, which validates it;
995
+ * a document the panel cannot read is reported here instead of blanking the
996
+ * whole workbench.
997
+ */
998
+ // Registry codes are protocol identifiers. The pane shows a localized label and
999
+ // keeps the raw code in the element title, so a Chinese UI never reads like a
1000
+ // dump of `fact-derived` / `protocol-exact-text` constants.
1001
+ const PROMPT_REASON_KEYS = Object.freeze({
1002
+ 'fact-derived': 'factDerived',
1003
+ 'aggregate-frozen': 'aggregateFrozen',
1004
+ 'protocol-exact-text': 'protocolExactText',
1005
+ 'protocol-structural': 'protocolStructural',
1006
+ 'shadows-host': 'shadowsHost',
1007
+ 'other-owner': 'otherOwner',
1008
+ })
1009
+
1010
+ function promptReasonLabel(t, reason) {
1011
+ const key = PROMPT_REASON_KEYS[reason]
1012
+ return key === undefined ? reason : t(`capabilities.prompt.reason.${key}`)
1013
+ }
1014
+
1015
+ function promptKindLabel(t, kind) {
1016
+ return kind === undefined ? '' : t(`capabilities.prompt.kind.${kind}`)
1017
+ }
1018
+
1019
+ function PromptTextSettings({ value, disabled, onChange, t, id }) {
1020
+ let document
1021
+ let readError
1022
+ try {
1023
+ document = parsePromptTextDocument(value.promptText)
1024
+ } catch (error) {
1025
+ readError = error.message
1026
+ document = { schema: PROMPT_TEXT_SCHEMA, overrides: {} }
1027
+ }
1028
+ // The workbench receives `disabled` as a per-field predicate, not a boolean.
1029
+ // The feature's own pause switch is separate from the settings lock: off means
1030
+ // every channel falls back to the built-in text while the saved document stays.
1031
+ const paused = value.capabilityPromptTextEnabled === false
1032
+ const blocked = paused || disabled === true
1033
+ || (typeof disabled === 'function' && disabled('promptText') === true)
1034
+ const resolved = resolvePromptText(value.promptText)
1035
+ const account = accountPromptText(resolved)
1036
+ // The pane stages edits in a draft and writes the document once, so the fields
1037
+ // and the preview always describe the same pending bytes. 保存 writes the draft;
1038
+ // 取消 drops it; a saved document still differs from the built-ins, so reopening
1039
+ // the pane shows that difference without a draft.
1040
+ const [draft, setDraft] = React.useState(null)
1041
+ const [selectedAnchorId, setSelectedAnchorId] = React.useState(null)
1042
+ const anchorPanelRef = React.useRef(null)
1043
+ const [openSlots, setOpenSlots] = React.useState({})
1044
+ const overrides = draft ?? document.overrides
1045
+ const byteCount = text => new TextEncoder().encode(text).length
1046
+ const group = number => String(Math.round(number)).replace(/\B(?=(\d{3})+(?!\d))/gu, ' ')
1047
+ const bytePair = bytes => t('capabilities.prompt.bytesPair', {
1048
+ bytes: group(bytes), kb: (bytes / 1024).toFixed(1),
1049
+ })
1050
+ const write = next => onChange(CONFIG_FIELDS.find(field => field.key === 'promptText'),
1051
+ JSON.stringify({ schema: PROMPT_TEXT_SCHEMA, overrides: next }))
1052
+ const update = (slotId, next) => {
1053
+ const base = { ...overrides }
1054
+ if (next === undefined) delete base[slotId]
1055
+ else base[slotId] = next
1056
+ setDraft(base)
1057
+ }
1058
+ const save = () => {
1059
+ if (draft === null) return
1060
+ write(draft)
1061
+ setDraft(null)
1062
+ }
1063
+ const cancel = () => setDraft(null)
1064
+ const pending = draft !== null
1065
+ const unmeasuredSlots = account.counts.unmeasuredSlots
1066
+ const dynamicSlots = account.counts.dynamicSlots
1067
+ const unwiredAnchors = account.anchors.filter(anchor => anchor.wired !== true)
1068
+ const measuredBytes = account.owners.A + account.owners.B + account.owners.C
1069
+ const ownerOf = anchor => (anchor.owner === 'mixed' ? 'mixed' : anchor.owner)
1070
+ const ownerAnchors = key => account.anchors.filter(anchor => ownerOf(anchor) === key).length
1071
+ // Icons are monochrome inline SVG: the mockup's cards carry a glyph, and an
1072
+ // icon font or an emoji would bring its own colour into the settings surface.
1073
+ const icon = path => h('svg', {
1074
+ viewBox: '0 0 20 20', width: 16, height: 16, fill: 'none', stroke: 'currentColor',
1075
+ 'stroke-width': 1.6, 'stroke-linecap': 'round', 'stroke-linejoin': 'round', 'aria-hidden': 'true',
1076
+ }, ...path.map((d, index) => h('path', { key: index, d })))
1077
+ const card = (key, labelKey, glyph) => {
1078
+ const mixed = key === 'mixed'
1079
+ const bytes = account.owners[key] ?? 0
1080
+ const value_ = mixed
1081
+ ? t('capabilities.prompt.cardMixed', { count: ownerAnchors('mixed') })
1082
+ : bytes > 0
1083
+ ? `${bytePair(bytes)} · ${t('capabilities.prompt.cardMetaAnchors', { count: ownerAnchors(key) })}`
1084
+ : t('capabilities.prompt.cardMetaAnchors', { count: ownerAnchors(key) })
1085
+ return h('div', { key, className: 'ptcPlusPromptCard', 'data-owner': key },
1086
+ h('span', { className: 'ptcPlusPromptCardIcon' }, glyph),
1087
+ h('span', { className: 'ptcPlusPromptCardBody' },
1088
+ h('span', { className: 'ptcPlusPromptCardLabel' }, t(labelKey)),
1089
+ mixed
1090
+ ? h('span', { className: 'ptcPlusPromptCardMeta' }, value_)
1091
+ : h('span', { className: 'ptcPlusPromptCardValue', 'data-owner-bytes': String(account.owners[key] ?? 0) }, value_)))
1092
+ }
1093
+ const cards = h('div', { className: 'ptcPlusPromptCards' },
1094
+ card('A', 'capabilities.prompt.ownerEditable', icon(['M4 16h3l9-9-3-3-9 9v3Z', 'M12.5 4.5 15.5 7.5'])),
1095
+ card('B', 'capabilities.prompt.ownerReadOnly', icon(['M5.5 9V6.5a4.5 4.5 0 0 1 9 0V9', 'M4.5 9h11v7h-11Z'])),
1096
+ 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'])),
1097
+ card('mixed', 'capabilities.prompt.ownerMixed', icon(['M4 6h12', 'M4 10h12', 'M4 14h12', 'M13 4v4', 'M7 8v4'])))
1098
+ const strip = h('p', { className: 'ptcPlusPromptStrip', 'data-prompt-strip': String(unwiredAnchors.length) },
1099
+ t('capabilities.prompt.strip', {
1100
+ anchors: unwiredAnchors.length, bytes: group(measuredBytes),
1101
+ kb: (measuredBytes / 1024).toFixed(1), total: account.counts.total,
1102
+ }))
1103
+ const reasonsOf = anchor => {
1104
+ const reasons = [...new Set(anchor.slots.map(slot => slot.reason).filter(Boolean))]
1105
+ return reasons.length === 0 ? '—' : reasons.map(reason => promptReasonLabel(t, reason)).join(' + ')
1106
+ }
1107
+ const kindsOf = anchor => [...new Set(anchor.slots.map(slot => slot.kind))]
1108
+ .map(kind => promptKindLabel(t, kind)).join(' + ')
1109
+ const splitOf = anchor => {
1110
+ const measured = anchor.slots.filter(slot => slot.measured === true && slot.bytes > 0)
1111
+ return measured.length > 1
1112
+ ? t('capabilities.prompt.anchorBytesSplit', {
1113
+ parts: measured.map(slot => `${slot.slotId.split('.').pop()} ${slot.bytes}`).join(' / '),
1114
+ })
1115
+ : null
1116
+ }
1117
+ const select = anchorId => setSelectedAnchorId(anchorId)
1118
+ // The expanded panel sits below the table, so on a short viewport selecting a
1119
+ // lower row would otherwise leave the editor it opened off-screen.
1120
+ React.useEffect(() => {
1121
+ if (selectedAnchorId === null) return
1122
+ const node = anchorPanelRef.current
1123
+ if (node === null || typeof node.scrollIntoView !== 'function') return
1124
+ node.scrollIntoView({ block: 'nearest', behavior: 'smooth' })
1125
+ }, [selectedAnchorId])
1126
+ const rowKey = (event, anchorId) => {
1127
+ if (event.key !== 'Enter' && event.key !== ' ') return
1128
+ event.preventDefault()
1129
+ select(anchorId)
1130
+ }
1131
+ const anchorRow = (anchor, index) => {
1132
+ const split = splitOf(anchor)
1133
+ return h('tr', {
1134
+ key: anchor.anchorId,
1135
+ className: 'ptcPlusPromptRow',
1136
+ 'data-anchor-id': anchor.anchorId,
1137
+ 'data-wired': anchor.wired === true ? 'true' : 'false',
1138
+ // A plain <table> row cannot carry aria-selected (that needs role="grid"),
1139
+ // so the expanded row is announced with aria-current instead.
1140
+ 'data-selected': selectedAnchorId === anchor.anchorId ? 'true' : 'false',
1141
+ 'aria-current': selectedAnchorId === anchor.anchorId ? 'true' : undefined,
1142
+ tabIndex: 0,
1143
+ onClick: () => select(anchor.anchorId),
1144
+ onKeyDown: event => rowKey(event, anchor.anchorId),
1145
+ },
1146
+ h('td', { className: 'ptcPlusPromptIndex' }, String(index)),
1147
+ h('td', null, h('span', { className: 'ptcPlusPromptChannel', 'data-channel': anchor.channel }, anchor.channel)),
1148
+ h('td', null, h('code', { className: 'ptcPlusPromptAnchorId', title: anchor.anchorId }, anchor.anchorId)),
1149
+ h('td', null, h('span', { className: 'ptcPlusPromptPresence', title: t(anchor.presence) }, t(anchor.presence))),
1150
+ h('td', { className: 'ptcPlusPromptBytesCell' },
1151
+ anchor.wired !== true
1152
+ ? h('span', { 'data-anchor-bytes': 'unwired' }, t('capabilities.prompt.unwiredRow', { slots: anchor.unmeasuredSlots }))
1153
+ : h('span', { 'data-anchor-bytes': String(anchor.bytes) },
1154
+ paused ? t('capabilities.prompt.pausedBytes', { bytes: anchor.bytes }) : t('capabilities.prompt.cardBytes', { bytes: anchor.bytes }),
1155
+ split === null ? null : h('small', { className: 'ptcPlusPromptSplit', 'data-anchor-split': 'true' }, split))),
1156
+ h('td', { className: 'ptcPlusPromptReasonCell' }, reasonsOf(anchor)),
1157
+ h('td', { className: 'ptcPlusPromptKindCell' }, kindsOf(anchor),
1158
+ anchor.wired !== true ? h('span', { className: 'ptcPlusPromptTag', 'data-prompt-unwired-tag': 'true' },
1159
+ t('capabilities.prompt.unwiredTag')) : null))
1160
+ }
1161
+ const hostRow = (section, index) => h('tr', {
1162
+ key: section.id, className: 'ptcPlusPromptRow ptcPlusPromptHostRow', 'data-host-row': section.id,
1163
+ },
1164
+ h('td', { className: 'ptcPlusPromptIndex' }, String(index)),
1165
+ h('td', null, h('span', { className: 'ptcPlusPromptChannel' }, 'prompt')),
1166
+ h('td', null, h('code', { className: 'ptcPlusPromptAnchorId', title: section.id }, section.id)),
1167
+ h('td', null, h('span', { className: 'ptcPlusPromptPresence' }, t(section.presence))),
1168
+ h('td', { className: 'ptcPlusPromptBytesCell' }, t('capabilities.prompt.hostUnavailable')),
1169
+ h('td', { className: 'ptcPlusPromptReasonCell' }, '—'),
1170
+ h('td', { className: 'ptcPlusPromptKindCell' }, t('capabilities.prompt.ownerHost')))
1171
+ const table = h('table', { className: 'ptcPlusPromptTable' },
1172
+ h('colgroup', null,
1173
+ h('col', { className: 'ptcPlusPromptColIndex' }),
1174
+ h('col', { className: 'ptcPlusPromptColChannel' }),
1175
+ h('col', { className: 'ptcPlusPromptColAnchor' }),
1176
+ h('col', { className: 'ptcPlusPromptColPresence' }),
1177
+ h('col', { className: 'ptcPlusPromptColBytes' }),
1178
+ h('col', { className: 'ptcPlusPromptColReason' }),
1179
+ h('col', { className: 'ptcPlusPromptColKind' })),
1180
+ h('thead', null, h('tr', null,
1181
+ h('th', { scope: 'col', className: 'ptcPlusPromptIndex' }, ''),
1182
+ h('th', { scope: 'col' }, t('capabilities.prompt.columnChannel')),
1183
+ h('th', { scope: 'col' }, t('capabilities.prompt.columnAnchor')),
1184
+ h('th', { scope: 'col' }, t('capabilities.prompt.columnPresence')),
1185
+ h('th', { scope: 'col' }, t('capabilities.prompt.columnBytesSplit')),
1186
+ h('th', { scope: 'col' }, t('capabilities.prompt.columnReason')),
1187
+ h('th', { scope: 'col' }, t('capabilities.prompt.columnKind')))),
1188
+ h('tbody', null,
1189
+ ...account.anchors.map((anchor, index) => anchorRow(anchor, index + 1)),
1190
+ ...HOST_CONTEXT_SECTIONS.map((section, index) => hostRow(section, account.anchors.length + index + 1))))
1191
+ const chip = (anchor, slot) => {
1192
+ const templateBytes = slot.measured === true && slot.kind === 'projected'
1193
+ const aggregateSet = slot.kind === 'aggregate'
1194
+ const shared = {
1195
+ className: 'ptcPlusPromptChip',
1196
+ 'data-slot-id': slot.slotId,
1197
+ 'data-slot-kind': slot.kind,
1198
+ ...(templateBytes ? { 'data-prompt-template-bytes': 'true' } : {}),
1199
+ ...(aggregateSet ? { 'data-prompt-aggregate-set': 'true' } : {}),
1200
+ title: templateBytes
1201
+ ? `${slot.slotId} · ${t('capabilities.prompt.templateBytes')}`
1202
+ : aggregateSet ? `${slot.slotId} · ${t('capabilities.prompt.aggregateFrozen')}` : slot.slotId,
1203
+ }
1204
+ if (slot.owner !== 'A') {
1205
+ return h('span', { key: slot.slotId, ...shared, 'data-readonly': 'true' },
1206
+ h('span', { className: 'ptcPlusPromptChipLabel' }, slot.slotId),
1207
+ h('span', { className: 'ptcPlusPromptChipNote' },
1208
+ t('capabilities.prompt.readonlyReason', { reason: promptReasonLabel(t, slot.reason), kind: promptKindLabel(t, slot.kind) })))
1209
+ }
1210
+ const selected = (openSlots[anchor.anchorId] ?? editableSlots(anchor)[0]?.slotId) === slot.slotId
1211
+ return h('button', {
1212
+ key: slot.slotId,
1213
+ ...shared,
1214
+ type: 'button',
1215
+ disabled: blocked,
1216
+ 'aria-pressed': selected,
1217
+ onClick: () => setOpenSlots(previous => ({ ...previous, [anchor.anchorId]: slot.slotId })),
1218
+ }, h('span', { className: 'ptcPlusPromptChipLabel' }, slot.slotId))
1219
+ }
1220
+ // One editor per anchor: the chips choose which editable slot is open, so the
1221
+ // run_code anchor keeps its three descriptions behind one field.
1222
+ const editableSlots = anchor => anchor.slots.filter(slot => slot.owner === 'A'
1223
+ && PROMPT_ASSETS.find(candidate => candidate.id === slot.slotId)?.defaultText !== null)
1224
+ const editorFor = anchor => {
1225
+ const slots = editableSlots(anchor)
1226
+ if (anchor.wired !== true || slots.length === 0) return null
1227
+ const selectedId = openSlots[anchor.anchorId] ?? slots[0].slotId
1228
+ const slot = slots.find(candidate => candidate.slotId === selectedId) ?? slots[0]
1229
+ const asset = PROMPT_ASSETS.find(candidate => candidate.id === slot.slotId)
1230
+ const override = Object.hasOwn(overrides, slot.slotId) ? overrides[slot.slotId] : undefined
1231
+ const current = override ?? asset.defaultText
1232
+ // The built-in sentence is the field's value, not a placeholder: the user must be
1233
+ // able to tweak the shipped wording without retyping it.
1234
+ return h('div', { key: slot.slotId, className: 'ptcPlusPromptEditor', 'data-editor-slot': slot.slotId },
1235
+ h('textarea', {
1236
+ id: `${id}-prompt-${slot.slotId}`, rows: 3, disabled: blocked, spellCheck: false,
1237
+ className: 'ptcPlusPromptTextarea', 'aria-label': slot.slotId, value: current,
1238
+ onChange: event => update(slot.slotId, event.target.value === '' ? undefined : event.target.value),
1239
+ }),
1240
+ h('div', { className: 'ptcPlusPromptEditorFoot' },
1241
+ h('span', { className: 'ptcPlusPromptHint' },
1242
+ t('capabilities.prompt.bytesOfLimit', { used: byteCount(current), max: PROMPT_TEXT_MAX_SLOT_BYTES })),
1243
+ h('span', { className: 'ptcPlusPromptEditorActions' },
1244
+ asset.omittable ? h('button', {
1245
+ type: 'button', className: 'ptcPlusButton', disabled: blocked,
1246
+ onClick: () => update(slot.slotId, ''),
1247
+ }, t('capabilities.prompt.omit')) : null,
1248
+ h('button', {
1249
+ type: 'button', className: 'ptcPlusButton', disabled: blocked || override === undefined,
1250
+ onClick: () => update(slot.slotId, undefined),
1251
+ }, t('capabilities.prompt.reset')))))
1252
+ }
1253
+ const defaultAnchor = account.anchors.find(anchor => anchor.wired === true && editableSlots(anchor).length > 0)
1254
+ const selectedAnchor = account.anchors.find(anchor => anchor.anchorId === selectedAnchorId) ?? defaultAnchor
1255
+ const selectedIndex = selectedAnchor === undefined ? 0 : account.anchors.indexOf(selectedAnchor) + 1
1256
+ const anchorPanel = h('section', { className: 'ptcPlusPromptPanel', 'data-prompt-panel': 'anchor', ref: anchorPanelRef },
1257
+ h('h5', { className: 'ptcPlusPromptPanelTitle' },
1258
+ t('capabilities.prompt.rowExpand', { index: selectedIndex })),
1259
+ selectedAnchor === undefined
1260
+ ? h('p', { className: 'ptcPlusPromptHint' }, t('capabilities.prompt.rowExpandHint'))
1261
+ : h('div', { className: 'ptcPlusPromptPanelBody' },
1262
+ h('div', { className: 'ptcPlusPromptChips', 'data-anchor-chips': selectedAnchor.anchorId },
1263
+ ...selectedAnchor.slots.map(slot => chip(selectedAnchor, slot))),
1264
+ selectedAnchor.wired !== true
1265
+ ? h('p', { className: 'ptcPlusPromptHint' }, t('capabilities.prompt.unwiredRow', { slots: selectedAnchor.unmeasuredSlots }))
1266
+ : editorFor(selectedAnchor)))
1267
+ // Saving shows what will change. The preview is derived from the same draft the
1268
+ // editors write, so it cannot disagree with them; a saved document that already
1269
+ // differs from the built-ins shows here too, because saving would rewrite it.
1270
+ const changes = account.anchors.flatMap(anchor => anchor.slots
1271
+ .map(slot => ({ slot, builtin: PROMPT_ASSETS.find(asset => asset.id === slot.slotId)?.defaultText }))
1272
+ .filter(entry => typeof entry.builtin === 'string'
1273
+ && Object.hasOwn(overrides, entry.slot.slotId)
1274
+ && overrides[entry.slot.slotId] !== entry.builtin)
1275
+ .map(entry => {
1276
+ const effective = overrides[entry.slot.slotId]
1277
+ const effect = anchor.channel === 'plan' ? t('capabilities.prompt.effectPlan')
1278
+ : anchor.channel === 'install' ? t('capabilities.prompt.effectInstall')
1279
+ : anchor.channel === 'prompt' ? t('capabilities.prompt.effectPrompt')
1280
+ : t('capabilities.prompt.effectTrigger')
1281
+ return {
1282
+ anchor, slot: entry.slot, effect,
1283
+ before: byteCount(entry.builtin), after: byteCount(effective), delta: byteCount(effective) - byteCount(entry.builtin),
1284
+ }
1285
+ }))
1286
+ const restoreSlot = slotId => update(slotId, undefined)
1287
+ const diffPanel = h('section', {
1288
+ className: 'ptcPlusPromptPanel',
1289
+ ...(changes.length === 0 ? {} : { 'data-prompt-diff': 'true' }),
1290
+ 'data-prompt-diff-panel': 'true',
1291
+ },
1292
+ h('h5', { className: 'ptcPlusPromptPanelTitle' }, t('capabilities.prompt.diffTitle')),
1293
+ changes.length === 0
1294
+ ? h('p', { className: 'ptcPlusPromptHint' }, t('capabilities.prompt.diffEmpty'))
1295
+ : h('ul', { className: 'ptcPlusPromptDiffList' },
1296
+ ...changes.map((entry, index) => h('li', {
1297
+ key: entry.slot.slotId, className: 'ptcPlusPromptDiffRow',
1298
+ 'data-diff-anchor': entry.anchor.anchorId, 'data-diff-slot': entry.slot.slotId,
1299
+ },
1300
+ h('span', { className: 'ptcPlusPromptDot', 'data-tone': String(index % 3) }),
1301
+ h('span', { className: 'ptcPlusPromptDiffBody' },
1302
+ h('span', { className: 'ptcPlusPromptDiffHead' },
1303
+ h('code', { className: 'ptcPlusPromptDiffSlot' }, entry.slot.slotId),
1304
+ h('span', { className: 'ptcPlusPromptHint' }, entry.effect)),
1305
+ h('span', { className: 'ptcPlusPromptDiffDelta' },
1306
+ t('capabilities.prompt.diffBytes', {
1307
+ before: group(entry.before), after: group(entry.after),
1308
+ delta: entry.delta > 0 ? `+${group(entry.delta)}` : group(entry.delta),
1309
+ }),
1310
+ h('button', {
1311
+ type: 'button', className: 'ptcPlusButton', disabled: blocked,
1312
+ onClick: () => restoreSlot(entry.slot.slotId),
1313
+ }, t('capabilities.prompt.reset'))))))),
1314
+ h('div', { className: 'ptcPlusPromptActions' },
1315
+ h('button', {
1316
+ type: 'button', className: 'ptcPlusButton', 'data-kind': 'primary', disabled: blocked || !pending,
1317
+ onClick: save,
1318
+ }, t('capabilities.prompt.save')),
1319
+ h('button', { type: 'button', className: 'ptcPlusButton', disabled: blocked || !pending, onClick: cancel },
1320
+ t('capabilities.prompt.cancel'))))
1321
+ return h('section', {
1322
+ className: 'ptcPlusPromptText', 'data-prompt-paused': paused ? 'true' : 'false',
1323
+ },
1324
+ h('header', { className: 'ptcPlusPromptHeader' },
1325
+ 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'])),
1326
+ h('span', { className: 'ptcPlusPromptHeaderBody' },
1327
+ h('h4', { className: 'ptcPlusPromptTitle' }, t('capabilities.prompt.title')),
1328
+ h('p', { className: 'ptcPlusPromptSubtitle' }, t('capabilities.prompt.subtitle')))),
1329
+ paused
1330
+ ? h('p', { className: 'ptcPlusPromptPaused', role: 'status', 'data-prompt-paused-notice': 'true' },
1331
+ t('capabilities.prompt.paused'))
1332
+ : null,
1333
+ readError === undefined ? null : h('p', { className: 'ptcPlusPromptHint' }, t('capabilities.prompt.unreadable', { error: readError })),
1334
+ ...resolved.reported.map((entry, index) => h('p', { key: `report-${index}`, className: 'ptcPlusPromptHint' },
1335
+ t('capabilities.prompt.reported', {
1336
+ asset: entry.slotId ?? entry.anchorId ?? 'document', reason: entry.reason,
1337
+ }))),
1338
+ cards,
1339
+ strip,
1340
+ h('p', { className: 'ptcPlusPromptHint', 'data-prompt-slot-counts': 'true' },
1341
+ t('capabilities.prompt.slotCounts', { bytes: group(measuredBytes), unmeasured: unmeasuredSlots, dynamic: dynamicSlots })),
1342
+ table,
1343
+ h('div', { className: 'ptcPlusPromptPanels' }, anchorPanel, diffPanel),
1344
+ h('p', { className: 'ptcPlusPromptIntro' }, t('capabilities.prompt.intro')))
1345
+ }
1346
+
892
1347
  function CapabilityWorkbench({ value, configurationReady, catalog, catalogError, plans, plansError, plansPending, plansStale, disabled, onChange, onReload, t, renderField, renderPlans, renderRefresh, id, selectedSessionId, onSessionSelect,
893
1348
  allocation, allocationError, allocationPending, onApplyConfiguration, configurationWritable, saving, saveStatus }) {
894
1349
  const [tab, setTab] = React.useState('manual')
895
1350
  const [contextSection, setContextSection] = React.useState('description')
896
- const tabs = ['manual', 'budgets', 'statistics', 'plans']
1351
+ const tabs = ['manual', 'budgets', 'statistics', 'prompt', 'plans']
897
1352
  const feedback = saving ? t('state.syncing') : saveStatus && saveStatus.key !== 'status.applied' ? t(saveStatus.key, saveStatus.params)
898
1353
  : t(configurationWritable ? 'capabilities.savedForNew' : 'footer.readOnly')
899
1354
  return h('div', { className: 'ptcPlusCapabilityWorkbench' },
900
1355
  h(CombinationSummary, { value, configurationReady, plans, plansError, plansPending, plansStale, selectedSessionId,
901
1356
  feedback, onReload, renderRefresh, renderPlans, t, id, onNavigate: key => {
902
- setTab(key === 'rules' ? 'manual' : key === 'statistics' ? 'statistics' : 'budgets')
1357
+ setTab(key === 'rules' ? 'manual' : key === 'statistics' ? 'statistics' : key === 'promptText' ? 'prompt' : 'budgets')
903
1358
  if (['description', 'recovery', 'preview'].includes(key)) setContextSection(key)
904
1359
  } }),
905
1360
  h('div', { className: 'ptcPlusCapabilityTabs', role: 'tablist', 'aria-label': t('capabilities.settingsTabs') },
@@ -909,7 +1364,7 @@ export function createCapabilitySettingsViews(React, { SearchIcon, Select, Modal
909
1364
  }, t(`capabilities.tab.${key}`)))),
910
1365
  ...tabs.map(key => h('div', { key, className: 'ptcPlusCapabilityPane', role: 'tabpanel', id: `${id}-pane-${key}`,
911
1366
  'aria-labelledby': `${id}-tab-${key}`, hidden: tab !== key },
912
- h('div', { className: 'ptcPlusTaskHead' }, h('h4', null, t(`capabilities.title.${key}`)),
1367
+ key === 'prompt' ? null : h('div', { className: 'ptcPlusTaskHead' }, h('h4', null, t(`capabilities.title.${key}`)),
913
1368
  key === 'manual' ? h('label', { className: 'ptcPlusCapabilityManagement' }, h('span', null, t('capabilities.management')),
914
1369
  h('input', { type: 'checkbox', role: 'switch', className: 'ptcPlusCheck', checked: value.capabilityManagementEnabled === true,
915
1370
  disabled: disabled('capabilityManagementEnabled'), 'aria-label': t('capabilities.management'),
@@ -926,7 +1381,10 @@ export function createCapabilitySettingsViews(React, { SearchIcon, Select, Modal
926
1381
  h(AllocationEvidence, { allocation, allocationError, allocationPending, selectedSessionId, statistics: true, onReload, t }))
927
1382
  : key === 'budgets' ? h(ContextSettings, { value, configurationReady, disabled, onChange, t, renderField, allocation, allocationError, allocationPending,
928
1383
  selectedSessionId, onReload, renderRefresh, id, section: contextSection, setSection: setContextSection })
929
- : h(ConfigurationProfiles, { value, plans, disabled, writable: configurationWritable, onChange, onApplyConfiguration, t }))))
1384
+ // Rendered only while its tab is active: eight idle textareas in a hidden
1385
+ // pane would change every generic textbox query in the settings card.
1386
+ : key === 'prompt' ? (tab === 'prompt' ? h(PromptTextSettings, { value, disabled, onChange, t, id }) : null)
1387
+ : h(ConfigurationProfiles, { value, plans, disabled, writable: configurationWritable, onChange, onApplyConfiguration, t }))))
930
1388
  }
931
1389
 
932
1390
  return { CapabilityWorkbench }
@@ -992,6 +1450,10 @@ export function createCapabilityRefreshView(React, commands) {
992
1450
  export function createSessionCapabilityControlsView(React, { commands, Modal, Select, inspectSessionCatalog }) {
993
1451
  const h = React.createElement
994
1452
  const emptyDrafts = () => ({})
1453
+ // The panel reports the three capability facts only where it holds evidence:
1454
+ // the receipts its own commands returned. A catalogue row says what the plan
1455
+ // will do; it cannot say a cell reached the capability.
1456
+ const emptyProofs = () => ({})
995
1457
  return function SessionCapabilityControls({ sessionId, open, enabled, t, onClose }) {
996
1458
  const source = React.useMemo(() => commands.source(sessionId), [sessionId])
997
1459
  const available = React.useSyncExternalStore(source.subscribe, source.getSnapshot)
@@ -1013,6 +1475,7 @@ export function createSessionCapabilityControlsView(React, { commands, Modal, Se
1013
1475
  const [sort, setSort] = React.useState(null)
1014
1476
  const [focusDimension, setFocusDimension] = React.useState(null)
1015
1477
  const [status, setStatus] = React.useState(null)
1478
+ const [proofs, setProofs] = React.useState(emptyProofs)
1016
1479
  const [pending, setPending] = React.useState(false)
1017
1480
  const request = React.useRef(null)
1018
1481
  const applying = React.useRef(false)
@@ -1045,6 +1508,7 @@ export function createSessionCapabilityControlsView(React, { commands, Modal, Se
1045
1508
  setPane('list')
1046
1509
  setFocusDimension(null)
1047
1510
  setStatus(null)
1511
+ setProofs(emptyProofs())
1048
1512
  setPending(false)
1049
1513
  setError(null)
1050
1514
  }, [sessionId])
@@ -1211,15 +1675,20 @@ export function createSessionCapabilityControlsView(React, { commands, Modal, Se
1211
1675
  let pendingCount = 0
1212
1676
  let failure = null
1213
1677
  let savedOnly = null
1678
+ const observed = {}
1214
1679
  for (const [id, selection, dimension] of staged) {
1215
1680
  if (controller.signal.aborted) break
1681
+ const line = `set ${targetScope} ${encodeURIComponent(id)} ${dimension ? dimension + ' ' : ''}${selection}`
1216
1682
  try {
1217
- const result = await commands.run(sessionId, `set ${targetScope} ${encodeURIComponent(id)} ${dimension ? dimension + ' ' : ''}${selection}`, controller.signal)
1683
+ const result = await commands.run(sessionId, line, controller.signal)
1218
1684
  const receipt = readCapabilityCommandReceipt(result.text)
1219
1685
  if (!receipt || receipt.sessionId !== sessionId || !['recorded', 'pending'].includes(receipt.state)) {
1220
1686
  throw new Error(receipt ? capabilityCommandText(t, result.text) : t('capabilities.quick.unknownReceipt'))
1221
1687
  }
1222
1688
  if (receipt?.state === 'pending') pendingCount += 1
1689
+ // A receipt that names the capability carries the only proof this
1690
+ // panel can hold: what the save did and did not establish.
1691
+ if (receipt.proof !== undefined) observed[receipt.proof.id] = { proof: receipt.proof, command: `/capabilities ${line}` }
1223
1692
  completed += 1
1224
1693
  } catch (cause) {
1225
1694
  failure = String(cause?.message ?? cause)
@@ -1228,6 +1697,7 @@ export function createSessionCapabilityControlsView(React, { commands, Modal, Se
1228
1697
  break
1229
1698
  }
1230
1699
  }
1700
+ if (mounted.current && Object.keys(observed).length) setProofs(current => ({ ...current, ...observed }))
1231
1701
  if (request.current !== controller || controller.signal.aborted || !mounted.current) return
1232
1702
  const refreshed = await load(controller.signal)
1233
1703
  if (request.current !== controller || controller.signal.aborted || !mounted.current) return
@@ -1465,6 +1935,13 @@ export function createSessionCapabilityControlsView(React, { commands, Modal, Se
1465
1935
  h('dt', null, t('capabilities.quick.effective')), h('dd', null, statusLabel(selected.effective)),
1466
1936
  selected.sourceDimensions ? h(React.Fragment, null, h('dt', null, t('capabilities.policy.frozenSource')), h('dd', null, dimensionFacts(selected.sourceDimensions))) : null,
1467
1937
  selected.effectiveDimensions ? h(React.Fragment, null, h('dt', null, t('capabilities.policy.effectiveDimensions')), h('dd', null, dimensionFacts(selected.effectiveDimensions))) : null),
1938
+ h('div', { className: 'ptcPlusCapabilityProof', 'data-capability-proof': selected.id },
1939
+ h('h4', null, t('capabilities.proof.title')),
1940
+ proofs[selected.id]
1941
+ ? h(React.Fragment, null,
1942
+ h('p', { className: 'ptcPlusDetail', role: 'status' }, capabilityProofCopy(t, proofs[selected.id].proof)),
1943
+ h('small', null, t('capabilities.proof.observed', { command: proofs[selected.id].command })))
1944
+ : h('p', { className: 'ptcPlusDetail' }, t('capabilities.proof.unobserved'))),
1468
1945
  selected.source?.kind === 'mcp' && selected.category !== 'server'
1469
1946
  ? (() => {
1470
1947
  const server = catalog.find(entry => entry.category === 'server' && entry.source?.server === selected.source.server)