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.
- package/README.en.md +10 -4
- package/README.md +10 -4
- package/client.cjs +1623 -104
- 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 +4 -2
- package/docs/adr/0019-plugin-settings-and-kill-switch.md +2 -2
- package/docs/adr/0023-global-user-bindings.md +12 -0
- package/docs/adr/0027-run-the-session-kernel-in-a-killable-helper-process.md +11 -1
- package/docs/adr/0028-attach-to-the-host-ptc-execution-seam.md +1 -1
- package/docs/adr/0030-serve-both-typert-codec-generations-from-one-codec.md +2 -2
- package/docs/adr/0032-own-session-log-facts-in-a-host-only-projection.md +31 -1
- package/docs/adr/0044-coexist-with-the-official-persistent-repl.md +4 -4
- 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/0048-supersede-a-rewritten-cell.md +48 -0
- package/docs/adr/0049-prove-a-settlement-against-its-published-record.md +53 -0
- package/docs/adr/0050-read-host-surfaces-through-their-published-contract.md +48 -0
- package/docs/adr/README.md +7 -1
- package/docs/architecture.md +3 -3
- package/docs/client-ui.md +5 -2
- package/docs/durability-design.md +15 -2
- package/docs/installation.md +9 -9
- package/docs/releases/v0.5.2.md +91 -0
- package/docs/releases/v0.5.3.md +77 -0
- package/docs/runtime-reference.md +43 -3
- package/docs/unmeasured-cell-recovery-repair-proposal.md +387 -0
- package/docs/user-bindings.en.md +15 -0
- package/docs/user-bindings.md +15 -0
- package/docs/v0.5.3-goals.md +117 -0
- package/docs/verification.md +7 -3
- package/index.js +100 -31
- package/internal/binding-adoption.js +136 -0
- package/internal/bounded-literal.js +85 -0
- package/internal/capability-command-receipt.js +18 -4
- package/internal/capability-delivery-proof.js +70 -0
- package/internal/capability-environment.js +200 -11
- package/internal/capability-plan.js +11 -8
- package/internal/cell-error.js +13 -0
- package/internal/config-spec.js +126 -6
- package/internal/cordis-tools-owner.js +2 -1
- package/internal/direct-surface-owner.js +39 -21
- package/internal/edit-transport-owner.js +81 -15
- package/internal/execution-provider-owner.js +6 -0
- package/internal/execution-seam-compat.js +25 -0
- package/internal/host-tool-sdk.js +308 -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 +31 -10
- package/internal/program-bindings.js +12 -1
- 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/repl-memory-projection.js +21 -2
- package/internal/runtime-bridge-owner.js +114 -9
- package/internal/runtime-config.js +2 -0
- package/internal/runtime-contexts.js +14 -5
- package/internal/runtime-messages.js +48 -15
- package/internal/session-cell-executor.js +132 -11
- package/internal/session-journal-recovery.js +1204 -116
- package/internal/session-journal-schema.js +10 -5
- package/internal/session-journal.js +214 -14
- package/internal/session-log-view.js +133 -26
- package/internal/session-runtime.js +746 -38
- 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 +112 -6
- 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 +16 -6
- package/src/client-activity.js +24 -4
- package/src/client-capabilities-view.js +504 -27
- package/src/client-copy.js +187 -2
- package/src/client-indicator-view.js +2 -1
- package/src/client-repl-view.js +35 -11
- package/src/client-styles.js +64 -2
- 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
|
-
|
|
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' },
|
|
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
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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' }, ...
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
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
|
-
|
|
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,
|
|
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)
|