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 +434 -48
- package/lib/collect/attribution.js +239 -0
- package/lib/collect/runtime.js +85 -2
- package/lib/index.js +42 -16
- package/lib/model.js +43 -3
- package/lib/panel.js +75 -8
- package/package.json +2 -1
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
|
-
|
|
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) =>
|
|
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', ['
|
|
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
|
-
|
|
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(
|
|
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' },
|
|
130
|
-
d.waiting.length ? ` · ${d.waiting.length}
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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' }, '
|
|
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
|
-
|
|
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
|
-
|
|
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' }, '
|
|
573
|
+
h('div', { className: 'xray-h3' }, t('h3.promptSections')),
|
|
206
574
|
h(Table, {
|
|
207
|
-
|
|
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
|
-
|
|
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
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
{
|
|
230
|
-
{ cls: 'xray-
|
|
231
|
-
|
|
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' }, '
|
|
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' },
|
|
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
|
-
|
|
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' }, '
|
|
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-
|
|
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
|
-
{
|
|
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
|
);
|