dsh-xray 0.5.0 → 0.7.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.
- package/README.md +31 -1
- package/README.zh.md +52 -13
- package/assets/hero.svg +1 -1
- package/assets/panel-deps.webp +0 -0
- package/assets/panel-summary.webp +0 -0
- package/bin/xray.js +8 -0
- package/docs/discussion-post-draft.md +88 -0
- package/lib/client.js +340 -0
- package/lib/index.js +39 -1
- package/lib/panel.js +213 -0
- package/package.json +10 -1
package/README.md
CHANGED
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
<a href="https://www.npmjs.com/package/dsh-xray"><img src="https://img.shields.io/npm/v/dsh-xray?style=flat-square&color=00d4aa" alt="npm"></a>
|
|
7
7
|
<a href="https://github.com/alloevil/dsh-xray/actions/workflows/check.yml"><img src="https://img.shields.io/github/actions/workflow/status/alloevil/dsh-xray/check.yml?style=flat-square&label=CI" alt="CI"></a>
|
|
8
8
|
<a href="./LICENSE"><img src="https://img.shields.io/npm/l/dsh-xray?style=flat-square" alt="license"></a>
|
|
9
|
+
<a href="https://scorecard.dev/viewer/?uri=github.com/alloevil/dsh-xray"><img src="https://api.securityscorecards.dev/projects/github.com/alloevil/dsh-xray/badge?style=flat-square" alt="OpenSSF Scorecard"></a>
|
|
10
|
+
<a href="https://codecov.io/gh/alloevil/dsh-xray"><img src="https://img.shields.io/codecov/c/github/alloevil/dsh-xray?style=flat-square" alt="coverage"></a>
|
|
9
11
|
<img src="https://img.shields.io/badge/language-JavaScript-f7df1e?style=flat-square&logo=javascript&logoColor=black" alt="JavaScript">
|
|
10
12
|
</p>
|
|
11
13
|
|
|
@@ -27,7 +29,7 @@
|
|
|
27
29
|
|
|
28
30
|
**dsh-xray does.**
|
|
29
31
|
|
|
30
|
-
>
|
|
32
|
+
> Static commands work even when dsh cannot boot; `deps`/`health`/`cost`/`shadow` and the agent tool need the plugin mounted.
|
|
31
33
|
|
|
32
34
|
---
|
|
33
35
|
|
|
@@ -79,6 +81,13 @@ Per-plugin fiber lifecycle state, startup failures, transition history.
|
|
|
79
81
|
### 🤖 Agent Self-Introspection
|
|
80
82
|
The `xray_composition` tool lets agents inspect their own capability set.
|
|
81
83
|
|
|
84
|
+
### 🖥️ Web Panel
|
|
85
|
+
Mounted in `dsh web`, the plugin serves a zero-dependency panel at **`/xray`** — summary, health, deps (with the disable-cascade table), cost, and shadow views, live from the running composition. JSON endpoints under `/xray/api/*` serve the same data.
|
|
86
|
+
|
|
87
|
+
The package also ships a client half (`dsh.client` declaration + `exports["./client"]`): the host discovers it automatically and mounts an **X-ray tab beside Chat / Trajectory** in every session, rendering the same five views natively in the GUI with the host's design tokens. The standalone `/xray` page stays available as the degradation path — it only needs the web server, not the client-module pipeline it helps diagnose.
|
|
88
|
+
|
|
89
|
+

|
|
90
|
+
|
|
82
91
|
What every request actually carries — prompt sections observed at assembly, blended with tool schemas:
|
|
83
92
|
|
|
84
93
|
```console
|
|
@@ -131,10 +140,31 @@ Mounted in the tree, dsh-xray registers an `xray_composition` tool (`view: summa
|
|
|
131
140
|
<img src="./assets/section-install.svg" width="100%" alt="Install">
|
|
132
141
|
</p>
|
|
133
142
|
|
|
143
|
+
Two ways to use it — they're independent:
|
|
144
|
+
|
|
145
|
+
**1. Static CLI only** (no install into dsh; works even when dsh cannot boot):
|
|
146
|
+
|
|
147
|
+
```sh
|
|
148
|
+
npx dsh-xray attribute # requires Node >= 22
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
**2. Mount the plugin** (adds the runtime commands, the `/xray` panel, and the agent tool):
|
|
152
|
+
|
|
134
153
|
```sh
|
|
135
154
|
dsh plugin --profile web add dsh-xray
|
|
155
|
+
# bundle plugins take effect on the next start — restart dsh web
|
|
136
156
|
```
|
|
137
157
|
|
|
158
|
+
Verify it took:
|
|
159
|
+
|
|
160
|
+
```sh
|
|
161
|
+
dsh --profile web --dump-config | grep dsh-xray # row present in the composed tree
|
|
162
|
+
npx dsh-xray health # reads the runtime snapshot
|
|
163
|
+
# then open http://localhost:3080/xray for the live panel
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
Uninstall: `dsh plugin --profile web remove dsh-xray`.
|
|
167
|
+
|
|
138
168
|
All commands take `--profile <name>` (default `web`) and `--json`.
|
|
139
169
|
|
|
140
170
|
| Command | Behavior |
|
package/README.zh.md
CHANGED
|
@@ -1,16 +1,26 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="./assets/hero.svg" width="100%" alt="dsh-xray — 给你的 DeepSeek Harness 拍 X 光">
|
|
3
|
+
</p>
|
|
4
|
+
|
|
5
|
+
<p align="center">
|
|
6
|
+
<a href="https://www.npmjs.com/package/dsh-xray"><img src="https://img.shields.io/npm/v/dsh-xray?style=flat-square&color=00d4aa" alt="npm"></a>
|
|
7
|
+
<a href="https://github.com/alloevil/dsh-xray/actions/workflows/check.yml"><img src="https://img.shields.io/github/actions/workflow/status/alloevil/dsh-xray/check.yml?style=flat-square&label=CI" alt="CI"></a>
|
|
8
|
+
<a href="./LICENSE"><img src="https://img.shields.io/npm/l/dsh-xray?style=flat-square" alt="license"></a>
|
|
9
|
+
<a href="https://scorecard.dev/viewer/?uri=github.com/alloevil/dsh-xray"><img src="https://api.securityscorecards.dev/projects/github.com/alloevil/dsh-xray/badge?style=flat-square" alt="OpenSSF Scorecard"></a>
|
|
10
|
+
<a href="https://codecov.io/gh/alloevil/dsh-xray"><img src="https://img.shields.io/codecov/c/github/alloevil/dsh-xray?style=flat-square" alt="coverage"></a>
|
|
11
|
+
</p>
|
|
12
|
+
|
|
13
|
+
<p align="center">
|
|
14
|
+
<strong>给 <a href="https://github.com/deepseek-ai/deepseek-harness">DeepSeek Harness</a> 拍 X 光</strong>——看清到底加载了什么、为什么在那、以及它悄悄花掉了你什么。
|
|
15
|
+
</p>
|
|
16
|
+
|
|
17
|
+
<p align="center">
|
|
18
|
+
<a href="./README.md">English</a>
|
|
19
|
+
</p>
|
|
8
20
|
|
|
9
21
|

|
|
10
22
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
> **状态:0.4.x — 静态 + 运行时成像,含上下文成本归因。** 静态命令在 dsh 起不来时照样能用;`deps`/`health`/`cost`/`shadow` 和 agent 工具需要插件已挂载。
|
|
23
|
+
> 静态命令在 dsh 起不来时照样能用;`deps`/`health`/`cost`/`shadow` 和 agent 工具需要插件已挂载。
|
|
14
24
|
|
|
15
25
|
`dsh --dump-config` 只给你原始组合树,插件面板只给你平铺列表。它们都不回答:这个插件*为什么*在这、停用它会*连带瘫掉什么*、它在*悄悄消耗什么*。dsh-xray 回答这些。
|
|
16
26
|
|
|
@@ -91,6 +101,14 @@ orphan overrides (silently skipped) (1)
|
|
|
91
101
|
|
|
92
102
|
挂载进树后,dsh-xray 注册 `xray_composition` 工具(`view: summary | deps | health | cost | shadow`),agent 可以自答"我有哪些能力 / 哪个插件提供 X / 为什么 Y 不可用"。
|
|
93
103
|
|
|
104
|
+
## Web 面板
|
|
105
|
+
|
|
106
|
+
在 `dsh web` 中挂载后,插件在 **`/xray`** 提供零依赖面板——summary、health、deps(含停用级联表)、cost、shadow 五个视图,数据实时来自运行中的组合树;`/xray/api/*` 提供同源 JSON。
|
|
107
|
+
|
|
108
|
+
包同时携带 client 半(`dsh.client` 声明 + `exports["./client"]`):宿主自动发现并在每个会话中挂载 **与 Chat / Trajectory 并列的 X-ray 标签页**,用宿主设计变量原生渲染同样五个视图。独立的 `/xray` 页面保留为降级通道——它只依赖 web 服务器,不依赖它所诊断的 client-module 加载链路。
|
|
109
|
+
|
|
110
|
+

|
|
111
|
+
|
|
94
112
|
## 安全立场
|
|
95
113
|
|
|
96
114
|
dsh-xray 只读不执行。patch 文件里的 loader `!!js` 表达式解析为不透明标记、绝不求值;CLI 从不执行插件代码(`audit` 是对源码文本的模式扫描);挂载的插件只写 `$DSH_HOME/xray/` 目录。详见 [SECURITY.md](./SECURITY.md)。
|
|
@@ -99,26 +117,47 @@ dsh-xray 只读不执行。patch 文件里的 loader `!!js` 表达式解析为
|
|
|
99
117
|
|
|
100
118
|
对运行中组合树的诊断成像——与 [dsh-doctor](https://www.npmjs.com/package/dsh-doctor)(救援与恢复)互补。
|
|
101
119
|
|
|
102
|
-
|
|
120
|
+
已交付:
|
|
103
121
|
|
|
104
122
|
- **来源归因** — 每个活跃插件来自哪一层:内核 bundle / profile 依赖 / `cordis.patch.yml` insert / repository 源
|
|
105
123
|
- **声明 vs 实际 diff** — 装了但没生效、卸了但残留 patch 行
|
|
106
124
|
- **冲突检测** — 多个插件 patch 同一配置行时,谁静默赢了
|
|
107
|
-
- **组合快照** — 把当前生效组合导出为 lockfile
|
|
125
|
+
- **组合快照** — 把当前生效组合导出为 lockfile;`--against` 对比漂移
|
|
108
126
|
- **服务依赖图** — 每个服务谁提供、谁消费;停用 X 会级联影响什么(`deps`)
|
|
109
127
|
- **运行时健康** — 每个插件的 fiber 生命周期状态、启动失败、状态迁移史(`health`)
|
|
110
128
|
- **Agent 自省** — `xray_composition` 工具让 agent 检视自己的能力集
|
|
111
|
-
|
|
112
129
|
- **能力审计** — 对 out-of-tree 插件的启发式静态扫描:网络外发、shell、文件系统、环境变量、动态求值(`audit`)
|
|
113
130
|
- **服务重名检测** — 被多个插件同时提供的服务,及每插件工具/命令注册数(`shadow`)
|
|
114
131
|
- **上下文成本** — prompt sections(观测自 system-prompt/assemble)+ 工具 schema 的估算 token 占用(`cost`)
|
|
132
|
+
- **Web 面板** — `/xray` 五视图,零依赖零构建
|
|
115
133
|
|
|
116
134
|
## 安装
|
|
117
135
|
|
|
136
|
+
两条使用路径,互相独立:
|
|
137
|
+
|
|
138
|
+
**1. 只用静态 CLI**(不装进 dsh;dsh 起不来时也能用):
|
|
139
|
+
|
|
140
|
+
```sh
|
|
141
|
+
npx dsh-xray attribute # 需要 Node >= 22
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
**2. 挂载插件**(解锁运行时命令、`/xray` 面板和 agent 工具):
|
|
145
|
+
|
|
118
146
|
```sh
|
|
119
147
|
dsh plugin --profile web add dsh-xray
|
|
148
|
+
# bundle 插件重启后生效——重启 dsh web
|
|
120
149
|
```
|
|
121
150
|
|
|
151
|
+
验证装好了:
|
|
152
|
+
|
|
153
|
+
```sh
|
|
154
|
+
dsh --profile web --dump-config | grep dsh-xray # 组合树里有这一行
|
|
155
|
+
npx dsh-xray health # 能读到运行时快照
|
|
156
|
+
# 然后打开 http://localhost:3080/xray 看实时面板
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
卸载:`dsh plugin --profile web remove dsh-xray`。
|
|
160
|
+
|
|
122
161
|
## 许可证
|
|
123
162
|
|
|
124
163
|
MIT
|
package/assets/hero.svg
CHANGED
|
@@ -117,7 +117,7 @@
|
|
|
117
117
|
<!-- Status badge -->
|
|
118
118
|
<g transform="translate(450, 250)">
|
|
119
119
|
<rect x="-100" y="-12" width="200" height="24" rx="12" fill="#00d4aa" opacity="0.1" stroke="#00d4aa" stroke-width="0.5" stroke-opacity="0.3"/>
|
|
120
|
-
<text x="0" y="4" text-anchor="middle" fill="#00d4aa" font-family="'SF Mono', monospace" font-size="11">
|
|
120
|
+
<text x="0" y="4" text-anchor="middle" fill="#00d4aa" font-family="'SF Mono', monospace" font-size="11">static + runtime imaging</text>
|
|
121
121
|
</g>
|
|
122
122
|
|
|
123
123
|
<!-- Feature pills -->
|
|
Binary file
|
|
Binary file
|
package/bin/xray.js
CHANGED
|
@@ -172,6 +172,10 @@ function readRuntimeSnapshot() {
|
|
|
172
172
|
return JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
173
173
|
}
|
|
174
174
|
|
|
175
|
+
function panelHint(args) {
|
|
176
|
+
if (!args.json) console.log('\nlive panel: http://localhost:3080/xray');
|
|
177
|
+
}
|
|
178
|
+
|
|
175
179
|
function cmdDeps(args) {
|
|
176
180
|
const snap = readRuntimeSnapshot();
|
|
177
181
|
const result = model.serviceGraph(snap);
|
|
@@ -196,6 +200,7 @@ function cmdDeps(args) {
|
|
|
196
200
|
console.log(`\n! ${result.unsatisfied.length} unsatisfied inject(s):`);
|
|
197
201
|
for (const u of result.unsatisfied) console.log(` ${u.plugin} wants ${u.service}`);
|
|
198
202
|
}
|
|
203
|
+
panelHint(args);
|
|
199
204
|
}
|
|
200
205
|
|
|
201
206
|
function cmdHealth(args) {
|
|
@@ -214,6 +219,7 @@ function cmdHealth(args) {
|
|
|
214
219
|
}
|
|
215
220
|
}
|
|
216
221
|
if (result.unhealthy.length) process.exitCode = 1;
|
|
222
|
+
panelHint(args);
|
|
217
223
|
}
|
|
218
224
|
|
|
219
225
|
function cmdCost(args) {
|
|
@@ -241,6 +247,7 @@ function cmdCost(args) {
|
|
|
241
247
|
`${pad(t.name, 32)} ${pad(`~${t.tokens}`, 8)} ${pad(`${t.share}%`, 7)} ${bar(t.share)}`,
|
|
242
248
|
);
|
|
243
249
|
}
|
|
250
|
+
panelHint(args);
|
|
244
251
|
}
|
|
245
252
|
|
|
246
253
|
function cmdShadow(args) {
|
|
@@ -258,6 +265,7 @@ function cmdShadow(args) {
|
|
|
258
265
|
}
|
|
259
266
|
}
|
|
260
267
|
if (result.services.length) process.exitCode = 1;
|
|
268
|
+
panelHint(args);
|
|
261
269
|
}
|
|
262
270
|
|
|
263
271
|
function cmdAudit(args) {
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# dsh-xray — 给你的 Harness 拍 X 光:组合树归因、依赖级联、上下文成本
|
|
2
|
+
|
|
3
|
+
(草稿 — 建议发到 deepseek-ai/deepseek-harness 的 Discussions · Show and tell 分类。
|
|
4
|
+
发布前把口吻改成你自己的;英文版附在下半部分,可以只发中文或双语。)
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
大家好,我做了一个诊断类插件 **dsh-xray**,想解决我自己折腾 profile 时反复遇到的三个问题:
|
|
9
|
+
|
|
10
|
+
1. `--dump-config` 打出 130 行,但**哪一行是谁引入的**?patch 改了不生效时,是 id 写错被静默跳过,还是被后面的层覆盖了?
|
|
11
|
+
2. 想停用一个插件,**会连带瘫掉什么**?
|
|
12
|
+
3. 装了一堆插件后,**每次请求的上下文里到底塞了多少东西**?
|
|
13
|
+
|
|
14
|
+
## 它长什么样
|
|
15
|
+
|
|
16
|
+
```console
|
|
17
|
+
$ npx dsh-xray attribute
|
|
18
|
+
# 130 rows in profile "web"
|
|
19
|
+
hmr @deepseek-ai/dsh-base ← patched by dsh-web-app [disabled]
|
|
20
|
+
session-query @deepseek-ai/dsh-base ← patched by dsh-web-app
|
|
21
|
+
|
|
22
|
+
$ npx dsh-xray deps
|
|
23
|
+
# disable-cascade (transitive consumers of each provider):
|
|
24
|
+
SessionStore → AgentLoop, ApiProxyService, JsonlSessionPersistence, … (11 plugins)
|
|
25
|
+
ToolRuntime → AgentLoop, ApiProxyService, DynamicCordisRunnerService, PlanModeController
|
|
26
|
+
|
|
27
|
+
$ npx dsh-xray cost
|
|
28
|
+
~1625 tokens: 1 tool schema(s) ~121 + 19 prompt section(s) ~1504
|
|
29
|
+
app:web-surface ~248 15.3% ████████
|
|
30
|
+
tool:goal ~184 11.3% ██████
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## 全部命令
|
|
34
|
+
|
|
35
|
+
| 命令 | 回答的问题 |
|
|
36
|
+
|---|---|
|
|
37
|
+
| `attribute` | 每一行由哪层引入(bundle / profile patch / home patch / repository 源),之后被谁改过 |
|
|
38
|
+
| `diff` | 声明 vs 实际:抓出被 dsh 静默跳过的 orphan patch 行、装了没生效的插件 |
|
|
39
|
+
| `conflicts` | 同一字段多个写者,谁赢了 |
|
|
40
|
+
| `deps` / `health` | 服务依赖图 + 停用级联;每个插件的 fiber 生命周期状态 |
|
|
41
|
+
| `cost` | prompt sections(观测自 system-prompt/assemble)+ tool schemas 的 token 估算 |
|
|
42
|
+
| `shadow` / `audit` | 多提供者服务;out-of-tree 插件的敏感触点静态扫描(网络/shell/fs/env/eval) |
|
|
43
|
+
| `snapshot --against` | 组合树 lockfile 导出与漂移对比 |
|
|
44
|
+
|
|
45
|
+
另外:
|
|
46
|
+
|
|
47
|
+
- **`/xray` Web 面板**(dsh web 里直接开,零依赖零构建)
|
|
48
|
+
- **`xray_composition` agent 工具** — agent 可以自查"我有哪些能力/为什么 Y 不可用"
|
|
49
|
+
- 静态命令(attribute/conflicts/diff/snapshot/audit)**在 dsh 起不来时照样能跑**
|
|
50
|
+
- 与恢复类工具(dsh-doctor 等)互补:它们负责"救回来",xray 负责"看清楚"
|
|
51
|
+
|
|
52
|
+
## 安全立场
|
|
53
|
+
|
|
54
|
+
只读不执行:patch 里的 `!!js` 解析为不透明标记、绝不求值;audit 是源码文本扫描,不运行插件代码;挂载后只写 `$DSH_HOME/xray/`。
|
|
55
|
+
|
|
56
|
+
## 安装
|
|
57
|
+
|
|
58
|
+
```sh
|
|
59
|
+
dsh plugin --profile web add dsh-xray # 挂载(面板 + agent 工具 + 运行时快照)
|
|
60
|
+
npx dsh-xray attribute # 或者不挂载,直接用静态 CLI
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
- GitHub: https://github.com/alloevil/dsh-xray (MIT, CI on Node 22/24, 39 tests)
|
|
64
|
+
- npm: https://www.npmjs.com/package/dsh-xray (发布带 GitHub Actions provenance)
|
|
65
|
+
|
|
66
|
+
欢迎 issue / PR,特别想听:你们排查 profile 问题时还缺什么视角?
|
|
67
|
+
|
|
68
|
+
---
|
|
69
|
+
|
|
70
|
+
## English version
|
|
71
|
+
|
|
72
|
+
Hi all — I built **dsh-xray**, a diagnostics plugin for three questions I kept hitting while hacking on profiles:
|
|
73
|
+
|
|
74
|
+
1. `--dump-config` prints 130 rows, but **which layer introduced each one**? When a patch "doesn't take", is the id wrong (silently skipped) or overridden by a later layer?
|
|
75
|
+
2. If I disable plugin X, **what breaks transitively**?
|
|
76
|
+
3. After installing a pile of plugins, **what does every request actually carry** in context?
|
|
77
|
+
|
|
78
|
+
Commands: `attribute` (per-row layer attribution), `diff` (declared vs actual — catches orphan patch rows), `conflicts` (last-writer-wins chains), `deps`/`health` (service graph with disable-cascade, fiber lifecycle), `cost` (prompt sections + tool schemas, estimated tokens), `shadow`/`audit` (multi-provider services, capability scan), `snapshot --against` (composition lockfile + drift).
|
|
79
|
+
|
|
80
|
+
Plus a zero-dependency web panel at `/xray`, and an `xray_composition` tool so agents can introspect their own capability set. Static commands work even when dsh cannot boot. Read-only by design: `!!js` never evaluated, `audit` never executes plugin code.
|
|
81
|
+
|
|
82
|
+
```sh
|
|
83
|
+
dsh plugin --profile web add dsh-xray
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
GitHub: https://github.com/alloevil/dsh-xray · npm: https://www.npmjs.com/package/dsh-xray
|
|
87
|
+
|
|
88
|
+
Feedback welcome — what else do you wish you could see when a profile misbehaves?
|
package/lib/client.js
ADDED
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
// dsh-xray browser half. React panel registered as one conversation view tab
|
|
2
|
+
// (`conversation.view` list slot, beside Chat / Trajectory); data comes from
|
|
3
|
+
// the same-origin /xray/api/* endpoints the node half already serves, so this
|
|
4
|
+
// layer stays a thin renderer — every computation lives in lib/model.js.
|
|
5
|
+
//
|
|
6
|
+
// Skeleton note: authored directly in ModuleLoader factory form (plain
|
|
7
|
+
// React.createElement, no build step) to prove the wiring; migrate to
|
|
8
|
+
// src/client/*.tsx + tsdown once the surface stabilizes.
|
|
9
|
+
window.__ModuleLoader__.load({
|
|
10
|
+
id: 'dsh-xray',
|
|
11
|
+
factory: (require) => {
|
|
12
|
+
const module = { exports: {} };
|
|
13
|
+
const exports = module.exports;
|
|
14
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
|
|
15
|
+
const react = require('react');
|
|
16
|
+
const h = react.createElement;
|
|
17
|
+
|
|
18
|
+
//#region styles (host design tokens; auto-claimed by client-modules)
|
|
19
|
+
const css = [
|
|
20
|
+
'.xray-panel{max-width:920px;margin:0 auto;padding:16px 20px;font-size:13px;color:var(--dsw-alias-label-primary)}',
|
|
21
|
+
'.xray-sub{color:var(--dsw-alias-label-tertiary);margin:0 0 14px;font-size:12px}',
|
|
22
|
+
'.xray-nav{display:flex;gap:6px;flex-wrap:wrap;margin-bottom:12px}',
|
|
23
|
+
'.xray-nav button{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:4px 10px;cursor:pointer}',
|
|
24
|
+
'.xray-nav button.active{color:#fff;background:var(--dsw-alias-brand-primary);border-color:var(--dsw-alias-brand-primary)}',
|
|
25
|
+
'.xray-table{border-collapse:collapse;width:100%;margin-top:6px}',
|
|
26
|
+
'.xray-table th,.xray-table td{text-align:left;padding:4px 10px;border-bottom:1px solid var(--dsw-alias-border-l2);vertical-align:top}',
|
|
27
|
+
'.xray-table th{color:var(--dsw-alias-label-tertiary);font-weight:normal}',
|
|
28
|
+
'.xray-num{text-align:right}',
|
|
29
|
+
'.xray-warn{color:var(--dsw-alias-label-error)}',
|
|
30
|
+
'.xray-ok{color:var(--dsw-alias-label-success,#7ee787)}',
|
|
31
|
+
'.xray-muted{color:var(--dsw-alias-label-tertiary)}',
|
|
32
|
+
'.xray-bar{background:var(--dsw-alias-brand-primary);height:10px;border-radius:2px;display:inline-block}',
|
|
33
|
+
'.xray-h3{font-size:13px;font-weight:600;margin:14px 0 4px}',
|
|
34
|
+
].join('\n');
|
|
35
|
+
if (
|
|
36
|
+
typeof document !== 'undefined' &&
|
|
37
|
+
document.querySelector('style[data-plugin-css="dsh-xray/panel"]') === null
|
|
38
|
+
) {
|
|
39
|
+
const tag = document.createElement('style');
|
|
40
|
+
tag.dataset.plugin = 'dsh-xray';
|
|
41
|
+
tag.dataset.pluginCss = 'dsh-xray/panel';
|
|
42
|
+
tag.textContent = css;
|
|
43
|
+
document.head.appendChild(tag);
|
|
44
|
+
}
|
|
45
|
+
//#endregion
|
|
46
|
+
|
|
47
|
+
//#region tiny view primitives
|
|
48
|
+
const VIEWS = ['summary', 'health', 'deps', 'cost', 'shadow'];
|
|
49
|
+
function Table({ headers, rows }) {
|
|
50
|
+
return h(
|
|
51
|
+
'table',
|
|
52
|
+
{ className: 'xray-table' },
|
|
53
|
+
h(
|
|
54
|
+
'thead',
|
|
55
|
+
null,
|
|
56
|
+
h(
|
|
57
|
+
'tr',
|
|
58
|
+
null,
|
|
59
|
+
headers.map((head, i) => h('th', { key: i }, head)),
|
|
60
|
+
),
|
|
61
|
+
),
|
|
62
|
+
h('tbody', null, rows),
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
function Bar({ share }) {
|
|
66
|
+
return h('span', { className: 'xray-bar', style: { width: `${Math.max(2, share * 2)}px` } });
|
|
67
|
+
}
|
|
68
|
+
function Row(key, cells) {
|
|
69
|
+
return h(
|
|
70
|
+
'tr',
|
|
71
|
+
{ key },
|
|
72
|
+
cells.map((cell, i) =>
|
|
73
|
+
h(
|
|
74
|
+
'td',
|
|
75
|
+
{ key: i, className: cell?.cls ? cell.cls : void 0 },
|
|
76
|
+
cell && cell.cls !== void 0 ? cell.text : cell,
|
|
77
|
+
),
|
|
78
|
+
),
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
//#endregion
|
|
82
|
+
|
|
83
|
+
//#region per-view renderers (mirror lib/panel.js, as components)
|
|
84
|
+
const renderers = {
|
|
85
|
+
summary: (d) =>
|
|
86
|
+
h(
|
|
87
|
+
react.Fragment,
|
|
88
|
+
null,
|
|
89
|
+
h(Table, {
|
|
90
|
+
headers: ['metric', 'value'],
|
|
91
|
+
rows: [
|
|
92
|
+
Row('p', ['plugins mounted', { cls: 'xray-num', text: String(d.plugins) }]),
|
|
93
|
+
Row('u', [
|
|
94
|
+
'unhealthy',
|
|
95
|
+
{
|
|
96
|
+
cls: `xray-num ${d.unhealthy ? 'xray-warn' : 'xray-ok'}`,
|
|
97
|
+
text: String(d.unhealthy),
|
|
98
|
+
},
|
|
99
|
+
]),
|
|
100
|
+
Row('s', ['services', { cls: 'xray-num', text: String(d.services) }]),
|
|
101
|
+
Row('t', [
|
|
102
|
+
'context tokens (tools + sections)',
|
|
103
|
+
{ cls: 'xray-num', text: `~${d.toolSchemaTokens}` },
|
|
104
|
+
]),
|
|
105
|
+
],
|
|
106
|
+
}),
|
|
107
|
+
h('p', { className: 'xray-muted', style: { marginTop: 12 } }, `captured ${d.capturedAt}`),
|
|
108
|
+
),
|
|
109
|
+
|
|
110
|
+
health: (d) =>
|
|
111
|
+
h(
|
|
112
|
+
react.Fragment,
|
|
113
|
+
null,
|
|
114
|
+
h(
|
|
115
|
+
'p',
|
|
116
|
+
null,
|
|
117
|
+
h('span', { className: 'xray-ok' }, `${d.healthy.length} healthy`),
|
|
118
|
+
d.waiting.length ? ` · ${d.waiting.length} waiting` : null,
|
|
119
|
+
d.unhealthy.length
|
|
120
|
+
? h(
|
|
121
|
+
react.Fragment,
|
|
122
|
+
null,
|
|
123
|
+
' · ',
|
|
124
|
+
h('span', { className: 'xray-warn' }, `${d.unhealthy.length} unhealthy`),
|
|
125
|
+
)
|
|
126
|
+
: null,
|
|
127
|
+
),
|
|
128
|
+
d.unhealthy.length
|
|
129
|
+
? h(Table, {
|
|
130
|
+
headers: ['plugin', 'fiber', 'state', 'error'],
|
|
131
|
+
rows: d.unhealthy.flatMap((p) =>
|
|
132
|
+
p.fibers.map((f) =>
|
|
133
|
+
Row(`${p.name}/${f.uid}`, [
|
|
134
|
+
p.name,
|
|
135
|
+
{ cls: 'xray-num', text: String(f.uid) },
|
|
136
|
+
{ cls: 'xray-warn', text: f.state },
|
|
137
|
+
f.error ?? '',
|
|
138
|
+
]),
|
|
139
|
+
),
|
|
140
|
+
),
|
|
141
|
+
})
|
|
142
|
+
: null,
|
|
143
|
+
d.waiting.length
|
|
144
|
+
? h(Table, {
|
|
145
|
+
headers: ['waiting plugin', 'wants'],
|
|
146
|
+
rows: d.waiting.map((p) => Row(p.name, [p.name, p.inject.join(', ')])),
|
|
147
|
+
})
|
|
148
|
+
: null,
|
|
149
|
+
),
|
|
150
|
+
|
|
151
|
+
deps: (d) =>
|
|
152
|
+
h(
|
|
153
|
+
react.Fragment,
|
|
154
|
+
null,
|
|
155
|
+
d.unsatisfied.length
|
|
156
|
+
? h('p', { className: 'xray-warn' }, `${d.unsatisfied.length} unsatisfied inject(s)`)
|
|
157
|
+
: null,
|
|
158
|
+
Object.keys(d.cascade).length
|
|
159
|
+
? h(
|
|
160
|
+
react.Fragment,
|
|
161
|
+
null,
|
|
162
|
+
h('div', { className: 'xray-h3' }, 'disable-cascade'),
|
|
163
|
+
h(Table, {
|
|
164
|
+
headers: ['provider', 'affects'],
|
|
165
|
+
rows: Object.entries(d.cascade).map(([provider, affected]) =>
|
|
166
|
+
Row(provider, [provider, affected.join(', ')]),
|
|
167
|
+
),
|
|
168
|
+
}),
|
|
169
|
+
)
|
|
170
|
+
: null,
|
|
171
|
+
h('div', { className: 'xray-h3' }, 'services'),
|
|
172
|
+
h(Table, {
|
|
173
|
+
headers: ['service', 'provided by', 'consumed by'],
|
|
174
|
+
rows: Object.entries(d.services).map(([name, node]) =>
|
|
175
|
+
Row(name, [name, node.providers.join(', ') || '—', node.consumers.join(', ') || '—']),
|
|
176
|
+
),
|
|
177
|
+
}),
|
|
178
|
+
),
|
|
179
|
+
|
|
180
|
+
cost: (d) =>
|
|
181
|
+
h(
|
|
182
|
+
react.Fragment,
|
|
183
|
+
null,
|
|
184
|
+
h(
|
|
185
|
+
'p',
|
|
186
|
+
null,
|
|
187
|
+
`~${d.totalTokens} tokens: ${d.toolCount} tool schema(s) ~${d.toolTokens} + ${d.sectionCount} prompt section(s) ~${d.sectionTokens}`,
|
|
188
|
+
),
|
|
189
|
+
d.sections.length
|
|
190
|
+
? h(
|
|
191
|
+
react.Fragment,
|
|
192
|
+
null,
|
|
193
|
+
h('div', { className: 'xray-h3' }, 'prompt sections'),
|
|
194
|
+
h(Table, {
|
|
195
|
+
headers: ['section', 'tokens', 'share', ''],
|
|
196
|
+
rows: d.sections.map((s) =>
|
|
197
|
+
Row(s.name, [
|
|
198
|
+
s.name,
|
|
199
|
+
{ cls: 'xray-num', text: `~${s.tokens}` },
|
|
200
|
+
{ cls: 'xray-num', text: `${s.share}%` },
|
|
201
|
+
h(Bar, { share: s.share }),
|
|
202
|
+
]),
|
|
203
|
+
),
|
|
204
|
+
}),
|
|
205
|
+
)
|
|
206
|
+
: h(
|
|
207
|
+
'p',
|
|
208
|
+
{ className: 'xray-muted' },
|
|
209
|
+
'no prompt assembly observed yet — send one agent message first',
|
|
210
|
+
),
|
|
211
|
+
h('div', { className: 'xray-h3' }, 'tool schemas'),
|
|
212
|
+
h(Table, {
|
|
213
|
+
headers: ['tool', 'tokens', 'share', ''],
|
|
214
|
+
rows: d.tools.map((t) =>
|
|
215
|
+
Row(t.name, [
|
|
216
|
+
t.name,
|
|
217
|
+
{ cls: 'xray-num', text: `~${t.tokens}` },
|
|
218
|
+
{ cls: 'xray-num', text: `${t.share}%` },
|
|
219
|
+
h(Bar, { share: t.share }),
|
|
220
|
+
]),
|
|
221
|
+
),
|
|
222
|
+
}),
|
|
223
|
+
),
|
|
224
|
+
|
|
225
|
+
shadow: (d) =>
|
|
226
|
+
h(
|
|
227
|
+
react.Fragment,
|
|
228
|
+
null,
|
|
229
|
+
d.services.length
|
|
230
|
+
? h(Table, {
|
|
231
|
+
headers: ['service', 'providers'],
|
|
232
|
+
rows: d.services.map((s) =>
|
|
233
|
+
Row(s.service, [
|
|
234
|
+
s.service,
|
|
235
|
+
{ cls: 'xray-warn', text: s.providers.join(' AND ') },
|
|
236
|
+
]),
|
|
237
|
+
),
|
|
238
|
+
})
|
|
239
|
+
: h('p', { className: 'xray-ok' }, 'no service is provided by more than one plugin'),
|
|
240
|
+
d.registrars.length
|
|
241
|
+
? h(
|
|
242
|
+
react.Fragment,
|
|
243
|
+
null,
|
|
244
|
+
h('div', { className: 'xray-h3' }, 'registrars'),
|
|
245
|
+
h(Table, {
|
|
246
|
+
headers: ['plugin', 'registrations'],
|
|
247
|
+
rows: d.registrars.map((r) =>
|
|
248
|
+
Row(r.plugin, [r.plugin, { cls: 'xray-num', text: String(r.registrations) }]),
|
|
249
|
+
),
|
|
250
|
+
}),
|
|
251
|
+
)
|
|
252
|
+
: null,
|
|
253
|
+
),
|
|
254
|
+
};
|
|
255
|
+
//#endregion
|
|
256
|
+
|
|
257
|
+
//#region panel (one conversation.view tab)
|
|
258
|
+
function XrayPanel() {
|
|
259
|
+
const [view, setView] = react.useState('summary');
|
|
260
|
+
// `for` stamps which view the payload belongs to: a click flips `view`
|
|
261
|
+
// synchronously while `state` still carries the previous view's data —
|
|
262
|
+
// rendering that mismatch with the new renderer throws and unmounts
|
|
263
|
+
// the whole tab. Mismatch renders as loading instead.
|
|
264
|
+
const [state, setState] = react.useState({ phase: 'loading', for: 'summary' });
|
|
265
|
+
react.useEffect(() => {
|
|
266
|
+
let alive = true;
|
|
267
|
+
setState({ phase: 'loading', for: view });
|
|
268
|
+
fetch(`/xray/api/${view}`)
|
|
269
|
+
.then(async (res) => {
|
|
270
|
+
if (!res.ok) throw new Error(await res.text());
|
|
271
|
+
return res.json();
|
|
272
|
+
})
|
|
273
|
+
.then((data) => {
|
|
274
|
+
if (alive) setState({ phase: 'ready', for: view, data });
|
|
275
|
+
})
|
|
276
|
+
.catch((err) => {
|
|
277
|
+
if (alive)
|
|
278
|
+
setState({
|
|
279
|
+
phase: 'error',
|
|
280
|
+
for: view,
|
|
281
|
+
message: err instanceof Error ? err.message : String(err),
|
|
282
|
+
});
|
|
283
|
+
});
|
|
284
|
+
return () => {
|
|
285
|
+
alive = false;
|
|
286
|
+
};
|
|
287
|
+
}, [view]);
|
|
288
|
+
let body;
|
|
289
|
+
if (state.for !== view || state.phase === 'loading')
|
|
290
|
+
body = h('p', { className: 'xray-muted' }, `loading ${view}…`);
|
|
291
|
+
else if (state.phase === 'error') body = h('p', { className: 'xray-warn' }, state.message);
|
|
292
|
+
else {
|
|
293
|
+
// A diagnostic surface must never take itself down on one bad
|
|
294
|
+
// payload: render the failure, keep the tab and its nav alive.
|
|
295
|
+
try {
|
|
296
|
+
body = renderers[view](state.data);
|
|
297
|
+
} catch (err) {
|
|
298
|
+
body = h(
|
|
299
|
+
'p',
|
|
300
|
+
{ className: 'xray-warn' },
|
|
301
|
+
`render failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
302
|
+
);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
return h(
|
|
306
|
+
'div',
|
|
307
|
+
{ className: 'xray-panel' },
|
|
308
|
+
h('p', { className: 'xray-sub' }, 'composition X-ray — live from this harness'),
|
|
309
|
+
h(
|
|
310
|
+
'nav',
|
|
311
|
+
{ className: 'xray-nav' },
|
|
312
|
+
VIEWS.map((name) =>
|
|
313
|
+
h(
|
|
314
|
+
'button',
|
|
315
|
+
{ key: name, className: name === view ? 'active' : '', onClick: () => setView(name) },
|
|
316
|
+
name,
|
|
317
|
+
),
|
|
318
|
+
),
|
|
319
|
+
),
|
|
320
|
+
body,
|
|
321
|
+
);
|
|
322
|
+
}
|
|
323
|
+
//#endregion
|
|
324
|
+
|
|
325
|
+
const inject = ['slots'];
|
|
326
|
+
/** Mount the X-ray tab into the conversation view ring (beside Chat / Trajectory). */
|
|
327
|
+
function apply(ctx) {
|
|
328
|
+
ctx.slots.inject('conversation.view', () =>
|
|
329
|
+
ctx.slots.register(
|
|
330
|
+
{ name: 'conversation.view', id: 'xray', order: 20, label: 'X-ray' },
|
|
331
|
+
XrayPanel,
|
|
332
|
+
),
|
|
333
|
+
);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
exports.apply = apply;
|
|
337
|
+
exports.inject = inject;
|
|
338
|
+
return module.exports;
|
|
339
|
+
},
|
|
340
|
+
});
|
package/lib/index.js
CHANGED
|
@@ -91,6 +91,43 @@ function apply(ctx) {
|
|
|
91
91
|
];
|
|
92
92
|
}, 'xray-runtime-snapshot');
|
|
93
93
|
|
|
94
|
+
// Web panel: mounts when the profile composes a webServer (dsh web).
|
|
95
|
+
// Headless profiles simply never activate this subplugin.
|
|
96
|
+
ctx.plugin({
|
|
97
|
+
name: 'dsh-xray-panel',
|
|
98
|
+
inject: ['webServer'],
|
|
99
|
+
apply: (wctx) => {
|
|
100
|
+
const { mountPanel } = require('./panel.js');
|
|
101
|
+
const freshSnap = () => {
|
|
102
|
+
const snap = snapshotRegistry(ctx);
|
|
103
|
+
snap.transitions = Object.fromEntries(transitions);
|
|
104
|
+
snap.promptAssembly = lastAssembly;
|
|
105
|
+
return snap;
|
|
106
|
+
};
|
|
107
|
+
wctx.effect(
|
|
108
|
+
() =>
|
|
109
|
+
mountPanel(wctx.webServer, {
|
|
110
|
+
summary: () => {
|
|
111
|
+
const snap = freshSnap();
|
|
112
|
+
return {
|
|
113
|
+
plugins: snap.plugins.length,
|
|
114
|
+
unhealthy: health(snap).unhealthy.length,
|
|
115
|
+
services: Object.keys(serviceGraph(snap).services).length,
|
|
116
|
+
toolSchemaTokens: contextCost(snap).totalTokens,
|
|
117
|
+
capturedAt: snap.capturedAt,
|
|
118
|
+
};
|
|
119
|
+
},
|
|
120
|
+
deps: () => serviceGraph(freshSnap()),
|
|
121
|
+
health: () => health(freshSnap()),
|
|
122
|
+
cost: () => contextCost(freshSnap()),
|
|
123
|
+
shadow: () => shadowing(freshSnap()),
|
|
124
|
+
}),
|
|
125
|
+
'xray-panel-routes',
|
|
126
|
+
);
|
|
127
|
+
logger.info('xray panel mounted at /xray');
|
|
128
|
+
},
|
|
129
|
+
});
|
|
130
|
+
|
|
94
131
|
// Agent self-introspection tool: only when a tool registry exists
|
|
95
132
|
// (headless/web both have one; keep it optional so xray mounts anywhere).
|
|
96
133
|
ctx.plugin({
|
|
@@ -119,7 +156,8 @@ function apply(ctx) {
|
|
|
119
156
|
'Introspect the live plugin composition of this harness: every mounted plugin, ' +
|
|
120
157
|
'the services it requires (inject) and provides, its lifecycle state, and ' +
|
|
121
158
|
'unhealthy plugins with their last transitions. Use to answer "what capabilities ' +
|
|
122
|
-
'do I have / what plugin provides X / why is Y unavailable".'
|
|
159
|
+
'do I have / what plugin provides X / why is Y unavailable". A human-browsable ' +
|
|
160
|
+
'panel with the same data is served at /xray on this harness.',
|
|
123
161
|
parameters: {
|
|
124
162
|
view: {
|
|
125
163
|
type: 'string',
|
package/lib/panel.js
ADDED
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
// Self-contained web panel: one HTML page + JSON endpoints, mounted on the
|
|
2
|
+
// host webServer. No client-module bundle, no React, no build step — the
|
|
3
|
+
// model layer already computes everything; this only renders it.
|
|
4
|
+
|
|
5
|
+
const PAGE = `<!doctype html>
|
|
6
|
+
<html lang="en">
|
|
7
|
+
<head>
|
|
8
|
+
<meta charset="utf-8">
|
|
9
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
10
|
+
<title>dsh-xray</title>
|
|
11
|
+
<style>
|
|
12
|
+
:root { color-scheme: dark; }
|
|
13
|
+
* { box-sizing: border-box; margin: 0; }
|
|
14
|
+
body { font: 13px/1.5 ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
15
|
+
background: #0d1117; color: #e6edf3; padding: 24px; }
|
|
16
|
+
h1 { font-size: 16px; margin-bottom: 4px; }
|
|
17
|
+
.sub { color: #8b949e; margin-bottom: 20px; }
|
|
18
|
+
nav { display: flex; gap: 8px; margin-bottom: 16px; flex-wrap: wrap; }
|
|
19
|
+
nav button { background: #21262d; color: #e6edf3; border: 1px solid #30363d;
|
|
20
|
+
border-radius: 6px; padding: 5px 12px; cursor: pointer; font: inherit; }
|
|
21
|
+
nav button.active { background: #1f6feb; border-color: #1f6feb; }
|
|
22
|
+
table { border-collapse: collapse; width: 100%; margin-top: 8px; }
|
|
23
|
+
th, td { text-align: left; padding: 4px 10px; border-bottom: 1px solid #21262d; }
|
|
24
|
+
th { color: #8b949e; font-weight: normal; }
|
|
25
|
+
.num { text-align: right; }
|
|
26
|
+
.bar { background: #1f6feb; height: 10px; border-radius: 2px; display: inline-block; }
|
|
27
|
+
.warn { color: #f85149; }
|
|
28
|
+
.ok { color: #7ee787; }
|
|
29
|
+
.muted { color: #8b949e; }
|
|
30
|
+
#status { margin: 12px 0; color: #8b949e; }
|
|
31
|
+
.tag { background: #21262d; border-radius: 4px; padding: 1px 6px; margin-left: 6px; font-size: 11px; }
|
|
32
|
+
</style>
|
|
33
|
+
</head>
|
|
34
|
+
<body>
|
|
35
|
+
<h1>dsh-xray</h1>
|
|
36
|
+
<div class="sub">composition X-ray — live from this harness</div>
|
|
37
|
+
<nav id="nav"></nav>
|
|
38
|
+
<div id="status"></div>
|
|
39
|
+
<div id="content"></div>
|
|
40
|
+
<script>
|
|
41
|
+
const views = ['summary', 'health', 'deps', 'cost', 'shadow'];
|
|
42
|
+
const esc = (s) => String(s ?? '').replace(/[&<>]/g, (c) => ({'&':'&','<':'<','>':'>'}[c]));
|
|
43
|
+
const nav = document.getElementById('nav');
|
|
44
|
+
const content = document.getElementById('content');
|
|
45
|
+
const status = document.getElementById('status');
|
|
46
|
+
let active = 'summary';
|
|
47
|
+
|
|
48
|
+
for (const v of views) {
|
|
49
|
+
const b = document.createElement('button');
|
|
50
|
+
b.textContent = v;
|
|
51
|
+
b.onclick = () => { active = v; render(); };
|
|
52
|
+
b.id = 'nav-' + v;
|
|
53
|
+
nav.appendChild(b);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function table(headers, rows) {
|
|
57
|
+
return '<table><tr>' + headers.map((h) => '<th>' + esc(h) + '</th>').join('') + '</tr>'
|
|
58
|
+
+ rows.join('') + '</table>';
|
|
59
|
+
}
|
|
60
|
+
function bar(share) {
|
|
61
|
+
return '<span class="bar" style="width:' + Math.max(2, share * 2) + 'px"></span>';
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const renderers = {
|
|
65
|
+
summary(d) {
|
|
66
|
+
return table(['metric', 'value'], [
|
|
67
|
+
'<tr><td>plugins mounted</td><td class="num">' + d.plugins + '</td></tr>',
|
|
68
|
+
'<tr><td>unhealthy</td><td class="num ' + (d.unhealthy ? 'warn' : 'ok') + '">' + d.unhealthy + '</td></tr>',
|
|
69
|
+
'<tr><td>services</td><td class="num">' + d.services + '</td></tr>',
|
|
70
|
+
'<tr><td>context tokens (tools + sections)</td><td class="num">~' + d.toolSchemaTokens + '</td></tr>',
|
|
71
|
+
]) + '<p class="muted" style="margin-top:12px">captured ' + esc(d.capturedAt) + '</p>';
|
|
72
|
+
},
|
|
73
|
+
health(d) {
|
|
74
|
+
let html = '<p><span class="ok">' + d.healthy.length + ' healthy</span>'
|
|
75
|
+
+ (d.waiting.length ? ' · ' + d.waiting.length + ' waiting' : '')
|
|
76
|
+
+ (d.unhealthy.length ? ' · <span class="warn">' + d.unhealthy.length + ' unhealthy</span>' : '') + '</p>';
|
|
77
|
+
if (d.unhealthy.length) {
|
|
78
|
+
html += table(['plugin', 'fiber', 'state', 'error'], d.unhealthy.flatMap((p) =>
|
|
79
|
+
p.fibers.map((f) => '<tr><td>' + esc(p.name) + '</td><td class="num">' + f.uid
|
|
80
|
+
+ '</td><td class="warn">' + esc(f.state) + '</td><td>' + esc(f.error ?? '') + '</td></tr>')));
|
|
81
|
+
}
|
|
82
|
+
if (d.waiting.length) {
|
|
83
|
+
html += table(['waiting plugin', 'wants'], d.waiting.map((p) =>
|
|
84
|
+
'<tr><td>' + esc(p.name) + '</td><td>' + esc(p.inject.join(', ')) + '</td></tr>'));
|
|
85
|
+
}
|
|
86
|
+
return html;
|
|
87
|
+
},
|
|
88
|
+
deps(d) {
|
|
89
|
+
const services = Object.entries(d.services).map(([name, node]) =>
|
|
90
|
+
'<tr><td>' + esc(name) + '</td><td>' + esc(node.providers.join(', ') || '—')
|
|
91
|
+
+ '</td><td>' + esc(node.consumers.join(', ') || '—') + '</td></tr>');
|
|
92
|
+
let html = table(['service', 'provided by', 'consumed by'], services);
|
|
93
|
+
const cascade = Object.entries(d.cascade);
|
|
94
|
+
if (cascade.length) {
|
|
95
|
+
html = '<h3 style="margin:8px 0">disable-cascade</h3>'
|
|
96
|
+
+ table(['provider', 'affects'], cascade.map(([p, a]) =>
|
|
97
|
+
'<tr><td>' + esc(p) + '</td><td>' + esc(a.join(', ')) + '</td></tr>'))
|
|
98
|
+
+ '<h3 style="margin:16px 0 8px">services</h3>' + html;
|
|
99
|
+
}
|
|
100
|
+
if (d.unsatisfied.length) {
|
|
101
|
+
html = '<p class="warn">' + d.unsatisfied.length + ' unsatisfied inject(s)</p>' + html;
|
|
102
|
+
}
|
|
103
|
+
return html;
|
|
104
|
+
},
|
|
105
|
+
cost(d) {
|
|
106
|
+
let html = '<p>~' + d.totalTokens + ' tokens: ' + d.toolCount + ' tool schema(s) ~' + d.toolTokens
|
|
107
|
+
+ ' + ' + d.sectionCount + ' prompt section(s) ~' + d.sectionTokens + '</p>';
|
|
108
|
+
if (d.sections.length) {
|
|
109
|
+
html += '<h3 style="margin:12px 0 4px">prompt sections</h3>'
|
|
110
|
+
+ table(['section', 'tokens', 'share', ''], d.sections.map((s) =>
|
|
111
|
+
'<tr><td>' + esc(s.name) + '</td><td class="num">~' + s.tokens + '</td><td class="num">'
|
|
112
|
+
+ s.share + '%</td><td>' + bar(s.share) + '</td></tr>'));
|
|
113
|
+
} else {
|
|
114
|
+
html += '<p class="muted">no prompt assembly observed yet — send one agent message first</p>';
|
|
115
|
+
}
|
|
116
|
+
html += '<h3 style="margin:12px 0 4px">tool schemas</h3>'
|
|
117
|
+
+ table(['tool', 'tokens', 'share', ''], d.tools.map((t) =>
|
|
118
|
+
'<tr><td>' + esc(t.name) + '</td><td class="num">~' + t.tokens + '</td><td class="num">'
|
|
119
|
+
+ t.share + '%</td><td>' + bar(t.share) + '</td></tr>'));
|
|
120
|
+
return html;
|
|
121
|
+
},
|
|
122
|
+
shadow(d) {
|
|
123
|
+
let html = d.services.length
|
|
124
|
+
? table(['service', 'providers'], d.services.map((s) =>
|
|
125
|
+
'<tr><td>' + esc(s.service) + '</td><td class="warn">' + esc(s.providers.join(' AND ')) + '</td></tr>'))
|
|
126
|
+
: '<p class="ok">no service is provided by more than one plugin</p>';
|
|
127
|
+
if (d.registrars.length) {
|
|
128
|
+
html += '<h3 style="margin:12px 0 4px">registrars</h3>'
|
|
129
|
+
+ table(['plugin', 'registrations'], d.registrars.map((r) =>
|
|
130
|
+
'<tr><td>' + esc(r.plugin) + '</td><td class="num">' + r.registrations + '</td></tr>'));
|
|
131
|
+
}
|
|
132
|
+
return html;
|
|
133
|
+
},
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
async function render() {
|
|
137
|
+
for (const v of views) document.getElementById('nav-' + v).className = v === active ? 'active' : '';
|
|
138
|
+
status.textContent = 'loading ' + active + '…';
|
|
139
|
+
try {
|
|
140
|
+
const res = await fetch('/xray/api/' + active);
|
|
141
|
+
if (!res.ok) throw new Error(await res.text());
|
|
142
|
+
const data = await res.json();
|
|
143
|
+
content.innerHTML = renderers[active](data);
|
|
144
|
+
status.textContent = '';
|
|
145
|
+
} catch (err) {
|
|
146
|
+
status.innerHTML = '<span class="warn">' + esc(err.message) + '</span>';
|
|
147
|
+
content.innerHTML = '';
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
render();
|
|
151
|
+
setInterval(() => { if (active === 'health' || active === 'summary') render(); }, 5000);
|
|
152
|
+
</script>
|
|
153
|
+
</body>
|
|
154
|
+
</html>`;
|
|
155
|
+
|
|
156
|
+
function sendJson(response, code, value) {
|
|
157
|
+
const body = JSON.stringify(value);
|
|
158
|
+
response.writeHead(code, {
|
|
159
|
+
'content-type': 'application/json; charset=utf-8',
|
|
160
|
+
'cache-control': 'no-store',
|
|
161
|
+
});
|
|
162
|
+
response.end(body);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Mount the panel routes. `views` supplies fresh data per request:
|
|
167
|
+
* { summary, deps, health, cost, shadow } — each a () => object.
|
|
168
|
+
* Returns the disposers webServer.register produced.
|
|
169
|
+
*/
|
|
170
|
+
function mountPanel(webServer, views) {
|
|
171
|
+
const disposers = [];
|
|
172
|
+
disposers.push(
|
|
173
|
+
webServer.register({
|
|
174
|
+
kind: 'exact',
|
|
175
|
+
path: '/xray',
|
|
176
|
+
handler: (request, response) => {
|
|
177
|
+
if (request.method !== 'GET') {
|
|
178
|
+
response.writeHead(405, { allow: 'GET' });
|
|
179
|
+
response.end();
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
response.writeHead(200, {
|
|
183
|
+
'content-type': 'text/html; charset=utf-8',
|
|
184
|
+
'cache-control': 'no-store',
|
|
185
|
+
});
|
|
186
|
+
response.end(PAGE);
|
|
187
|
+
},
|
|
188
|
+
}),
|
|
189
|
+
);
|
|
190
|
+
for (const [name, compute] of Object.entries(views)) {
|
|
191
|
+
disposers.push(
|
|
192
|
+
webServer.register({
|
|
193
|
+
kind: 'exact',
|
|
194
|
+
path: `/xray/api/${name}`,
|
|
195
|
+
handler: (request, response) => {
|
|
196
|
+
if (request.method !== 'GET') {
|
|
197
|
+
response.writeHead(405, { allow: 'GET' });
|
|
198
|
+
response.end();
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
try {
|
|
202
|
+
sendJson(response, 200, compute());
|
|
203
|
+
} catch (err) {
|
|
204
|
+
sendJson(response, 500, { error: err.message });
|
|
205
|
+
}
|
|
206
|
+
},
|
|
207
|
+
}),
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
return disposers;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
module.exports = { mountPanel, PAGE };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-xray",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "X-ray for your DeepSeek Harness — see what's actually loaded, why, and what it costs you.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
},
|
|
16
16
|
"exports": {
|
|
17
17
|
".": "./lib/index.js",
|
|
18
|
+
"./client": "./lib/client.js",
|
|
18
19
|
"./package.json": "./package.json",
|
|
19
20
|
"./cordis.patch.yml": "./cordis.patch.yml"
|
|
20
21
|
},
|
|
@@ -56,6 +57,14 @@
|
|
|
56
57
|
"dsh": {
|
|
57
58
|
"bundle": {
|
|
58
59
|
"patch": "./cordis.patch.yml"
|
|
60
|
+
},
|
|
61
|
+
"client": {
|
|
62
|
+
"platform": "web",
|
|
63
|
+
"inject": [
|
|
64
|
+
"@deepseek-ai/dsh-client-runtime",
|
|
65
|
+
"@deepseek-ai/dsh-client-ui-slots",
|
|
66
|
+
"@deepseek-ai/dsh-client-ui-conversation"
|
|
67
|
+
]
|
|
59
68
|
}
|
|
60
69
|
},
|
|
61
70
|
"peerDependencies": {
|