@elvisw/dsh-magic-context 0.0.0-bootstrap.0 → 0.0.0-stage
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.md +2 -452
- package/package.json +4 -108
- package/LICENSE +0 -23
- package/NOTICE +0 -93
- package/README.zh-CN.md +0 -368
- package/cordis.patch.yml +0 -20
- package/lib/client.js +0 -2270
- package/lib/core/compartments.js +0 -424
- package/lib/core/corpus-store.js +0 -176
- package/lib/core/directive-detect.js +0 -147
- package/lib/core/domain-reconcile.js +0 -394
- package/lib/core/domains.js +0 -127
- package/lib/core/embedding-local.js +0 -247
- package/lib/core/global-autostage.js +0 -223
- package/lib/core/global-domains.js +0 -302
- package/lib/core/journal.js +0 -248
- package/lib/core/llm-deadline.js +0 -53
- package/lib/core/machine-domain.js +0 -106
- package/lib/core/maintenance.js +0 -380
- package/lib/core/memory-bridge.js +0 -818
- package/lib/core/memory-categories.js +0 -84
- package/lib/core/memory-scoring.js +0 -65
- package/lib/core/memory-search.js +0 -414
- package/lib/core/memory-write.js +0 -1176
- package/lib/core/policy.js +0 -111
- package/lib/core/profile-confirmation.js +0 -55
- package/lib/core/profile-review.js +0 -375
- package/lib/core/profile-write.js +0 -283
- package/lib/core/reference-seeds.js +0 -63
- package/lib/core/route-health.js +0 -106
- package/lib/core/stores.js +0 -1021
- package/lib/core/tagger.js +0 -7
- package/lib/core/types.js +0 -15
- package/lib/core/vector-backfill.js +0 -140
- package/lib/core/vector-lane.js +0 -184
- package/lib/dsh/command-surface.js +0 -376
- package/lib/dsh/compaction-engine.js +0 -231
- package/lib/dsh/config-switches.js +0 -56
- package/lib/dsh/config.js +0 -13
- package/lib/dsh/context-pressure.js +0 -36
- package/lib/dsh/drop-landing.js +0 -42
- package/lib/dsh/emergency.js +0 -89
- package/lib/dsh/flush-command.js +0 -148
- package/lib/dsh/global-review.js +0 -412
- package/lib/dsh/historian.js +0 -907
- package/lib/dsh/live-journal.js +0 -81
- package/lib/dsh/maintenance-wiring.js +0 -229
- package/lib/dsh/management-pass.js +0 -564
- package/lib/dsh/memory-lane.js +0 -535
- package/lib/dsh/pre-step.js +0 -675
- package/lib/dsh/project-drift.js +0 -116
- package/lib/dsh/recomp.js +0 -192
- package/lib/dsh/reduce-guidance.js +0 -188
- package/lib/dsh/seams.js +0 -588
- package/lib/dsh/search-hint.js +0 -186
- package/lib/dsh/session-notice.js +0 -100
- package/lib/dsh/status-command.js +0 -75
- package/lib/dsh/telemetry.js +0 -349
- package/lib/dsh/tool-bridge.js +0 -1009
- package/lib/dsh/volatile.js +0 -89
- package/lib/index.js +0 -931
- package/tools/migrate-profile-package-name.mjs +0 -317
- package/vendor/onnx/LICENSE +0 -27
- package/vendor/onnx/ort-wasm-simd-threaded.mjs +0 -59
- package/vendor/onnx/ort-wasm-simd-threaded.wasm +0 -0
- package/vendor/onnx/ort.node.min.mjs +0 -8
package/lib/client.js
DELETED
|
@@ -1,2270 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Magic Context — the adapter's browser half.
|
|
3
|
-
*
|
|
4
|
-
* This file is plain JavaScript on purpose. The official client build preset
|
|
5
|
-
* (`@deepseek-ai/dsh-client-tsdown`) is not published (npm E404), so the bundle
|
|
6
|
-
* is hand-written in the shape that preset emits: a `__ModuleLoader__.load`
|
|
7
|
-
* registration whose factory returns the Cordis plugin object. `scripts/copy-client.mjs`
|
|
8
|
-
* copies this file verbatim to `lib/client.js`; there is no compile step, so the
|
|
9
|
-
* served artifact and this source are byte-identical by construction.
|
|
10
|
-
*
|
|
11
|
-
* The half contributes three surfaces, all of which degrade to nothing when the
|
|
12
|
-
* deployment does not offer their seat or data:
|
|
13
|
-
*
|
|
14
|
-
* 0. The `magic-context` locale namespace. Every string the two surfaces below
|
|
15
|
-
* render goes through a typed dictionary (`ctx.locale.register`), and each
|
|
16
|
-
* registration declares `locale: LOCALE_NS` so the slot renderer hands the
|
|
17
|
-
* component the framework `t` seat — the panel follows General → Language
|
|
18
|
-
* with no reload. See the Localization section.
|
|
19
|
-
*
|
|
20
|
-
* 1. `shell.overlay` — a telemetry badge/pill. It follows the Host half's
|
|
21
|
-
* `/mc-telemetry/events` SSE stream (falling back to one
|
|
22
|
-
* `/mc-telemetry/summary` read) and renders adapter state plus the most
|
|
23
|
-
* recent journal events. Collapsed it is a small pill; clicking it opens the
|
|
24
|
-
* detail panel. The overlay layer is click-through, so the pill opts back
|
|
25
|
-
* into pointer events itself. Nothing here is a credential or a message
|
|
26
|
-
* body: only the trimmed telemetry the Host route already exposes.
|
|
27
|
-
*
|
|
28
|
-
* 2. The Plugins page settings card for the row's `magic-context-dsh` settings
|
|
29
|
-
* namespace (`ctx.configForms`). It writes through the shared settings form,
|
|
30
|
-
* i.e. `remote.settings.mutate` → config editor → the profile's
|
|
31
|
-
* `cordis.patch.yml` override row. This half never touches a file.
|
|
32
|
-
*
|
|
33
|
-
* Only modules from the shell's static module table are required (`react`,
|
|
34
|
-
* `@deepseek-ai/dsh-client-ui-primitives`); every other collaborator arrives as
|
|
35
|
-
* a Cordis service (`slots`, `configForms`).
|
|
36
|
-
*/
|
|
37
|
-
|
|
38
|
-
window.__ModuleLoader__.load({
|
|
39
|
-
/**
|
|
40
|
-
* The registered id must equal `name` in package.json. The harness keys the
|
|
41
|
-
* boot-graph row, the `/plugins/??<id>/client.js&rev=…` URL, and the
|
|
42
|
-
* "loaded without registering <id>" check that gates module materialization
|
|
43
|
-
* all by the package name — not by the Loader row id (`magic-context-dsh`),
|
|
44
|
-
* which is only the settings namespace.
|
|
45
|
-
*/
|
|
46
|
-
id: '@elvisw/dsh-magic-context',
|
|
47
|
-
factory: (require) => {
|
|
48
|
-
const React = require('react')
|
|
49
|
-
const { useEffect, useRef, useState } = React
|
|
50
|
-
const el = React.createElement
|
|
51
|
-
|
|
52
|
-
/* ------------------------------------------------------------------ *
|
|
53
|
-
* Contracts
|
|
54
|
-
* ------------------------------------------------------------------ */
|
|
55
|
-
|
|
56
|
-
/** Loader row id of the Host half; also the settings namespace. */
|
|
57
|
-
const CFG_NS = 'magic-context-dsh'
|
|
58
|
-
/** Package name; the `plugins.row.config` key is `<package name>#<row id>`. */
|
|
59
|
-
const PKG_NAME = '@elvisw/dsh-magic-context'
|
|
60
|
-
const ROW_CONFIG_KEY = PKG_NAME + '#' + CFG_NS
|
|
61
|
-
/** Host telemetry routes (loopback only, no auth — telemetry only). */
|
|
62
|
-
const SUMMARY_URL = '/mc-telemetry/summary'
|
|
63
|
-
const EVENTS_URL = '/mc-telemetry/events'
|
|
64
|
-
|
|
65
|
-
/**
|
|
66
|
-
* Scope a telemetry URL to one Session.
|
|
67
|
-
*
|
|
68
|
-
* The badge sits in a session-scoped seat, so it asks the Host for that
|
|
69
|
-
* Session's rows (`?session=`), which is what makes the counts and the
|
|
70
|
-
* project identity belong to the work in front of the user rather than to
|
|
71
|
-
* every project on the machine. Without an id — an older shell, or a seat
|
|
72
|
-
* that hands the plugin no id — the request keeps the Host's original
|
|
73
|
-
* whole-store view.
|
|
74
|
-
*/
|
|
75
|
-
function telemetryUrl(base, sessionId) {
|
|
76
|
-
return typeof sessionId === 'string' && sessionId !== ''
|
|
77
|
-
? base + '?session=' + encodeURIComponent(sessionId)
|
|
78
|
-
: base
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
/** `git:a81f6372` — enough of a project identity to tell projects apart. */
|
|
82
|
-
function shortIdentity(identity) {
|
|
83
|
-
if (typeof identity !== 'string' || identity === '') return undefined
|
|
84
|
-
const separator = identity.indexOf(':')
|
|
85
|
-
if (separator === -1 || identity.length <= separator + 9) return identity
|
|
86
|
-
return identity.slice(0, separator + 1) + identity.slice(separator + 1, separator + 9)
|
|
87
|
-
}
|
|
88
|
-
/** How many recent events the expanded panel keeps (records + noise alike). */
|
|
89
|
-
const MAX_RECENT = 48
|
|
90
|
-
/** How often the open panel re-reads `/mc-telemetry/summary` for its header counters. */
|
|
91
|
-
const HEALTH_REFRESH_MS = 60_000
|
|
92
|
-
/**
|
|
93
|
-
* How long an `error` row keeps the pill red.
|
|
94
|
-
*
|
|
95
|
-
* The pill answers "is anything wrong NOW", so it must not ride a cumulative
|
|
96
|
-
* counter: `errorsSinceBoot` spans the whole process lifetime, and a long-lived
|
|
97
|
-
* process (measured: 21.5 h of uptime) stayed red all day on provider-side
|
|
98
|
-
* stream failures the adapter had already classified and retried. 15 minutes
|
|
99
|
-
* exceeds the 300 s stream idle timeout, so a run that is genuinely failing
|
|
100
|
-
* still shows red while it retries.
|
|
101
|
-
*/
|
|
102
|
-
const ERROR_RECENT_MS = 15 * 60_000
|
|
103
|
-
/**
|
|
104
|
-
* The composer's DOCK row — the "status bar" beneath the input line.
|
|
105
|
-
*
|
|
106
|
-
* The composer is two rows: the input line (left `+` / permission / `input.left`
|
|
107
|
-
* and right `input.right` / model selector) and the dock row
|
|
108
|
-
* (`InputBar`'s `data-composer-dock`), which already carries the session
|
|
109
|
-
* statistics: `ui-chat` registers an activity pill (order 0) and the Token
|
|
110
|
-
* usage pill (order 1) here, and the context meter follows the slot. That is
|
|
111
|
-
* where the user expects project telemetry, so we append at order 2.
|
|
112
|
-
*
|
|
113
|
-
* `list` + `session` scope: the seat hands us the current `sessionId`, which
|
|
114
|
-
* is what scopes our telemetry requests to this conversation.
|
|
115
|
-
*/
|
|
116
|
-
const STATUS_SEAT = 'conversation.composer.dock'
|
|
117
|
-
/** Position after the activity (0) and usage (1) pills, before the context meter. */
|
|
118
|
-
const STATUS_SEAT_ORDER = 2
|
|
119
|
-
/** The overlay only appears when the status seat is still absent after this long (an older shell). */
|
|
120
|
-
const OVERLAY_FALLBACK_MS = 1_500
|
|
121
|
-
|
|
122
|
-
/**
|
|
123
|
-
* `@deepseek-ai/dsh-client-ui-primitives` is one of the shell's platform
|
|
124
|
-
* modules. The require is guarded so a deployment whose module table lacks
|
|
125
|
-
* it still materializes this bundle and keeps the telemetry badge; the
|
|
126
|
-
* settings card falls back to plain controls.
|
|
127
|
-
*/
|
|
128
|
-
let ui = {}
|
|
129
|
-
try {
|
|
130
|
-
ui = require('@deepseek-ai/dsh-client-ui-primitives') || {}
|
|
131
|
-
} catch (error) {
|
|
132
|
-
ui = {}
|
|
133
|
-
}
|
|
134
|
-
const SettingsForm = typeof ui.SettingsForm === 'function' ? ui.SettingsForm : undefined
|
|
135
|
-
const SettingsValueField = typeof ui.SettingsValueField === 'function' ? ui.SettingsValueField : undefined
|
|
136
|
-
const Switch = typeof ui.Switch === 'function' ? ui.Switch : undefined
|
|
137
|
-
/** The official dark tooltip bubble (side top, 200ms delay) — same skin as the context meter's. */
|
|
138
|
-
const Tooltip = typeof ui.Tooltip === 'function' ? ui.Tooltip : undefined
|
|
139
|
-
|
|
140
|
-
/* ------------------------------------------------------------------ *
|
|
141
|
-
* The editable settings surface
|
|
142
|
-
* ------------------------------------------------------------------ */
|
|
143
|
-
|
|
144
|
-
/**
|
|
145
|
-
* The two route pins are PAIRS: a pin is active only when both halves are
|
|
146
|
-
* non-empty (the same rule on the host side — `maintenance-wiring.ts` for
|
|
147
|
-
* the verify lane, `liveRoute` for the generation lane), so the card stages
|
|
148
|
-
* and saves each pair as one unit.
|
|
149
|
-
*/
|
|
150
|
-
const ROUTES = [
|
|
151
|
-
{
|
|
152
|
-
id: 'maintenance',
|
|
153
|
-
providerPath: ['maintenance', 'verifyProvider'],
|
|
154
|
-
modelPath: ['maintenance', 'verifyModel'],
|
|
155
|
-
},
|
|
156
|
-
{
|
|
157
|
-
id: 'historian',
|
|
158
|
-
providerPath: ['historian', 'provider'],
|
|
159
|
-
modelPath: ['historian', 'model'],
|
|
160
|
-
},
|
|
161
|
-
]
|
|
162
|
-
|
|
163
|
-
/**
|
|
164
|
-
* The writes for one pin pair: both halves set → both written; either half
|
|
165
|
-
* empty → BOTH cleared ("Auto" = follow the session route). Pure, so the
|
|
166
|
-
* node-side spec can pin the pair semantics without React.
|
|
167
|
-
*/
|
|
168
|
-
function routePairOps(providerText, modelText, providerPath, modelPath) {
|
|
169
|
-
if (providerText === '' || modelText === '') {
|
|
170
|
-
return [
|
|
171
|
-
{ op: 'unset', path: providerPath.slice() },
|
|
172
|
-
{ op: 'unset', path: modelPath.slice() },
|
|
173
|
-
]
|
|
174
|
-
}
|
|
175
|
-
return [
|
|
176
|
-
{ op: 'set', path: providerPath.slice(), value: providerText },
|
|
177
|
-
{ op: 'set', path: modelPath.slice(), value: modelText },
|
|
178
|
-
]
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
/**
|
|
182
|
-
* One leaf the card edits.
|
|
183
|
-
*
|
|
184
|
-
* `path` is the path inside the settings namespace section (`policy` is a
|
|
185
|
-
* nested object in the Host `Config`), and the Host writes it as
|
|
186
|
-
* `{ op: 'set', path: ['policy', 'executeThreshold'], value }` — the shared
|
|
187
|
-
* `ConfigForm` accepts nested paths even though the convenience
|
|
188
|
-
* `SettingsFormModel` only models flat fields, which is why this card owns
|
|
189
|
-
* its own small staging model.
|
|
190
|
-
*/
|
|
191
|
-
const FIELDS = [
|
|
192
|
-
{
|
|
193
|
-
path: ['policy', 'executeThreshold'], kind: 'number', label: 'Execute threshold (%)',
|
|
194
|
-
hint: 'Adoption percentage at which the execute band opens (0-100).',
|
|
195
|
-
},
|
|
196
|
-
{
|
|
197
|
-
path: ['policy', 'forceThreshold'], kind: 'number', label: 'Force threshold (%)',
|
|
198
|
-
hint: 'Adoption percentage at which reclaim becomes unconditional (0-100).',
|
|
199
|
-
},
|
|
200
|
-
{
|
|
201
|
-
path: ['policy', 'assumedContextWindow'], kind: 'number', label: 'Assumed window (tokens)',
|
|
202
|
-
hint: 'Window used for scheduling when the routed model discloses none.',
|
|
203
|
-
},
|
|
204
|
-
{
|
|
205
|
-
path: ['policy', 'ttlMs'], kind: 'number', label: 'Reclaim TTL (ms)',
|
|
206
|
-
hint: 'How long a reclaimed compartment stays reversible.',
|
|
207
|
-
},
|
|
208
|
-
{
|
|
209
|
-
path: ['policy', 'protectedToolResults'], kind: 'number', label: 'Protected tool results',
|
|
210
|
-
hint: 'Newest tool results the policy never reclaims.',
|
|
211
|
-
},
|
|
212
|
-
{
|
|
213
|
-
path: ['policy', 'historianMinNodes'], kind: 'number', label: 'Historian minimum nodes',
|
|
214
|
-
hint: 'Assistant/tool nodes required before the historian runs.',
|
|
215
|
-
},
|
|
216
|
-
{
|
|
217
|
-
path: ['policy', 'historianMaxTokens'], kind: 'number', label: 'Historian max tokens',
|
|
218
|
-
hint: 'Generation cap for one historian call; anything below 8192 is raised to 8192.',
|
|
219
|
-
},
|
|
220
|
-
{
|
|
221
|
-
path: ['policy', 'historianMaxTokensCeiling'], kind: 'number', label: 'Historian retry ceiling',
|
|
222
|
-
hint: 'Ceiling the historian may escalate to after a token-cap truncation (doubles per attempt).',
|
|
223
|
-
},
|
|
224
|
-
{
|
|
225
|
-
path: ['policy', 'historianTimeoutMs'], kind: 'number', label: 'Historian timeout (ms)',
|
|
226
|
-
hint: 'Wall-clock budget for one historian run; a spent budget journals a failed run.',
|
|
227
|
-
},
|
|
228
|
-
{
|
|
229
|
-
path: ['policy', 'baselineTokens'], kind: 'number', label: 'Baseline tokens',
|
|
230
|
-
hint: 'Token budget reserved for the m[0]/m[1] baseline.',
|
|
231
|
-
},
|
|
232
|
-
{
|
|
233
|
-
path: ['policy', 'memoryBudgetTokens'], kind: 'number', label: 'Memory budget tokens',
|
|
234
|
-
hint: 'Token budget the recalled-memory block may spend.',
|
|
235
|
-
},
|
|
236
|
-
{
|
|
237
|
-
path: ['policy', 'memoryLimit'], kind: 'number', label: 'Memory row limit',
|
|
238
|
-
hint: 'Maximum shared memory rows recalled per pass.',
|
|
239
|
-
},
|
|
240
|
-
{
|
|
241
|
-
path: ['journal', 'retentionDays'], kind: 'number', label: 'Journal retention (days)',
|
|
242
|
-
hint: 'How long journal rows are kept. Re-prunes once the value changes (minimum 1 day).',
|
|
243
|
-
},
|
|
244
|
-
{ path: ['historianEnabled'], kind: 'boolean', label: 'Historian enabled', hint: 'Run the out-of-band historian.' },
|
|
245
|
-
{
|
|
246
|
-
path: ['registerCompactionEngine'], kind: 'boolean', label: 'Register compaction engine',
|
|
247
|
-
hint: 'Register the adapter as the compaction engine seam. Mounts or unmounts on the next agent step — disable the host compaction row first.',
|
|
248
|
-
},
|
|
249
|
-
{
|
|
250
|
-
path: ROUTES[0].providerPath, kind: 'route-provider', routeId: 'maintenance', label: 'Verify provider',
|
|
251
|
-
hint: 'Provider for the maintenance verify lane. “Auto” leaves the pin off and follows the session route.',
|
|
252
|
-
},
|
|
253
|
-
{
|
|
254
|
-
path: ROUTES[0].modelPath, kind: 'route-model', routeId: 'maintenance', label: 'Verify model',
|
|
255
|
-
hint: 'Model for the maintenance verify lane. Set both provider and model to pin; “Auto” follows the session route.',
|
|
256
|
-
},
|
|
257
|
-
{
|
|
258
|
-
path: ROUTES[1].providerPath, kind: 'route-provider', routeId: 'historian', label: 'Historian provider',
|
|
259
|
-
hint: 'Provider for the historian and /compact summaries. “Auto” follows the session route (recommended).',
|
|
260
|
-
},
|
|
261
|
-
{
|
|
262
|
-
path: ROUTES[1].modelPath, kind: 'route-model', routeId: 'historian', label: 'Historian model',
|
|
263
|
-
hint: 'Model for the historian and /compact summaries. Set both halves to pin; “Auto” follows the session route.',
|
|
264
|
-
},
|
|
265
|
-
{
|
|
266
|
-
path: ['historian', 'reasoningEffort'], kind: 'text', label: 'Historian reasoning effort',
|
|
267
|
-
hint: 'Optional effort id advertised by the routed model (for example low/medium/high). Blank uses the model default.',
|
|
268
|
-
},
|
|
269
|
-
{
|
|
270
|
-
path: ['journal', 'enabled'], kind: 'boolean', label: 'Journal enabled',
|
|
271
|
-
hint: 'Record adapter activity. Turning off applies immediately; turning on from an off start needs an app restart.',
|
|
272
|
-
},
|
|
273
|
-
{
|
|
274
|
-
path: ['journal', 'mirror'], kind: 'boolean', label: 'Journal text mirror',
|
|
275
|
-
hint: 'Also write the one-line activity mirror file. Applies to entries written after the change.',
|
|
276
|
-
},
|
|
277
|
-
]
|
|
278
|
-
|
|
279
|
-
/** Copy the shared settings form frame renders. */
|
|
280
|
-
const FORM_LABELS = {
|
|
281
|
-
unavailable: 'Magic Context settings are not served by this deployment.',
|
|
282
|
-
readOnly: 'This deployment stores settings read-only.',
|
|
283
|
-
saveFailed: 'The Host did not accept the save; the values below are unsaved.',
|
|
284
|
-
save: 'Save',
|
|
285
|
-
saving: 'Saving…',
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
/** Read a value along a path inside a nested plain object. */
|
|
289
|
-
function readPath(root, path) {
|
|
290
|
-
let current = root
|
|
291
|
-
for (let index = 0; index < path.length; index += 1) {
|
|
292
|
-
if (current === null || typeof current !== 'object') return undefined
|
|
293
|
-
current = current[path[index]]
|
|
294
|
-
}
|
|
295
|
-
return current
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
/** Whether the raw user layer carries an entry at this exact path. */
|
|
299
|
-
function hasPath(root, path) {
|
|
300
|
-
let current = root
|
|
301
|
-
for (let index = 0; index < path.length; index += 1) {
|
|
302
|
-
if (current === null || typeof current !== 'object') return false
|
|
303
|
-
if (index === path.length - 1) return Object.prototype.hasOwnProperty.call(current, path[index])
|
|
304
|
-
current = current[path[index]]
|
|
305
|
-
}
|
|
306
|
-
return false
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
/** Render a number field's stored value as draft text (empty when absent). */
|
|
310
|
-
function formatNumber(value) {
|
|
311
|
-
return typeof value === 'number' && Number.isFinite(value) ? String(value) : ''
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
/** Render a text field's stored value as draft text (empty when absent). */
|
|
315
|
-
function formatText(value) {
|
|
316
|
-
return typeof value === 'string' ? value : ''
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
/**
|
|
320
|
-
* Convert draft text into the write the field stages.
|
|
321
|
-
* @returns `{ kind: 'clear' }`, `{ kind: 'set', value }`, or `undefined` for
|
|
322
|
-
* text this field does not accept (which blocks the save).
|
|
323
|
-
*/
|
|
324
|
-
function parseNumberText(text) {
|
|
325
|
-
const trimmed = String(text).trim()
|
|
326
|
-
if (trimmed === '') return { kind: 'clear' }
|
|
327
|
-
const parsed = Number(trimmed)
|
|
328
|
-
return Number.isFinite(parsed) ? { kind: 'set', value: parsed } : undefined
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
/** Coerce a stored/undefined value to a checkbox state. */
|
|
332
|
-
function asBool(value) {
|
|
333
|
-
return value === true
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
/** The path key a draft is staged under. */
|
|
337
|
-
function fieldKey(path) {
|
|
338
|
-
return path.join('.')
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
/**
|
|
342
|
-
* Age past which opening the settings card revalidates the model directory.
|
|
343
|
-
* Long enough that reopening the card a few times costs nothing, short enough
|
|
344
|
-
* that a provider edit made while the card was closed is never compared
|
|
345
|
-
* against a directory from another day.
|
|
346
|
-
*/
|
|
347
|
-
const MODEL_CATALOG_MAX_AGE_MS = 60_000
|
|
348
|
-
|
|
349
|
-
/**
|
|
350
|
-
* One shared read of the host model catalog (`remote.session.modelCatalog()`) —
|
|
351
|
-
* the provider/model directory the composer's model picker renders from.
|
|
352
|
-
* The card reads it so the maintenance verify pin can be PICKED from the
|
|
353
|
-
* models the profile actually configured instead of typed from memory.
|
|
354
|
-
*
|
|
355
|
-
* Read-only and optional: a deployment whose client shell exposes no
|
|
356
|
-
* catalog (or no `remote.session` namespace) leaves the card on plain text
|
|
357
|
-
* inputs. Shape: `{ status, groups: [{ id, models: [{ id, name }] }], error }`.
|
|
358
|
-
*/
|
|
359
|
-
function createModelCatalog() {
|
|
360
|
-
const listeners = new Set()
|
|
361
|
-
let state = { status: 'idle', groups: [], error: null }
|
|
362
|
-
let remoteCtx
|
|
363
|
-
let inflight
|
|
364
|
-
/** When the current directory was last read successfully. */
|
|
365
|
-
let loadedAt = 0
|
|
366
|
-
|
|
367
|
-
const emit = () => {
|
|
368
|
-
for (const listener of listeners) {
|
|
369
|
-
try {
|
|
370
|
-
listener()
|
|
371
|
-
} catch (error) {
|
|
372
|
-
/* a subscriber must never break the card */
|
|
373
|
-
}
|
|
374
|
-
}
|
|
375
|
-
}
|
|
376
|
-
const set = (next) => {
|
|
377
|
-
state = next
|
|
378
|
-
emit()
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
const load = () => {
|
|
382
|
-
if (remoteCtx === undefined
|
|
383
|
-
|| remoteCtx.remote === undefined
|
|
384
|
-
|| remoteCtx.remote.session === undefined
|
|
385
|
-
|| typeof remoteCtx.remote.session.modelCatalog !== 'function') {
|
|
386
|
-
return Promise.resolve()
|
|
387
|
-
}
|
|
388
|
-
if (inflight !== undefined) return inflight
|
|
389
|
-
if (state.status === 'ready') return Promise.resolve()
|
|
390
|
-
set({ status: 'loading', groups: state.groups, error: null })
|
|
391
|
-
inflight = Promise.resolve(remoteCtx.remote.session.modelCatalog()).then(
|
|
392
|
-
(response) => {
|
|
393
|
-
if (response === null || typeof response !== 'object' || response.ok !== true) {
|
|
394
|
-
const detail = response !== null && typeof response === 'object' && response.error !== undefined
|
|
395
|
-
&& typeof response.error === 'object' && typeof response.error.message === 'string'
|
|
396
|
-
? response.error.message
|
|
397
|
-
: 'model catalog unavailable'
|
|
398
|
-
set({ status: 'error', groups: [], error: detail })
|
|
399
|
-
return
|
|
400
|
-
}
|
|
401
|
-
const groups = []
|
|
402
|
-
for (const group of response.value.groups ?? []) {
|
|
403
|
-
if (group === null || typeof group !== 'object') continue
|
|
404
|
-
if (typeof group.id !== 'string' || group.id === '') continue
|
|
405
|
-
const models = []
|
|
406
|
-
for (const model of group.models ?? []) {
|
|
407
|
-
if (model === null || typeof model !== 'object') continue
|
|
408
|
-
if (typeof model.id !== 'string' || model.id === '') continue
|
|
409
|
-
models.push({
|
|
410
|
-
id: model.id,
|
|
411
|
-
name: typeof model.name === 'string' && model.name !== '' ? model.name : model.id,
|
|
412
|
-
})
|
|
413
|
-
}
|
|
414
|
-
groups.push({ id: group.id, models })
|
|
415
|
-
}
|
|
416
|
-
loadedAt = Date.now()
|
|
417
|
-
set({ status: 'ready', groups, error: null })
|
|
418
|
-
},
|
|
419
|
-
(error) => {
|
|
420
|
-
set({ status: 'error', groups: [], error: error instanceof Error ? error.message : String(error) })
|
|
421
|
-
},
|
|
422
|
-
).finally(() => {
|
|
423
|
-
inflight = undefined
|
|
424
|
-
})
|
|
425
|
-
return inflight
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
const refresh = () => {
|
|
429
|
-
if (state.status === 'ready') set({ status: 'idle', groups: state.groups, error: null })
|
|
430
|
-
load()
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
/**
|
|
434
|
-
* Reload a directory older than `maxAgeMs` (and load one that was never
|
|
435
|
-
* read). The `llm/models-updated` event covers live provider edits, but a
|
|
436
|
-
* card that merely opens after an event it missed — or one whose event was
|
|
437
|
-
* raced by a remount — would otherwise keep comparing against a stale
|
|
438
|
-
* directory and surface a false "(not in catalog)" on a valid pin. One
|
|
439
|
-
* cheap RPC per card open is worth never showing that lie.
|
|
440
|
-
* @param maxAgeMs - age past which the cached directory is re-read.
|
|
441
|
-
* @returns completion of the read in flight or started here.
|
|
442
|
-
*/
|
|
443
|
-
const ensureFresh = (maxAgeMs = MODEL_CATALOG_MAX_AGE_MS) => {
|
|
444
|
-
if (state.status !== 'ready') return load()
|
|
445
|
-
if (Date.now() - loadedAt <= maxAgeMs) return Promise.resolve()
|
|
446
|
-
return refresh()
|
|
447
|
-
}
|
|
448
|
-
|
|
449
|
-
return {
|
|
450
|
-
/** Bind the client context that can reach `remote.session`. */
|
|
451
|
-
attach(context) {
|
|
452
|
-
remoteCtx = context
|
|
453
|
-
if (context.remote === undefined
|
|
454
|
-
|| typeof context.remote.$on !== 'function'
|
|
455
|
-
|| context.remote.session === undefined) {
|
|
456
|
-
return
|
|
457
|
-
}
|
|
458
|
-
context.effect(() => {
|
|
459
|
-
const disposers = [
|
|
460
|
-
context.remote.$on('llm/models-updated', () => refresh()),
|
|
461
|
-
context.on('connection/reset', () => refresh()),
|
|
462
|
-
]
|
|
463
|
-
return () => {
|
|
464
|
-
for (const dispose of disposers) dispose()
|
|
465
|
-
}
|
|
466
|
-
}, 'magic-context: model catalog')
|
|
467
|
-
load()
|
|
468
|
-
},
|
|
469
|
-
getSnapshot: () => state,
|
|
470
|
-
ensureFresh,
|
|
471
|
-
subscribe(listener) {
|
|
472
|
-
listeners.add(listener)
|
|
473
|
-
return () => {
|
|
474
|
-
listeners.delete(listener)
|
|
475
|
-
}
|
|
476
|
-
},
|
|
477
|
-
load,
|
|
478
|
-
}
|
|
479
|
-
}
|
|
480
|
-
|
|
481
|
-
/**
|
|
482
|
-
* Subscribe one card to the catalog; re-reads a stale directory when the
|
|
483
|
-
* card mounts so an edit made while it was closed cannot leave the route
|
|
484
|
-
* selects comparing against yesterday's providers.
|
|
485
|
-
*/
|
|
486
|
-
function useModelCatalog(catalog) {
|
|
487
|
-
const [state, setState] = useState(catalog.getSnapshot())
|
|
488
|
-
useEffect(() => {
|
|
489
|
-
setState(catalog.getSnapshot())
|
|
490
|
-
const off = catalog.subscribe(() => setState(catalog.getSnapshot()))
|
|
491
|
-
catalog.ensureFresh()
|
|
492
|
-
return off
|
|
493
|
-
}, [catalog])
|
|
494
|
-
return state
|
|
495
|
-
}
|
|
496
|
-
|
|
497
|
-
/**
|
|
498
|
-
* Build the Plugins page card bound to the shared configuration form of the
|
|
499
|
-
* `magic-context-dsh` namespace.
|
|
500
|
-
*
|
|
501
|
-
* Staging mirrors the official cards: nothing is written until Save, and a
|
|
502
|
-
* save posts every staged edit as one revision-fenced mutation. The
|
|
503
|
-
* `SettingsFormModel` is not reused because it addresses only flat section
|
|
504
|
-
* fields, while every interesting magic-context threshold lives under
|
|
505
|
-
* `policy`/`journal`.
|
|
506
|
-
*
|
|
507
|
-
* @param scope - the `ConfigForm` for the namespace (`ctx.configForms.get`).
|
|
508
|
-
* @param catalog - the shared model catalog (see {@link createModelCatalog});
|
|
509
|
-
* absent data falls back to plain text for the verify pin.
|
|
510
|
-
* @returns the React component the two configuration seats register.
|
|
511
|
-
*/
|
|
512
|
-
function makeCard(scope, catalog) {
|
|
513
|
-
return function MagicContextCard(props) {
|
|
514
|
-
const [, setRevision] = useState(0)
|
|
515
|
-
const drafts = useRef(new Map())
|
|
516
|
-
const [saving, setSaving] = useState(false)
|
|
517
|
-
const [failed, setFailed] = useState(false)
|
|
518
|
-
const catalogState = useModelCatalog(catalog)
|
|
519
|
-
|
|
520
|
-
useEffect(() => scope.subscribe(() => setRevision((value) => value + 1)), [scope])
|
|
521
|
-
|
|
522
|
-
const snapshot = scope.getSnapshot()
|
|
523
|
-
const staged = drafts.current
|
|
524
|
-
|
|
525
|
-
// The summary view is the one-liner the Plugins page shows in its list
|
|
526
|
-
// (and as a row's description fallback); it must render without the form.
|
|
527
|
-
if (props.view === 'summary') {
|
|
528
|
-
const execute = formatNumber(readPath(snapshot.value, ['policy', 'executeThreshold']))
|
|
529
|
-
const force = formatNumber(readPath(snapshot.value, ['policy', 'forceThreshold']))
|
|
530
|
-
const historian = asBool(readPath(snapshot.value, ['historianEnabled']))
|
|
531
|
-
if (snapshot.status !== 'ready') return 'Magic Context adapter settings'
|
|
532
|
-
const pinnedRoutes = ROUTES.flatMap((route) => {
|
|
533
|
-
const provider = formatText(readPath(snapshot.value, route.providerPath))
|
|
534
|
-
const model = formatText(readPath(snapshot.value, route.modelPath))
|
|
535
|
-
if (provider === '' || model === '') return []
|
|
536
|
-
return [' · ' + (route.id === 'maintenance' ? 'verify ' : 'gen ') + provider + '/' + model]
|
|
537
|
-
})
|
|
538
|
-
return 'execute ' + (execute || '—') + '% · force ' + (force || '—') + '% · historian '
|
|
539
|
-
+ (historian ? 'on' : 'off') + pinnedRoutes.join('')
|
|
540
|
-
}
|
|
541
|
-
|
|
542
|
-
/** Stage one field's draft and re-render. */
|
|
543
|
-
const stage = (key, draft) => {
|
|
544
|
-
staged.set(key, draft)
|
|
545
|
-
setFailed(false)
|
|
546
|
-
setRevision((value) => value + 1)
|
|
547
|
-
}
|
|
548
|
-
/** Drop every draft. */
|
|
549
|
-
const discard = () => {
|
|
550
|
-
if (staged.size === 0 && !failed) return
|
|
551
|
-
staged.clear()
|
|
552
|
-
setFailed(false)
|
|
553
|
-
setRevision((value) => value + 1)
|
|
554
|
-
}
|
|
555
|
-
|
|
556
|
-
/** The current view of one field: draft, effective value, override mark. */
|
|
557
|
-
const view = (spec) => {
|
|
558
|
-
const key = fieldKey(spec.path)
|
|
559
|
-
const draft = staged.get(key)
|
|
560
|
-
const sectionValue = readPath(snapshot.value, spec.path)
|
|
561
|
-
const baseValue = readPath(snapshot.base, spec.path)
|
|
562
|
-
const stored = hasPath(snapshot.user, spec.path)
|
|
563
|
-
if (spec.kind === 'boolean') {
|
|
564
|
-
const value = draft === undefined
|
|
565
|
-
? asBool(sectionValue)
|
|
566
|
-
: (draft.clear ? asBool(baseValue) : draft.bool === true)
|
|
567
|
-
return {
|
|
568
|
-
value,
|
|
569
|
-
overridden: draft === undefined ? stored : (draft.clear ? false : value !== asBool(baseValue)),
|
|
570
|
-
}
|
|
571
|
-
}
|
|
572
|
-
if (spec.kind === 'number') {
|
|
573
|
-
const text = draft === undefined
|
|
574
|
-
? formatNumber(sectionValue)
|
|
575
|
-
: (draft.clear ? formatNumber(baseValue) : draft.text)
|
|
576
|
-
const write = draft === undefined || draft.clear ? undefined : parseNumberText(draft.text)
|
|
577
|
-
return {
|
|
578
|
-
text,
|
|
579
|
-
invalid: draft !== undefined && !draft.clear && write === undefined,
|
|
580
|
-
overridden: draft === undefined ? stored : (draft.clear ? false : write !== undefined),
|
|
581
|
-
}
|
|
582
|
-
}
|
|
583
|
-
// Text leaves (the verify pin): a non-number value is never invalid.
|
|
584
|
-
const text = draft === undefined
|
|
585
|
-
? formatText(sectionValue)
|
|
586
|
-
: (draft.clear ? formatText(baseValue) : draft.text)
|
|
587
|
-
return {
|
|
588
|
-
text,
|
|
589
|
-
invalid: false,
|
|
590
|
-
overridden: draft === undefined ? stored : (draft.clear ? false : text !== formatText(baseValue)),
|
|
591
|
-
}
|
|
592
|
-
}
|
|
593
|
-
|
|
594
|
-
/**
|
|
595
|
-
* The route pins, resolved once per render. Each pin is a PAIR (both
|
|
596
|
-
* halves non-empty — the host rule), so the card treats it as one unit:
|
|
597
|
-
* one save writes or clears the pair, and a provider switch drops a
|
|
598
|
-
* model that does not belong to the new provider.
|
|
599
|
-
*/
|
|
600
|
-
const routes = ROUTES.map((route) => {
|
|
601
|
-
const providerSpec = FIELDS.find((spec) => spec.routeId === route.id && spec.kind === 'route-provider')
|
|
602
|
-
const modelSpec = FIELDS.find((spec) => spec.routeId === route.id && spec.kind === 'route-model')
|
|
603
|
-
return {
|
|
604
|
-
...route,
|
|
605
|
-
providerSpec,
|
|
606
|
-
modelSpec,
|
|
607
|
-
providerText: view(providerSpec).text,
|
|
608
|
-
modelText: view(modelSpec).text,
|
|
609
|
-
}
|
|
610
|
-
})
|
|
611
|
-
const routeOf = (spec) => routes.find((route) => route.id === spec.routeId)
|
|
612
|
-
const catalogReady = catalogState.status === 'ready' && catalogState.groups.length > 0
|
|
613
|
-
const catalogGroups = catalogReady ? catalogState.groups : []
|
|
614
|
-
/** Models the catalog advertises for one provider. */
|
|
615
|
-
const modelsOf = (providerId) => {
|
|
616
|
-
if (providerId === '') return []
|
|
617
|
-
const group = catalogGroups.find((candidate) => candidate.id === providerId)
|
|
618
|
-
return group === undefined ? [] : group.models
|
|
619
|
-
}
|
|
620
|
-
|
|
621
|
-
/** Stage one side of a pin; a provider switch drops a stale model. */
|
|
622
|
-
const stageRoute = (spec, value) => {
|
|
623
|
-
const route = routeOf(spec)
|
|
624
|
-
const textValue = value === undefined || value === null ? '' : String(value)
|
|
625
|
-
stage(fieldKey(spec.path), textValue === '' ? { clear: true } : { clear: false, text: textValue })
|
|
626
|
-
if (spec.kind !== 'route-provider') return
|
|
627
|
-
// Only a catalog-backed switch knows the new provider's models; the
|
|
628
|
-
// text fallback (no catalog) must never wipe a hand-typed model.
|
|
629
|
-
if (!catalogReady) return
|
|
630
|
-
const currentModel = view(route.modelSpec).text
|
|
631
|
-
if (currentModel !== '' && !modelsOf(textValue).some((model) => model.id === currentModel)) {
|
|
632
|
-
stage(fieldKey(route.modelSpec.path), { clear: true })
|
|
633
|
-
}
|
|
634
|
-
}
|
|
635
|
-
|
|
636
|
-
/**
|
|
637
|
-
* One route field: a catalog-backed select when the host serves the
|
|
638
|
-
* model directory, plain text otherwise (the manual fallback).
|
|
639
|
-
*/
|
|
640
|
-
const renderRouteField = (spec, state, usePrimitives) => {
|
|
641
|
-
const route = routeOf(spec)
|
|
642
|
-
const id = 'mc-field-' + fieldKey(spec.path)
|
|
643
|
-
const isProvider = spec.kind === 'route-provider'
|
|
644
|
-
const providerText = route.providerText
|
|
645
|
-
const currentText = isProvider ? providerText : route.modelText
|
|
646
|
-
const disabled = !shell.writable || (!isProvider && catalogReady && providerText === '')
|
|
647
|
-
if (catalogReady) {
|
|
648
|
-
const options = []
|
|
649
|
-
if (isProvider) {
|
|
650
|
-
options.push({ value: '', label: 'Auto — follow the session route' })
|
|
651
|
-
for (const group of catalogGroups) options.push({ value: group.id, label: group.id })
|
|
652
|
-
} else {
|
|
653
|
-
options.push({
|
|
654
|
-
value: '',
|
|
655
|
-
label: providerText === ''
|
|
656
|
-
? 'Auto — pick a provider first'
|
|
657
|
-
: 'Auto — follow the session route',
|
|
658
|
-
})
|
|
659
|
-
for (const model of modelsOf(providerText)) {
|
|
660
|
-
options.push({
|
|
661
|
-
value: model.id,
|
|
662
|
-
label: model.name === model.id ? model.id : model.name + ' (' + model.id + ')',
|
|
663
|
-
})
|
|
664
|
-
}
|
|
665
|
-
}
|
|
666
|
-
if (currentText !== '' && !options.some((option) => option.value === currentText)) {
|
|
667
|
-
options.push({ value: currentText, label: currentText + ' (not in catalog)' })
|
|
668
|
-
}
|
|
669
|
-
return el('div', { key: id, style: { display: 'grid', gap: 4, margin: '8px 0' } }, [
|
|
670
|
-
el('span', { key: 'label', style: { fontSize: 12 } }, spec.label),
|
|
671
|
-
el('select', {
|
|
672
|
-
key: 'select',
|
|
673
|
-
id,
|
|
674
|
-
value: currentText,
|
|
675
|
-
disabled,
|
|
676
|
-
onChange: (event) => stageRoute(spec, event.target.value),
|
|
677
|
-
style: {
|
|
678
|
-
width: '100%',
|
|
679
|
-
maxWidth: 520,
|
|
680
|
-
boxSizing: 'border-box',
|
|
681
|
-
height: 32,
|
|
682
|
-
padding: '0 8px',
|
|
683
|
-
borderRadius: 'var(--dsw-radius-sm, 6px)',
|
|
684
|
-
border: '1px solid var(--dsw-alias-border-secondary, rgba(127,127,127,.35))',
|
|
685
|
-
background: 'var(--dsw-alias-bg-layer-3, transparent)',
|
|
686
|
-
color: 'var(--dsw-alias-label-primary, inherit)',
|
|
687
|
-
font: 'inherit',
|
|
688
|
-
fontSize: 12.5,
|
|
689
|
-
},
|
|
690
|
-
}, options.map((option) => el('option', { key: option.value, value: option.value }, option.label))),
|
|
691
|
-
el('span', { key: 'hint', style: { fontSize: 11, opacity: 0.6 } }, spec.hint),
|
|
692
|
-
])
|
|
693
|
-
}
|
|
694
|
-
if (usePrimitives && SettingsValueField !== undefined) {
|
|
695
|
-
return el(SettingsValueField, {
|
|
696
|
-
key: id,
|
|
697
|
-
id,
|
|
698
|
-
label: spec.label,
|
|
699
|
-
hint: spec.hint,
|
|
700
|
-
numeric: false,
|
|
701
|
-
text: state.text,
|
|
702
|
-
overridden: state.overridden,
|
|
703
|
-
invalid: false,
|
|
704
|
-
overriddenLabel: 'Modified',
|
|
705
|
-
resetLabel: 'Reset',
|
|
706
|
-
invalidLabel: 'Invalid',
|
|
707
|
-
disabled: !shell.writable,
|
|
708
|
-
onEdit: (text) => stageRoute(spec, text),
|
|
709
|
-
onReset: () => stage(fieldKey(spec.path), { clear: true }),
|
|
710
|
-
})
|
|
711
|
-
}
|
|
712
|
-
return el('div', { key: id, style: { margin: '8px 0' } }, [
|
|
713
|
-
el('span', { key: 'label', style: { display: 'block', fontSize: 12 } }, spec.label),
|
|
714
|
-
el('input', {
|
|
715
|
-
key: 'input',
|
|
716
|
-
id,
|
|
717
|
-
type: 'text',
|
|
718
|
-
value: state.text,
|
|
719
|
-
placeholder: isProvider ? 'provider id' : 'model id',
|
|
720
|
-
disabled: !shell.writable,
|
|
721
|
-
onChange: (event) => stageRoute(spec, event.target.value),
|
|
722
|
-
}),
|
|
723
|
-
el('span', { key: 'hint', style: { display: 'block', fontSize: 11, opacity: 0.6 } }, spec.hint),
|
|
724
|
-
])
|
|
725
|
-
}
|
|
726
|
-
|
|
727
|
-
/**
|
|
728
|
-
* The mutations a save would post, in field order. `blocked` is true when
|
|
729
|
-
* a staged draft is not a value its field accepts (save refuses rather
|
|
730
|
-
* than dropping the edit).
|
|
731
|
-
*/
|
|
732
|
-
const plan = () => {
|
|
733
|
-
const ops = []
|
|
734
|
-
let blocked = false
|
|
735
|
-
for (const spec of FIELDS) {
|
|
736
|
-
// The route pairs are coupled and planned below.
|
|
737
|
-
if (spec.kind === 'route-provider' || spec.kind === 'route-model') continue
|
|
738
|
-
const draft = staged.get(fieldKey(spec.path))
|
|
739
|
-
if (draft === undefined) continue
|
|
740
|
-
if (draft.clear) {
|
|
741
|
-
ops.push({ op: 'unset', path: spec.path.slice() })
|
|
742
|
-
continue
|
|
743
|
-
}
|
|
744
|
-
if (spec.kind === 'boolean') {
|
|
745
|
-
ops.push({ op: 'set', path: spec.path.slice(), value: draft.bool === true })
|
|
746
|
-
continue
|
|
747
|
-
}
|
|
748
|
-
if (spec.kind === 'text') {
|
|
749
|
-
const text = String(draft.text ?? '').trim()
|
|
750
|
-
if (text === '') ops.push({ op: 'unset', path: spec.path.slice() })
|
|
751
|
-
else ops.push({ op: 'set', path: spec.path.slice(), value: text })
|
|
752
|
-
continue
|
|
753
|
-
}
|
|
754
|
-
const write = parseNumberText(draft.text)
|
|
755
|
-
if (write === undefined) {
|
|
756
|
-
blocked = true
|
|
757
|
-
continue
|
|
758
|
-
}
|
|
759
|
-
if (write.kind === 'clear') ops.push({ op: 'unset', path: spec.path.slice() })
|
|
760
|
-
else ops.push({ op: 'set', path: spec.path.slice(), value: write.value })
|
|
761
|
-
}
|
|
762
|
-
for (const route of routes) {
|
|
763
|
-
const providerStaged = staged.get(fieldKey(route.providerPath))
|
|
764
|
-
const modelStaged = staged.get(fieldKey(route.modelPath))
|
|
765
|
-
if (providerStaged === undefined && modelStaged === undefined) continue
|
|
766
|
-
// The final text of one side: draft → base → empty ("Auto").
|
|
767
|
-
const finalText = (path) => {
|
|
768
|
-
const draft = staged.get(fieldKey(path))
|
|
769
|
-
if (draft === undefined) return formatText(readPath(snapshot.value, path)).trim()
|
|
770
|
-
if (draft.clear) return ''
|
|
771
|
-
return String(draft.text ?? '').trim()
|
|
772
|
-
}
|
|
773
|
-
// "Auto" (or a half-filled pin, which the host cannot activate) is
|
|
774
|
-
// written as BOTH paths unset, so the profile stays unambiguous.
|
|
775
|
-
ops.push(...routePairOps(
|
|
776
|
-
finalText(route.providerPath),
|
|
777
|
-
finalText(route.modelPath),
|
|
778
|
-
route.providerPath,
|
|
779
|
-
route.modelPath,
|
|
780
|
-
))
|
|
781
|
-
}
|
|
782
|
-
return { ops, blocked }
|
|
783
|
-
}
|
|
784
|
-
|
|
785
|
-
const save = () => {
|
|
786
|
-
if (saving || !snapshot.writable || staged.size === 0) return
|
|
787
|
-
const { ops, blocked } = plan()
|
|
788
|
-
if (blocked || ops.length === 0) return
|
|
789
|
-
setSaving(true)
|
|
790
|
-
setFailed(false)
|
|
791
|
-
Promise.resolve(scope.mutate(ops, snapshot.revision)).then((accepted) => {
|
|
792
|
-
if (accepted) staged.clear()
|
|
793
|
-
setSaving(false)
|
|
794
|
-
setFailed(!accepted)
|
|
795
|
-
setRevision((value) => value + 1)
|
|
796
|
-
}, () => {
|
|
797
|
-
setSaving(false)
|
|
798
|
-
setFailed(true)
|
|
799
|
-
setRevision((value) => value + 1)
|
|
800
|
-
})
|
|
801
|
-
}
|
|
802
|
-
|
|
803
|
-
const shell = {
|
|
804
|
-
available: snapshot.status === 'ready',
|
|
805
|
-
writable: snapshot.writable === true,
|
|
806
|
-
dirty: staged.size > 0,
|
|
807
|
-
invalid: plan().blocked,
|
|
808
|
-
saving,
|
|
809
|
-
failed,
|
|
810
|
-
}
|
|
811
|
-
|
|
812
|
-
// Plain-control fallback for a module table without ui-primitives: the
|
|
813
|
-
// same staging and the same writes, only the chrome differs.
|
|
814
|
-
if (SettingsForm === undefined || SettingsValueField === undefined) {
|
|
815
|
-
const rows = FIELDS.map((spec) => {
|
|
816
|
-
const state = view(spec)
|
|
817
|
-
const id = 'mc-field-' + fieldKey(spec.path)
|
|
818
|
-
if (spec.kind === 'boolean') {
|
|
819
|
-
return el('div', { key: id, style: { margin: '6px 0' } }, [
|
|
820
|
-
el('input', {
|
|
821
|
-
key: 'input',
|
|
822
|
-
id,
|
|
823
|
-
type: 'checkbox',
|
|
824
|
-
checked: state.value,
|
|
825
|
-
disabled: !shell.writable,
|
|
826
|
-
onChange: (event) => stage(fieldKey(spec.path), { clear: false, bool: event.target.checked }),
|
|
827
|
-
}),
|
|
828
|
-
el('label', { key: 'label', htmlFor: id, style: { marginLeft: 6, fontSize: 12 } }, spec.label),
|
|
829
|
-
])
|
|
830
|
-
}
|
|
831
|
-
if (spec.kind === 'route-provider' || spec.kind === 'route-model') {
|
|
832
|
-
return renderRouteField(spec, state, false)
|
|
833
|
-
}
|
|
834
|
-
return el('div', { key: id, style: { margin: '6px 0' } }, [
|
|
835
|
-
el('label', { key: 'label', htmlFor: id, style: { display: 'block', fontSize: 12 } }, spec.label),
|
|
836
|
-
el('input', {
|
|
837
|
-
key: 'input',
|
|
838
|
-
id,
|
|
839
|
-
type: 'text',
|
|
840
|
-
value: state.text,
|
|
841
|
-
disabled: !shell.writable,
|
|
842
|
-
onChange: (event) => stage(fieldKey(spec.path), { clear: false, text: event.target.value }),
|
|
843
|
-
}),
|
|
844
|
-
])
|
|
845
|
-
})
|
|
846
|
-
return el('div', { style: { display: 'grid', gap: 4, maxWidth: 520 } }, [
|
|
847
|
-
el('p', { key: 'unavailable', style: { margin: 0, fontSize: 12 } },
|
|
848
|
-
shell.available ? '' : FORM_LABELS.unavailable),
|
|
849
|
-
...rows,
|
|
850
|
-
el('button', {
|
|
851
|
-
key: 'save', type: 'button', disabled: !shell.dirty || shell.invalid || shell.saving || !shell.writable,
|
|
852
|
-
onClick: save,
|
|
853
|
-
}, shell.saving ? FORM_LABELS.saving : FORM_LABELS.save),
|
|
854
|
-
failed ? el('span', { key: 'failed', style: { color: 'var(--dsw-alias-label-error, #c33)', fontSize: 12 } },
|
|
855
|
-
FORM_LABELS.saveFailed) : null,
|
|
856
|
-
])
|
|
857
|
-
}
|
|
858
|
-
|
|
859
|
-
const controls = FIELDS.map((spec) => {
|
|
860
|
-
const state = view(spec)
|
|
861
|
-
const id = 'mc-field-' + fieldKey(spec.path)
|
|
862
|
-
if (spec.kind === 'boolean') {
|
|
863
|
-
return el('div', { key: id, style: { display: 'flex', alignItems: 'center', gap: 8, margin: '8px 0' } }, [
|
|
864
|
-
el('span', { key: 'label', style: { fontSize: 12 } }, spec.label),
|
|
865
|
-
Switch !== undefined
|
|
866
|
-
? el(Switch, {
|
|
867
|
-
key: 'switch',
|
|
868
|
-
checked: state.value,
|
|
869
|
-
disabled: !shell.writable,
|
|
870
|
-
label: spec.label,
|
|
871
|
-
onChange: (next) => stage(fieldKey(spec.path), { clear: false, bool: next }),
|
|
872
|
-
})
|
|
873
|
-
: el('input', {
|
|
874
|
-
key: 'switch',
|
|
875
|
-
type: 'checkbox',
|
|
876
|
-
checked: state.value,
|
|
877
|
-
disabled: !shell.writable,
|
|
878
|
-
'aria-label': spec.label,
|
|
879
|
-
onChange: (event) => stage(fieldKey(spec.path), { clear: false, bool: event.target.checked }),
|
|
880
|
-
}),
|
|
881
|
-
state.overridden
|
|
882
|
-
? el('button', {
|
|
883
|
-
key: 'reset',
|
|
884
|
-
type: 'button',
|
|
885
|
-
disabled: !shell.writable,
|
|
886
|
-
onClick: () => stage(fieldKey(spec.path), { clear: true }),
|
|
887
|
-
}, 'Reset')
|
|
888
|
-
: null,
|
|
889
|
-
])
|
|
890
|
-
}
|
|
891
|
-
if (spec.kind === 'route-provider' || spec.kind === 'route-model') {
|
|
892
|
-
return renderRouteField(spec, state, true)
|
|
893
|
-
}
|
|
894
|
-
return el(SettingsValueField, {
|
|
895
|
-
key: id,
|
|
896
|
-
id,
|
|
897
|
-
label: spec.label,
|
|
898
|
-
hint: spec.hint,
|
|
899
|
-
numeric: spec.kind === 'number',
|
|
900
|
-
text: state.text,
|
|
901
|
-
overridden: state.overridden,
|
|
902
|
-
invalid: state.invalid,
|
|
903
|
-
overriddenLabel: 'Modified',
|
|
904
|
-
resetLabel: 'Reset',
|
|
905
|
-
invalidLabel: 'Not a number',
|
|
906
|
-
disabled: !shell.writable,
|
|
907
|
-
onEdit: (text) => stage(fieldKey(spec.path), { clear: false, text }),
|
|
908
|
-
onReset: () => stage(fieldKey(spec.path), { clear: true }),
|
|
909
|
-
})
|
|
910
|
-
})
|
|
911
|
-
|
|
912
|
-
return el(SettingsForm, {
|
|
913
|
-
labels: FORM_LABELS,
|
|
914
|
-
state: shell,
|
|
915
|
-
onSave: save,
|
|
916
|
-
onDiscard: discard,
|
|
917
|
-
}, controls)
|
|
918
|
-
}
|
|
919
|
-
}
|
|
920
|
-
|
|
921
|
-
/* ------------------------------------------------------------------ *
|
|
922
|
-
* The telemetry badge
|
|
923
|
-
* ------------------------------------------------------------------ */
|
|
924
|
-
|
|
925
|
-
/**
|
|
926
|
-
* The errors the badge counts as "now": `level === 'error'` journal rows inside
|
|
927
|
-
* `ERROR_RECENT_MS`. Read off `recent` (newest-first, owned by the live frames)
|
|
928
|
-
* rather than the summary, so the window closes on its own as frames arrive —
|
|
929
|
-
* the timer in the component only covers a session that has gone quiet.
|
|
930
|
-
*/
|
|
931
|
-
function countRecentErrors(recent, now) {
|
|
932
|
-
if (!Array.isArray(recent)) return 0
|
|
933
|
-
const cutoff = now - ERROR_RECENT_MS
|
|
934
|
-
let count = 0
|
|
935
|
-
for (const entry of recent) {
|
|
936
|
-
if (entry !== null && typeof entry === 'object' && entry.level === 'error'
|
|
937
|
-
&& typeof entry.at === 'number' && entry.at >= cutoff) count += 1
|
|
938
|
-
}
|
|
939
|
-
return count
|
|
940
|
-
}
|
|
941
|
-
|
|
942
|
-
/** Trim the Host summary into what the badge renders. */
|
|
943
|
-
function trimSummary(summary) {
|
|
944
|
-
const adapter = summary && typeof summary.adapter === 'object' && summary.adapter !== null ? summary.adapter : {}
|
|
945
|
-
const health = summary && typeof summary.health === 'object' && summary.health !== null ? summary.health : {}
|
|
946
|
-
const project = summary && typeof summary.project === 'object' && summary.project !== null ? summary.project : {}
|
|
947
|
-
const num = (value) => (typeof value === 'number' ? value : undefined)
|
|
948
|
-
const obj = (value) => (value !== null && typeof value === 'object' ? value : undefined)
|
|
949
|
-
return {
|
|
950
|
-
scope: summary && summary.scope === 'session' ? 'session' : 'global',
|
|
951
|
-
schemaVersion: adapter.schemaVersion,
|
|
952
|
-
engine: typeof adapter.engine === 'string' ? adapter.engine : 'unknown',
|
|
953
|
-
uptimeMs: adapter.uptimeMs,
|
|
954
|
-
memory: adapter.memory,
|
|
955
|
-
memoryWrite: adapter.memoryWrite,
|
|
956
|
-
// The shipped Host half names this `toolsMode`; the contract sketch said
|
|
957
|
-
// `tools`. Read both so neither revision renders an empty row.
|
|
958
|
-
tools: adapter.tools === undefined ? adapter.toolsMode : adapter.tools,
|
|
959
|
-
projectIdentity: shortIdentity(project.identity),
|
|
960
|
-
projectMemories: num(project.memories),
|
|
961
|
-
journalRows: health.journalRows,
|
|
962
|
-
journalRowsAll: health.journalRowsAll,
|
|
963
|
-
lastEventAt: health.lastEventAt,
|
|
964
|
-
passes24h: health.passes24h,
|
|
965
|
-
errorsSinceBoot: health.errorsSinceBoot,
|
|
966
|
-
drops24h: health.drops24h,
|
|
967
|
-
compartments24h: health.compartments24h,
|
|
968
|
-
// The density pass: the numbers a user actually acts on.
|
|
969
|
-
lastPressure: num(health.lastPressure),
|
|
970
|
-
emergency24h: num(health.emergency24h),
|
|
971
|
-
promote24h: num(health.promote24h),
|
|
972
|
-
reclaimed24h: num(health.reclaimed24h),
|
|
973
|
-
// Panel v3: absolute context size + the dreamer-slice KPIs.
|
|
974
|
-
lastTokens: num(health.lastTokens),
|
|
975
|
-
lastContextWindow: num(health.lastContextWindow),
|
|
976
|
-
memoryStats: obj(health.memoryStats) === undefined ? undefined : {
|
|
977
|
-
verified: num(health.memoryStats.verified),
|
|
978
|
-
unverified: num(health.memoryStats.unverified),
|
|
979
|
-
},
|
|
980
|
-
maintenanceLast: obj(health.maintenanceLast) === undefined ? undefined : {
|
|
981
|
-
at: num(health.maintenanceLast.at),
|
|
982
|
-
expired: num(health.maintenanceLast.expired),
|
|
983
|
-
verified: num(health.maintenanceLast.verified),
|
|
984
|
-
updated: num(health.maintenanceLast.updated),
|
|
985
|
-
archived: num(health.maintenanceLast.archived),
|
|
986
|
-
},
|
|
987
|
-
}
|
|
988
|
-
}
|
|
989
|
-
|
|
990
|
-
/** `HH:MM` for an epoch-millisecond stamp — the record row's stamp. */
|
|
991
|
-
function timeShort(at) {
|
|
992
|
-
const full = timeOf(at)
|
|
993
|
-
return full === '' ? '' : full.slice(0, 5)
|
|
994
|
-
}
|
|
995
|
-
|
|
996
|
-
/** `125k` / `1.2M` / `912` — the ONE token-scale formatter: no bare
|
|
997
|
-
* five-digit token numbers anywhere in the panel. */
|
|
998
|
-
function compactTok(value) {
|
|
999
|
-
if (typeof value !== 'number' || !Number.isFinite(value)) return ''
|
|
1000
|
-
if (value >= 1_000_000) return (value / 1_000_000).toFixed(value >= 10_000_000 ? 0 : 1).replace(/\.0$/, '') + 'M'
|
|
1001
|
-
if (value >= 1_000) return (value / 1_000).toFixed(value >= 100_000 ? 0 : 1).replace(/\.0$/, '') + 'k'
|
|
1002
|
-
return String(Math.round(value))
|
|
1003
|
-
}
|
|
1004
|
-
|
|
1005
|
-
/** `1,234` — the ONE counting formatter: every 条/个/节点/次 figure gets
|
|
1006
|
-
* a thousands separator, never a bare long number. */
|
|
1007
|
-
function fmtInt(value) {
|
|
1008
|
-
if (typeof value !== 'number' || !Number.isFinite(value)) return ''
|
|
1009
|
-
return Math.round(value).toLocaleString('en-US')
|
|
1010
|
-
}
|
|
1011
|
-
|
|
1012
|
-
/** `HH:MM:SS` for an epoch-millisecond stamp, when it is one. */
|
|
1013
|
-
function timeOf(at) {
|
|
1014
|
-
if (typeof at !== 'number' || !Number.isFinite(at)) return ''
|
|
1015
|
-
try {
|
|
1016
|
-
return new Date(at).toLocaleTimeString()
|
|
1017
|
-
} catch (error) {
|
|
1018
|
-
return ''
|
|
1019
|
-
}
|
|
1020
|
-
}
|
|
1021
|
-
|
|
1022
|
-
/* ------------------------------------------------------------------ *
|
|
1023
|
-
* Localization
|
|
1024
|
-
* ------------------------------------------------------------------ */
|
|
1025
|
-
|
|
1026
|
-
/**
|
|
1027
|
-
* The locale namespace this half registers its dictionaries under.
|
|
1028
|
-
*
|
|
1029
|
-
* The shell's locale service (`@deepseek-ai/dsh-client-locale`) merges the
|
|
1030
|
-
* namespace into whatever language is active; a slot entry that declares
|
|
1031
|
-
* `locale: LOCALE_NS` receives the matching `t` seat as a prop, and every
|
|
1032
|
-
* outlet re-renders on a switch — the panel and the pill follow General →
|
|
1033
|
-
* Language with no reload. `ctx.locale.bind` returns a STABLE function that
|
|
1034
|
-
* resolves the active language on every call, so a `t` captured once in
|
|
1035
|
-
* `apply` (the slot label thunks) stays correct across switches.
|
|
1036
|
-
*/
|
|
1037
|
-
const LOCALE_NS = 'magic-context'
|
|
1038
|
-
|
|
1039
|
-
/**
|
|
1040
|
-
* The shipped dictionaries. Both locales must carry the same key set:
|
|
1041
|
-
* registration is runtime-only (this half is hand-written JS, so no
|
|
1042
|
-
* compiler checks the union) and a missing key renders as the key itself.
|
|
1043
|
-
* `__diagnostics.dictionaryKeys` exists so the key parity can be asserted
|
|
1044
|
-
* outside a browser.
|
|
1045
|
-
*/
|
|
1046
|
-
const DICTIONARIES = {
|
|
1047
|
-
zh: {
|
|
1048
|
-
// Operation categories (the chips / row labels).
|
|
1049
|
-
'category.write': '写入记忆',
|
|
1050
|
-
'category.read': '加载记忆',
|
|
1051
|
-
'category.reclaim': '压缩上下文',
|
|
1052
|
-
'category.drop': '丢弃上下文',
|
|
1053
|
-
'category.maintain': '维护',
|
|
1054
|
-
'category.archive': '历史归档',
|
|
1055
|
-
'category.error': '失败',
|
|
1056
|
-
// Per-turn beats folded into one line.
|
|
1057
|
-
'noise.memoryRecall': '加载记忆',
|
|
1058
|
-
'noise.baselineRefresh': '基线刷新',
|
|
1059
|
-
// Record titles.
|
|
1060
|
-
'record.reclaimPass': '执行回收',
|
|
1061
|
-
'record.promotion': '记忆提升',
|
|
1062
|
-
'record.memoryWritten': '写入记忆',
|
|
1063
|
-
'record.memorySearch': '检索记忆',
|
|
1064
|
-
'record.compacted': '压缩上下文',
|
|
1065
|
-
'record.baselineHard': '基线重建 (HARD)',
|
|
1066
|
-
'record.emergencyDrop': '丢弃上下文',
|
|
1067
|
-
'record.wireRepaired': '线材修复',
|
|
1068
|
-
'record.dropLanded': '丢弃落地',
|
|
1069
|
-
'record.reduceQueued': '回收入队',
|
|
1070
|
-
'record.expandRestored': '恢复原文',
|
|
1071
|
-
'record.expandRange': '读取上下文',
|
|
1072
|
-
'record.remountRecovery': '重挂载恢复',
|
|
1073
|
-
'record.historianFailed': '历史归档失败',
|
|
1074
|
-
'record.archived': '归档已存',
|
|
1075
|
-
'record.maintenance': '维护巡检',
|
|
1076
|
-
'record.failed': '执行失败',
|
|
1077
|
-
// Record details ({n} figures).
|
|
1078
|
-
'detail.added': '新增 {n}',
|
|
1079
|
-
'detail.staged': '入队 {n}',
|
|
1080
|
-
'detail.seenAgain': '去重命中 {n}',
|
|
1081
|
-
'detail.skipped': '跳过 {n}',
|
|
1082
|
-
'detail.verb.write': '写入',
|
|
1083
|
-
'detail.verb.update': '更新',
|
|
1084
|
-
'detail.verb.archive': '归档',
|
|
1085
|
-
'detail.verb.merge': '合并',
|
|
1086
|
-
'detail.verb.list': '检索',
|
|
1087
|
-
'detail.verb.get': '读取',
|
|
1088
|
-
'detail.verb.search': '检索',
|
|
1089
|
-
'detail.queued': '入队 {n}',
|
|
1090
|
-
'detail.refused': '被拒 {n}',
|
|
1091
|
-
'detail.restored': '恢复 {n}',
|
|
1092
|
-
'detail.rangeEvents': '{n} 个事件',
|
|
1093
|
-
'detail.rangeSkipped': '跳过 {n}',
|
|
1094
|
-
'detail.hits': '命中 {n}',
|
|
1095
|
-
'detail.rejected': '被拒',
|
|
1096
|
-
'detail.replacedNodes': '替换 {n} 节点',
|
|
1097
|
-
'detail.nodes': '{n} 节点',
|
|
1098
|
-
'detail.reclaimed': '回收 {n} tok',
|
|
1099
|
-
'detail.droppedToolOutputs': '丢弃 {n} 条工具输出',
|
|
1100
|
-
'detail.wireSwept': '清除 {n} 条失配结果',
|
|
1101
|
-
'detail.toolOutputs': '{n} 条工具输出',
|
|
1102
|
-
'detail.backfilled': '补记中断的历史归档',
|
|
1103
|
-
'detail.ttlExpired': 'TTL 归档 {n}',
|
|
1104
|
-
'detail.verified': '验证 {n}',
|
|
1105
|
-
'detail.revised': '修订 {n}',
|
|
1106
|
-
'detail.nothingToDo': '无需处理',
|
|
1107
|
-
'detail.facts': 'facts {n}',
|
|
1108
|
-
// Panel status + pill tooltip.
|
|
1109
|
-
'status.ok': '正常',
|
|
1110
|
-
'status.connecting': '连接中',
|
|
1111
|
-
'status.unavailable': '不可用',
|
|
1112
|
-
'tip.unavailable': '遥测不可用',
|
|
1113
|
-
'tip.connecting': '连接中…',
|
|
1114
|
-
'tip.errors': '{n} 个近期错误',
|
|
1115
|
-
'tip.pressure': '上下文已用 {pressure}',
|
|
1116
|
-
'tip.pressureWithMemories': '上下文已用 {pressure} · 记忆 {memories} 条',
|
|
1117
|
-
'tip.live': 'Magic Context 运行中',
|
|
1118
|
-
// The single-letter ledger's hover names.
|
|
1119
|
-
'ledger.O': '占用(原生投影:下一次请求将看到多少)',
|
|
1120
|
-
'ledger.L': '压缩后账面(最近一次压缩后的 surface 量)',
|
|
1121
|
-
'ledger.M': '本项目记忆总数',
|
|
1122
|
-
'ledger.P': '24h 提升的记忆',
|
|
1123
|
-
'ledger.U': '待验证记忆',
|
|
1124
|
-
'ledger.T': '最近维护 tick',
|
|
1125
|
-
'ledger.K': 'TTL 到期归档',
|
|
1126
|
-
'ledger.V': '维护验证通过',
|
|
1127
|
-
'ledger.R': '维护修订',
|
|
1128
|
-
'ledger.E': '本进程错误',
|
|
1129
|
-
// Operation feed.
|
|
1130
|
-
'records.empty': '暂无操作记录',
|
|
1131
|
-
'records.noRoute': '此宿主未挂遥测路由',
|
|
1132
|
-
'records.folded': '{n} 次',
|
|
1133
|
-
'records.head': '最近操作',
|
|
1134
|
-
'records.count': '近 {n} 项',
|
|
1135
|
-
'records.foot': '悬停条目看详情 · /ctx-status 看全量',
|
|
1136
|
-
'records.beats': '节拍: {beats}',
|
|
1137
|
-
// Slot labels (ledger rows the shell projects).
|
|
1138
|
-
'title': 'Magic Context',
|
|
1139
|
-
'label.telemetry': 'Magic Context 遥测',
|
|
1140
|
-
},
|
|
1141
|
-
en: {
|
|
1142
|
-
'category.write': 'Memory written',
|
|
1143
|
-
'category.read': 'Memory loaded',
|
|
1144
|
-
'category.reclaim': 'Context reclaimed',
|
|
1145
|
-
'category.drop': 'Context dropped',
|
|
1146
|
-
'category.maintain': 'Maintenance',
|
|
1147
|
-
'category.archive': 'Archived',
|
|
1148
|
-
'category.error': 'Failed',
|
|
1149
|
-
'noise.memoryRecall': 'memories loaded',
|
|
1150
|
-
'noise.baselineRefresh': 'baseline refreshed',
|
|
1151
|
-
'record.reclaimPass': 'Reclaim pass',
|
|
1152
|
-
'record.promotion': 'Memory promotion',
|
|
1153
|
-
'record.memoryWritten': 'Memory written',
|
|
1154
|
-
'record.memorySearch': 'Memory search',
|
|
1155
|
-
'record.compacted': 'Context compacted',
|
|
1156
|
-
'record.baselineHard': 'Baseline rebuild (HARD)',
|
|
1157
|
-
'record.emergencyDrop': 'Emergency drop',
|
|
1158
|
-
'record.wireRepaired': 'Wire repair',
|
|
1159
|
-
'record.dropLanded': 'Drop landed',
|
|
1160
|
-
'record.reduceQueued': 'Reclaim queued',
|
|
1161
|
-
'record.expandRestored': 'Content restored',
|
|
1162
|
-
'record.expandRange': 'Context read',
|
|
1163
|
-
'record.remountRecovery': 'Remount recovery',
|
|
1164
|
-
'record.historianFailed': 'Historian failed',
|
|
1165
|
-
'record.archived': 'Compartment stored',
|
|
1166
|
-
'record.maintenance': 'Maintenance sweep',
|
|
1167
|
-
'record.failed': 'Operation failed',
|
|
1168
|
-
'detail.added': 'added {n}',
|
|
1169
|
-
'detail.staged': 'staged {n}',
|
|
1170
|
-
'detail.seenAgain': 'seen again {n}',
|
|
1171
|
-
'detail.skipped': 'skipped {n}',
|
|
1172
|
-
'detail.verb.write': 'write',
|
|
1173
|
-
'detail.verb.update': 'update',
|
|
1174
|
-
'detail.verb.archive': 'archive',
|
|
1175
|
-
'detail.verb.merge': 'merge',
|
|
1176
|
-
'detail.verb.list': 'list',
|
|
1177
|
-
'detail.verb.get': 'read',
|
|
1178
|
-
'detail.verb.search': 'search',
|
|
1179
|
-
'detail.queued': 'queued {n}',
|
|
1180
|
-
'detail.refused': 'refused {n}',
|
|
1181
|
-
'detail.restored': 'restored {n}',
|
|
1182
|
-
'detail.rangeEvents': '{n} events',
|
|
1183
|
-
'detail.rangeSkipped': 'skipped {n}',
|
|
1184
|
-
'detail.hits': '{n} hits',
|
|
1185
|
-
'detail.rejected': 'rejected',
|
|
1186
|
-
'detail.replacedNodes': 'replaced {n} nodes',
|
|
1187
|
-
'detail.nodes': '{n} nodes',
|
|
1188
|
-
'detail.reclaimed': 'reclaimed {n} tok',
|
|
1189
|
-
'detail.droppedToolOutputs': 'dropped {n} tool outputs',
|
|
1190
|
-
'detail.wireSwept': 'swept {n} stranded results',
|
|
1191
|
-
'detail.toolOutputs': '{n} tool outputs',
|
|
1192
|
-
'detail.backfilled': 'backfilled the interrupted archive',
|
|
1193
|
-
'detail.ttlExpired': 'TTL expired {n}',
|
|
1194
|
-
'detail.verified': 'verified {n}',
|
|
1195
|
-
'detail.revised': 'revised {n}',
|
|
1196
|
-
'detail.nothingToDo': 'nothing to do',
|
|
1197
|
-
'detail.facts': 'facts {n}',
|
|
1198
|
-
'status.ok': 'healthy',
|
|
1199
|
-
'status.connecting': 'connecting',
|
|
1200
|
-
'status.unavailable': 'unavailable',
|
|
1201
|
-
'tip.unavailable': 'Telemetry unavailable',
|
|
1202
|
-
'tip.connecting': 'Connecting…',
|
|
1203
|
-
'tip.errors': '{n} recent errors',
|
|
1204
|
-
'tip.pressure': '{pressure} of context used',
|
|
1205
|
-
'tip.pressureWithMemories': '{pressure} of context used · {memories} memories',
|
|
1206
|
-
'tip.live': 'Magic Context live',
|
|
1207
|
-
'ledger.O': 'Occupancy (native projection: what the next request sees)',
|
|
1208
|
-
'ledger.L': 'Post-compaction ledger (surface after the last compaction)',
|
|
1209
|
-
'ledger.M': 'Memories in this project',
|
|
1210
|
-
'ledger.P': 'Memories promoted in 24h',
|
|
1211
|
-
'ledger.U': 'Unverified memories',
|
|
1212
|
-
'ledger.T': 'Last maintenance tick',
|
|
1213
|
-
'ledger.K': 'TTL-expired archives',
|
|
1214
|
-
'ledger.V': 'Maintenance verifications passed',
|
|
1215
|
-
'ledger.R': 'Maintenance revisions',
|
|
1216
|
-
'ledger.E': 'Errors in this process',
|
|
1217
|
-
'records.empty': 'No operations recorded yet',
|
|
1218
|
-
'records.noRoute': 'This host exposes no telemetry route',
|
|
1219
|
-
'records.folded': '×{n}',
|
|
1220
|
-
'records.head': 'Recent operations',
|
|
1221
|
-
'records.count': 'last {n}',
|
|
1222
|
-
'records.foot': 'Hover a row for detail · /ctx-status for the full ledger',
|
|
1223
|
-
'records.beats': 'Beats: {beats}',
|
|
1224
|
-
'title': 'Magic Context',
|
|
1225
|
-
'label.telemetry': 'Magic Context telemetry',
|
|
1226
|
-
},
|
|
1227
|
-
}
|
|
1228
|
-
|
|
1229
|
-
/**
|
|
1230
|
-
* A dictionary-backed translator in the same shape as the slot `t` seat:
|
|
1231
|
-
* `t(key, params)` interpolates `{name}` and falls back en → key. Used as
|
|
1232
|
-
* the default for the pure record functions, whose direct callers
|
|
1233
|
-
* (`__diagnostics`, the records-check script) read the shipped zh copy;
|
|
1234
|
-
* every rendered string goes through the framework `t` instead.
|
|
1235
|
-
*/
|
|
1236
|
-
function dictionaryTranslator(locale) {
|
|
1237
|
-
const table = DICTIONARIES[locale] !== undefined ? DICTIONARIES[locale] : DICTIONARIES.en
|
|
1238
|
-
const fallback = DICTIONARIES.en
|
|
1239
|
-
return (key, params) => {
|
|
1240
|
-
const template = table[key] !== undefined ? table[key] : fallback[key] !== undefined ? fallback[key] : key
|
|
1241
|
-
if (params === undefined || params === null) return template
|
|
1242
|
-
return template.replace(/\{(\w+)\}/g, (match, name) => (name in params ? String(params[name]) : match))
|
|
1243
|
-
}
|
|
1244
|
-
}
|
|
1245
|
-
|
|
1246
|
-
/** The reference translator for callers that pass no `t`. */
|
|
1247
|
-
const REFERENCE_T = dictionaryTranslator('zh')
|
|
1248
|
-
|
|
1249
|
-
/** Every key the dictionaries carry, for the parity check outside a browser. */
|
|
1250
|
-
function dictionaryKeys() {
|
|
1251
|
-
return {
|
|
1252
|
-
zh: Object.keys(DICTIONARIES.zh).sort(),
|
|
1253
|
-
en: Object.keys(DICTIONARIES.en).sort(),
|
|
1254
|
-
}
|
|
1255
|
-
}
|
|
1256
|
-
|
|
1257
|
-
/* ------------------------------------------------------------------ *
|
|
1258
|
-
* Operation records (what the adapter just did, in the operator's words)
|
|
1259
|
-
* ------------------------------------------------------------------ */
|
|
1260
|
-
|
|
1261
|
-
/**
|
|
1262
|
-
* The categories the panel groups operations into.
|
|
1263
|
-
*
|
|
1264
|
-
* The panel is the window onto the three things a reviewing operator cares
|
|
1265
|
-
* about — memories going in, memories coming out, and context being
|
|
1266
|
-
* reclaimed or dropped — so every journal kind that represents one of them
|
|
1267
|
-
* is mapped here. Process chatter (`pass` defers, boots, engine notices,
|
|
1268
|
-
* session-first-seen) is counted but never listed, otherwise 480 defer rows
|
|
1269
|
-
* bury the handful of real operations.
|
|
1270
|
-
*/
|
|
1271
|
-
const RECORD_CATEGORIES = {
|
|
1272
|
-
write: { icon: '🧠', labelKey: 'category.write', tone: '#a371f7' },
|
|
1273
|
-
read: { icon: '🔎', labelKey: 'category.read', tone: '#2f81f7' },
|
|
1274
|
-
reclaim: { icon: '✂️', labelKey: 'category.reclaim', tone: '#22a06b' },
|
|
1275
|
-
drop: { icon: '🗑️', labelKey: 'category.drop', tone: '#d08700' },
|
|
1276
|
-
maintain: { icon: '🧰', labelKey: 'category.maintain', tone: '#8250df' },
|
|
1277
|
-
archive: { icon: '🧭', labelKey: 'category.archive', tone: '#8a8f98' },
|
|
1278
|
-
error: { icon: '⚠️', labelKey: 'category.error', tone: '#e5484d' },
|
|
1279
|
-
}
|
|
1280
|
-
/** Kinds that are process noise unless they carry a real operation. */
|
|
1281
|
-
// Telemetry-only kinds: real signals, but never operation rows — they
|
|
1282
|
-
// would flood the feed (context-pressure fires per distinct reading) or
|
|
1283
|
-
// read as raw English summaries under the archive fallback.
|
|
1284
|
-
const NOISE_KINDS = { pass: true, boot: true, 'session-first-seen': true, engine: true, 'context-pressure': true, 'memory-recall': true }
|
|
1285
|
-
/** How many records the panel lists (the stream keeps `MAX_RECENT`). */
|
|
1286
|
-
const MAX_RECORDS = 12
|
|
1287
|
-
|
|
1288
|
-
function numberField(data, key) {
|
|
1289
|
-
if (data === null || typeof data !== 'object') return undefined
|
|
1290
|
-
const value = data[key]
|
|
1291
|
-
return typeof value === 'number' && Number.isFinite(value) ? value : undefined
|
|
1292
|
-
}
|
|
1293
|
-
|
|
1294
|
-
function arrayLength(data, key) {
|
|
1295
|
-
if (data === null || typeof data !== 'object') return undefined
|
|
1296
|
-
return Array.isArray(data[key]) ? data[key].length : undefined
|
|
1297
|
-
}
|
|
1298
|
-
|
|
1299
|
-
/** `−70.7k tok`; '' when the Host sent no number. Same compact scale as
|
|
1300
|
-
* every other token figure in the panel — no thousands-separated bills. */
|
|
1301
|
-
function tokensOf(value) {
|
|
1302
|
-
if (typeof value !== 'number' || !Number.isFinite(value)) return ''
|
|
1303
|
-
const sign = value < 0 ? '−' : ''
|
|
1304
|
-
return sign + compactTok(Math.abs(value)) + ' tok'
|
|
1305
|
-
}
|
|
1306
|
-
|
|
1307
|
-
/** `135ms` / `9.7s`; '' when the Host sent no number. */
|
|
1308
|
-
function spanOf(value) {
|
|
1309
|
-
if (typeof value !== 'number' || !Number.isFinite(value)) return ''
|
|
1310
|
-
return value < 1000 ? Math.round(value) + 'ms' : (value / 1000).toFixed(1) + 's'
|
|
1311
|
-
}
|
|
1312
|
-
|
|
1313
|
-
function joinDetail(parts) {
|
|
1314
|
-
return parts.filter((part) => typeof part === 'string' && part !== '').join(' · ')
|
|
1315
|
-
}
|
|
1316
|
-
|
|
1317
|
-
/**
|
|
1318
|
-
* One journal row → one record, or null for noise.
|
|
1319
|
-
*
|
|
1320
|
-
* `detail` reads numbers off the SSE payload; the snapshot strip carries no
|
|
1321
|
-
* `data`, so those rows fall back to the Host's one-line summary (rendered
|
|
1322
|
-
* by the caller) instead of losing their content entirely.
|
|
1323
|
-
*
|
|
1324
|
-
* `t` is the active translator (the slot `t` seat in the panel, the shipped
|
|
1325
|
-
* zh dictionary for `__diagnostics` callers that pass none), so every
|
|
1326
|
-
* string this returns is already in the reader's language.
|
|
1327
|
-
*/
|
|
1328
|
-
function recordOf(event, t = REFERENCE_T) {
|
|
1329
|
-
if (event === null || typeof event !== 'object') return null
|
|
1330
|
-
const kind = typeof event.kind === 'string' ? event.kind : 'event'
|
|
1331
|
-
const data = event.data !== null && typeof event.data === 'object' ? event.data : undefined
|
|
1332
|
-
const level = typeof event.level === 'string' ? event.level : 'info'
|
|
1333
|
-
const at = typeof event.at === 'number' ? event.at : undefined
|
|
1334
|
-
const action = data !== undefined && typeof data.action === 'string' ? data.action : undefined
|
|
1335
|
-
const record = (category, key, params, detail) => ({
|
|
1336
|
-
category,
|
|
1337
|
-
text: t(key, params),
|
|
1338
|
-
detail: joinDetail(detail),
|
|
1339
|
-
at,
|
|
1340
|
-
summary: event.summary,
|
|
1341
|
-
})
|
|
1342
|
-
switch (kind) {
|
|
1343
|
-
case 'pass': {
|
|
1344
|
-
if (data === undefined || data.decision !== 'execute') return null
|
|
1345
|
-
// An execute pass that moved 0 tokens is a no-op: the real reclamation
|
|
1346
|
-
// is already reported by its `compartment-published`/`baseline` rows,
|
|
1347
|
-
// so listing it would bury them under "0 tok" lines.
|
|
1348
|
-
const delta = numberField(data, 'tokensDelta')
|
|
1349
|
-
if (delta === undefined || delta === 0) return null
|
|
1350
|
-
const passPressure = numberField(data, 'pressure')
|
|
1351
|
-
return record('reclaim', 'record.reclaimPass', undefined, [
|
|
1352
|
-
passPressure === undefined ? '' : Math.round(passPressure) + '%',
|
|
1353
|
-
tokensOf(delta),
|
|
1354
|
-
])
|
|
1355
|
-
}
|
|
1356
|
-
case 'memory-promote': {
|
|
1357
|
-
const inserted = arrayLength(data, 'inserted')
|
|
1358
|
-
const seenAgain = arrayLength(data, 'seenAgain')
|
|
1359
|
-
const skipped = arrayLength(data, 'skipped')
|
|
1360
|
-
const staged = arrayLength(data, 'staged')
|
|
1361
|
-
const detail = [
|
|
1362
|
-
inserted === undefined ? '' : t('detail.added', { n: fmtInt(inserted) }),
|
|
1363
|
-
// A machine/org auto-stage row (`phase: auto-stage`) carries `staged`
|
|
1364
|
-
// instead of `inserted` — the candidate pool is filled, no project
|
|
1365
|
-
// row is written. Show the candidating count for it.
|
|
1366
|
-
staged === undefined ? '' : t('detail.staged', { n: fmtInt(staged) }),
|
|
1367
|
-
]
|
|
1368
|
-
// The panel's promotion detail must always carry a real count
|
|
1369
|
-
// (mc-records-check asserts /新增 \d+/ on the rendered line).
|
|
1370
|
-
const count = detail.find((part) => /^\D*?\d+/.test(part))
|
|
1371
|
-
if (count !== undefined) {
|
|
1372
|
-
return record('write', 'record.promotion', undefined, [
|
|
1373
|
-
count,
|
|
1374
|
-
seenAgain ? t('detail.seenAgain', { n: fmtInt(seenAgain) }) : '',
|
|
1375
|
-
skipped ? t('detail.skipped', { n: fmtInt(skipped) }) : '',
|
|
1376
|
-
])
|
|
1377
|
-
}
|
|
1378
|
-
return record('write', 'record.promotion', undefined, detail)
|
|
1379
|
-
}
|
|
1380
|
-
case 'tool-call': {
|
|
1381
|
-
// The reclamation lane first: ctx_reduce/ctx_expand rows carry `tool`
|
|
1382
|
-
// plus tag arrays and NO `action` (tool-bridge.ts:217,277), so the
|
|
1383
|
-
// action branches below dropped them entirely — the panel never showed
|
|
1384
|
-
// the agent's own drops or restores, the two operations it exists for.
|
|
1385
|
-
const tool = data !== undefined && typeof data.tool === 'string' ? data.tool : undefined
|
|
1386
|
-
if (tool === 'ctx_reduce') {
|
|
1387
|
-
const queued = arrayLength(data, 'queued')
|
|
1388
|
-
const refused = arrayLength(data, 'refused')
|
|
1389
|
-
return record('drop', 'record.reduceQueued', undefined, [
|
|
1390
|
-
queued === undefined ? '' : t('detail.queued', { n: fmtInt(queued) }),
|
|
1391
|
-
refused ? t('detail.refused', { n: fmtInt(refused) }) : '',
|
|
1392
|
-
spanOf(numberField(data, 'ms')),
|
|
1393
|
-
])
|
|
1394
|
-
}
|
|
1395
|
-
if (tool === 'ctx_expand') {
|
|
1396
|
-
if (data !== undefined && data.locator === 'range') {
|
|
1397
|
-
const events = arrayLength(data, 'events')
|
|
1398
|
-
const skipped = numberField(data, 'skipped')
|
|
1399
|
-
return record('read', 'record.expandRange', undefined, [
|
|
1400
|
-
events === undefined ? '' : t('detail.rangeEvents', { n: fmtInt(events) }),
|
|
1401
|
-
skipped ? t('detail.rangeSkipped', { n: fmtInt(skipped) }) : '',
|
|
1402
|
-
spanOf(numberField(data, 'ms')),
|
|
1403
|
-
])
|
|
1404
|
-
}
|
|
1405
|
-
const restored = arrayLength(data, 'restored')
|
|
1406
|
-
const refused = arrayLength(data, 'refused')
|
|
1407
|
-
return record('archive', 'record.expandRestored', undefined, [
|
|
1408
|
-
restored === undefined ? '' : t('detail.restored', { n: fmtInt(restored) }),
|
|
1409
|
-
refused ? t('detail.refused', { n: fmtInt(refused) }) : '',
|
|
1410
|
-
spanOf(numberField(data, 'ms')),
|
|
1411
|
-
])
|
|
1412
|
-
}
|
|
1413
|
-
const writeish = action === 'write' || action === 'update' || action === 'archive' || action === 'merge'
|
|
1414
|
-
// `search` is the agent's read lane (MC ctx_search's memory half) and
|
|
1415
|
-
// used to be dropped for not being list/get.
|
|
1416
|
-
const readish = action === 'list' || action === 'get' || action === 'search'
|
|
1417
|
-
if (!writeish && !readish) return null
|
|
1418
|
-
const hits = numberField(data, 'hits')
|
|
1419
|
-
return record(writeish ? 'write' : 'read', writeish ? 'record.memoryWritten' : 'record.memorySearch', undefined, [
|
|
1420
|
-
t('detail.verb.' + String(action)),
|
|
1421
|
-
readish && hits !== undefined ? t('detail.hits', { n: fmtInt(hits) }) : '',
|
|
1422
|
-
data !== undefined && data.ok === false ? t('detail.rejected') : '',
|
|
1423
|
-
spanOf(numberField(data, 'ms')),
|
|
1424
|
-
])
|
|
1425
|
-
}
|
|
1426
|
-
case 'memory-recall': {
|
|
1427
|
-
// Every pass re-reports the same injection; the per-turn beat lives
|
|
1428
|
-
// in the beat line, not the record list.
|
|
1429
|
-
return null
|
|
1430
|
-
}
|
|
1431
|
-
case 'compartment-published': {
|
|
1432
|
-
const nodes = numberField(data, 'nodes')
|
|
1433
|
-
const reclaimed = numberField(data, 'estTokensReclaimed')
|
|
1434
|
-
return record('reclaim', 'record.compacted', undefined, [
|
|
1435
|
-
nodes === undefined ? '' : t('detail.replacedNodes', { n: fmtInt(nodes) }),
|
|
1436
|
-
reclaimed === undefined ? '' : t('detail.reclaimed', { n: compactTok(reclaimed) }),
|
|
1437
|
-
])
|
|
1438
|
-
}
|
|
1439
|
-
case 'baseline': {
|
|
1440
|
-
// A SOFT refresh is the engine's per-execute heartbeat; only a HARD
|
|
1441
|
-
// rebuild (a priced cache bust) is worth a row.
|
|
1442
|
-
if (data === undefined || data.mode !== 'hard') return null
|
|
1443
|
-
const bytes = numberField(data, 'bytes')
|
|
1444
|
-
const est = numberField(data, 'estTokens')
|
|
1445
|
-
return record('reclaim', 'record.baselineHard', undefined, [
|
|
1446
|
-
bytes === undefined ? '' : compactTok(bytes) + ' B',
|
|
1447
|
-
est === undefined ? '' : compactTok(est) + ' tok',
|
|
1448
|
-
])
|
|
1449
|
-
}
|
|
1450
|
-
case 'emergency-reclaim': {
|
|
1451
|
-
const dropped = numberField(data, 'dropped')
|
|
1452
|
-
return record('drop', 'record.emergencyDrop', undefined, [
|
|
1453
|
-
dropped === undefined ? '' : t('detail.droppedToolOutputs', { n: fmtInt(dropped) }),
|
|
1454
|
-
data !== undefined && typeof data.reason === 'string' ? data.reason : '',
|
|
1455
|
-
])
|
|
1456
|
-
}
|
|
1457
|
-
case 'wire-repair': {
|
|
1458
|
-
// A wire defect means the adapter once broke pairing on the live
|
|
1459
|
-
// surface; the sweep is the fix, and the row is the receipt.
|
|
1460
|
-
const swept = numberField(data, 'swept')
|
|
1461
|
-
return record('maintain', 'record.wireRepaired', undefined, [
|
|
1462
|
-
swept === undefined ? '' : t('detail.wireSwept', { n: fmtInt(swept) }),
|
|
1463
|
-
])
|
|
1464
|
-
}
|
|
1465
|
-
case 'drop-landed': {
|
|
1466
|
-
const count = numberField(data, 'count')
|
|
1467
|
-
const est = numberField(data, 'estTokens')
|
|
1468
|
-
return record('drop', 'record.dropLanded', undefined, [
|
|
1469
|
-
count === undefined ? '' : t('detail.toolOutputs', { n: fmtInt(count) }),
|
|
1470
|
-
est === undefined ? '' : t('detail.reclaimed', { n: compactTok(est) }),
|
|
1471
|
-
])
|
|
1472
|
-
}
|
|
1473
|
-
case 'historian': {
|
|
1474
|
-
const phase = data === undefined || typeof data.phase !== 'string' ? '' : data.phase
|
|
1475
|
-
if (phase === 'failed') {
|
|
1476
|
-
// A remount-killed run is repaired host-side at the next mount —
|
|
1477
|
-
// it is bookkeeping, not an operation failure, so it reads as a
|
|
1478
|
-
// dim archive row instead of a red one.
|
|
1479
|
-
if (data !== undefined && data.kind === 'adapter-remount') {
|
|
1480
|
-
return record('maintain', 'record.remountRecovery', undefined, [t('detail.backfilled')])
|
|
1481
|
-
}
|
|
1482
|
-
const reason = typeof data.reason === 'string' ? data.reason : ''
|
|
1483
|
-
return record('error', 'record.historianFailed', undefined, [reason.slice(0, 42)])
|
|
1484
|
-
}
|
|
1485
|
-
if (phase !== 'stored') return null // start/done are the run's inner beat
|
|
1486
|
-
const facts = numberField(data, 'facts')
|
|
1487
|
-
const nodes = numberField(data, 'nodes')
|
|
1488
|
-
return record('archive', 'record.archived', undefined, [
|
|
1489
|
-
data !== undefined && typeof data.compartmentId === 'number' ? '§C' + String(data.compartmentId) + '§' : '',
|
|
1490
|
-
facts === undefined ? '' : t('detail.facts', { n: fmtInt(facts) }),
|
|
1491
|
-
nodes === undefined ? '' : t('detail.nodes', { n: fmtInt(nodes) }),
|
|
1492
|
-
spanOf(numberField(data, 'durationMs')),
|
|
1493
|
-
])
|
|
1494
|
-
}
|
|
1495
|
-
case 'maintenance': {
|
|
1496
|
-
const parts = [
|
|
1497
|
-
numberField(data, 'expired') === undefined ? '' : t('detail.ttlExpired', { n: fmtInt(data.expired) }),
|
|
1498
|
-
numberField(data, 'verified') === undefined ? '' : t('detail.verified', { n: fmtInt(data.verified) }),
|
|
1499
|
-
numberField(data, 'updated') === undefined ? '' : t('detail.revised', { n: fmtInt(data.updated) }),
|
|
1500
|
-
]
|
|
1501
|
-
if (parts.every((part) => part === '')) {
|
|
1502
|
-
return record('maintain', 'record.maintenance', undefined, [t('detail.nothingToDo')])
|
|
1503
|
-
}
|
|
1504
|
-
return record('maintain', 'record.maintenance', undefined, parts)
|
|
1505
|
-
}
|
|
1506
|
-
case 'session-notice':
|
|
1507
|
-
// The notice is the conversation's own inline row; mirroring it here
|
|
1508
|
-
// duplicates what the transcript already shows.
|
|
1509
|
-
return null
|
|
1510
|
-
default:
|
|
1511
|
-
if (level === 'error') return record('error', 'record.failed', undefined, [kind])
|
|
1512
|
-
return NOISE_KINDS[kind] === true ? null : record('archive', kind, undefined, [])
|
|
1513
|
-
}
|
|
1514
|
-
}
|
|
1515
|
-
|
|
1516
|
-
/**
|
|
1517
|
-
* Newest-first records out of the raw stream, with adjacent repeats folded.
|
|
1518
|
-
*
|
|
1519
|
-
* The automatic m[0]/m[1] injection runs on every turn and reports the same
|
|
1520
|
-
* "m[0] 40 条 · m[1] +26 条" line each time; listing that forty times would
|
|
1521
|
-
* hide the operations that actually changed something. Consecutive identical
|
|
1522
|
-
* records therefore collapse into one with a `×N` count (the same way a
|
|
1523
|
-
* repeated tool call reads in a coding agent's transcript).
|
|
1524
|
-
*/
|
|
1525
|
-
function recordsOf(events, t = REFERENCE_T) {
|
|
1526
|
-
const out = []
|
|
1527
|
-
for (const event of events) {
|
|
1528
|
-
const record = recordOf(event, t)
|
|
1529
|
-
if (record === null) continue
|
|
1530
|
-
const last = out.length === 0 ? undefined : out[out.length - 1]
|
|
1531
|
-
if (last !== undefined && last.category === record.category && last.text === record.text && last.detail === record.detail) {
|
|
1532
|
-
out[out.length - 1] = { ...last, count: last.count + 1 }
|
|
1533
|
-
continue
|
|
1534
|
-
}
|
|
1535
|
-
out.push({ ...record, count: 1 })
|
|
1536
|
-
}
|
|
1537
|
-
return out
|
|
1538
|
-
}
|
|
1539
|
-
|
|
1540
|
-
/** `[{category, icon, label, labelKey, tone, count}]`, only for categories that occurred. */
|
|
1541
|
-
function recordCounts(records, t = REFERENCE_T) {
|
|
1542
|
-
const out = []
|
|
1543
|
-
for (const key of Object.keys(RECORD_CATEGORIES)) {
|
|
1544
|
-
let count = 0
|
|
1545
|
-
for (const record of records) if (record.category === key) count++
|
|
1546
|
-
const meta = RECORD_CATEGORIES[key]
|
|
1547
|
-
if (count > 0) out.push({ key, count, ...meta, label: t(meta.labelKey) })
|
|
1548
|
-
}
|
|
1549
|
-
return out
|
|
1550
|
-
}
|
|
1551
|
-
|
|
1552
|
-
/** The per-turn beats worth one folded line: routine injections only. */
|
|
1553
|
-
function noiseLine(events, t = REFERENCE_T) {
|
|
1554
|
-
let recalled = 0
|
|
1555
|
-
let refreshed = 0
|
|
1556
|
-
for (const event of events) {
|
|
1557
|
-
if (event === null || typeof event !== 'object' || typeof event.kind !== 'string') continue
|
|
1558
|
-
if (event.kind === 'memory-recall') recalled += 1
|
|
1559
|
-
else if (event.kind === 'baseline' && !(event.data !== null && typeof event.data === 'object' && event.data.mode === 'hard')) refreshed += 1
|
|
1560
|
-
}
|
|
1561
|
-
return [
|
|
1562
|
-
recalled === 0 ? undefined : t('noise.memoryRecall') + ' ×' + recalled,
|
|
1563
|
-
refreshed === 0 ? undefined : t('noise.baselineRefresh') + ' ×' + refreshed,
|
|
1564
|
-
].filter((line) => line !== undefined).join(' · ')
|
|
1565
|
-
}
|
|
1566
|
-
|
|
1567
|
-
/* ------------------------------------------------------------------ *
|
|
1568
|
-
* Inline records in the conversation itself
|
|
1569
|
-
* ------------------------------------------------------------------ */
|
|
1570
|
-
|
|
1571
|
-
// The per-Turn tail blocks and the live strip were removed (see `apply`):
|
|
1572
|
-
// their shared SSE plumbing (`subscribeLive`, the TURN_*/LIVE maps) went
|
|
1573
|
-
// with them. The record model below (`recordOf`/`recordsOf`/`recordCounts`/
|
|
1574
|
-
// `noiseLine`) stays — the pill's expanded panel still renders journal
|
|
1575
|
-
// rows with it.
|
|
1576
|
-
|
|
1577
|
-
/** Where the expanded panel opens when the pill cannot be measured. */
|
|
1578
|
-
const PANEL_FALLBACK_ANCHOR = { right: 12, bottom: 44, maxHeight: 400 }
|
|
1579
|
-
|
|
1580
|
-
/**
|
|
1581
|
-
* Anchor the panel just above and right-aligned with the pill.
|
|
1582
|
-
*
|
|
1583
|
-
* The panel is `position: fixed` on purpose: the status bar sits inside the
|
|
1584
|
-
* composer, whose scroll/overflow containers would otherwise clip a panel
|
|
1585
|
-
* opened upward. Measuring the pill keeps it attached to whichever seat
|
|
1586
|
-
* rendered it, and the values are clamped so the panel cannot leave the
|
|
1587
|
-
* viewport.
|
|
1588
|
-
*/
|
|
1589
|
-
function anchorAbove(node) {
|
|
1590
|
-
if (node === null || node === undefined || typeof node.getBoundingClientRect !== 'function' || typeof window === 'undefined') {
|
|
1591
|
-
return PANEL_FALLBACK_ANCHOR
|
|
1592
|
-
}
|
|
1593
|
-
try {
|
|
1594
|
-
const rect = node.getBoundingClientRect()
|
|
1595
|
-
const viewportHeight = typeof window.innerHeight === 'number' ? window.innerHeight : 0
|
|
1596
|
-
const viewportWidth = typeof window.innerWidth === 'number' ? window.innerWidth : 0
|
|
1597
|
-
const bottom = Math.max(8, viewportHeight - rect.top + 8)
|
|
1598
|
-
return {
|
|
1599
|
-
right: Math.max(8, viewportWidth - rect.right),
|
|
1600
|
-
bottom,
|
|
1601
|
-
maxHeight: Math.max(160, Math.min(480, viewportHeight - bottom - 8)),
|
|
1602
|
-
}
|
|
1603
|
-
} catch (error) {
|
|
1604
|
-
return PANEL_FALLBACK_ANCHOR
|
|
1605
|
-
}
|
|
1606
|
-
}
|
|
1607
|
-
|
|
1608
|
-
/**
|
|
1609
|
-
* The telemetry pill.
|
|
1610
|
-
*
|
|
1611
|
-
* It is rendered in whichever seat the plugin managed to take: the composer
|
|
1612
|
-
* status bar (`placement: 'statusbar'`, the intended home) or the
|
|
1613
|
-
* `shell.overlay` fallback (`placement: 'overlay'`) for a shell that never
|
|
1614
|
-
* declares the status seat. Collapsed it is one very short line — the
|
|
1615
|
-
* status bar already carries three statistics — and the hover title holds
|
|
1616
|
-
* the full summary. The expanded panel always renders above the pill.
|
|
1617
|
-
*
|
|
1618
|
-
* The Host route is optional: an absent route (404 on `EventSource`, or no
|
|
1619
|
-
* `EventSource` at all) makes the badge read `MC · —` and nothing else in
|
|
1620
|
-
* the UI is affected. The overlay layer is click-through, so both the pill
|
|
1621
|
-
* and the panel opt back into pointer events.
|
|
1622
|
-
*/
|
|
1623
|
-
function TelemetryBadge(props) {
|
|
1624
|
-
const placement = props !== undefined && props.placement === 'statusbar' ? 'statusbar' : 'overlay'
|
|
1625
|
-
const sessionId = props !== undefined && typeof props.sessionId === 'string' && props.sessionId !== ''
|
|
1626
|
-
? props.sessionId
|
|
1627
|
-
: undefined
|
|
1628
|
-
// The locale seat the shell injects for a `locale`-declaring entry. The
|
|
1629
|
-
// outlet re-renders on a switch, so this resolves to the new language
|
|
1630
|
-
// without any subscription of our own; the shipped zh copy is the
|
|
1631
|
-
// fallback for a shell that hands us none.
|
|
1632
|
-
const t = props !== undefined && typeof props.t === 'function' ? props.t : REFERENCE_T
|
|
1633
|
-
const eventsUrl = telemetryUrl(EVENTS_URL, sessionId)
|
|
1634
|
-
const summaryUrl = telemetryUrl(SUMMARY_URL, sessionId)
|
|
1635
|
-
const [state, setState] = useState({ status: 'connecting', summary: null, recent: [] })
|
|
1636
|
-
const [open, setOpen] = useState(false)
|
|
1637
|
-
const [anchor, setAnchor] = useState(null)
|
|
1638
|
-
const [hovered, setHovered] = useState(false)
|
|
1639
|
-
const pillRef = useRef(null)
|
|
1640
|
-
// Computed here (not down with the other render-time values) because the decay
|
|
1641
|
-
// timer below has to know whether the badge is currently red.
|
|
1642
|
-
const recentErrors = countRecentErrors(state.recent, Date.now())
|
|
1643
|
-
const redNow = recentErrors > 0
|
|
1644
|
-
const [, setClock] = useState(0)
|
|
1645
|
-
// A quiet session emits no frames, so a red badge would never age out on its
|
|
1646
|
-
// own; while (and only while) it is red, re-render once a minute so the
|
|
1647
|
-
// window closes on time. Green costs nothing.
|
|
1648
|
-
useEffect(() => {
|
|
1649
|
-
if (!redNow) return undefined
|
|
1650
|
-
const timer = setInterval(() => { setClock((value) => value + 1) }, 60_000)
|
|
1651
|
-
return () => { clearInterval(timer) }
|
|
1652
|
-
}, [redNow])
|
|
1653
|
-
/** The one summary fetch: `ok → json`, anything else rejects. Lives at
|
|
1654
|
-
* component scope — both the SSE effect (probe) and the open-panel
|
|
1655
|
-
* effect (refresh) call it. */
|
|
1656
|
-
const fetchSummaryJson = () => Promise.resolve(fetch(summaryUrl, { headers: { accept: 'application/json' } })).then(
|
|
1657
|
-
(response) => (response !== undefined && response.ok ? response.json() : Promise.reject(new Error('unavailable'))),
|
|
1658
|
-
)
|
|
1659
|
-
|
|
1660
|
-
useEffect(() => {
|
|
1661
|
-
let disposed = false
|
|
1662
|
-
let sawSnapshot = false
|
|
1663
|
-
|
|
1664
|
-
const acceptSummary = (payload) => {
|
|
1665
|
-
if (disposed || payload === null || typeof payload !== 'object') return
|
|
1666
|
-
sawSnapshot = true
|
|
1667
|
-
const recent = Array.isArray(payload.recent) ? payload.recent.slice(-MAX_RECENT).reverse() : []
|
|
1668
|
-
setState({ status: 'ok', summary: payload, recent })
|
|
1669
|
-
}
|
|
1670
|
-
const acceptEvent = (event) => {
|
|
1671
|
-
if (disposed || event === null || typeof event !== 'object') return
|
|
1672
|
-
setState((previous) => {
|
|
1673
|
-
if (previous.status === 'unavailable') return previous
|
|
1674
|
-
return { status: 'ok', summary: previous.summary, recent: [event, ...previous.recent].slice(0, MAX_RECENT) }
|
|
1675
|
-
})
|
|
1676
|
-
}
|
|
1677
|
-
// One tolerant reader for both framings: the Host's named `snapshot` /
|
|
1678
|
-
// `journal` events, and a bare `data:` frame from an older revision.
|
|
1679
|
-
const acceptAny = (payload) => {
|
|
1680
|
-
if (payload === null || typeof payload !== 'object') return
|
|
1681
|
-
if (payload.adapter !== undefined || payload.health !== undefined) acceptSummary(payload)
|
|
1682
|
-
else if (payload.kind !== undefined || payload.summary !== undefined) acceptEvent(payload)
|
|
1683
|
-
}
|
|
1684
|
-
const parseFrame = (data, accept) => {
|
|
1685
|
-
try {
|
|
1686
|
-
accept(JSON.parse(data))
|
|
1687
|
-
} catch (error) {
|
|
1688
|
-
/* a malformed frame must not break the badge */
|
|
1689
|
-
}
|
|
1690
|
-
}
|
|
1691
|
-
const probe = () => {
|
|
1692
|
-
if (disposed || sawSnapshot || typeof fetch !== 'function') return
|
|
1693
|
-
fetchSummaryJson().then((payload) => {
|
|
1694
|
-
if (!disposed) acceptSummary(payload)
|
|
1695
|
-
}, () => {
|
|
1696
|
-
if (!disposed && !sawSnapshot) setState((previous) => (previous.summary !== null ? previous : { status: 'unavailable', summary: null, recent: [] }))
|
|
1697
|
-
})
|
|
1698
|
-
}
|
|
1699
|
-
|
|
1700
|
-
if (typeof EventSource !== 'function') {
|
|
1701
|
-
probe()
|
|
1702
|
-
} else {
|
|
1703
|
-
let source
|
|
1704
|
-
try {
|
|
1705
|
-
source = new EventSource(eventsUrl)
|
|
1706
|
-
} catch (error) {
|
|
1707
|
-
source = undefined
|
|
1708
|
-
}
|
|
1709
|
-
if (source === undefined) {
|
|
1710
|
-
probe()
|
|
1711
|
-
} else {
|
|
1712
|
-
source.addEventListener('snapshot', (event) => parseFrame(event.data, acceptSummary))
|
|
1713
|
-
source.addEventListener('journal', (event) => parseFrame(event.data, acceptEvent))
|
|
1714
|
-
source.onmessage = (event) => parseFrame(event.data, acceptAny)
|
|
1715
|
-
source.onerror = () => {
|
|
1716
|
-
// The browser reconnects a dropped stream by itself; a route that
|
|
1717
|
-
// was never mounted settles as CLOSED and is probed once instead.
|
|
1718
|
-
if (source.readyState === 2 /* CLOSED */ && !sawSnapshot) probe()
|
|
1719
|
-
}
|
|
1720
|
-
return () => {
|
|
1721
|
-
disposed = true
|
|
1722
|
-
source.close()
|
|
1723
|
-
}
|
|
1724
|
-
}
|
|
1725
|
-
}
|
|
1726
|
-
return () => { disposed = true }
|
|
1727
|
-
}, [eventsUrl])
|
|
1728
|
-
|
|
1729
|
-
// The stream's `snapshot` frame is read once, at connect, so the header
|
|
1730
|
-
// counters would grow stale for as long as the page stays open. Refresh
|
|
1731
|
-
// the summary while (and only while) the panel is open: immediately on
|
|
1732
|
-
// open, then every HEALTH_REFRESH_MS. `recent` stays owned by the live
|
|
1733
|
-
// frames, and a failed refresh keeps the last good header.
|
|
1734
|
-
useEffect(() => {
|
|
1735
|
-
if (!open) return undefined
|
|
1736
|
-
let disposed = false
|
|
1737
|
-
const refresh = () => {
|
|
1738
|
-
if (typeof fetch !== 'function') return
|
|
1739
|
-
fetchSummaryJson().then((payload) => {
|
|
1740
|
-
if (disposed || payload === null || typeof payload !== 'object') return
|
|
1741
|
-
setState((previous) => ({ status: 'ok', summary: payload, recent: previous.recent }))
|
|
1742
|
-
}, () => {
|
|
1743
|
-
/* the last good header stays on screen */
|
|
1744
|
-
})
|
|
1745
|
-
}
|
|
1746
|
-
refresh()
|
|
1747
|
-
const timer = setInterval(refresh, HEALTH_REFRESH_MS)
|
|
1748
|
-
return () => {
|
|
1749
|
-
disposed = true
|
|
1750
|
-
clearInterval(timer)
|
|
1751
|
-
}
|
|
1752
|
-
}, [open, summaryUrl])
|
|
1753
|
-
|
|
1754
|
-
// The panel is a popover, so it closes the moment it stops being the thing
|
|
1755
|
-
// the user is looking at: a pointer press outside it, Escape, or the
|
|
1756
|
-
// window losing focus. Both surfaces carry `data-mc-surface` (the panel is
|
|
1757
|
-
// `position: fixed`, so hit-testing its box would be fragile).
|
|
1758
|
-
useEffect(() => {
|
|
1759
|
-
if (!open) return undefined
|
|
1760
|
-
const onPointerDown = (event) => {
|
|
1761
|
-
const target = event === null || event === undefined ? null : event.target
|
|
1762
|
-
const inside = target !== null && target !== undefined
|
|
1763
|
-
&& typeof target.closest === 'function' && target.closest('[data-mc-surface]') !== null
|
|
1764
|
-
if (!inside) setOpen(false)
|
|
1765
|
-
}
|
|
1766
|
-
const onKeyDown = (event) => {
|
|
1767
|
-
if (event !== null && event !== undefined && event.key === 'Escape') setOpen(false)
|
|
1768
|
-
}
|
|
1769
|
-
const onWindowBlur = () => setOpen(false)
|
|
1770
|
-
document.addEventListener('pointerdown', onPointerDown, true)
|
|
1771
|
-
document.addEventListener('keydown', onKeyDown, true)
|
|
1772
|
-
window.addEventListener('blur', onWindowBlur)
|
|
1773
|
-
return () => {
|
|
1774
|
-
document.removeEventListener('pointerdown', onPointerDown, true)
|
|
1775
|
-
document.removeEventListener('keydown', onKeyDown, true)
|
|
1776
|
-
window.removeEventListener('blur', onWindowBlur)
|
|
1777
|
-
}
|
|
1778
|
-
}, [open])
|
|
1779
|
-
|
|
1780
|
-
const summary = state.summary
|
|
1781
|
-
const health = trimSummary(summary)
|
|
1782
|
-
// The pill answers "is anything wrong NOW": a RECENT error (`recentErrors`,
|
|
1783
|
-
// 15-minute window), not the cumulative `errorsSinceBoot` — that count in a
|
|
1784
|
-
// long-lived process kept the badge red for a whole day on provider-side
|
|
1785
|
-
// failures the adapter itself had already classified and retried. The
|
|
1786
|
-
// cumulative number is still what the panel's ledger `E` cell reports.
|
|
1787
|
-
const errorsSinceBoot = typeof health.errorsSinceBoot === 'number' ? health.errorsSinceBoot : 0
|
|
1788
|
-
const errors = recentErrors
|
|
1789
|
-
const passes = typeof health.passes24h === 'number' ? health.passes24h : 0
|
|
1790
|
-
// The one number that matters is the context pressure, not the pass
|
|
1791
|
-
// count (92% of passes are defers — noise). Tone follows it: green
|
|
1792
|
-
// below the execute band, amber approaching, orange inside it.
|
|
1793
|
-
const pressure = health.lastPressure
|
|
1794
|
-
const tone = state.status === 'unavailable' ? '#d08700'
|
|
1795
|
-
: state.status !== 'ok' ? '#8a8f98'
|
|
1796
|
-
: errors > 0 ? '#e5484d'
|
|
1797
|
-
: pressure === undefined ? '#22a06b'
|
|
1798
|
-
: pressure >= 65 ? '#e5484d'
|
|
1799
|
-
: pressure >= 50 ? '#d08700'
|
|
1800
|
-
: '#22a06b'
|
|
1801
|
-
const pressureText = pressure === undefined ? '' : Math.round(pressure) + '%'
|
|
1802
|
-
// Shortest possible line: the pressure, or the error count when red.
|
|
1803
|
-
const collapsed = state.status === 'unavailable'
|
|
1804
|
-
? 'MC · —'
|
|
1805
|
-
: state.status !== 'ok'
|
|
1806
|
-
? 'MC · …'
|
|
1807
|
-
: errors > 0
|
|
1808
|
-
? 'MC · ' + errors + ' err'
|
|
1809
|
-
: pressureText !== ''
|
|
1810
|
-
? 'MC · ' + pressureText
|
|
1811
|
-
: 'MC · ' + passes + ' pass'
|
|
1812
|
-
const lastSeen = timeOf(health.lastEventAt)
|
|
1813
|
-
const scopeLabel = health.scope === 'session' ? 'this session' : 'all sessions'
|
|
1814
|
-
// The pill's hover bubble mirrors the native pills' tooltip (one dark
|
|
1815
|
-
// rounded line); the full diagnostic title lives on the panel only.
|
|
1816
|
-
const tipText = state.status === 'unavailable'
|
|
1817
|
-
? t('tip.unavailable')
|
|
1818
|
-
: state.status !== 'ok'
|
|
1819
|
-
? t('tip.connecting')
|
|
1820
|
-
: errors > 0
|
|
1821
|
-
? t('tip.errors', { n: errors })
|
|
1822
|
-
: pressureText !== ''
|
|
1823
|
-
? health.projectMemories !== undefined
|
|
1824
|
-
? t('tip.pressureWithMemories', { pressure: pressureText, memories: fmtInt(health.projectMemories) })
|
|
1825
|
-
: t('tip.pressure', { pressure: pressureText })
|
|
1826
|
-
: t('tip.live')
|
|
1827
|
-
|
|
1828
|
-
const inDock = placement === 'statusbar'
|
|
1829
|
-
// The official pill skin: hover + open keep the interactive background
|
|
1830
|
-
// (injected once — inline styles cannot express :hover), and the pill
|
|
1831
|
-
// rides in the official Tooltip bubble when the primitives module exists.
|
|
1832
|
-
const pillCore = el('button', {
|
|
1833
|
-
type: 'button',
|
|
1834
|
-
'data-mc-surface': '1',
|
|
1835
|
-
ref: pillRef,
|
|
1836
|
-
'aria-label': tipText,
|
|
1837
|
-
'aria-expanded': open,
|
|
1838
|
-
onClick: () => {
|
|
1839
|
-
if (!open) setAnchor(anchorAbove(pillRef === null || pillRef === undefined ? null : pillRef.current))
|
|
1840
|
-
setOpen((value) => !value)
|
|
1841
|
-
},
|
|
1842
|
-
// The official pill's hover/open skin, driven by state: an injected
|
|
1843
|
-
// :hover rule kept losing to (or sticking past) the inline styles, so
|
|
1844
|
-
// the background is computed here instead — same token, no specificity.
|
|
1845
|
-
onMouseEnter: () => setHovered(true),
|
|
1846
|
-
onMouseLeave: () => setHovered(false),
|
|
1847
|
-
style: {
|
|
1848
|
-
pointerEvents: 'auto',
|
|
1849
|
-
display: 'inline-flex',
|
|
1850
|
-
alignItems: 'center',
|
|
1851
|
-
// In the dock row: adopt the neighbouring pills' skin (12/20 tertiary
|
|
1852
|
-
// tier, transparent, nowrap, tabular figures) so the row reads as one
|
|
1853
|
-
// set. The overlay fallback keeps its own compact monospace badge.
|
|
1854
|
-
gap: inDock ? 5 : 6,
|
|
1855
|
-
font: inDock ? 'inherit' : '11px/1.4 ui-monospace, SFMono-Regular, Menlo, monospace',
|
|
1856
|
-
fontVariantNumeric: inDock ? 'tabular-nums' : undefined,
|
|
1857
|
-
padding: inDock ? '1px 8px' : '4px 9px',
|
|
1858
|
-
borderRadius: inDock ? 'var(--dsw-radius-sm, 6px)' : 999,
|
|
1859
|
-
border: inDock ? 'none' : '1px solid var(--dsw-alias-border-secondary, rgba(127,127,127,.35))',
|
|
1860
|
-
// Hover/open skin (the official pill's token), computed from state.
|
|
1861
|
-
// The dock's own stylesheet paints buttons a static grey, so the
|
|
1862
|
-
// resting state must be an EXPLICIT transparent — leaving it unset
|
|
1863
|
-
// lets that grey through, and a :hover rule cannot win against the
|
|
1864
|
-
// inline style anyway.
|
|
1865
|
-
background: inDock
|
|
1866
|
-
? (hovered || open ? 'var(--dsw-alias-interactive-bg-hover, rgba(127,127,127,.16))' : 'transparent')
|
|
1867
|
-
: 'transparent',
|
|
1868
|
-
color: errors > 0 && state.status === 'ok'
|
|
1869
|
-
? 'var(--dsw-alias-label-error, #e5484d)'
|
|
1870
|
-
: inDock
|
|
1871
|
-
? 'var(--dsw-alias-label-tertiary, currentColor)'
|
|
1872
|
-
: 'var(--dsw-alias-label-secondary, currentColor)',
|
|
1873
|
-
cursor: 'pointer',
|
|
1874
|
-
whiteSpace: 'nowrap',
|
|
1875
|
-
transition: 'background-color .12s ease',
|
|
1876
|
-
},
|
|
1877
|
-
}, [
|
|
1878
|
-
el('span', {
|
|
1879
|
-
key: 'dot',
|
|
1880
|
-
style: {
|
|
1881
|
-
width: inDock ? 6 : 7,
|
|
1882
|
-
height: inDock ? 6 : 7,
|
|
1883
|
-
borderRadius: '50%',
|
|
1884
|
-
background: tone,
|
|
1885
|
-
display: 'inline-block',
|
|
1886
|
-
flex: '0 0 auto',
|
|
1887
|
-
},
|
|
1888
|
-
}),
|
|
1889
|
-
el('span', { key: 'text' }, collapsed),
|
|
1890
|
-
])
|
|
1891
|
-
const pill = Tooltip !== undefined
|
|
1892
|
-
? el(Tooltip, { label: tipText, side: 'top', delayMs: 200, disabled: open }, pillCore)
|
|
1893
|
-
: pillCore
|
|
1894
|
-
|
|
1895
|
-
const place = anchor === null ? PANEL_FALLBACK_ANCHOR : anchor
|
|
1896
|
-
|
|
1897
|
-
if (!open) {
|
|
1898
|
-
return placement === 'overlay'
|
|
1899
|
-
? el('div', { style: { position: 'fixed', right: 12, bottom: 10, zIndex: 40, pointerEvents: 'none' } }, pill)
|
|
1900
|
-
// Dock entry root: shrink-wraps the pill (like the neighbouring stat
|
|
1901
|
-
// pills' anchor) and inherits the dock row's 12/20 text tier, so the
|
|
1902
|
-
// fixed-position panel measures the pill and the row stays one line.
|
|
1903
|
-
: el('div', {
|
|
1904
|
-
style: {
|
|
1905
|
-
display: 'inline-flex',
|
|
1906
|
-
alignItems: 'center',
|
|
1907
|
-
minWidth: 0,
|
|
1908
|
-
pointerEvents: 'auto',
|
|
1909
|
-
fontSize: 'calc(var(--dsh-content-font-size-secondary, 13px) - 1px)',
|
|
1910
|
-
lineHeight: 'calc(20px + var(--dsh-content-font-delta-secondary, 0px))',
|
|
1911
|
-
},
|
|
1912
|
-
}, pill)
|
|
1913
|
-
}
|
|
1914
|
-
|
|
1915
|
-
// ---- Panel v3: status dot + pressure gauge, label-grid summary,
|
|
1916
|
-
// categorized operation feed. System font for prose, tabular numerals
|
|
1917
|
-
// for figures; the old full-monospace panel read like a debug console.
|
|
1918
|
-
const statusMeta = {
|
|
1919
|
-
ok: [t('status.ok'), '#22a06b'],
|
|
1920
|
-
connecting: [t('status.connecting'), '#d08700'],
|
|
1921
|
-
unavailable: [t('status.unavailable'), '#e5484d'],
|
|
1922
|
-
}
|
|
1923
|
-
const [statusLabel, statusTone] = statusMeta[state.status] ?? [String(state.status), '#8a8f98']
|
|
1924
|
-
const pressureValue = pressure === undefined ? undefined : Math.round(pressure)
|
|
1925
|
-
/** Ledger precision: two decimals, so O and the big %-age can differ
|
|
1926
|
-
* honestly (73.00% vs the rounded 73%). */
|
|
1927
|
-
const pressureExact = pressure === undefined ? undefined : pressure.toFixed(2)
|
|
1928
|
-
const pressureTone = pressureValue === undefined ? 'inherit' : pressureValue >= 85 ? '#e5484d' : pressureValue >= 60 ? '#d08700' : '#22a06b'
|
|
1929
|
-
|
|
1930
|
-
/** Ledger cell → the full name a hover shows (official Tooltip). */
|
|
1931
|
-
const LEDGER_LABELS = {
|
|
1932
|
-
O: t('ledger.O'),
|
|
1933
|
-
L: t('ledger.L'),
|
|
1934
|
-
M: t('ledger.M'),
|
|
1935
|
-
P: t('ledger.P'),
|
|
1936
|
-
U: t('ledger.U'),
|
|
1937
|
-
T: t('ledger.T'),
|
|
1938
|
-
K: t('ledger.K'),
|
|
1939
|
-
V: t('ledger.V'),
|
|
1940
|
-
R: t('ledger.R'),
|
|
1941
|
-
E: t('ledger.E'),
|
|
1942
|
-
}
|
|
1943
|
-
|
|
1944
|
-
/** One dense single-letter line; every cell is hoverable and shows its
|
|
1945
|
-
* full name through the official Tooltip (when the primitives module
|
|
1946
|
-
* exists). Cells stay on one line, wrapping only as a last resort. */
|
|
1947
|
-
const kv = (cells) => {
|
|
1948
|
-
const parts = []
|
|
1949
|
-
cells.forEach((cell) => {
|
|
1950
|
-
if (cell === '' || cell === undefined || cell === null) return
|
|
1951
|
-
const label = LEDGER_LABELS[cell.charAt(0)]
|
|
1952
|
-
const span = el('span', { style: { minWidth: 0 } }, cell)
|
|
1953
|
-
parts.push(label !== undefined && Tooltip !== undefined
|
|
1954
|
-
? el(Tooltip, { key: 't-' + cell, label, side: 'top', delayMs: 200 }, span)
|
|
1955
|
-
: span)
|
|
1956
|
-
})
|
|
1957
|
-
if (parts.length === 0) return null
|
|
1958
|
-
const withSeparators = []
|
|
1959
|
-
parts.forEach((part, index) => {
|
|
1960
|
-
if (index > 0) withSeparators.push(el('span', { key: 'sep-' + index, style: { opacity: 0.45 } }, '·'))
|
|
1961
|
-
withSeparators.push(part)
|
|
1962
|
-
})
|
|
1963
|
-
return el('div', {
|
|
1964
|
-
key: 'kv-' + cells.filter(Boolean).join('').slice(0, 10),
|
|
1965
|
-
style: { display: 'flex', flexWrap: 'wrap', columnGap: 6, rowGap: 2, padding: '2.5px 0', fontVariantNumeric: 'tabular-nums', alignItems: 'baseline', minWidth: 0 },
|
|
1966
|
-
}, withSeparators)
|
|
1967
|
-
}
|
|
1968
|
-
|
|
1969
|
-
// The single-letter ledger (compact enough to stay on ONE line):
|
|
1970
|
-
// O occupancy % (native projection, 2dp) L post-compaction ledger
|
|
1971
|
-
// M memories in this project P promoted in 24h
|
|
1972
|
-
// U unverified rows T last maintenance tick
|
|
1973
|
-
// K ttl-killed / V verified / R revised E errors
|
|
1974
|
-
const projectTotal = health.memoryStats !== undefined && health.memoryStats.total !== undefined
|
|
1975
|
-
? health.memoryStats.total
|
|
1976
|
-
: health.projectMemories
|
|
1977
|
-
const mt = health.maintenanceLast
|
|
1978
|
-
const summaryRows = [
|
|
1979
|
-
kv([
|
|
1980
|
-
pressureExact !== undefined ? 'O ' + pressureExact + '%' : '',
|
|
1981
|
-
health.lastTokens !== undefined && health.lastContextWindow
|
|
1982
|
-
? 'L ' + compactTok(health.lastTokens) + '/' + compactTok(health.lastContextWindow)
|
|
1983
|
-
: '',
|
|
1984
|
-
projectTotal !== undefined ? 'M ' + fmtInt(projectTotal) : '',
|
|
1985
|
-
health.promote24h > 0 ? 'P ' + fmtInt(health.promote24h) : '',
|
|
1986
|
-
health.memoryStats !== undefined && health.memoryStats.unverified !== undefined
|
|
1987
|
-
? 'U ' + fmtInt(health.memoryStats.unverified)
|
|
1988
|
-
: '',
|
|
1989
|
-
mt === undefined ? '' : typeof mt.at === 'number' ? 'T ' + timeShort(mt.at) : 'T',
|
|
1990
|
-
mt !== undefined && mt.expired ? 'K ' + fmtInt(mt.expired) : '',
|
|
1991
|
-
mt !== undefined && mt.verified ? 'V ' + fmtInt(mt.verified) : '',
|
|
1992
|
-
mt !== undefined && mt.updated ? 'R ' + fmtInt(mt.updated) : '',
|
|
1993
|
-
errorsSinceBoot > 0 ? 'E ' + fmtInt(errorsSinceBoot) : '',
|
|
1994
|
-
]),
|
|
1995
|
-
]
|
|
1996
|
-
|
|
1997
|
-
const records = recordsOf(state.recent, t)
|
|
1998
|
-
const noise = noiseLine(state.recent, t)
|
|
1999
|
-
const recordNodes = records.length === 0
|
|
2000
|
-
? [el('div', { key: 'empty', style: { padding: '4px 0', opacity: 0.55 } }, state.status === 'unavailable' ? t('records.noRoute') : t('records.empty'))]
|
|
2001
|
-
: records.slice(0, MAX_RECORDS).map((record, index) => {
|
|
2002
|
-
const meta = RECORD_CATEGORIES[record.category] === undefined ? RECORD_CATEGORIES.archive : RECORD_CATEGORIES[record.category]
|
|
2003
|
-
const label = t(meta.labelKey)
|
|
2004
|
-
const body = record.detail !== '' ? record.detail : typeof record.summary === 'string' ? record.summary : ''
|
|
2005
|
-
const folded = record.count > 1 ? body + ' · ' + t('records.folded', { n: record.count }) : body
|
|
2006
|
-
return el('div', {
|
|
2007
|
-
key: 'record-' + index,
|
|
2008
|
-
title: label + (record.count > 1 ? ' ×' + record.count : '') + (record.summary !== '' && record.summary !== body ? '\n' + record.summary : ''),
|
|
2009
|
-
style: {
|
|
2010
|
-
display: 'flex',
|
|
2011
|
-
gap: 8,
|
|
2012
|
-
alignItems: 'center',
|
|
2013
|
-
padding: '5px 0',
|
|
2014
|
-
borderTop: index === 0 ? 'none' : '1px solid rgba(127,127,127,.12)',
|
|
2015
|
-
},
|
|
2016
|
-
}, [
|
|
2017
|
-
el('span', { key: 'dot', style: { flex: '0 0 auto', width: 7, height: 7, borderRadius: 4, background: meta.tone } }),
|
|
2018
|
-
el('span', { key: 'label', style: { flex: '0 0 auto' } }, label),
|
|
2019
|
-
el('span', {
|
|
2020
|
-
key: 'detail',
|
|
2021
|
-
style: { flex: '1 1 auto', minWidth: 0, opacity: 0.6, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' },
|
|
2022
|
-
}, folded),
|
|
2023
|
-
el('span', { key: 'at', style: { flex: '0 0 auto', opacity: 0.45, fontVariantNumeric: 'tabular-nums' } }, timeShort(record.at)),
|
|
2024
|
-
])
|
|
2025
|
-
})
|
|
2026
|
-
|
|
2027
|
-
const sectionTitle = (key, title, extra) => el('div', {
|
|
2028
|
-
key,
|
|
2029
|
-
style: { display: 'flex', justifyContent: 'space-between', alignItems: 'baseline', margin: '10px 0 3px' },
|
|
2030
|
-
}, [
|
|
2031
|
-
el('span', { key: 't', style: { fontSize: 10, letterSpacing: '.08em', textTransform: 'uppercase', opacity: 0.5 } }, title),
|
|
2032
|
-
extra === undefined ? null : el('span', { key: 'e', style: { fontSize: 10.5, opacity: 0.5 } }, extra),
|
|
2033
|
-
])
|
|
2034
|
-
|
|
2035
|
-
const panel = el('div', {
|
|
2036
|
-
'data-mc-surface': '1',
|
|
2037
|
-
style: {
|
|
2038
|
-
position: 'fixed',
|
|
2039
|
-
right: place.right,
|
|
2040
|
-
bottom: place.bottom,
|
|
2041
|
-
zIndex: 60,
|
|
2042
|
-
pointerEvents: 'auto',
|
|
2043
|
-
width: 352,
|
|
2044
|
-
maxWidth: 'calc(100vw - 24px)',
|
|
2045
|
-
maxHeight: place.maxHeight,
|
|
2046
|
-
overflowY: 'auto',
|
|
2047
|
-
padding: '12px 14px 10px',
|
|
2048
|
-
borderRadius: 12,
|
|
2049
|
-
border: '1px solid var(--dsw-alias-border-secondary, rgba(127,127,127,.3))',
|
|
2050
|
-
background: 'var(--dsw-alias-bg-layer-1, rgba(24,24,27,.97))',
|
|
2051
|
-
color: 'var(--dsw-alias-label-primary, #e6e6e6)',
|
|
2052
|
-
boxShadow: '0 12px 32px rgba(0,0,0,.22)',
|
|
2053
|
-
font: '12px/1.55 -apple-system, "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif',
|
|
2054
|
-
},
|
|
2055
|
-
}, [
|
|
2056
|
-
el('div', { key: 'head', style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center' } }, [
|
|
2057
|
-
el('div', { key: 'who', style: { display: 'flex', alignItems: 'center', gap: 7 } }, [
|
|
2058
|
-
el('span', { key: 'dot', style: { width: 8, height: 8, borderRadius: 4, background: statusTone, boxShadow: '0 0 6px ' + statusTone } }),
|
|
2059
|
-
el('strong', { key: 'title', style: { fontSize: 12.5, fontWeight: 600, letterSpacing: '.01em' } }, 'Magic Context'),
|
|
2060
|
-
el('span', { key: 'status', style: { fontSize: 10.5, opacity: 0.55 } }, statusLabel),
|
|
2061
|
-
]),
|
|
2062
|
-
pressureValue === undefined
|
|
2063
|
-
? el('span', { key: 'p', style: { fontSize: 10.5, opacity: 0.4 } }, '—')
|
|
2064
|
-
: el('span', {
|
|
2065
|
-
key: 'p',
|
|
2066
|
-
style: { fontSize: 17, fontWeight: 600, lineHeight: 1, color: pressureTone, fontVariantNumeric: 'tabular-nums' },
|
|
2067
|
-
}, pressureValue + '%'),
|
|
2068
|
-
]),
|
|
2069
|
-
el('div', { key: 'summary', style: { marginTop: 8, paddingTop: 8, borderTop: '1px solid rgba(127,127,127,.14)' } }, summaryRows),
|
|
2070
|
-
sectionTitle('records-head', t('records.head'), records.length === 0 ? undefined : t('records.count', { n: records.length })),
|
|
2071
|
-
el('div', { key: 'records', style: { display: 'grid', gridTemplateColumns: 'minmax(0, 1fr)' } }, recordNodes),
|
|
2072
|
-
el('div', {
|
|
2073
|
-
key: 'foot',
|
|
2074
|
-
title: noise === '' ? undefined : t('records.beats', { beats: noise }),
|
|
2075
|
-
style: { marginTop: 8, paddingTop: 6, borderTop: '1px solid rgba(127,127,127,.14)', opacity: 0.45, fontSize: 10.5 },
|
|
2076
|
-
}, t('records.foot')),
|
|
2077
|
-
])
|
|
2078
|
-
|
|
2079
|
-
return placement === 'overlay'
|
|
2080
|
-
? el('div', {
|
|
2081
|
-
style: { position: 'fixed', right: 12, bottom: 10, zIndex: 40, display: 'flex', flexDirection: 'column', alignItems: 'flex-end', pointerEvents: 'none' },
|
|
2082
|
-
}, [panel, pill])
|
|
2083
|
-
: el('div', {
|
|
2084
|
-
style: {
|
|
2085
|
-
display: 'inline-flex',
|
|
2086
|
-
alignItems: 'center',
|
|
2087
|
-
minWidth: 0,
|
|
2088
|
-
pointerEvents: 'auto',
|
|
2089
|
-
fontSize: 'calc(var(--dsh-content-font-size-secondary, 13px) - 1px)',
|
|
2090
|
-
lineHeight: 'calc(20px + var(--dsh-content-font-delta-secondary, 0px))',
|
|
2091
|
-
},
|
|
2092
|
-
}, [panel, pill])
|
|
2093
|
-
}
|
|
2094
|
-
|
|
2095
|
-
/**
|
|
2096
|
-
* The dock pill (`conversation.composer.dock`, the composer's status row).
|
|
2097
|
-
*
|
|
2098
|
-
* The seat is session-scoped, so the framework hands the component the
|
|
2099
|
-
* current `sessionId`; that id is what scopes both telemetry requests to the
|
|
2100
|
-
* conversation in front of the user. A shell that omits it (or an older
|
|
2101
|
-
* shell) degrades to the Host's whole-store view instead of failing.
|
|
2102
|
-
*/
|
|
2103
|
-
function StatusBarBadge(props) {
|
|
2104
|
-
return TelemetryBadge({
|
|
2105
|
-
placement: 'statusbar',
|
|
2106
|
-
sessionId: props === undefined || props === null ? undefined : props.sessionId,
|
|
2107
|
-
t: props === undefined || props === null ? undefined : props.t,
|
|
2108
|
-
})
|
|
2109
|
-
}
|
|
2110
|
-
|
|
2111
|
-
/** The floating pill (`shell.overlay`), used only when the status seat never appears. */
|
|
2112
|
-
function OverlayBadge(props) {
|
|
2113
|
-
return TelemetryBadge({
|
|
2114
|
-
placement: 'overlay',
|
|
2115
|
-
t: props === undefined || props === null ? undefined : props.t,
|
|
2116
|
-
})
|
|
2117
|
-
}
|
|
2118
|
-
|
|
2119
|
-
/* ------------------------------------------------------------------ *
|
|
2120
|
-
* Plugin face
|
|
2121
|
-
* ------------------------------------------------------------------ */
|
|
2122
|
-
|
|
2123
|
-
/**
|
|
2124
|
-
* Register the telemetry pill and the settings card.
|
|
2125
|
-
*
|
|
2126
|
-
* The pill goes into the composer's dock row (`conversation.composer.dock`,
|
|
2127
|
-
* the "status bar" under the input line) next to the activity and Token
|
|
2128
|
-
* usage pills, but ONLY there — one seat, so no duplicate pill. The
|
|
2129
|
-
* `shell.overlay` registration is the fallback for a shell that never
|
|
2130
|
-
* declares that seat, and it is dropped as soon as the dock seat arrives
|
|
2131
|
-
* (so the two never coexist). The overlay is mounted only after
|
|
2132
|
-
* {@link OVERLAY_FALLBACK_MS} so a normal boot shows no corner flash, and
|
|
2133
|
-
* a shell whose composer never mounts still gets a badge.
|
|
2134
|
-
*
|
|
2135
|
-
* Both seats are taken through `ctx.slots.inject`, which tolerates a late
|
|
2136
|
-
* declaration, and both registrations live on the parent context, so the
|
|
2137
|
-
* badge survives a deployment without the settings domain. The card waits
|
|
2138
|
-
* on `configForms` through `ctx.inject` rather than a hard plugin
|
|
2139
|
-
* dependency, so the badge is never held back by it.
|
|
2140
|
-
*
|
|
2141
|
-
* @param ctx - the browser plugin context.
|
|
2142
|
-
*/
|
|
2143
|
-
function apply(ctx) {
|
|
2144
|
-
// The live strip (conversation.input.dock) and the per-Turn frozen blocks
|
|
2145
|
-
// (conversation.chat.turnTail) were REMOVED by decision on 2026-10-07:
|
|
2146
|
-
// the session stream now carries the durable record — one `notice` entry
|
|
2147
|
-
// per action-bearing management pass, appended by the adapter itself
|
|
2148
|
-
// (src/dsh/session-notice.ts) and rendered natively by the transcript.
|
|
2149
|
-
// The pill + panel below remain the real-time/telemetry surface.
|
|
2150
|
-
|
|
2151
|
-
// The dictionaries are registered in an effect so a reload re-registers
|
|
2152
|
-
// them: `locale.register` throws on a duplicate namespace+locale, and the
|
|
2153
|
-
// effect's disposer drops the entries before the re-run.
|
|
2154
|
-
ctx.effect(
|
|
2155
|
-
() => ctx.locale.register(LOCALE_NS, { zh: DICTIONARIES.zh, en: DICTIONARIES.en }),
|
|
2156
|
-
'magic-context: dictionaries',
|
|
2157
|
-
)
|
|
2158
|
-
// `bind` returns one stable function that resolves the ACTIVE language on
|
|
2159
|
-
// every call, so these thunks follow a switch without being re-created.
|
|
2160
|
-
const t = ctx.locale.bind(LOCALE_NS)
|
|
2161
|
-
const telemetryLabel = () => t('label.telemetry')
|
|
2162
|
-
|
|
2163
|
-
ctx.effect(() => {
|
|
2164
|
-
let overlayOff
|
|
2165
|
-
let seatTaken = false
|
|
2166
|
-
const mountOverlay = () => {
|
|
2167
|
-
if (seatTaken || overlayOff !== undefined) return
|
|
2168
|
-
overlayOff = ctx.slots.inject('shell.overlay', () => ctx.slots.register({
|
|
2169
|
-
name: 'shell.overlay',
|
|
2170
|
-
id: 'magic-context.telemetry',
|
|
2171
|
-
order: 100,
|
|
2172
|
-
label: telemetryLabel,
|
|
2173
|
-
locale: LOCALE_NS,
|
|
2174
|
-
}, OverlayBadge))
|
|
2175
|
-
}
|
|
2176
|
-
const unmountOverlay = () => {
|
|
2177
|
-
if (overlayOff === undefined) return
|
|
2178
|
-
const off = overlayOff
|
|
2179
|
-
overlayOff = undefined
|
|
2180
|
-
off()
|
|
2181
|
-
}
|
|
2182
|
-
const fallback = setTimeout(mountOverlay, OVERLAY_FALLBACK_MS)
|
|
2183
|
-
const seatOff = ctx.slots.inject(STATUS_SEAT, () => {
|
|
2184
|
-
seatTaken = true
|
|
2185
|
-
clearTimeout(fallback)
|
|
2186
|
-
const offPill = ctx.slots.register({
|
|
2187
|
-
name: STATUS_SEAT,
|
|
2188
|
-
id: 'magic-context.telemetry',
|
|
2189
|
-
order: STATUS_SEAT_ORDER,
|
|
2190
|
-
label: telemetryLabel,
|
|
2191
|
-
locale: LOCALE_NS,
|
|
2192
|
-
}, StatusBarBadge)
|
|
2193
|
-
unmountOverlay()
|
|
2194
|
-
return offPill
|
|
2195
|
-
})
|
|
2196
|
-
return () => {
|
|
2197
|
-
clearTimeout(fallback)
|
|
2198
|
-
seatOff()
|
|
2199
|
-
unmountOverlay()
|
|
2200
|
-
}
|
|
2201
|
-
}, 'magic-context: telemetry badge')
|
|
2202
|
-
|
|
2203
|
-
// The model catalog is optional: without `remote.session` (or its
|
|
2204
|
-
// `modelCatalog` RPC) the card stays on plain text for the verify pin.
|
|
2205
|
-
const catalog = createModelCatalog()
|
|
2206
|
-
ctx.inject(['remote', 'remote.session'], (remoteCtx) => {
|
|
2207
|
-
catalog.attach(remoteCtx)
|
|
2208
|
-
})
|
|
2209
|
-
|
|
2210
|
-
ctx.inject(['configForms'], (settingsCtx) => {
|
|
2211
|
-
const scope = settingsCtx.configForms.get(CFG_NS)
|
|
2212
|
-
const Card = makeCard(scope, catalog)
|
|
2213
|
-
settingsCtx.effect(() => settingsCtx.configForms.whileServed([CFG_NS], () => {
|
|
2214
|
-
const offItem = ctx.slots.inject('plugins.item', () => ctx.slots.register({
|
|
2215
|
-
name: 'plugins.item',
|
|
2216
|
-
id: CFG_NS,
|
|
2217
|
-
order: 20,
|
|
2218
|
-
label: () => t('title'),
|
|
2219
|
-
locale: LOCALE_NS,
|
|
2220
|
-
}, Card))
|
|
2221
|
-
const offRow = ctx.slots.inject('plugins.row.config', () => ctx.slots.register({
|
|
2222
|
-
name: 'plugins.row.config',
|
|
2223
|
-
key: ROW_CONFIG_KEY,
|
|
2224
|
-
order: 20,
|
|
2225
|
-
locale: LOCALE_NS,
|
|
2226
|
-
}, Card))
|
|
2227
|
-
return () => {
|
|
2228
|
-
offItem()
|
|
2229
|
-
offRow()
|
|
2230
|
-
}
|
|
2231
|
-
}), 'magic-context: settings card')
|
|
2232
|
-
})
|
|
2233
|
-
}
|
|
2234
|
-
|
|
2235
|
-
/**
|
|
2236
|
-
* `apply` is the only thing the loader needs. `__diagnostics` exists so the
|
|
2237
|
-
* record mapping can be exercised against a real journal outside a browser
|
|
2238
|
-
* (this repo has no client test runner): feed it journal rows and compare
|
|
2239
|
-
* what the panel would print. It is pure and reads nothing.
|
|
2240
|
-
*/
|
|
2241
|
-
return {
|
|
2242
|
-
name: 'magic-context-client',
|
|
2243
|
-
inject: ['slots', 'locale'],
|
|
2244
|
-
apply,
|
|
2245
|
-
__diagnostics: {
|
|
2246
|
-
recordOf, recordsOf, recordCounts, noiseLine, dictionaryKeys, dictionaryTranslator, dictionaries: DICTIONARIES,
|
|
2247
|
-
// Package identity (the client module id and the row-config key).
|
|
2248
|
-
PKG_NAME, ROW_CONFIG_KEY,
|
|
2249
|
-
// The category table the panel renders from. Exposed so node-side checks
|
|
2250
|
-
// use THIS table instead of a hand-copied mirror — the mirror had
|
|
2251
|
-
// already drifted (`maintain` was missing) and the records-check script
|
|
2252
|
-
// crashed on it instead of reporting a category it did not know.
|
|
2253
|
-
recordCategories: RECORD_CATEGORIES,
|
|
2254
|
-
/** The verify-pin picker reads the host model catalog; pinned on node. */
|
|
2255
|
-
createModelCatalog,
|
|
2256
|
-
/** The pin-pair write semantics (pure; pinned by the client spec). */
|
|
2257
|
-
routePairOps,
|
|
2258
|
-
// The number/text formatters are the panel's display contract; the
|
|
2259
|
-
// node-side spec pins them so a formatting regression (the scope bug
|
|
2260
|
-
// that briefly unmounted the pill) cannot come back silently.
|
|
2261
|
-
compactTok, fmtInt, spanOf, trimSummary, timeShort,
|
|
2262
|
-
// The pill's "is anything wrong NOW" window: pinned by
|
|
2263
|
-
// tests/client-pure.spec.ts so the badge cannot silently fall back to the
|
|
2264
|
-
// cumulative count (it stayed red for 21.5 h of uptime on provider-side
|
|
2265
|
-
// failures the adapter had already handled).
|
|
2266
|
-
countRecentErrors, errorRecentMs: ERROR_RECENT_MS,
|
|
2267
|
-
},
|
|
2268
|
-
}
|
|
2269
|
-
},
|
|
2270
|
-
})
|