@elvisw/dsh-magic-context 0.0.0-stage → 0.14.0

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