dsh-wsl-workspace 0.4.2 → 0.4.3

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/TESTING.md CHANGED
@@ -1,109 +1,109 @@
1
- # Testing
2
-
3
- This document describes how to verify `dsh-wsl-workspace` after a change or before a release. The suite covers unit tests, the preset-materialization integration test, a real-WSL smoke test, and a post-build lib verification gate.
4
-
5
- ## Prerequisites
6
-
7
- - Windows host with WSL2 and at least one distribution installed (`wsl.exe` on `PATH`).
8
- - Node.js 24+ (the tests run with Node's built-in TypeScript support; `tsx` is not required).
9
- - The DeepSeek Harness checkout (for `tsc`/`tsdown` and the `@deepseek-ai/*` type declarations the `tsconfig.json` paths point at).
10
-
11
- ## Unit tests
12
-
13
- Run the unit tests from the plugin directory:
14
-
15
- ```powershell
16
- node --experimental-strip-types --test tests/variants.test.ts tests/fs-execution-context.test.ts tests/shell.test.ts tests/paths.test.ts tests/wsl-skills.test.ts
17
- ```
18
-
19
- Coverage:
20
-
21
- | File | What it verifies |
22
- |---|---|
23
- | `tests/variants.test.ts` | The WSL preset-variant transform: world rows are dropped, the WSL realm is injected, `str-replace-editor` is re-injected exactly once (and only when the source references it), prefab-family rows (`custom-bash`, `bootstrap-filesystem`) are removed, unknown rows are preserved verbatim. |
24
- | `tests/fs-execution-context.test.ts` | `WslFileSystem` inherits the calling session's cwd through `AsyncLocalStorage` on `tools/execute`; agentless calls fall back to the configured distro. |
25
- | `tests/shell.test.ts` | The login-shell `cd` prefix preserves the resolved workdir (including single-quote escaping); non-login shells leave the command unchanged. |
26
- | `tests/paths.test.ts` | UNC ↔ Linux path translation, `/mnt/<drive>` mapping, canonical Windows path keys, WSL username validation. |
27
- | `tests/wsl-skills.test.ts` | The WSL skill provider (issue #10): non-WSL lookups return nothing, nested `.dsh/skills` / `.agents/skills` discovery with host ranks/sources, `get()` body loading, pruning of `node_modules` / dot-directories, frontmatter validation **including block scalars**, depth and skill-root budgets, the nearest-`.git`-ancestor rule (a cwd deeper than the project root still sees the project's skills, and skills above that ancestor do not leak), the skill-root cap, the **10-second per-scan-root lookup cache** (copy semantics, TTL expiry, `get()` staying live), **directory-symlink following** (linked projects discovered, aliasing deduplicated, dangling symlinks pruned, hops bounded by depth). |
28
-
29
- ## Provider parity and compatibility checks
30
-
31
- - `node scripts/check-rank-parity.mjs` — the provider's project ranks are copied from `@deepseek-ai/dsh-skill-filesystem` (the host does not export them). This script parses the host's built lib when the package is resolvable on this machine and fails on drift. Run it before every release on a machine with the harness installed.
32
- - `scripts/verify-dsh-compat.sh <version>...` — disposable-Profile install/start/uninstall evidence against specific `@deepseek-ai/dsh` releases: fully isolated (`DSH_HOME` redirected to a temp tree, own port), boots the published harness version with the plugin added by name, probes `POST /wsl-workspace/api`, then removes the plugin and verifies the route disappears. Emits per-version verdict lines used for the `dsh.compatibility.dshReleases` manifest records.
33
-
34
- ## Preset materialization integration test
35
-
36
- Boots the host plugin's `apply()` against a fake context with `DSH_HOME` pointed at a temp directory, then asserts the generated variant rows reference real built lib files and the composition carries the WSL execution-world realm:
37
-
38
- ```powershell
39
- node tests/host-materialize.mjs
40
- ```
41
-
42
- This covers variant generation, opaque source-directory mirroring (third-party assets travel with the variant), atomic publication (a failed regeneration preserves the previous complete variant), stale-variant cleanup, and legacy `wsl` preset removal.
43
-
44
- ## Real-WSL smoke test
45
-
46
- Requires a running WSL distribution (the first listed distro is used; infrastructure distros such as `docker-desktop` are skipped):
47
-
48
- ```powershell
49
- node --experimental-strip-types tests/smoke.ts
50
- ```
51
-
52
- This exercises the filesystem round-trip (resolve/write/read/edit/stat/version/listDir/contains/fileUrl), bash execution inside WSL (cwd translation, WSLENV pass-through, stdin, background jobs), Linux-workdir resolution through the session distro fact, `/mnt/<drive>` dual access, and the no-config default-distro fallback.
53
-
54
- ## Post-build lib verification
55
-
56
- `scripts/verify-lib.mjs` parses every `lib/*.js` entry and fails the build when a bare call to a Node builtin export has no matching `node:*` import. This catches the class of bug where a symbol is used but never imported (for example `statSync` in 0.2.3, which made the Add-WSL-Workspace dialog report every path as non-existent at runtime):
57
-
58
- ```powershell
59
- node scripts/verify-lib.mjs
60
- ```
61
-
62
- The `build` script chains it after `tsdown`:
63
-
64
- ```powershell
65
- pnpm build # tsdown && node scripts/verify-lib.mjs
66
- ```
67
-
68
- ## Nested skill-catalog regression (issue #10)
69
-
70
- The WSL skill provider publishes `.dsh/skills` / `.agents/skills` from nested projects below a WSL workspace (and from the cwd's nearest `.git` ancestor). Regression-test it on the real 9P share:
71
-
72
- 1. Rebuild the repro tree inside the distribution (`scripts/repro-setup.sh` creates `~/repro-ws-root` with nested projects, pruned traps, and an over-budget deep skill):
73
-
74
- ```powershell
75
- cp scripts/repro-setup.sh //wsl.localhost/<distro>/tmp/
76
- wsl -d <distro> -- bash -c "bash /tmp/repro-setup.sh"
77
- ```
78
-
79
- 2. Drive the provider against the real `\\wsl.localhost` share — four assertions print (workspace-root cwd finds root + nested skills; nested-project cwd finds only that project; `get()` loads a body; non-WSL cwd returns nothing). Override the target with `WSL_COMPAT_DISTRO` / `WSL_COMPAT_USER` / `WSL_COMPAT_ROOT`:
80
-
81
- ```powershell
82
- node scripts/repro-e2e.mjs
83
- ```
84
-
85
- The script hardcodes `\\wsl.localhost\Ubuntu\home\mille\repro-ws-root`; adjust the two paths at the top when running as another user or distro.
86
- 3. In the running harness, open a session on the repro workspace and ask the agent to load the nested skills (`brainstorming`, `systematic-debugging`, `writing-plans`) through its skill tool — each must load with the `wsl-workspace` provider attribution, and no duplicate entries may appear. In a non-WSL workspace session the same skills must be "unknown".
87
- 4. Clean-install check (simulates another user): `npm pack`, `npm install <tarball>` in an empty temp project (peers must resolve), then `dsh plugin --profile web add <extracted tarball dir>`, restart `dsh web`, and repeat the end-to-end checks below plus the nested-skill probe above.
88
-
89
- ## End-to-end verification in the running harness
90
-
91
- After installing the plugin into a profile and restarting `dsh web`:
92
-
93
- 1. The **W** button appears beside Settings at the sidebar foot.
94
- 2. Open "Add WSL workspace…", browse to a directory (e.g. `/home`), and click "Create & open" — the workspace must be created without a "path does not exist" error.
95
- 3. In the new session, the mode picker shows the WSL variant (e.g. `WSL · Standard mode(标准模式)`); the bash tool runs inside the distribution (`pwd` returns a Linux path, `uname -s` returns `Linux`).
96
- 4. `read`/`write`/`edit` operate on WSL files; Windows files stay reachable under `/mnt/<drive>`.
97
- 5. Switch modes (Standard / PTC / Minimal / Creative) — each lands on its WSL variant and the tool catalog matches the mode.
98
- 6. The plugin API responds correctly: `POST /wsl-workspace/api` with `{"method":"check","params":{"distro":"<distro>","path":"/home"}}` returns `{"ok":true,"value":{"exists":true,"isDirectory":true}}`.
99
-
100
- ## Release checklist
101
-
102
- 1. `pnpm build` — rebuilds `lib/` and runs the verification gate.
103
- 2. `node --experimental-strip-types --test tests/variants.test.ts tests/fs-execution-context.test.ts tests/shell.test.ts tests/paths.test.ts tests/wsl-skills.test.ts` — all green.
104
- 3. `node tests/host-materialize.mjs` — all assertions pass.
105
- 4. `node --experimental-strip-types tests/smoke.ts` — real-WSL round-trip passes.
106
- 5. `node scripts/check-rank-parity.mjs` — host rank constants still match our copies.
107
- 6. `node scripts/repro-e2e.mjs` (after `scripts/repro-setup.sh`) — nested skill-catalog assertions pass.
108
- 7. `npm pack --dry-run` — confirm the tarball carries only live `lib/` chunks, `src/`, `cordis.patch.yml`, READMEs, `LICENSE`, and `NOTICE` (tsdown uses `clean: false`, so remove stale chunks from `lib/` before packing).
1
+ # Testing
2
+
3
+ This document describes how to verify `dsh-wsl-workspace` after a change or before a release. The suite covers unit tests, the preset-materialization integration test, a real-WSL smoke test, and a post-build lib verification gate.
4
+
5
+ ## Prerequisites
6
+
7
+ - Windows host with WSL2 and at least one distribution installed (`wsl.exe` on `PATH`).
8
+ - Node.js 24+ (the tests run with Node's built-in TypeScript support; `tsx` is not required).
9
+ - The DeepSeek Harness checkout (for `tsc`/`tsdown` and the `@deepseek-ai/*` type declarations the `tsconfig.json` paths point at).
10
+
11
+ ## Unit tests
12
+
13
+ Run the unit tests from the plugin directory:
14
+
15
+ ```powershell
16
+ node --experimental-strip-types --test tests/variants.test.ts tests/fs-execution-context.test.ts tests/shell.test.ts tests/paths.test.ts tests/wsl-skills.test.ts
17
+ ```
18
+
19
+ Coverage:
20
+
21
+ | File | What it verifies |
22
+ |---|---|
23
+ | `tests/variants.test.ts` | The WSL preset-variant transform: world rows are dropped, the WSL realm is injected, `str-replace-editor` is re-injected exactly once (and only when the source references it), prefab-family rows (`custom-bash`, `bootstrap-filesystem`) are removed, unknown rows are preserved verbatim. |
24
+ | `tests/fs-execution-context.test.ts` | `WslFileSystem` inherits the calling session's cwd through `AsyncLocalStorage` on `tools/execute`; agentless calls fall back to the configured distro. |
25
+ | `tests/shell.test.ts` | The login-shell `cd` prefix preserves the resolved workdir (including single-quote escaping); non-login shells leave the command unchanged. |
26
+ | `tests/paths.test.ts` | UNC ↔ Linux path translation, `/mnt/<drive>` mapping, canonical Windows path keys, WSL username validation. |
27
+ | `tests/wsl-skills.test.ts` | The WSL skill provider (issue #10): non-WSL lookups return nothing, nested `.dsh/skills` / `.agents/skills` discovery with host ranks/sources, `get()` body loading, pruning of `node_modules` / dot-directories, frontmatter validation **including block scalars**, depth and skill-root budgets, the nearest-`.git`-ancestor rule (a cwd deeper than the project root still sees the project's skills, and skills above that ancestor do not leak), the skill-root cap, the **10-second per-scan-root lookup cache** (copy semantics, TTL expiry, `get()` staying live), **directory-symlink following** (linked projects discovered, aliasing deduplicated, dangling symlinks pruned, hops bounded by depth). |
28
+
29
+ ## Provider parity and compatibility checks
30
+
31
+ - `node scripts/check-rank-parity.mjs` — the provider's project ranks are copied from `@deepseek-ai/dsh-skill-filesystem` (the host does not export them). This script parses the host's built lib when the package is resolvable on this machine and fails on drift. Run it before every release on a machine with the harness installed.
32
+ - `scripts/verify-dsh-compat.sh <version>...` — disposable-Profile install/start/uninstall evidence against specific `@deepseek-ai/dsh` releases: fully isolated (`DSH_HOME` redirected to a temp tree, own port), boots the published harness version with the plugin added by name, probes `POST /wsl-workspace/api`, then removes the plugin and verifies the route disappears. Emits per-version verdict lines used for the `dsh.compatibility.dshReleases` manifest records.
33
+
34
+ ## Preset materialization integration test
35
+
36
+ Boots the host plugin's `apply()` against a fake context with `DSH_HOME` pointed at a temp directory, then asserts the generated variant rows reference real built lib files and the composition carries the WSL execution-world realm:
37
+
38
+ ```powershell
39
+ node tests/host-materialize.mjs
40
+ ```
41
+
42
+ This covers variant generation, opaque source-directory mirroring (third-party assets travel with the variant), atomic publication (a failed regeneration preserves the previous complete variant), stale-variant cleanup, and legacy `wsl` preset removal.
43
+
44
+ ## Real-WSL smoke test
45
+
46
+ Requires a running WSL distribution (the first listed distro is used; infrastructure distros such as `docker-desktop` are skipped):
47
+
48
+ ```powershell
49
+ node --experimental-strip-types tests/smoke.ts
50
+ ```
51
+
52
+ This exercises the filesystem round-trip (resolve/write/read/edit/stat/version/listDir/contains/fileUrl), bash execution inside WSL (cwd translation, WSLENV pass-through, stdin, background jobs), Linux-workdir resolution through the session distro fact, `/mnt/<drive>` dual access, and the no-config default-distro fallback.
53
+
54
+ ## Post-build lib verification
55
+
56
+ `scripts/verify-lib.mjs` parses every `lib/*.js` entry and fails the build when a bare call to a Node builtin export has no matching `node:*` import. This catches the class of bug where a symbol is used but never imported (for example `statSync` in 0.2.3, which made the Add-WSL-Workspace dialog report every path as non-existent at runtime):
57
+
58
+ ```powershell
59
+ node scripts/verify-lib.mjs
60
+ ```
61
+
62
+ The `build` script chains it after `tsdown`:
63
+
64
+ ```powershell
65
+ pnpm build # tsdown && node scripts/verify-lib.mjs
66
+ ```
67
+
68
+ ## Nested skill-catalog regression (issue #10)
69
+
70
+ The WSL skill provider publishes `.dsh/skills` / `.agents/skills` from nested projects below a WSL workspace (and from the cwd's nearest `.git` ancestor). Regression-test it on the real 9P share:
71
+
72
+ 1. Rebuild the repro tree inside the distribution (`scripts/repro-setup.sh` creates `~/repro-ws-root` with nested projects, pruned traps, and an over-budget deep skill):
73
+
74
+ ```powershell
75
+ cp scripts/repro-setup.sh //wsl.localhost/<distro>/tmp/
76
+ wsl -d <distro> -- bash -c "bash /tmp/repro-setup.sh"
77
+ ```
78
+
79
+ 2. Drive the provider against the real `\\wsl.localhost` share — four assertions print (workspace-root cwd finds root + nested skills; nested-project cwd finds only that project; `get()` loads a body; non-WSL cwd returns nothing). Override the target with `WSL_COMPAT_DISTRO` / `WSL_COMPAT_USER` / `WSL_COMPAT_ROOT`:
80
+
81
+ ```powershell
82
+ node scripts/repro-e2e.mjs
83
+ ```
84
+
85
+ The script hardcodes `\\wsl.localhost\Ubuntu\home\mille\repro-ws-root`; adjust the two paths at the top when running as another user or distro.
86
+ 3. In the running harness, open a session on the repro workspace and ask the agent to load the nested skills (`brainstorming`, `systematic-debugging`, `writing-plans`) through its skill tool — each must load with the `wsl-workspace` provider attribution, and no duplicate entries may appear. In a non-WSL workspace session the same skills must be "unknown".
87
+ 4. Clean-install check (simulates another user): `npm pack`, `npm install <tarball>` in an empty temp project (peers must resolve), then `dsh plugin --profile web add <extracted tarball dir>`, restart `dsh web`, and repeat the end-to-end checks below plus the nested-skill probe above.
88
+
89
+ ## End-to-end verification in the running harness
90
+
91
+ After installing the plugin into a profile and restarting `dsh web`:
92
+
93
+ 1. The **W** button appears beside Settings at the sidebar foot.
94
+ 2. Open "Add WSL workspace…", browse to a directory (e.g. `/home`), and click "Create & open" — the workspace must be created without a "path does not exist" error.
95
+ 3. In the new session, the mode picker shows the WSL variant (e.g. `WSL · Standard mode(标准模式)`); the bash tool runs inside the distribution (`pwd` returns a Linux path, `uname -s` returns `Linux`).
96
+ 4. `read`/`write`/`edit` operate on WSL files; Windows files stay reachable under `/mnt/<drive>`.
97
+ 5. Switch modes (Standard / PTC / Minimal / Creative) — each lands on its WSL variant and the tool catalog matches the mode.
98
+ 6. The plugin API responds correctly: `POST /wsl-workspace/api` with `{"method":"check","params":{"distro":"<distro>","path":"/home"}}` returns `{"ok":true,"value":{"exists":true,"isDirectory":true}}`.
99
+
100
+ ## Release checklist
101
+
102
+ 1. `pnpm build` — rebuilds `lib/` and runs the verification gate.
103
+ 2. `node --experimental-strip-types --test tests/variants.test.ts tests/fs-execution-context.test.ts tests/shell.test.ts tests/paths.test.ts tests/wsl-skills.test.ts` — all green.
104
+ 3. `node tests/host-materialize.mjs` — all assertions pass.
105
+ 4. `node --experimental-strip-types tests/smoke.ts` — real-WSL round-trip passes.
106
+ 5. `node scripts/check-rank-parity.mjs` — host rank constants still match our copies.
107
+ 6. `node scripts/repro-e2e.mjs` (after `scripts/repro-setup.sh`) — nested skill-catalog assertions pass.
108
+ 7. `npm pack --dry-run` — confirm the tarball carries only live `lib/` chunks, `src/`, `cordis.patch.yml`, READMEs, `LICENSE`, and `NOTICE` (tsdown uses `clean: false`, so remove stale chunks from `lib/` before packing).
109
109
  8. Install the tarball into a clean profile (`dsh plugin --profile web add <tarball>`), restart `dsh web`, and run the end-to-end checks above plus the nested-skill probe. When the compatibility manifest changes, also run `scripts/verify-dsh-compat.sh` for every declared release.
package/lib/client.js CHANGED
@@ -131,6 +131,14 @@ async function registerWindows(linuxPath, distro, username) {
131
131
  async function listWorkspaces() {
132
132
  return call("listWorkspaces", {});
133
133
  }
134
+ /**
135
+ * Read the plugin build version and its declared DSH compatibility matrix,
136
+ * for the dialog help panel.
137
+ * @returns the self-description reported by the host plugin.
138
+ */
139
+ async function describe() {
140
+ return call("describe", {});
141
+ }
134
142
 
135
143
  //#endregion
136
144
  //#region src/shared/paths.ts
@@ -246,6 +254,92 @@ function isValidWslUsername(value) {
246
254
  return WSL_USERNAME_PATTERN.test(value);
247
255
  }
248
256
 
257
+ //#endregion
258
+ //#region src/client/help.tsx
259
+ /**
260
+ * Split one dictionary entry into its bullet lines.
261
+ * @param value - the multi-line dictionary string.
262
+ * @returns the non-empty lines, trimmed.
263
+ */
264
+ function bullets(value) {
265
+ return value.split("\n").map((line) => line.trim()).filter((line) => line !== "");
266
+ }
267
+ /** One titled section of the panel. */
268
+ function Section({ title, children }) {
269
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
270
+ className: "dww-help-section",
271
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
272
+ className: "dww-help-title",
273
+ children: title
274
+ }), children]
275
+ });
276
+ }
277
+ /**
278
+ * Render the help panel shown behind the dialog's "?" button.
279
+ * @param props - translate function plus the host's self-description.
280
+ */
281
+ function WslHelp({ t, description }) {
282
+ const releases = description?.releases ?? [];
283
+ const version = description === null ? t("help.compat.unknown") : `${t("help.compat.versionLabel")} v${description.version}`;
284
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
285
+ className: "dww-help",
286
+ role: "region",
287
+ "aria-label": t("help.button"),
288
+ children: [
289
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Section, {
290
+ title: t("help.compat.title"),
291
+ children: [
292
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
293
+ className: "dww-help-meta",
294
+ children: version
295
+ }),
296
+ releases.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
297
+ className: "dww-help-chips",
298
+ children: releases.map((entry) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
299
+ className: "dww-help-chip",
300
+ children: entry.id
301
+ }, entry.id))
302
+ }) : null,
303
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
304
+ className: "dww-help-list",
305
+ children: bullets(t("help.compat.body")).map((line) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", { children: line }, line))
306
+ })
307
+ ]
308
+ }),
309
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Section, {
310
+ title: t("help.usage.title"),
311
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
312
+ className: "dww-help-list",
313
+ children: bullets(t("help.usage.body")).map((line) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", { children: line }, line))
314
+ })
315
+ }),
316
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Section, {
317
+ title: t("help.known.title"),
318
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
319
+ className: "dww-help-list dww-help-list--known",
320
+ children: bullets(t("help.known.body")).map((line) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", { children: line }, line))
321
+ })
322
+ }),
323
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
324
+ className: "dww-help-footer",
325
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("a", {
326
+ className: "dww-help-link",
327
+ href: "https://www.npmjs.com/package/dsh-wsl-workspace",
328
+ target: "_blank",
329
+ rel: "noreferrer",
330
+ children: t("help.footer.npm")
331
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("a", {
332
+ className: "dww-help-link",
333
+ href: "https://github.com/6Mikao9/dsh-wsl-workspace",
334
+ target: "_blank",
335
+ rel: "noreferrer",
336
+ children: t("help.footer.repo")
337
+ })]
338
+ })
339
+ ]
340
+ });
341
+ }
342
+
249
343
  //#endregion
250
344
  //#region src/client/AddWslWorkspace.tsx
251
345
  /**
@@ -296,7 +390,7 @@ function WslGlyph({ size = 16 }) {
296
390
  * The "Add WSL workspace…" footer action and its dialog.
297
391
  * @param props - owner share + injected face.
298
392
  */
299
- function AddWslWorkspace({ wide, t, checkPreset, listDistros: listDistros$1, listDir: listDir$1, check: check$1, createWorkspace }) {
393
+ function AddWslWorkspace({ wide, t, describe: describe$1, checkPreset, listDistros: listDistros$1, listDir: listDir$1, check: check$1, createWorkspace }) {
300
394
  const [open, setOpen] = (0, react.useState)(false);
301
395
  const [opening, setOpening] = (0, react.useState)(false);
302
396
  const [distros, setDistros] = (0, react.useState)([]);
@@ -308,6 +402,8 @@ function AddWslWorkspace({ wide, t, checkPreset, listDistros: listDistros$1, lis
308
402
  const [browsing, setBrowsing] = (0, react.useState)(false);
309
403
  const [error, setError] = (0, react.useState)(null);
310
404
  const [busy, setBusy] = (0, react.useState)(false);
405
+ const [helpOpen, setHelpOpen] = (0, react.useState)(false);
406
+ const [selfDescription, setSelfDescription] = (0, react.useState)(null);
311
407
  const browseSeq = (0, react.useRef)(0);
312
408
  const refreshBrowse = async (root, targetDistro) => {
313
409
  const seq = ++browseSeq.current;
@@ -329,6 +425,11 @@ function AddWslWorkspace({ wide, t, checkPreset, listDistros: listDistros$1, lis
329
425
  if (!open) return;
330
426
  let cancelled = false;
331
427
  setError(null);
428
+ describe$1().then((value) => {
429
+ if (!cancelled) setSelfDescription(value);
430
+ }).catch(() => {
431
+ if (!cancelled) setSelfDescription(null);
432
+ });
332
433
  setOpening(true);
333
434
  (async () => {
334
435
  let presetIssue;
@@ -467,20 +568,35 @@ function AddWslWorkspace({ wide, t, checkPreset, listDistros: listDistros$1, lis
467
568
  children: [
468
569
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
469
570
  className: "dww-header",
470
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h2", {
471
- className: "dww-title",
472
- children: t("dialog.title")
473
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
474
- type: "button",
475
- className: "dww-close",
476
- "aria-label": t("dialog.cancel"),
477
- onClick: maskClick,
478
- children: "✕"
479
- })]
571
+ children: [
572
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h2", {
573
+ className: "dww-title",
574
+ children: t("dialog.title")
575
+ }),
576
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
577
+ type: "button",
578
+ className: "dww-help-btn",
579
+ title: t("help.button"),
580
+ "aria-label": t("help.button"),
581
+ "aria-pressed": helpOpen,
582
+ onClick: () => setHelpOpen((value) => !value),
583
+ children: "?"
584
+ }),
585
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
586
+ type: "button",
587
+ className: "dww-close",
588
+ "aria-label": t("dialog.cancel"),
589
+ onClick: maskClick,
590
+ children: "✕"
591
+ })
592
+ ]
480
593
  }),
481
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
594
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
482
595
  className: "dww-body",
483
- children: [
596
+ children: helpOpen ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(WslHelp, {
597
+ t,
598
+ description: selfDescription
599
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
484
600
  error !== null ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
485
601
  className: "dww-error",
486
602
  children: [error, /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
@@ -579,7 +695,7 @@ function AddWslWorkspace({ wide, t, checkPreset, listDistros: listDistros$1, lis
579
695
  }, name))]
580
696
  })]
581
697
  })
582
- ]
698
+ ] })
583
699
  }),
584
700
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
585
701
  className: "dww-actions",
@@ -870,6 +986,71 @@ const STYLES = `
870
986
  }
871
987
  .dww-btn--primary:hover:not(:disabled) { background: var(--dsw-alias-button-primary-hover); }
872
988
  .dww-btn:disabled { cursor: default; opacity: 0.6; }
989
+ /* Help panel behind the dialog's "?" button. */
990
+ .dww-help-btn {
991
+ flex: none;
992
+ margin-left: auto;
993
+ display: inline-flex;
994
+ align-items: center;
995
+ justify-content: center;
996
+ width: 26px;
997
+ height: 26px;
998
+ border: 1px solid var(--dsw-alias-border-l2);
999
+ border-radius: 50%;
1000
+ background: transparent;
1001
+ color: var(--dsw-alias-label-secondary);
1002
+ cursor: pointer;
1003
+ font-size: 13px;
1004
+ line-height: 1;
1005
+ }
1006
+ .dww-help-btn:hover { background: var(--dsw-alias-interactive-bg-hover); }
1007
+ .dww-help-btn[aria-pressed='true'] {
1008
+ background: var(--dsw-alias-interactive-bg-hover);
1009
+ color: var(--dsw-alias-label-primary);
1010
+ }
1011
+ .dww-help {
1012
+ display: flex;
1013
+ flex-direction: column;
1014
+ gap: 14px;
1015
+ max-height: 52vh;
1016
+ overflow-y: auto;
1017
+ padding-right: 4px;
1018
+ }
1019
+ .dww-help-section { display: flex; flex-direction: column; gap: 6px; }
1020
+ .dww-help-title {
1021
+ margin: 0;
1022
+ font-size: 13px;
1023
+ font-weight: 600;
1024
+ color: var(--dsw-alias-label-primary);
1025
+ }
1026
+ .dww-help-meta { font-size: 12px; color: var(--dsw-alias-label-tertiary); }
1027
+ .dww-help-chips { display: flex; flex-wrap: wrap; gap: 6px; }
1028
+ .dww-help-chip {
1029
+ padding: 2px 8px;
1030
+ border: 1px solid var(--dsw-alias-border-l2);
1031
+ border-radius: 999px;
1032
+ font-size: 12px;
1033
+ color: var(--dsw-alias-label-secondary);
1034
+ }
1035
+ .dww-help-list {
1036
+ margin: 0;
1037
+ padding-left: 18px;
1038
+ display: flex;
1039
+ flex-direction: column;
1040
+ gap: 6px;
1041
+ font-size: 13px;
1042
+ line-height: 1.6;
1043
+ color: var(--dsw-alias-label-secondary);
1044
+ }
1045
+ .dww-help-list--known { color: var(--dsw-alias-label-primary); }
1046
+ .dww-help-footer {
1047
+ display: flex;
1048
+ gap: 14px;
1049
+ padding-top: 8px;
1050
+ border-top: 1px solid var(--dsw-alias-border-l2);
1051
+ }
1052
+ .dww-help-link { font-size: 12px; color: var(--dsw-alias-label-tertiary); text-decoration: underline; }
1053
+ .dww-help-link:hover { color: var(--dsw-alias-label-primary); }
873
1054
  `;
874
1055
  /**
875
1056
  * Idempotently inject the plugin stylesheet. No-op when a tag with the
@@ -917,7 +1098,18 @@ const zh = {
917
1098
  "error.invalidPath": "请输入以 / 开头的 Linux 绝对路径",
918
1099
  "error.invalidUsername": "用户名无效:需以字母或下划线开头,仅含字母、数字、_、.、-",
919
1100
  "error.pathNotFound": "该路径不存在或是文件,请选择一个文件夹",
920
- "error.createFailed": "创建工作区失败"
1101
+ "error.createFailed": "创建工作区失败",
1102
+ "help.button": "插件说明",
1103
+ "help.compat.title": "兼容性",
1104
+ "help.compat.versionLabel": "插件版本",
1105
+ "help.compat.unknown": "未能读取版本与兼容声明(宿主端没有响应)",
1106
+ "help.compat.body": "上方是这份构建声明兼容的 DSH 版本,每一条都在隔离实例上实测过(独立 DSH_HOME、依赖固定到该版本、跑满十项门禁)。\n插件在运行时会识别 DSH 版本:0.1.2 线用 uiWorkspace + remote.agentPresets,更早的版本用 workspaces.startSession + connection.api;两边都没有时会明确报错,而不是留下一个空工作区。\n版本不在列表里通常仍然可用,但未经验证。",
1107
+ "help.usage.title": "用法与特性",
1108
+ "help.usage.body": "点击侧边栏底部的 W 按钮 → 选发行版 → 输入或浏览 Linux 路径 → 检查 → 创建并打开。\n创建出的会话里,bash 与文件工具都落在该发行版内,模型看到的路径全是 Linux 路径;Windows 盘可从会话内通过 /mnt/<盘符> 访问。\n四种模式(标准 / PTC / 极简 / 创造)各有 WSL 变体,在模式选择器里直接选即可,名字形如 WSL · Standard mode(标准模式)。\n用户名可选,等价于 wsl.exe -u <用户名>,只改变 bash 的运行身份;文件工具走 Windows 侧共享,不受它影响。\n技能目录从会话 cwd 最近的 .git 祖先开始向下扫描 .dsh/skills 与 .agents/skills(含嵌套项目),上限 4 层目录 / 64 个技能目录 / 4096 个已访问目录,并按扫描根缓存 10 秒。\n为了让 UNC 工作区也能拿到目录,插件生成预设时会把技能行的 watch 关掉,目录改由会话启动时扫描一次:会话中途新加入的技能要等下一个会话才出现,而技能内容(get)始终是实时读取的。\nbash 在发行版内运行,不受 DSH 文件策略约束;文件工具(read/write/edit)受策略约束,工作区内修改模式下只能写工作区内。",
1109
+ "help.known.title": "已知问题",
1110
+ "help.known.body": "Windows 侧共享无法解析 Linux 符号链接:用 ln -s 链进工作区的项目发现不了(扫描会跳过、不报错),请把工作区注册在真实项目目录所在的层级。\n会话运行中途新加入的技能不会即时出现在目录里(因为对 UNC 关闭了文件监视,见「用法与特性」);重开一个会话即可看到。技能内容本身始终实时读取。\n只有经本插件注册的 WSL 工作区才会默认用 WSL 变体;在其它工作区里手动新建的会话用宿主默认预设,bash/文件工具就是 Windows 侧的。\n0.4.3 已修复:UNC 工作区下模型收不到技能目录(宿主 fs.watch 在 \\\\wsl.localhost\\… 上失败导致整条目录消息被丢弃),现在生成预设时会关闭该监视,目录照常注入。",
1111
+ "help.footer.npm": "npm 包",
1112
+ "help.footer.repo": "GitHub 仓库"
921
1113
  };
922
1114
  /**
923
1115
  * The `wslWorkspace` translations (English).
@@ -946,7 +1138,18 @@ const en = {
946
1138
  "error.invalidPath": "Enter an absolute Linux path starting with /",
947
1139
  "error.invalidUsername": "Invalid username: start with a letter or underscore; only letters, digits, _ . -",
948
1140
  "error.pathNotFound": "The path does not exist or is a file; choose a folder",
949
- "error.createFailed": "Failed to create the workspace"
1141
+ "error.createFailed": "Failed to create the workspace",
1142
+ "help.button": "About this plugin",
1143
+ "help.compat.title": "Compatibility",
1144
+ "help.compat.versionLabel": "Plugin version",
1145
+ "help.compat.unknown": "Version and compatibility declaration unavailable (the host side did not answer)",
1146
+ "help.compat.body": "The chips above are the DSH releases this build declares, each verified on an isolated instance (own DSH_HOME, dependencies pinned to that release, full check suite).\nThe plugin detects the DSH generation at runtime: the 0.1.2 line uses uiWorkspace + remote.agentPresets, older releases use workspaces.startSession + connection.api, and a release exposing neither fails loudly instead of leaving an empty workspace.\nA release outside the list usually still works, but is unverified.",
1147
+ "help.usage.title": "Usage and features",
1148
+ "help.usage.body": "Click the W button at the sidebar foot, pick a distribution, type or browse to a Linux path, press Check, then Create & open.\nIn that session the bash tool and the file tools run inside the distribution, so every path the model sees is a Linux path; Windows drives stay reachable as /mnt/<drive>.\nEach mode (Standard / PTC / Minimal / Creator) has a WSL variant in the mode picker, named like WSL · Standard mode.\nThe optional username behaves like wsl.exe -u <user> for the bash tool only; the file tools go through the Windows-side share and are unaffected.\nThe skill catalog is discovered from the nearest .git ancestor of the session cwd downwards (.dsh/skills and .agents/skills, nested projects included), bounded to 4 levels / 64 skill directories / 4096 visited directories, and cached per scan root for 10 seconds.\nSo that a UNC workspace still receives a catalog, the generated preset turns that row's `watch` off and the catalog is scanned once at session start: a skill added mid-session appears in the next session, while skill bodies are always read live.\nbash runs inside the distribution and is not wrapped by the DSH file policy; read/write/edit are, and workspace-write only writes inside the workspace.",
1149
+ "help.known.title": "Known issues",
1150
+ "help.known.body": "The Windows-side share cannot resolve Linux symlinks, so a project linked in with ln -s is not discoverable (the scan skips it without failing); register the workspace at a level that holds the real project directories.\nA skill added while a session is running does not show up in that session's catalog (file watching is off for UNC workspaces, see Usage and features); start a new session to pick it up. Skill bodies are always read live.\nOnly workspaces registered through this plugin default to a WSL variant; a session started by hand in another workspace keeps the host default preset, so its bash and file tools run on the Windows side.\nFixed in 0.4.3: on a UNC workspace the model did not receive the skill catalog at all (the host fs.watch call fails on \\\\wsl.localhost\\... and the whole catalog message was dropped). The generated preset now disables that watcher, and the catalog is injected as usual.",
1151
+ "help.footer.npm": "npm package",
1152
+ "help.footer.repo": "GitHub repository"
950
1153
  };
951
1154
 
952
1155
  //#endregion
@@ -1075,6 +1278,7 @@ function apply(ctx) {
1075
1278
  return void 0;
1076
1279
  },
1077
1280
  listDistros: () => listDistros(),
1281
+ describe: () => describe(),
1078
1282
  listDir: (distro, path) => listDir(distro, path),
1079
1283
  check: (distro, path) => check(distro, path),
1080
1284
  createWorkspace: async (linuxPath, username, distro) => {