dsh-xray 0.7.2 → 0.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/client.js CHANGED
@@ -37,6 +37,17 @@ window.__ModuleLoader__.load({
37
37
  '.xray-table{border-collapse:collapse;width:100%;margin-top:6px}',
38
38
  '.xray-table th,.xray-table td{text-align:left;padding:4px 10px;border-bottom:1px solid var(--dsw-alias-border-l2);vertical-align:top}',
39
39
  '.xray-table th{color:var(--dsw-alias-label-tertiary);font-weight:normal}',
40
+ // entry inspection: clickable names + a centered modal with the raw text
41
+ '.xray-entry-link{font:inherit;color:var(--dsw-alias-state-business-primary);background:none;border:none;padding:0;cursor:pointer;text-align:left}',
42
+ '.xray-entry-link:hover{text-decoration:underline}',
43
+ '.xray-entry-overlay{position:fixed;inset:0;z-index:70;background:rgba(0,0,0,.45);display:flex;align-items:center;justify-content:center}',
44
+ '.xray-entry-modal{width:min(760px,90vw);max-height:80vh;display:flex;flex-direction:column;background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l2);border-radius:12px;padding:16px 20px}',
45
+ '.xray-entry-head{display:flex;align-items:baseline;gap:10px;margin-bottom:8px}',
46
+ '.xray-entry-name{font-weight:600;word-break:break-all}',
47
+ '.xray-entry-stats{color:var(--dsw-alias-label-tertiary);font-size:12px}',
48
+ '.xray-entry-close{margin-left:auto;font:inherit;font-size:12px;color:var(--dsw-alias-label-secondary);background:var(--dsw-alias-bg-layer-3);border:1px solid var(--dsw-alias-border-l2);border-radius:6px;padding:2px 10px;cursor:pointer}',
49
+ '.xray-entry-text{flex:1;min-height:0;overflow:auto;margin:0;padding:12px;background:var(--dsw-alias-bg-layer-3);border-radius:8px;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:12px;line-height:1.5;white-space:pre-wrap;word-break:break-word}',
50
+ '.xray-owner-entry{display:inline-block;padding-left:22px}',
40
51
  '.xray-num{text-align:right}',
41
52
  '.xray-warn{color:var(--dsw-alias-state-error-primary,#f85149)}',
42
53
  '.xray-ok{color:var(--dsw-alias-state-success-primary,#3fb950)}',
@@ -58,7 +69,182 @@ window.__ModuleLoader__.load({
58
69
 
59
70
  //#region tiny view primitives
60
71
  const VIEWS = ['summary', 'health', 'deps', 'cost', 'shadow'];
61
- function Table({ headers, rows }) {
72
+ const NS = 'xray';
73
+
74
+ /** English messages (also the key vocabulary; zh mirrors every key). */
75
+ const en = {
76
+ 'view.xray': 'X-Ray',
77
+ 'panel.sub': 'composition X-ray — live from this harness',
78
+ 'panel.loading': 'loading {view}…',
79
+ 'panel.renderFailed': 'render failed: {message}',
80
+ // one question per view: what am I looking at, what does trouble look like?
81
+ 'intro.summary':
82
+ 'Composition at a glance. A non-zero "unhealthy" count means some plugin failed to start — see the health view.',
83
+ 'intro.health':
84
+ 'Plugin lifecycle. "Waiting" plugins declared a dependency that no active plugin provides yet; "unhealthy" fibers failed to start and their features are absent.',
85
+ 'intro.deps':
86
+ 'Who provides and consumes each service. The disable-cascade table answers: if I disable this plugin, which dependents stop working with it?',
87
+ 'intro.cost':
88
+ 'What every LLM request carries before your message: prompt sections + tool schemas, attributed to the plugin that registered each. "By plugin" is each plugin\'s per-request context tax.',
89
+ 'intro.shadow':
90
+ 'Same-name registrations. A service provided by two plugins means one silently wins — usually intended (an override), occasionally a conflict.',
91
+ // hover glossary (native title tooltips), keyed by column header
92
+ 'tip.share':
93
+ 'Percentage of the total estimated context (sections + tool schemas) this row costs on every request',
94
+ 'tip.tokens': 'Rough estimate: ~4 characters per token',
95
+ 'tip.owner': 'The plugin whose registration put this entry into the context',
96
+ 'tip.unattributed':
97
+ 'Registered before dsh-xray mounted and not reconcilable to a single plugin — mount dsh-xray earlier in the profile to shrink this row',
98
+ 'tip.wants':
99
+ 'Services this plugin declared via inject that are not (yet) provided by any active plugin',
100
+ 'tip.fiber': 'One mounted instance of the plugin (a Cordis fiber uid)',
101
+ 'tip.state': 'Cordis lifecycle state: ACTIVE is healthy; FAILED means apply() threw',
102
+ 'tip.affects': 'Transitive consumers: disabling the provider takes these down with it',
103
+ 'tip.providers':
104
+ 'Every plugin claiming this service name; the last one to load wins silently',
105
+ 'tip.registrations':
106
+ 'How many tools/commands this plugin registered on the shared registries',
107
+ 'tip.sections': 'Prompt sections this plugin contributes to the system prompt',
108
+ 'tip.tools': 'Tool schemas this plugin registers (each costs context on every request)',
109
+ // column headers / row labels
110
+ 'col.metric': 'metric',
111
+ 'col.value': 'value',
112
+ 'col.plugin': 'plugin',
113
+ 'col.fiber': 'fiber',
114
+ 'col.state': 'state',
115
+ 'col.error': 'error',
116
+ 'col.waitingPlugin': 'waiting plugin',
117
+ 'col.wants': 'wants',
118
+ 'col.service': 'service',
119
+ 'col.providedBy': 'provided by',
120
+ 'col.consumedBy': 'consumed by',
121
+ 'col.provider': 'provider',
122
+ 'col.affects': 'affects',
123
+ 'col.section': 'section',
124
+ 'col.tool': 'tool',
125
+ 'col.owner': 'owner',
126
+ 'col.tokens': 'tokens',
127
+ 'col.share': 'share',
128
+ 'col.sections': 'sections',
129
+ 'col.tools': 'tools',
130
+ 'col.providers': 'providers',
131
+ 'col.registrations': 'registrations',
132
+ 'row.pluginsMounted': 'plugins mounted',
133
+ 'row.unhealthy': 'unhealthy',
134
+ 'row.services': 'services',
135
+ 'row.contextTokens': 'context tokens (tools + sections)',
136
+ 'summary.captured': 'captured {at}',
137
+ 'health.healthy': '{n} healthy',
138
+ 'health.waiting': '{n} waiting',
139
+ 'health.unhealthy': '{n} unhealthy',
140
+ 'deps.unsatisfied':
141
+ '{n} unsatisfied inject(s) — these plugins wait forever unless a provider is added',
142
+ 'h3.disableCascade': 'disable-cascade',
143
+ 'h3.services': 'services',
144
+ 'h3.byPlugin': 'by plugin',
145
+ 'h3.promptSections': 'prompt sections',
146
+ 'h3.toolSchemas': 'tool schemas',
147
+ 'h3.registrars': 'registrars',
148
+ 'cost.headline':
149
+ '~{total} tokens: {toolCount} tool schema(s) ~{toolTokens} + {sectionCount} prompt section(s) ~{sectionTokens}',
150
+ 'cost.noAssembly': 'no prompt assembly observed yet — send one agent message first',
151
+ 'cost.unattributed': 'unattributed',
152
+ 'shadow.clean': 'no service is provided by more than one plugin',
153
+ 'entry.loading': 'loading entry…',
154
+ 'entry.stats': '{chars} chars · ~{tokens} tokens ({estimator})',
155
+ 'entry.close': 'close',
156
+ 'tip.clickEntry': 'Click to view the exact text this entry puts into every request',
157
+ 'tip.expandPlugin':
158
+ 'A plugin has no single text — its cost is the sum of the entries it registered; click to unfold them',
159
+ 'tip.contextTokens':
160
+ 'Estimated tokens every request carries before your message — see the cost view for the full breakdown',
161
+ };
162
+
163
+ /** Simplified Chinese mirror of every en key. */
164
+ const zh = {
165
+ 'view.xray': 'X 光',
166
+ 'panel.sub': '组合 X 光——实时来自当前 harness',
167
+ 'panel.loading': '正在加载 {view}…',
168
+ 'panel.renderFailed': '渲染失败:{message}',
169
+ 'intro.summary': '组合总览。"unhealthy" 非零表示有插件启动失败——去 health 视图查看。',
170
+ 'intro.health':
171
+ '插件生命周期。"waiting" 表示插件声明的依赖服务尚无活跃提供者;"unhealthy" 表示 fiber 启动失败,其功能缺失。',
172
+ 'intro.deps':
173
+ '每个服务由谁提供、被谁消费。disable-cascade 表回答:禁用某插件后,哪些依赖它的插件会随之失效?',
174
+ 'intro.cost':
175
+ '每次 LLM 请求在你的消息之前携带的内容:prompt sections + 工具 schema,并归因到注册它们的插件。"by plugin" 是每个插件的每请求上下文税。',
176
+ 'intro.shadow':
177
+ '同名注册。一个服务被两个插件同时提供意味着有一方静默胜出——通常是有意覆盖,偶尔是冲突。',
178
+ 'tip.share': '本行在每次请求中占估算总上下文(sections + 工具 schema)的百分比',
179
+ 'tip.tokens': '粗略估算:约 4 个字符折合 1 token',
180
+ 'tip.owner': '把这个条目注入上下文的插件',
181
+ 'tip.unattributed':
182
+ '在 dsh-xray 挂载之前注册、无法唯一归因到某个插件——把 dsh-xray 在 profile 中提前挂载可缩小此行',
183
+ 'tip.wants': '该插件通过 inject 声明、但当前没有任何活跃插件提供的服务',
184
+ 'tip.fiber': '插件的一个挂载实例(Cordis fiber uid)',
185
+ 'tip.state': 'Cordis 生命周期状态:ACTIVE 为健康;FAILED 表示 apply() 抛出了异常',
186
+ 'tip.affects': '传递消费者:禁用该提供者会连带使这些插件失效',
187
+ 'tip.providers': '声明提供此服务的全部插件;后加载者静默胜出',
188
+ 'tip.registrations': '该插件在共享注册表上注册的工具/命令数量',
189
+ 'tip.sections': '该插件贡献给 system prompt 的 sections',
190
+ 'tip.tools': '该插件注册的工具 schema(每个都在每次请求中占用上下文)',
191
+ 'col.metric': '指标',
192
+ 'col.value': '值',
193
+ 'col.plugin': '插件',
194
+ 'col.fiber': 'fiber',
195
+ 'col.state': '状态',
196
+ 'col.error': '错误',
197
+ 'col.waitingPlugin': '等待中的插件',
198
+ 'col.wants': '等待的服务',
199
+ 'col.service': '服务',
200
+ 'col.providedBy': '提供者',
201
+ 'col.consumedBy': '消费者',
202
+ 'col.provider': '提供者',
203
+ 'col.affects': '波及',
204
+ 'col.section': 'section',
205
+ 'col.tool': '工具',
206
+ 'col.owner': '归属',
207
+ 'col.tokens': 'tokens',
208
+ 'col.share': '占比',
209
+ 'col.sections': 'sections',
210
+ 'col.tools': '工具',
211
+ 'col.providers': '提供者',
212
+ 'col.registrations': '注册数',
213
+ 'row.pluginsMounted': '已挂载插件',
214
+ 'row.unhealthy': '不健康',
215
+ 'row.services': '服务',
216
+ 'row.contextTokens': '上下文 tokens(工具 + sections)',
217
+ 'summary.captured': '采集于 {at}',
218
+ 'health.healthy': '{n} 个健康',
219
+ 'health.waiting': '{n} 个等待中',
220
+ 'health.unhealthy': '{n} 个不健康',
221
+ 'deps.unsatisfied': '{n} 个未满足的 inject——除非补上提供者,这些插件将永远等待',
222
+ 'h3.disableCascade': '停用级联',
223
+ 'h3.services': '服务',
224
+ 'h3.byPlugin': '按插件',
225
+ 'h3.promptSections': 'prompt sections',
226
+ 'h3.toolSchemas': '工具 schema',
227
+ 'h3.registrars': '注册方',
228
+ 'cost.headline':
229
+ '约 {total} tokens:{toolCount} 个工具 schema 约 {toolTokens} + {sectionCount} 个 prompt section 约 {sectionTokens}',
230
+ 'cost.noAssembly': '尚未观测到 prompt 装配——先发送一条 agent 消息',
231
+ 'cost.unattributed': '未归因',
232
+ 'shadow.clean': '没有服务被多个插件同时提供',
233
+ 'entry.loading': '正在加载条目…',
234
+ 'entry.stats': '{chars} 字符 · 约 {tokens} tokens({estimator})',
235
+ 'entry.close': '关闭',
236
+ 'tip.clickEntry': '点击查看该条目每次请求实际注入的完整文本',
237
+ 'tip.expandPlugin': '插件本身没有单一文本——它的成本是其注册的全部条目之和;点击展开这些条目',
238
+ 'tip.contextTokens': '每次请求在你的消息之前携带的估算 tokens——完整明细见 cost 视图',
239
+ };
240
+
241
+ /** Fill {placeholders}; the host t() resolves the key, we interpolate. */
242
+ const fill = (text, vars) =>
243
+ vars === undefined
244
+ ? text
245
+ : text.replace(/\{(\w+)\}/g, (m, k) => (k in vars ? String(vars[k]) : m));
246
+
247
+ function Table({ headers, rows, t }) {
62
248
  return h(
63
249
  'table',
64
250
  { className: 'xray-table' },
@@ -68,7 +254,13 @@ window.__ModuleLoader__.load({
68
254
  h(
69
255
  'tr',
70
256
  null,
71
- headers.map((head, i) => h('th', { key: i }, head)),
257
+ headers.map((head, i) =>
258
+ h(
259
+ 'th',
260
+ { key: i, title: head !== '' && en[`tip.${head}`] ? t(`tip.${head}`) : undefined },
261
+ head === '' ? '' : t(`col.${head}`),
262
+ ),
263
+ ),
72
264
  ),
73
265
  ),
74
266
  h('tbody', null, rows),
@@ -90,55 +282,210 @@ window.__ModuleLoader__.load({
90
282
  ),
91
283
  );
92
284
  }
285
+
286
+ /** Clickable entry name: opens the raw-text inspector for one entry. */
287
+ function EntryLink({ kind, name, onInspect, t }) {
288
+ return h(
289
+ 'button',
290
+ {
291
+ className: 'xray-entry-link',
292
+ title: t('tip.clickEntry'),
293
+ onClick: () => onInspect({ kind, name }),
294
+ },
295
+ name,
296
+ );
297
+ }
298
+
299
+ /** By-plugin rollup with expandable rows: a plugin has no single text —
300
+ * its cost is the SUM of the entries it registered, so clicking a row
301
+ * unfolds those entries (each linking to its raw text). */
302
+ function OwnersTable({ owners, onInspect, t }) {
303
+ const [open, setOpen] = react.useState(() => new Set());
304
+ const toggle = (plugin) =>
305
+ setOpen((current) => {
306
+ const next = new Set(current);
307
+ if (next.has(plugin)) next.delete(plugin);
308
+ else next.add(plugin);
309
+ return next;
310
+ });
311
+ const rows = [];
312
+ for (const o of owners) {
313
+ const expanded = open.has(o.plugin);
314
+ rows.push(
315
+ Row(o.plugin, [
316
+ h(
317
+ 'button',
318
+ {
319
+ className: 'xray-entry-link',
320
+ title: t('tip.expandPlugin'),
321
+ onClick: () => toggle(o.plugin),
322
+ },
323
+ `${expanded ? '▾' : '▸'} `,
324
+ o.plugin === 'unattributed'
325
+ ? h(
326
+ 'span',
327
+ { className: 'xray-muted', title: t('tip.unattributed') },
328
+ t('cost.unattributed'),
329
+ )
330
+ : o.plugin,
331
+ ),
332
+ { cls: 'xray-num', text: String(o.sections) },
333
+ { cls: 'xray-num', text: String(o.tools) },
334
+ { cls: 'xray-num', text: `~${o.tokens}` },
335
+ { cls: 'xray-num', text: `${o.share}%` },
336
+ h(Bar, { share: o.share }),
337
+ ]),
338
+ );
339
+ if (expanded) {
340
+ for (const entry of o.entries) {
341
+ rows.push(
342
+ Row(`${o.plugin}/${entry.kind}/${entry.name}`, [
343
+ h(
344
+ 'span',
345
+ { className: 'xray-owner-entry' },
346
+ h('span', { className: 'xray-muted' }, `${entry.kind} · `),
347
+ h(EntryLink, { kind: entry.kind, name: entry.name, onInspect, t }),
348
+ ),
349
+ '',
350
+ '',
351
+ { cls: 'xray-num', text: `~${entry.tokens}` },
352
+ '',
353
+ '',
354
+ ]),
355
+ );
356
+ }
357
+ }
358
+ }
359
+ return h(Table, { t, headers: ['plugin', 'sections', 'tools', 'tokens', 'share', ''], rows });
360
+ }
361
+
362
+ /** Raw-text inspector: fetches one entry's live text on open. Fetched per
363
+ * view, never cached — the text IS the audit artifact. */
364
+ function EntryModal({ target, onClose, t }) {
365
+ const [state, setState] = react.useState({ phase: 'loading' });
366
+ react.useEffect(() => {
367
+ let alive = true;
368
+ setState({ phase: 'loading' });
369
+ fetch(
370
+ `/xray/api/entry?kind=${encodeURIComponent(target.kind)}&name=${encodeURIComponent(target.name)}`,
371
+ )
372
+ .then(async (res) => {
373
+ if (!res.ok)
374
+ throw new Error((await res.json().catch(() => null))?.error ?? `HTTP ${res.status}`);
375
+ return res.json();
376
+ })
377
+ .then((data) => {
378
+ if (alive) setState({ phase: 'ready', data });
379
+ })
380
+ .catch((err) => {
381
+ if (alive)
382
+ setState({
383
+ phase: 'error',
384
+ message: err instanceof Error ? err.message : String(err),
385
+ });
386
+ });
387
+ return () => {
388
+ alive = false;
389
+ };
390
+ }, [target]);
391
+ return h(
392
+ 'div',
393
+ { className: 'xray-entry-overlay', onClick: onClose },
394
+ h(
395
+ 'div',
396
+ { className: 'xray-entry-modal', onClick: (event) => event.stopPropagation() },
397
+ h(
398
+ 'div',
399
+ { className: 'xray-entry-head' },
400
+ h('span', { className: 'xray-entry-name' }, target.name),
401
+ state.phase === 'ready'
402
+ ? h(
403
+ 'span',
404
+ { className: 'xray-entry-stats' },
405
+ fill(t('entry.stats'), {
406
+ chars: state.data.chars,
407
+ tokens: state.data.tokens,
408
+ estimator: state.data.estimator,
409
+ }),
410
+ )
411
+ : null,
412
+ h('button', { className: 'xray-entry-close', onClick: onClose }, t('entry.close')),
413
+ ),
414
+ state.phase === 'loading'
415
+ ? h('p', { className: 'xray-muted' }, t('entry.loading'))
416
+ : state.phase === 'error'
417
+ ? h('p', { className: 'xray-warn' }, state.message)
418
+ : h('pre', { className: 'xray-entry-text' }, state.data.text),
419
+ ),
420
+ );
421
+ }
93
422
  //#endregion
94
423
 
95
- //#region per-view renderers (mirror lib/panel.js, as components)
424
+ //#region per-view renderers (mirror lib/panel.js, as components; all copy through t)
96
425
  const renderers = {
97
- summary: (d) =>
426
+ summary: (d, t, _onInspect, goToView) =>
98
427
  h(
99
428
  react.Fragment,
100
429
  null,
101
430
  h(Table, {
431
+ t,
102
432
  headers: ['metric', 'value'],
103
433
  rows: [
104
- Row('p', ['plugins mounted', { cls: 'xray-num', text: String(d.plugins) }]),
434
+ Row('p', [t('row.pluginsMounted'), { cls: 'xray-num', text: String(d.plugins) }]),
105
435
  Row('u', [
106
- 'unhealthy',
436
+ t('row.unhealthy'),
107
437
  {
108
438
  cls: `xray-num ${d.unhealthy ? 'xray-warn' : 'xray-ok'}`,
109
439
  text: String(d.unhealthy),
110
440
  },
111
441
  ]),
112
- Row('s', ['services', { cls: 'xray-num', text: String(d.services) }]),
442
+ Row('s', [t('row.services'), { cls: 'xray-num', text: String(d.services) }]),
113
443
  Row('t', [
114
- 'context tokens (tools + sections)',
444
+ h(
445
+ 'button',
446
+ {
447
+ className: 'xray-entry-link',
448
+ title: t('tip.contextTokens'),
449
+ onClick: () => goToView('cost'),
450
+ },
451
+ t('row.contextTokens'),
452
+ ),
115
453
  { cls: 'xray-num', text: `~${d.toolSchemaTokens}` },
116
454
  ]),
117
455
  ],
118
456
  }),
119
- h('p', { className: 'xray-muted', style: { marginTop: 12 } }, `captured ${d.capturedAt}`),
457
+ h(
458
+ 'p',
459
+ { className: 'xray-muted', style: { marginTop: 12 } },
460
+ fill(t('summary.captured'), { at: d.capturedAt }),
461
+ ),
120
462
  ),
121
463
 
122
- health: (d) =>
464
+ health: (d, t) =>
123
465
  h(
124
466
  react.Fragment,
125
467
  null,
126
468
  h(
127
469
  'p',
128
470
  null,
129
- h('span', { className: 'xray-ok' }, `${d.healthy.length} healthy`),
130
- d.waiting.length ? ` · ${d.waiting.length} waiting` : null,
471
+ h('span', { className: 'xray-ok' }, fill(t('health.healthy'), { n: d.healthy.length })),
472
+ d.waiting.length ? ` · ${fill(t('health.waiting'), { n: d.waiting.length })}` : null,
131
473
  d.unhealthy.length
132
474
  ? h(
133
475
  react.Fragment,
134
476
  null,
135
477
  ' · ',
136
- h('span', { className: 'xray-warn' }, `${d.unhealthy.length} unhealthy`),
478
+ h(
479
+ 'span',
480
+ { className: 'xray-warn' },
481
+ fill(t('health.unhealthy'), { n: d.unhealthy.length }),
482
+ ),
137
483
  )
138
484
  : null,
139
485
  ),
140
486
  d.unhealthy.length
141
487
  ? h(Table, {
488
+ t,
142
489
  headers: ['plugin', 'fiber', 'state', 'error'],
143
490
  rows: d.unhealthy.flatMap((p) =>
144
491
  p.fibers.map((f) =>
@@ -154,25 +501,31 @@ window.__ModuleLoader__.load({
154
501
  : null,
155
502
  d.waiting.length
156
503
  ? h(Table, {
157
- headers: ['waiting plugin', 'wants'],
504
+ t,
505
+ headers: ['waitingPlugin', 'wants'],
158
506
  rows: d.waiting.map((p) => Row(p.name, [p.name, p.inject.join(', ')])),
159
507
  })
160
508
  : null,
161
509
  ),
162
510
 
163
- deps: (d) =>
511
+ deps: (d, t) =>
164
512
  h(
165
513
  react.Fragment,
166
514
  null,
167
515
  d.unsatisfied.length
168
- ? h('p', { className: 'xray-warn' }, `${d.unsatisfied.length} unsatisfied inject(s)`)
516
+ ? h(
517
+ 'p',
518
+ { className: 'xray-warn', title: t('tip.wants') },
519
+ fill(t('deps.unsatisfied'), { n: d.unsatisfied.length }),
520
+ )
169
521
  : null,
170
522
  Object.keys(d.cascade).length
171
523
  ? h(
172
524
  react.Fragment,
173
525
  null,
174
- h('div', { className: 'xray-h3' }, 'disable-cascade'),
526
+ h('div', { className: 'xray-h3' }, t('h3.disableCascade')),
175
527
  h(Table, {
528
+ t,
176
529
  headers: ['provider', 'affects'],
177
530
  rows: Object.entries(d.cascade).map(([provider, affected]) =>
178
531
  Row(provider, [provider, affected.join(', ')]),
@@ -180,34 +533,51 @@ window.__ModuleLoader__.load({
180
533
  }),
181
534
  )
182
535
  : null,
183
- h('div', { className: 'xray-h3' }, 'services'),
536
+ h('div', { className: 'xray-h3' }, t('h3.services')),
184
537
  h(Table, {
185
- headers: ['service', 'provided by', 'consumed by'],
538
+ t,
539
+ headers: ['service', 'providedBy', 'consumedBy'],
186
540
  rows: Object.entries(d.services).map(([name, node]) =>
187
541
  Row(name, [name, node.providers.join(', ') || '—', node.consumers.join(', ') || '—']),
188
542
  ),
189
543
  }),
190
544
  ),
191
545
 
192
- cost: (d) =>
546
+ cost: (d, t, onInspect) =>
193
547
  h(
194
548
  react.Fragment,
195
549
  null,
196
550
  h(
197
551
  'p',
198
552
  null,
199
- `~${d.totalTokens} tokens: ${d.toolCount} tool schema(s) ~${d.toolTokens} + ${d.sectionCount} prompt section(s) ~${d.sectionTokens}`,
553
+ fill(t('cost.headline'), {
554
+ total: d.totalTokens,
555
+ toolCount: d.toolCount,
556
+ toolTokens: d.toolTokens,
557
+ sectionCount: d.sectionCount,
558
+ sectionTokens: d.sectionTokens,
559
+ }),
200
560
  ),
561
+ d.owners?.length
562
+ ? h(
563
+ react.Fragment,
564
+ null,
565
+ h('div', { className: 'xray-h3' }, t('h3.byPlugin')),
566
+ h(OwnersTable, { owners: d.owners, onInspect, t }),
567
+ )
568
+ : null,
201
569
  d.sections.length
202
570
  ? h(
203
571
  react.Fragment,
204
572
  null,
205
- h('div', { className: 'xray-h3' }, 'prompt sections'),
573
+ h('div', { className: 'xray-h3' }, t('h3.promptSections')),
206
574
  h(Table, {
207
- headers: ['section', 'tokens', 'share', ''],
575
+ t,
576
+ headers: ['section', 'owner', 'tokens', 'share', ''],
208
577
  rows: d.sections.map((s) =>
209
578
  Row(s.name, [
210
- s.name,
579
+ h(EntryLink, { kind: 'section', name: s.name, onInspect, t }),
580
+ { cls: 'xray-muted', text: s.owner ?? '—' },
211
581
  { cls: 'xray-num', text: `~${s.tokens}` },
212
582
  { cls: 'xray-num', text: `${s.share}%` },
213
583
  h(Bar, { share: s.share }),
@@ -215,31 +585,30 @@ window.__ModuleLoader__.load({
215
585
  ),
216
586
  }),
217
587
  )
218
- : h(
219
- 'p',
220
- { className: 'xray-muted' },
221
- 'no prompt assembly observed yet — send one agent message first',
222
- ),
223
- h('div', { className: 'xray-h3' }, 'tool schemas'),
588
+ : h('p', { className: 'xray-muted' }, t('cost.noAssembly')),
589
+ h('div', { className: 'xray-h3' }, t('h3.toolSchemas')),
224
590
  h(Table, {
225
- headers: ['tool', 'tokens', 'share', ''],
226
- rows: d.tools.map((t) =>
227
- Row(t.name, [
228
- t.name,
229
- { cls: 'xray-num', text: `~${t.tokens}` },
230
- { cls: 'xray-num', text: `${t.share}%` },
231
- h(Bar, { share: t.share }),
591
+ t,
592
+ headers: ['tool', 'owner', 'tokens', 'share', ''],
593
+ rows: d.tools.map((row) =>
594
+ Row(row.name, [
595
+ h(EntryLink, { kind: 'tool', name: row.name, onInspect, t }),
596
+ { cls: 'xray-muted', text: row.owner ?? '—' },
597
+ { cls: 'xray-num', text: `~${row.tokens}` },
598
+ { cls: 'xray-num', text: `${row.share}%` },
599
+ h(Bar, { share: row.share }),
232
600
  ]),
233
601
  ),
234
602
  }),
235
603
  ),
236
604
 
237
- shadow: (d) =>
605
+ shadow: (d, t) =>
238
606
  h(
239
607
  react.Fragment,
240
608
  null,
241
609
  d.services.length
242
610
  ? h(Table, {
611
+ t,
243
612
  headers: ['service', 'providers'],
244
613
  rows: d.services.map((s) =>
245
614
  Row(s.service, [
@@ -248,13 +617,14 @@ window.__ModuleLoader__.load({
248
617
  ]),
249
618
  ),
250
619
  })
251
- : h('p', { className: 'xray-ok' }, 'no service is provided by more than one plugin'),
620
+ : h('p', { className: 'xray-ok' }, t('shadow.clean')),
252
621
  d.registrars.length
253
622
  ? h(
254
623
  react.Fragment,
255
624
  null,
256
- h('div', { className: 'xray-h3' }, 'registrars'),
625
+ h('div', { className: 'xray-h3' }, t('h3.registrars')),
257
626
  h(Table, {
627
+ t,
258
628
  headers: ['plugin', 'registrations'],
259
629
  rows: d.registrars.map((r) =>
260
630
  Row(r.plugin, [r.plugin, { cls: 'xray-num', text: String(r.registrations) }]),
@@ -267,7 +637,7 @@ window.__ModuleLoader__.load({
267
637
  //#endregion
268
638
 
269
639
  //#region panel (one conversation.view tab)
270
- function XrayPanel() {
640
+ function XrayPanel({ t }) {
271
641
  const [view, setView] = react.useState('summary');
272
642
  // `for` stamps which view the payload belongs to: a click flips `view`
273
643
  // synchronously while `state` still carries the previous view's data —
@@ -277,6 +647,8 @@ window.__ModuleLoader__.load({
277
647
  // the layout changes once per click instead of collapsing to a
278
648
  // one-line loading row and re-expanding.
279
649
  const [state, setState] = react.useState({ phase: 'loading', for: 'summary' });
650
+ // Entry inspector: {kind, name} of the entry whose raw text is open.
651
+ const [inspecting, setInspecting] = react.useState(null);
280
652
  react.useEffect(() => {
281
653
  let alive = true;
282
654
  fetch(`/xray/api/${view}`)
@@ -304,18 +676,20 @@ window.__ModuleLoader__.load({
304
676
  // the fresh payload is in flight.
305
677
  const settled = state.phase !== 'loading';
306
678
  let body;
307
- if (!settled) body = h('p', { className: 'xray-muted' }, `loading ${view}…`);
679
+ if (!settled) body = h('p', { className: 'xray-muted' }, fill(t('panel.loading'), { view }));
308
680
  else if (state.phase === 'error') body = h('p', { className: 'xray-warn' }, state.message);
309
681
  else {
310
682
  // A diagnostic surface must never take itself down on one bad
311
683
  // payload: render the failure, keep the tab and its nav alive.
312
684
  try {
313
- body = renderers[state.for](state.data);
685
+ body = renderers[state.for](state.data, t, setInspecting, setView);
314
686
  } catch (err) {
315
687
  body = h(
316
688
  'p',
317
689
  { className: 'xray-warn' },
318
- `render failed: ${err instanceof Error ? err.message : String(err)}`,
690
+ fill(t('panel.renderFailed'), {
691
+ message: err instanceof Error ? err.message : String(err),
692
+ }),
319
693
  );
320
694
  }
321
695
  }
@@ -323,7 +697,7 @@ window.__ModuleLoader__.load({
323
697
  return h(
324
698
  'div',
325
699
  { className: 'xray-panel' },
326
- h('p', { className: 'xray-sub' }, 'composition X-ray — live from this harness'),
700
+ h('p', { className: 'xray-sub' }, t('panel.sub')),
327
701
  h(
328
702
  'nav',
329
703
  { className: 'xray-nav' },
@@ -335,17 +709,29 @@ window.__ModuleLoader__.load({
335
709
  ),
336
710
  ),
337
711
  ),
712
+ h('p', { className: 'xray-muted', style: { margin: '0 0 10px' } }, t(`intro.${view}`)),
338
713
  h('div', { className: stale ? 'xray-body xray-stale' : 'xray-body' }, body),
714
+ inspecting
715
+ ? h(EntryModal, { target: inspecting, onClose: () => setInspecting(null), t })
716
+ : null,
339
717
  );
340
718
  }
341
719
  //#endregion
342
720
 
343
- const inject = ['slots'];
344
- /** Mount the X-ray tab into the conversation view ring (beside Chat / Trajectory). */
721
+ const inject = ['slots', 'locale'];
722
+ /** Mount the X-Ray tab into the conversation view ring (beside Chat / Trajectory). */
345
723
  function apply(ctx) {
724
+ ctx.effect(() => ctx.locale.register(NS, { zh, en }), 'xray: dictionaries');
725
+ const t = ctx.locale.bind(NS);
346
726
  ctx.slots.inject('conversation.view', () =>
347
727
  ctx.slots.register(
348
- { name: 'conversation.view', id: 'xray', order: 20, label: 'X-ray' },
728
+ {
729
+ name: 'conversation.view',
730
+ id: 'xray',
731
+ order: 20,
732
+ label: () => t('view.xray'),
733
+ locale: NS,
734
+ },
349
735
  XrayPanel,
350
736
  ),
351
737
  );