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