dsh-xray 0.6.0 → 0.7.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/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
- > **Status: 0.6.x — static + runtime imaging, context-cost attribution, and a web panel.** Static commands work even when dsh cannot boot; `deps`/`health`/`cost`/`shadow` and the agent tool need the plugin mounted.
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
 
@@ -82,6 +84,10 @@ The `xray_composition` tool lets agents inspect their own capability set.
82
84
  ### 🖥️ Web Panel
83
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.
84
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
+ ![The /xray panel: deps view with the disable-cascade table](./assets/panel-deps.webp)
90
+
85
91
  What every request actually carries — prompt sections observed at assembly, blended with tool schemas:
86
92
 
87
93
  ```console
@@ -134,10 +140,31 @@ Mounted in the tree, dsh-xray registers an `xray_composition` tool (`view: summa
134
140
  <img src="./assets/section-install.svg" width="100%" alt="Install">
135
141
  </p>
136
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
+
137
153
  ```sh
138
154
  dsh plugin --profile web add dsh-xray
155
+ # bundle plugins take effect on the next start — restart dsh web
156
+ ```
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
139
164
  ```
140
165
 
166
+ Uninstall: `dsh plugin --profile web remove dsh-xray`.
167
+
141
168
  All commands take `--profile <name>` (default `web`) and `--json`.
142
169
 
143
170
  | Command | Behavior |
package/README.zh.md CHANGED
@@ -1,16 +1,26 @@
1
- # dsh-xray
2
-
3
- [![npm](https://img.shields.io/npm/v/dsh-xray)](https://www.npmjs.com/package/dsh-xray)
4
- [![CI](https://github.com/alloevil/dsh-xray/actions/workflows/check.yml/badge.svg)](https://github.com/alloevil/dsh-xray/actions/workflows/check.yml)
5
- [![license](https://img.shields.io/npm/l/dsh-xray)](./LICENSE)
6
-
7
- [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) 拍 X 光——看清到底加载了什么、为什么在那、以及它悄悄花掉了你什么。
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
  ![dsh-xray 演示](./docs/demo.svg)
10
22
 
11
- [English](./README.md)
12
-
13
- > **状态:0.6.x — 静态 + 运行时成像、上下文成本归因、Web 面板。** 静态命令在 dsh 起不来时照样能用;`deps`/`health`/`cost`/`shadow` 和 agent 工具需要插件已挂载。
23
+ > 静态命令在 dsh 起不来时照样能用;`deps`/`health`/`cost`/`shadow` 和 agent 工具需要插件已挂载。
14
24
 
15
25
  `dsh --dump-config` 只给你原始组合树,插件面板只给你平铺列表。它们都不回答:这个插件*为什么*在这、停用它会*连带瘫掉什么*、它在*悄悄消耗什么*。dsh-xray 回答这些。
16
26
 
@@ -95,6 +105,10 @@ orphan overrides (silently skipped) (1)
95
105
 
96
106
  在 `dsh web` 中挂载后,插件在 **`/xray`** 提供零依赖面板——summary、health、deps(含停用级联表)、cost、shadow 五个视图,数据实时来自运行中的组合树;`/xray/api/*` 提供同源 JSON。
97
107
 
108
+ 包同时携带 client 半(`dsh.client` 声明 + `exports["./client"]`):宿主自动发现并在每个会话中挂载 **与 Chat / Trajectory 并列的 X-ray 标签页**,用宿主设计变量原生渲染同样五个视图。独立的 `/xray` 页面保留为降级通道——它只依赖 web 服务器,不依赖它所诊断的 client-module 加载链路。
109
+
110
+ ![/xray 面板:deps 视图与停用级联表](./assets/panel-deps.webp)
111
+
98
112
  ## 安全立场
99
113
 
100
114
  dsh-xray 只读不执行。patch 文件里的 loader `!!js` 表达式解析为不透明标记、绝不求值;CLI 从不执行插件代码(`audit` 是对源码文本的模式扫描);挂载的插件只写 `$DSH_HOME/xray/` 目录。详见 [SECURITY.md](./SECURITY.md)。
@@ -103,26 +117,47 @@ dsh-xray 只读不执行。patch 文件里的 loader `!!js` 表达式解析为
103
117
 
104
118
  对运行中组合树的诊断成像——与 [dsh-doctor](https://www.npmjs.com/package/dsh-doctor)(救援与恢复)互补。
105
119
 
106
- 0.4.x 已交付:
120
+ 已交付:
107
121
 
108
122
  - **来源归因** — 每个活跃插件来自哪一层:内核 bundle / profile 依赖 / `cordis.patch.yml` insert / repository 源
109
123
  - **声明 vs 实际 diff** — 装了但没生效、卸了但残留 patch 行
110
124
  - **冲突检测** — 多个插件 patch 同一配置行时,谁静默赢了
111
- - **组合快照** — 把当前生效组合导出为 lockfile,异地一键复现
125
+ - **组合快照** — 把当前生效组合导出为 lockfile;`--against` 对比漂移
112
126
  - **服务依赖图** — 每个服务谁提供、谁消费;停用 X 会级联影响什么(`deps`)
113
127
  - **运行时健康** — 每个插件的 fiber 生命周期状态、启动失败、状态迁移史(`health`)
114
128
  - **Agent 自省** — `xray_composition` 工具让 agent 检视自己的能力集
115
-
116
129
  - **能力审计** — 对 out-of-tree 插件的启发式静态扫描:网络外发、shell、文件系统、环境变量、动态求值(`audit`)
117
130
  - **服务重名检测** — 被多个插件同时提供的服务,及每插件工具/命令注册数(`shadow`)
118
131
  - **上下文成本** — prompt sections(观测自 system-prompt/assemble)+ 工具 schema 的估算 token 占用(`cost`)
132
+ - **Web 面板** — `/xray` 五视图,零依赖零构建
119
133
 
120
134
  ## 安装
121
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
+
122
146
  ```sh
123
147
  dsh plugin --profile web add dsh-xray
148
+ # bundle 插件重启后生效——重启 dsh web
149
+ ```
150
+
151
+ 验证装好了:
152
+
153
+ ```sh
154
+ dsh --profile web --dump-config | grep dsh-xray # 组合树里有这一行
155
+ npx dsh-xray health # 能读到运行时快照
156
+ # 然后打开 http://localhost:3080/xray 看实时面板
124
157
  ```
125
158
 
159
+ 卸载:`dsh plugin --profile web remove dsh-xray`。
160
+
126
161
  ## 许可证
127
162
 
128
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">v0.2.x · static + runtime</text>
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,355 @@
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:hover{background:var(--dsw-alias-interactive-bg-hover)}',
25
+ // active view: the host's own tab-active family (state-business-*) —
26
+ // visible in both themes, unlike brand-primary which resolves to
27
+ // near-white in dark mode (white-on-white active buttons).
28
+ '.xray-nav button.active{color:var(--dsw-alias-state-business-primary);background:var(--dsw-alias-state-business-tertiary);border-color:var(--dsw-alias-state-business-primary);font-weight:600}',
29
+ // min-height pads the short views (summary is 4 rows, deps dozens) so
30
+ // switching between them moves the layout as little as possible;
31
+ // .xray-stale dims outgoing content while the next payload is in flight.
32
+ '.xray-body{min-height:320px}',
33
+ '.xray-stale{opacity:.45;transition:opacity .15s;pointer-events:none}',
34
+ '.xray-table{border-collapse:collapse;width:100%;margin-top:6px}',
35
+ '.xray-table th,.xray-table td{text-align:left;padding:4px 10px;border-bottom:1px solid var(--dsw-alias-border-l2);vertical-align:top}',
36
+ '.xray-table th{color:var(--dsw-alias-label-tertiary);font-weight:normal}',
37
+ '.xray-num{text-align:right}',
38
+ '.xray-warn{color:var(--dsw-alias-state-error-primary,#f85149)}',
39
+ '.xray-ok{color:var(--dsw-alias-state-success-primary,#3fb950)}',
40
+ '.xray-muted{color:var(--dsw-alias-label-tertiary)}',
41
+ '.xray-bar{background:var(--dsw-alias-state-business-primary);height:10px;border-radius:2px;display:inline-block}',
42
+ '.xray-h3{font-size:13px;font-weight:600;margin:14px 0 4px}',
43
+ ].join('\n');
44
+ if (
45
+ typeof document !== 'undefined' &&
46
+ document.querySelector('style[data-plugin-css="dsh-xray/panel"]') === null
47
+ ) {
48
+ const tag = document.createElement('style');
49
+ tag.dataset.plugin = 'dsh-xray';
50
+ tag.dataset.pluginCss = 'dsh-xray/panel';
51
+ tag.textContent = css;
52
+ document.head.appendChild(tag);
53
+ }
54
+ //#endregion
55
+
56
+ //#region tiny view primitives
57
+ const VIEWS = ['summary', 'health', 'deps', 'cost', 'shadow'];
58
+ function Table({ headers, rows }) {
59
+ return h(
60
+ 'table',
61
+ { className: 'xray-table' },
62
+ h(
63
+ 'thead',
64
+ null,
65
+ h(
66
+ 'tr',
67
+ null,
68
+ headers.map((head, i) => h('th', { key: i }, head)),
69
+ ),
70
+ ),
71
+ h('tbody', null, rows),
72
+ );
73
+ }
74
+ function Bar({ share }) {
75
+ return h('span', { className: 'xray-bar', style: { width: `${Math.max(2, share * 2)}px` } });
76
+ }
77
+ function Row(key, cells) {
78
+ return h(
79
+ 'tr',
80
+ { key },
81
+ cells.map((cell, i) =>
82
+ h(
83
+ 'td',
84
+ { key: i, className: cell?.cls ? cell.cls : void 0 },
85
+ cell && cell.cls !== void 0 ? cell.text : cell,
86
+ ),
87
+ ),
88
+ );
89
+ }
90
+ //#endregion
91
+
92
+ //#region per-view renderers (mirror lib/panel.js, as components)
93
+ const renderers = {
94
+ summary: (d) =>
95
+ h(
96
+ react.Fragment,
97
+ null,
98
+ h(Table, {
99
+ headers: ['metric', 'value'],
100
+ rows: [
101
+ Row('p', ['plugins mounted', { cls: 'xray-num', text: String(d.plugins) }]),
102
+ Row('u', [
103
+ 'unhealthy',
104
+ {
105
+ cls: `xray-num ${d.unhealthy ? 'xray-warn' : 'xray-ok'}`,
106
+ text: String(d.unhealthy),
107
+ },
108
+ ]),
109
+ Row('s', ['services', { cls: 'xray-num', text: String(d.services) }]),
110
+ Row('t', [
111
+ 'context tokens (tools + sections)',
112
+ { cls: 'xray-num', text: `~${d.toolSchemaTokens}` },
113
+ ]),
114
+ ],
115
+ }),
116
+ h('p', { className: 'xray-muted', style: { marginTop: 12 } }, `captured ${d.capturedAt}`),
117
+ ),
118
+
119
+ health: (d) =>
120
+ h(
121
+ react.Fragment,
122
+ null,
123
+ h(
124
+ 'p',
125
+ null,
126
+ h('span', { className: 'xray-ok' }, `${d.healthy.length} healthy`),
127
+ d.waiting.length ? ` · ${d.waiting.length} waiting` : null,
128
+ d.unhealthy.length
129
+ ? h(
130
+ react.Fragment,
131
+ null,
132
+ ' · ',
133
+ h('span', { className: 'xray-warn' }, `${d.unhealthy.length} unhealthy`),
134
+ )
135
+ : null,
136
+ ),
137
+ d.unhealthy.length
138
+ ? h(Table, {
139
+ headers: ['plugin', 'fiber', 'state', 'error'],
140
+ rows: d.unhealthy.flatMap((p) =>
141
+ p.fibers.map((f) =>
142
+ Row(`${p.name}/${f.uid}`, [
143
+ p.name,
144
+ { cls: 'xray-num', text: String(f.uid) },
145
+ { cls: 'xray-warn', text: f.state },
146
+ f.error ?? '',
147
+ ]),
148
+ ),
149
+ ),
150
+ })
151
+ : null,
152
+ d.waiting.length
153
+ ? h(Table, {
154
+ headers: ['waiting plugin', 'wants'],
155
+ rows: d.waiting.map((p) => Row(p.name, [p.name, p.inject.join(', ')])),
156
+ })
157
+ : null,
158
+ ),
159
+
160
+ deps: (d) =>
161
+ h(
162
+ react.Fragment,
163
+ null,
164
+ d.unsatisfied.length
165
+ ? h('p', { className: 'xray-warn' }, `${d.unsatisfied.length} unsatisfied inject(s)`)
166
+ : null,
167
+ Object.keys(d.cascade).length
168
+ ? h(
169
+ react.Fragment,
170
+ null,
171
+ h('div', { className: 'xray-h3' }, 'disable-cascade'),
172
+ h(Table, {
173
+ headers: ['provider', 'affects'],
174
+ rows: Object.entries(d.cascade).map(([provider, affected]) =>
175
+ Row(provider, [provider, affected.join(', ')]),
176
+ ),
177
+ }),
178
+ )
179
+ : null,
180
+ h('div', { className: 'xray-h3' }, 'services'),
181
+ h(Table, {
182
+ headers: ['service', 'provided by', 'consumed by'],
183
+ rows: Object.entries(d.services).map(([name, node]) =>
184
+ Row(name, [name, node.providers.join(', ') || '—', node.consumers.join(', ') || '—']),
185
+ ),
186
+ }),
187
+ ),
188
+
189
+ cost: (d) =>
190
+ h(
191
+ react.Fragment,
192
+ null,
193
+ h(
194
+ 'p',
195
+ null,
196
+ `~${d.totalTokens} tokens: ${d.toolCount} tool schema(s) ~${d.toolTokens} + ${d.sectionCount} prompt section(s) ~${d.sectionTokens}`,
197
+ ),
198
+ d.sections.length
199
+ ? h(
200
+ react.Fragment,
201
+ null,
202
+ h('div', { className: 'xray-h3' }, 'prompt sections'),
203
+ h(Table, {
204
+ headers: ['section', 'tokens', 'share', ''],
205
+ rows: d.sections.map((s) =>
206
+ Row(s.name, [
207
+ s.name,
208
+ { cls: 'xray-num', text: `~${s.tokens}` },
209
+ { cls: 'xray-num', text: `${s.share}%` },
210
+ h(Bar, { share: s.share }),
211
+ ]),
212
+ ),
213
+ }),
214
+ )
215
+ : h(
216
+ 'p',
217
+ { className: 'xray-muted' },
218
+ 'no prompt assembly observed yet — send one agent message first',
219
+ ),
220
+ h('div', { className: 'xray-h3' }, 'tool schemas'),
221
+ h(Table, {
222
+ headers: ['tool', 'tokens', 'share', ''],
223
+ rows: d.tools.map((t) =>
224
+ Row(t.name, [
225
+ t.name,
226
+ { cls: 'xray-num', text: `~${t.tokens}` },
227
+ { cls: 'xray-num', text: `${t.share}%` },
228
+ h(Bar, { share: t.share }),
229
+ ]),
230
+ ),
231
+ }),
232
+ ),
233
+
234
+ shadow: (d) =>
235
+ h(
236
+ react.Fragment,
237
+ null,
238
+ d.services.length
239
+ ? h(Table, {
240
+ headers: ['service', 'providers'],
241
+ rows: d.services.map((s) =>
242
+ Row(s.service, [
243
+ s.service,
244
+ { cls: 'xray-warn', text: s.providers.join(' AND ') },
245
+ ]),
246
+ ),
247
+ })
248
+ : h('p', { className: 'xray-ok' }, 'no service is provided by more than one plugin'),
249
+ d.registrars.length
250
+ ? h(
251
+ react.Fragment,
252
+ null,
253
+ h('div', { className: 'xray-h3' }, 'registrars'),
254
+ h(Table, {
255
+ headers: ['plugin', 'registrations'],
256
+ rows: d.registrars.map((r) =>
257
+ Row(r.plugin, [r.plugin, { cls: 'xray-num', text: String(r.registrations) }]),
258
+ ),
259
+ }),
260
+ )
261
+ : null,
262
+ ),
263
+ };
264
+ //#endregion
265
+
266
+ //#region panel (one conversation.view tab)
267
+ function XrayPanel() {
268
+ const [view, setView] = react.useState('summary');
269
+ // `for` stamps which view the payload belongs to: a click flips `view`
270
+ // synchronously while `state` still carries the previous view's data —
271
+ // rendering that mismatch with the new renderer throws and unmounts
272
+ // the whole tab. During the switch the previous view's content stays
273
+ // up (dimmed) and is replaced in one paint when the payload lands, so
274
+ // the layout changes once per click instead of collapsing to a
275
+ // one-line loading row and re-expanding.
276
+ const [state, setState] = react.useState({ phase: 'loading', for: 'summary' });
277
+ react.useEffect(() => {
278
+ let alive = true;
279
+ fetch(`/xray/api/${view}`)
280
+ .then(async (res) => {
281
+ if (!res.ok) throw new Error(await res.text());
282
+ return res.json();
283
+ })
284
+ .then((data) => {
285
+ if (alive) setState({ phase: 'ready', for: view, data });
286
+ })
287
+ .catch((err) => {
288
+ if (alive)
289
+ setState({
290
+ phase: 'error',
291
+ for: view,
292
+ message: err instanceof Error ? err.message : String(err),
293
+ });
294
+ });
295
+ return () => {
296
+ alive = false;
297
+ };
298
+ }, [view]);
299
+ // Render whatever payload we HAVE (state.for), not the view the user
300
+ // just requested — the stale content keeps the scaffold stable while
301
+ // the fresh payload is in flight.
302
+ const settled = state.phase !== 'loading';
303
+ let body;
304
+ if (!settled) body = h('p', { className: 'xray-muted' }, `loading ${view}…`);
305
+ else if (state.phase === 'error') body = h('p', { className: 'xray-warn' }, state.message);
306
+ else {
307
+ // A diagnostic surface must never take itself down on one bad
308
+ // payload: render the failure, keep the tab and its nav alive.
309
+ try {
310
+ body = renderers[state.for](state.data);
311
+ } catch (err) {
312
+ body = h(
313
+ 'p',
314
+ { className: 'xray-warn' },
315
+ `render failed: ${err instanceof Error ? err.message : String(err)}`,
316
+ );
317
+ }
318
+ }
319
+ const stale = settled && state.for !== view;
320
+ return h(
321
+ 'div',
322
+ { className: 'xray-panel' },
323
+ h('p', { className: 'xray-sub' }, 'composition X-ray — live from this harness'),
324
+ h(
325
+ 'nav',
326
+ { className: 'xray-nav' },
327
+ VIEWS.map((name) =>
328
+ h(
329
+ 'button',
330
+ { key: name, className: name === view ? 'active' : '', onClick: () => setView(name) },
331
+ name,
332
+ ),
333
+ ),
334
+ ),
335
+ h('div', { className: stale ? 'xray-body xray-stale' : 'xray-body' }, body),
336
+ );
337
+ }
338
+ //#endregion
339
+
340
+ const inject = ['slots'];
341
+ /** Mount the X-ray tab into the conversation view ring (beside Chat / Trajectory). */
342
+ function apply(ctx) {
343
+ ctx.slots.inject('conversation.view', () =>
344
+ ctx.slots.register(
345
+ { name: 'conversation.view', id: 'xray', order: 20, label: 'X-ray' },
346
+ XrayPanel,
347
+ ),
348
+ );
349
+ }
350
+
351
+ exports.apply = apply;
352
+ exports.inject = inject;
353
+ return module.exports;
354
+ },
355
+ });
package/lib/index.js CHANGED
@@ -156,7 +156,8 @@ function apply(ctx) {
156
156
  'Introspect the live plugin composition of this harness: every mounted plugin, ' +
157
157
  'the services it requires (inject) and provides, its lifecycle state, and ' +
158
158
  'unhealthy plugins with their last transitions. Use to answer "what capabilities ' +
159
- '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.',
160
161
  parameters: {
161
162
  view: {
162
163
  type: 'string',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-xray",
3
- "version": "0.6.0",
3
+ "version": "0.7.1",
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": {