dsh-plugin-toolkit 0.9.7 → 0.10.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/CHANGELOG.md CHANGED
@@ -1,3 +1,18 @@
1
+ ## 0.10.0
2
+
3
+ - **The checker is docked under the conversation.** It used to live only in the
4
+ right sidebar, behind a guide list nobody opens; now it sits above the input
5
+ bar as one collapsed row, and opens into the full guide: who it is for, how to
6
+ use it, what each check catches, and the caveats (read-only, folder not
7
+ profile, advisory vs hard, runtime behaviour is not provable).
8
+ - The dock and the sidebar tab share one `useChecker()` hook and one `Form` /
9
+ `Result` pair, so the two cannot drift apart.
10
+ - `dsh.client.inject` now names `@deepseek-ai/dsh-client-ui-conversation`, the
11
+ package that owns the dock seat.
12
+ - Three tests: the dock registers under `conversation.input.dock`, it opens onto
13
+ the guide (and stays collapsed before that), and it runs the same request as
14
+ the tab.
15
+
1
16
  ## 0.9.7
2
17
 
3
18
  - **The tab type is now reachable.** Registering a tab *type* is not enough: the
package/README.md CHANGED
@@ -90,6 +90,14 @@ next: npm install && npm test && npm run doctor
90
90
 
91
91
  What you get is not a snippet: it is a plugin whose `doctor` already passes (`engines.dsh`, the patch file, the `files` list), with a client bundle that registers a sidebar tab, a testkit test for it, and a CI workflow that runs both. `init` refuses a directory that is not empty unless you pass `--force`, and refuses a name npm would refuse — those are the two mistakes that cost the most time later.
92
92
 
93
+ ## Where it lives
94
+
95
+ The checker is docked **under the conversation**, above the input bar: a single
96
+ row, collapsed by default. Open it and it carries the whole guide — who it is
97
+ for, how to use it, what each check means, and what it cannot tell you — plus
98
+ the folder field and the results. The same panel is also a **Plugin doctor** tab
99
+ in the right sidebar, for when the conversation is busy.
100
+
93
101
  ## Test a panel without a browser
94
102
 
95
103
  A plugin's client code is one file that calls `window.__ModuleLoader__.load({factory})` — there is no module to import, so the usual React test setup does not apply. `testkit` supplies the missing half: a DOM, the real React, and a host context that records what the panel registers.
package/README.zh.md CHANGED
@@ -53,6 +53,10 @@ cd my-panel && npm install && npm test
53
53
 
54
54
  版本号取自当前运行的 CLI 自身,所以脚手架不会落后于你正在读的文档。
55
55
 
56
+ ## 它在哪里
57
+
58
+ 检查器固定在**对话栏下方、输入框上方**:默认收起成一行,点开就是完整指南——这是给谁用的、怎么用、每一项检查拦什么、以及它证明不了什么——下面才是文件夹输入框和结果。同一个面板也在右侧栏有一个 **Plugin doctor** 标签页,对话区忙的时候用那个。
59
+
56
60
  ## 不用浏览器测面板
57
61
 
58
62
  ```js
package/client.js CHANGED
@@ -21,6 +21,8 @@ window.__ModuleLoader__.load({
21
21
  noDir: 'Name the folder that holds the plugin\'s package.json.',
22
22
  failed: 'Could not run the checks',
23
23
  guideDescription: 'Run the install checks on a plugin folder',
24
+ dockTitle: 'Plugin doctor',
25
+ dockSub: 'Will this plugin survive an install? Check a folder before you publish it.',
24
26
  },
25
27
  zh: {
26
28
  tabTitle: '插件体检',
@@ -34,6 +36,94 @@ window.__ModuleLoader__.load({
34
36
  noDir: '请填写放着 package.json 的插件文件夹。',
35
37
  failed: '检查没能跑起来',
36
38
  guideDescription: '对任意插件文件夹跑一遍安装前的检查',
39
+ dockTitle: '插件体检',
40
+ dockSub: '这个插件装上去还能用吗?发布前先对着文件夹查一遍。',
41
+ },
42
+ };
43
+
44
+ /**
45
+ * The full guide, rendered in the dock. It is deliberately long: the panel
46
+ * used to be a bare input with no explanation of what it was for, which is
47
+ * the first question anyone asks.
48
+ */
49
+ const GUIDE = {
50
+ en: {
51
+ who: {
52
+ title: 'Who this is for',
53
+ body: 'People who write DSH plugins. It is not a daily tool — if you never publish a plugin, it has nothing for you.',
54
+ },
55
+ steps: {
56
+ title: 'How to use it',
57
+ items: [
58
+ 'Paste the folder that holds the plugin\'s package.json (a folder, not a file).',
59
+ 'Press Check. Nothing is installed and none of your files are touched.',
60
+ 'Read the list: ✓ passes, ✗ would break the install, with one line saying why.',
61
+ ],
62
+ },
63
+ checks: {
64
+ title: 'What it checks',
65
+ rows: [
66
+ ['manifest', 'name, version, engines.dsh'],
67
+ ['root export', 'the package exports apply — the loader mounts nothing without it'],
68
+ ['client declaration', 'dsh.client agrees with exports["./client"], which is what makes the host serve the panel'],
69
+ ['files list', 'npm pack would actually ship every path package.json points at'],
70
+ ['patch', 'cordis.patch.yml is exactly one - insert: row'],
71
+ ['readme', 'the README has a Requirements section (the market index asks for it)'],
72
+ ['tarball', 'the packed tarball installs and its root export imports'],
73
+ ],
74
+ },
75
+ notes: {
76
+ title: 'Before you trust it',
77
+ items: [
78
+ 'Read-only: it does not install anything, write files, or make network calls beyond this one request.',
79
+ 'It looks at a folder, not at what you have installed — use dsh-plugin audit --host … for that.',
80
+ 'engines.dsh and README are market-index requirements; a plugin missing them still runs.',
81
+ 'It cannot prove runtime behaviour. A panel can still fail inside the real app when a service arrives late or a registration throws; those failures are appended to ~/.dsh/toolkit-client-report.ndjson.',
82
+ ],
83
+ },
84
+ cli: {
85
+ title: 'The same checks in a terminal',
86
+ body: 'dsh-plugin verify . · dsh-plugin doctor . · dsh-plugin audit --host http://127.0.0.1:PORT',
87
+ },
88
+ },
89
+ zh: {
90
+ who: {
91
+ title: '这是给谁用的',
92
+ body: '写 DSH 插件的人。它不是日常工具——如果你不发布插件,它对你没有用处。',
93
+ },
94
+ steps: {
95
+ title: '怎么用',
96
+ items: [
97
+ '粘贴放着 package.json 的那个文件夹(是文件夹,不是文件)。',
98
+ '按「检查」。不会安装任何东西,也不会改你的文件。',
99
+ '看列表:✓ 通过,✗ 是装上去会坏的地方,右边一行说明原因。',
100
+ ],
101
+ },
102
+ checks: {
103
+ title: '检查什么',
104
+ rows: [
105
+ ['manifest', 'name、version、engines.dsh'],
106
+ ['root export', '包导出的东西里有没有 apply——没有它加载器什么都挂不上'],
107
+ ['client declaration', 'dsh.client 与 exports["./client"] 是否一致,这决定宿主会不会加载面板'],
108
+ ['files list', 'npm pack 是否真的会带上 package.json 指到的每个文件'],
109
+ ['patch', 'cordis.patch.yml 是不是恰好一条 - insert:'],
110
+ ['readme', 'README 有没有 Requirements 段(市场索引会看)'],
111
+ ['tarball', '打包后的 tarball 能不能装上、根导出能不能 import'],
112
+ ],
113
+ },
114
+ notes: {
115
+ title: '用之前要知道的',
116
+ items: [
117
+ '只读:不安装、不写文件、除这一次请求外不联网。',
118
+ '它看的是文件夹,不是你已装的插件——查已装的用 dsh-plugin audit --host …。',
119
+ 'engines.dsh 与 README 是市场索引的要求,缺了插件照样能跑。',
120
+ '它证明不了运行期行为。面板在真实应用里仍可能因为服务来晚了、注册抛错而失败;那些失败会写进 ~/.dsh/toolkit-client-report.ndjson。',
121
+ ],
122
+ },
123
+ cli: {
124
+ title: '同样的检查,命令行里',
125
+ body: 'dsh-plugin verify . · dsh-plugin doctor . · dsh-plugin audit --host http://127.0.0.1:PORT',
126
+ },
37
127
  },
38
128
  };
39
129
 
@@ -75,6 +165,28 @@ window.__ModuleLoader__.load({
75
165
  '.tk-check{font-size:11px;text-transform:uppercase;letter-spacing:.04em;',
76
166
  'color:var(--dsw-alias-label-tertiary,#888)}',
77
167
  '.tk-message{font-size:12px;overflow-wrap:anywhere}',
168
+ '.tk-dock{border-top:0.5px solid var(--dsw-alias-border-l2,#e5e5e5);margin:0 12px}',
169
+ '.tk-dock-head{display:flex}',
170
+ '.tk-dock-toggle{display:flex;align-items:center;gap:8px;width:100%;padding:7px 2px;',
171
+ 'border:0;background:none;font:inherit;font-size:12px;color:inherit;cursor:pointer;text-align:left}',
172
+ '.tk-dock-toggle:focus-visible{outline:1px solid var(--dsw-alias-state-business-primary,#4176e6);',
173
+ 'outline-offset:-1px;border-radius:4px}',
174
+ '.tk-dock-title{font-weight:600}',
175
+ '.tk-dock-sub{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;',
176
+ 'color:var(--dsw-alias-label-tertiary,#888)}',
177
+ '.tk-dock-chevron{flex:none;color:var(--dsw-alias-label-tertiary,#888)}',
178
+ '.tk-dock-body{display:flex;flex-direction:column;gap:12px;padding:2px 2px 14px;',
179
+ 'max-height:46vh;overflow:auto;font-size:12px}',
180
+ '.tk-guide{margin:0;display:flex;flex-direction:column;gap:5px}',
181
+ '.tk-guide h3{margin:0;font-size:12px;font-weight:600}',
182
+ '.tk-guide p{margin:0;color:var(--dsw-alias-label-secondary,#555)}',
183
+ '.tk-guide ol,.tk-guide ul{margin:0;padding-left:18px;display:flex;flex-direction:column;gap:3px;',
184
+ 'color:var(--dsw-alias-label-secondary,#555)}',
185
+ '.tk-guide-rows{display:grid;grid-template-columns:auto 1fr;gap:3px 10px;margin:0}',
186
+ '.tk-guide-rows dt{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:11px;',
187
+ 'color:var(--dsw-alias-label-primary,#222)}',
188
+ '.tk-guide-rows dd{margin:0;color:var(--dsw-alias-label-secondary,#555)}',
189
+ '.tk-guide code{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:11px}',
78
190
  ].join('');
79
191
 
80
192
  function injectStyles(document) {
@@ -96,16 +208,19 @@ window.__ModuleLoader__.load({
96
208
  );
97
209
  }
98
210
 
99
- function Panel() {
211
+ /**
212
+ * The check itself, shared by the dock and the sidebar tab so the two can
213
+ * never drift apart.
214
+ */
215
+ function useChecker() {
100
216
  const [dir, setDir] = useState('');
101
217
  const [result, setResult] = useState(null);
102
218
  const [busy, setBusy] = useState(false);
103
219
  const [failed, setFailed] = useState(null);
104
- const tr = translate;
105
220
 
106
221
  const check = useCallback(() => {
107
222
  if (dir.trim().length === 0) {
108
- setFailed(tr('noDir'));
223
+ setFailed(translate('noDir'));
109
224
  return;
110
225
  }
111
226
  setBusy(true);
@@ -113,35 +228,96 @@ window.__ModuleLoader__.load({
113
228
  fetch(`/toolkit/check?dir=${encodeURIComponent(dir.trim())}`)
114
229
  .then((response) => (response.ok === true ? response.json() : Promise.reject(new Error(String(response.status)))))
115
230
  .then((payload) => setResult(payload))
116
- .catch((error) => setFailed(`${tr('failed')} · ${error.message}`))
231
+ .catch((error) => setFailed(`${translate('failed')} · ${error.message}`))
117
232
  .finally(() => setBusy(false));
118
233
  }, [dir]);
119
234
 
235
+ return { dir, setDir, result, busy, failed, check };
236
+ }
237
+
238
+ function Form({ checker }) {
239
+ return h('div', { className: 'tk-form' },
240
+ h('input', {
241
+ className: 'tk-input',
242
+ type: 'text',
243
+ value: checker.dir,
244
+ placeholder: translate('placeholder'),
245
+ 'aria-label': translate('placeholder'),
246
+ onChange: (event) => checker.setDir(event.target.value),
247
+ onKeyDown: (event) => { if (event.key === 'Enter') checker.check(); },
248
+ }),
249
+ h('button', {
250
+ className: 'tk-button',
251
+ type: 'button',
252
+ disabled: checker.busy === true,
253
+ onClick: checker.check,
254
+ }, checker.busy === true ? translate('checking') : translate('check')));
255
+ }
256
+
257
+ function Result({ checker }) {
258
+ return h('div', { className: 'tk-result' },
259
+ checker.failed !== null ? h('p', { className: 'tk-failed', role: 'status' }, checker.failed) : null,
260
+ checker.result !== null
261
+ ? h('div', { className: 'tk-result-body' },
262
+ h('p', { className: 'tk-verdict', 'data-ok': checker.result.ok === true ? 'true' : 'false', role: 'status' },
263
+ checker.result.ok === true ? translate('pass') : translate('fail')),
264
+ h('ul', { className: 'tk-rows' }, checker.result.checks.map((entry) => h(Row, { key: entry.check, entry }))))
265
+ : null);
266
+ }
267
+
268
+ function Panel() {
269
+ const checker = useChecker();
120
270
  return h('div', { className: 'tk-panel' },
121
- h('h2', { className: 'tk-title' }, tr('title')),
122
- h('p', { className: 'tk-hint' }, tr('hint')),
123
- h('div', { className: 'tk-form' },
124
- h('input', {
125
- className: 'tk-input',
126
- type: 'text',
127
- value: dir,
128
- placeholder: tr('placeholder'),
129
- 'aria-label': tr('placeholder'),
130
- onChange: (event) => setDir(event.target.value),
131
- onKeyDown: (event) => { if (event.key === 'Enter') check(); },
132
- }),
271
+ h('h2', { className: 'tk-title' }, translate('title')),
272
+ h('p', { className: 'tk-hint' }, translate('hint')),
273
+ h(Form, { checker }),
274
+ h(Result, { checker }));
275
+ }
276
+
277
+ /**
278
+ * The same checker, docked under the conversation where people actually
279
+ * look. Collapsed it is one row; opened it carries the whole guide, because
280
+ * "what is this for" is the question the sidebar tab never answered.
281
+ */
282
+ function Dock() {
283
+ const [open, setOpen] = useState(false);
284
+ const checker = useChecker();
285
+ const guide = GUIDE[typeof document === 'undefined' || document.documentElement.lang !== 'zh' ? 'en' : 'zh'];
286
+
287
+ return h('section', { className: 'tk-dock', 'data-open': open === true ? 'true' : 'false' },
288
+ h('div', { className: 'tk-dock-head' },
133
289
  h('button', {
134
- className: 'tk-button',
290
+ className: 'tk-dock-toggle',
135
291
  type: 'button',
136
- disabled: busy === true,
137
- onClick: check,
138
- }, busy === true ? tr('checking') : tr('check'))),
139
- failed !== null ? h('p', { className: 'tk-failed', role: 'status' }, failed) : null,
140
- result !== null
141
- ? h('div', { className: 'tk-result' },
142
- h('p', { className: 'tk-verdict', 'data-ok': result.ok === true ? 'true' : 'false', role: 'status' },
143
- result.ok === true ? tr('pass') : tr('fail')),
144
- h('ul', { className: 'tk-rows' }, result.checks.map((entry) => h(Row, { key: entry.check, entry }))))
292
+ 'aria-expanded': open === true ? 'true' : 'false',
293
+ onClick: () => setOpen(open !== true),
294
+ },
295
+ h('span', { className: 'tk-dock-title' }, translate('dockTitle')),
296
+ h('span', { className: 'tk-dock-sub' }, translate('dockSub')),
297
+ h('span', { className: 'tk-dock-chevron', 'aria-hidden': 'true' }, open === true ? '▾' : '▸'))),
298
+ open === true
299
+ ? h('div', { className: 'tk-dock-body' },
300
+ h('div', { className: 'tk-guide' },
301
+ h('h3', {}, guide.who.title),
302
+ h('p', {}, guide.who.body)),
303
+ h('div', { className: 'tk-guide' },
304
+ h('h3', {}, guide.steps.title),
305
+ h('ol', {}, guide.steps.items.map((item) => h('li', { key: item }, item)))),
306
+ h('div', { className: 'tk-guide' },
307
+ h('h3', {}, guide.checks.title),
308
+ h('dl', { className: 'tk-guide-rows' },
309
+ guide.checks.rows.map(([name, what]) => [
310
+ h('dt', { key: `${name}-k` }, name),
311
+ h('dd', { key: `${name}-v` }, what),
312
+ ]))),
313
+ h('div', { className: 'tk-guide' },
314
+ h('h3', {}, guide.notes.title),
315
+ h('ul', {}, guide.notes.items.map((item) => h('li', { key: item }, item)))),
316
+ h('div', { className: 'tk-guide' },
317
+ h('h3', {}, guide.cli.title),
318
+ h('p', {}, h('code', {}, guide.cli.body))),
319
+ h(Form, { checker }),
320
+ h(Result, { checker }))
145
321
  : null);
146
322
  }
147
323
 
@@ -204,11 +380,21 @@ window.__ModuleLoader__.load({
204
380
  report('no-inject', 'ctx.inject is not a function');
205
381
  return;
206
382
  }
207
- ctx.inject(['sidebarRightTabs', 'sidebarRight'], (tabCtx) => {
383
+ ctx.inject(['slots', 'sidebarRightTabs', 'sidebarRight'], (tabCtx) => {
208
384
  // Failures inside this callback happen after apply() returned, so the
209
385
  // guards above do not cover them — this try/catch is the one that
210
386
  // decides whether the row lands in "failed".
211
387
  try {
388
+ // The dock lives under the conversation and needs nothing but
389
+ // `slots`, so it is registered before the sidebar is required.
390
+ if (tabCtx !== undefined && tabCtx.slots !== undefined) {
391
+ tabCtx.effect(() => tabCtx.slots.inject('conversation.input.dock', () => tabCtx.slots.register({
392
+ name: 'conversation.input.dock',
393
+ id: NS,
394
+ order: 30,
395
+ locale: NS,
396
+ }, Dock)));
397
+ }
212
398
  if (tabCtx === undefined || tabCtx.slots === undefined || tabCtx.sidebarRightTabs === undefined) {
213
399
  report('no-sidebar', `tabCtx slots=${String(tabCtx !== undefined && tabCtx.slots !== undefined)} sidebarRightTabs=${String(tabCtx !== undefined && tabCtx.sidebarRightTabs !== undefined)}`);
214
400
  return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-plugin-toolkit",
3
- "version": "0.9.7",
3
+ "version": "0.10.0",
4
4
  "description": "Preflight, release and test DeepSeek Harness plugins: checks what the host needs, waits for npm to actually serve the version, and keeps the market index honest.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -81,6 +81,7 @@
81
81
  "platform": "web",
82
82
  "immediately": true,
83
83
  "inject": [
84
+ "@deepseek-ai/dsh-client-ui-conversation",
84
85
  "@deepseek-ai/dsh-client-ui-sidebar-right"
85
86
  ]
86
87
  }