dsh-wsl-workspace 0.7.1 → 0.7.2
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 +20 -0
- package/README.zh.md +5 -0
- package/TESTING.md +3 -1
- package/lib/client.js +4 -4
- package/lib/client.js.map +1 -1
- package/lib/index.js +111 -61
- package/lib/index.js.map +1 -1
- package/package.json +4 -2
- package/src/client/locales.ts +4 -4
- package/src/host/wsl-skills.ts +141 -62
package/README.md
CHANGED
|
@@ -43,6 +43,26 @@ Click "Create & open" to start a new session in the workspace. In the new sessio
|
|
|
43
43
|
|
|
44
44
|
## Changelog
|
|
45
45
|
|
|
46
|
+
### 0.7.2 — 2026-09-21
|
|
47
|
+
|
|
48
|
+
- **The WSL skill catalog is no longer re-walked on the request path (issue #25).**
|
|
49
|
+
The host rebuilds the catalog during a request and awaits each provider's
|
|
50
|
+
`list()`, and this provider kept its own answer for only 10 s — so every time the
|
|
51
|
+
catalog was re-collected (a new session or scope, or simply a lookup more than
|
|
52
|
+
10 s after the last one) that request paid a full walk of the workspace, one
|
|
53
|
+
directory at a time: two `stat`s and one `readdir` each. A `readdir` over the
|
|
54
|
+
`\\wsl.localhost\…` 9P share measures 3-16 ms here and the walk's budget is 4096
|
|
55
|
+
directories, which is why a large workspace cost 20.4 s, on the request path. A
|
|
56
|
+
published catalog is now served as-is, and only the provider's own change detector
|
|
57
|
+
can drop it: a repeat lookup costs 1-3 ms and no filesystem traffic. The freshness
|
|
58
|
+
contract is unchanged — a new nested skills directory still appears within 30 s,
|
|
59
|
+
and an added, removed or edited skill within 3 s.
|
|
60
|
+
- The walk itself is cheaper: one BFS layer is probed concurrently (bounded) and
|
|
61
|
+
published in frontier order, so the catalog stays deterministic, and a
|
|
62
|
+
directory's `.dsh/skills` / `.agents/skills` are probed only when its own listing
|
|
63
|
+
showed that marker. The budget-sized walk went from 20.4 s to 4.8 s on the same
|
|
64
|
+
machine; node's filesystem thread pool caps the real parallelism.
|
|
65
|
+
|
|
46
66
|
### 0.7.1 — 2026-09-20
|
|
47
67
|
|
|
48
68
|
- **`npm install dsh-wsl-workspace` no longer fails.** Verifying the published
|
package/README.zh.md
CHANGED
|
@@ -44,6 +44,11 @@ dsh plugin --profile web add D:\path\to\dsh-wsl-workspace
|
|
|
44
44
|
|
|
45
45
|
英文完整历史见 [README.md](README.md),本节为对应中文记录(0.4.3 及更早为摘要)。
|
|
46
46
|
|
|
47
|
+
### 0.7.2 — 2026-09-21
|
|
48
|
+
|
|
49
|
+
- **WSL 技能目录不再在请求路径上重走(issue #25)**。宿主会在请求期间重建技能目录并等待每个 provider 的 `list()`,而本插件只把答案保留 10 秒——于是每次重新收集(新会话、新作用域,或距上次查询超过 10 秒)都会让那个请求付一次整棵工作区的走查,而且是逐个目录:每目录两次 `stat` 加一次 `readdir`。本机 `\\wsl.localhost\…` 9P 共享的单次 `readdir` 实测 3~16 毫秒,走查预算固定 4096 个目录,所以大工作区那次走查是 20.4 秒、并且付在请求路径上。现在已发布的目录直接照原样返回,只有插件自己的变更探针能丢弃它:重复查找 1~3 毫秒、完全不碰文件系统。新鲜度契约不变——新增的嵌套技能目录仍在 30 秒内出现,新增、删除或改写技能仍在 3 秒内生效。
|
|
50
|
+
- 走查本身也变便宜了:BFS 同一层并发探测(有上限)、按 frontier 顺序发布以保证目录顺序确定,且只在目录自身的清单里出现 `.dsh` / `.agents` 时才探测对应 `skills`。同机实测命中预算的走查从 20.4 秒降到 4.8 秒;真正的并行度由 node 的文件系统线程池决定。
|
|
51
|
+
|
|
47
52
|
### 0.7.1 — 2026-09-20
|
|
48
53
|
|
|
49
54
|
- **`npm install dsh-wsl-workspace` 不再失败**。校验已发布产物时发现了这次发布自己引入的回归:npm 会自动安装缺失的同伴依赖,而 0.6.0 新加的 `@deepseek-ai/dsh-tool-fs-search` 同伴自己又依赖 `@deepseek-ai/dsh-retention`,后者**并未公开发布**——于是纯 `npm install` 直接以 `E404 … @deepseek-ai/dsh-retention` 失败(0.4.3 装得没问题,所以是我们引入的)。`dsh plugin add` 走 pnpm,对未满足的同伴依赖**只警告**,这正是所有门禁与真实安装都没抓到的原因。十个宿主同伴依赖现在都在 `peerDependenciesMeta` 里标为 optional:包仍然声明宿主必须提供什么,但 npm 不再尝试去下载它们。
|
package/TESTING.md
CHANGED
|
@@ -111,7 +111,9 @@ After installing the plugin into a profile and restarting `dsh web`:
|
|
|
111
111
|
5. `node scripts/check-rank-parity.mjs` — host rank constants still match our copies.
|
|
112
112
|
6. `node scripts/repro-e2e.mjs` (after `scripts/repro-setup.sh`) — nested skill-catalog assertions pass.
|
|
113
113
|
7. `npm pack --dry-run` — confirm the tarball carries only live `lib/` chunks, `src/`, `cordis.patch.yml`, READMEs, `LICENSE`, and `NOTICE`.
|
|
114
|
-
8.
|
|
114
|
+
8. `npm run verify:install` — packs the tree and installs the tarball with **plain npm** into a scratch directory, with no pnpm and no host packages present. This is the gate that would have caught 0.7.0, whose `peerDependencies` made npm auto-install an unpublished package (`E404 @deepseek-ai/dsh-retention`): every other check and every real session goes through `dsh plugin add` (pnpm), which only *warns* about unmet peers and installs anyway. `prepublishOnly` runs it, so `npm publish` now refuses to ship a package that npm users cannot install.
|
|
115
|
+
9. 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.
|
|
116
|
+
10. For a release, install the *published* version by name into one isolated case per declared release and confirm each boots (the launcher only reports ready once the plugin's API route answers) — the check that proves the artifact on the registry, not just the local tree.
|
|
115
117
|
|
|
116
118
|
### The multi-release check harness
|
|
117
119
|
|
package/lib/client.js
CHANGED
|
@@ -1136,8 +1136,8 @@ const zh = {
|
|
|
1136
1136
|
"help.compat.versionLabel": "插件版本",
|
|
1137
1137
|
"help.compat.unknown": "未能读取版本与兼容声明(宿主端没有响应)",
|
|
1138
1138
|
"help.compat.body": "上方是这份构建声明兼容的 DSH 版本,每一条都在隔离实例上实测过(独立 DSH_HOME、依赖固定到该版本、跑满十项门禁)。\n插件在运行时自动识别 DSH 版本并选用对应的 API;两边都不支持时会明确报错,而不是留下一个空工作区。\n版本不在列表里通常仍然可用,但未经验证。",
|
|
1139
|
-
"help.news.title": "本次更新(0.7.
|
|
1140
|
-
"help.news.body": "修掉了 npm 安装失败:npm 会自动安装缺失的同伴依赖,而 @deepseek-ai/dsh-tool-fs-search 自己又依赖并未公开发布的 @deepseek-ai/dsh-retention,于是 npm install dsh-wsl-workspace 直接 E404 失败(dsh plugin 走 pnpm、只警告,所以一直没暴露)。\n十个宿主同伴依赖现在都标为 optional:包仍声明宿主必须提供什么,但 npm 不再尝试下载它们。\nWSL 世界现在在会话能察觉到的每一处都与宿主一致:Linux 符号链接、会话的访问模式、发行版内的 grep / glob、实时技能目录、有状态的 shell,以及可跟踪的后台任务。\n新增 bash_background:持久 shell 的 schema 只有 command,没有生产者时 job_list 永远说\"没有后台任务\",而传给 bash 的 run_in_background 会被静默忽略;现在后台任务可跟踪,job_output 增量读取、job_kill 取消都照常工作。\nbash 的工具描述写明两件宿主没说的事:cd / export 跨调用保留,命令以 & 结尾会把整条命令后台化(请写成单独一行的 ( 长任务 > log 2>&1 ) &)。0.1.0-rc.7 的宿主缺少 Windows 进程检查器、持久 shell 起不来,插件会在启动时探测并回退到一次性 bash,不再让每次调用都报错。",
|
|
1139
|
+
"help.news.title": "本次更新(0.7.2)",
|
|
1140
|
+
"help.news.body": "修掉了 WSL 技能目录在请求路径上重走的问题(issue #25):宿主会在请求期间重建技能目录并等待各 provider 的 list(),而插件只把答案保留 10 秒,于是每次重新收集都得重走整棵工作区,工作区一大就正好卡在走查的 4096 目录预算上。\n9P 共享上单次 readdir 实测 3~16 毫秒,所以命中预算的工作区那次走查要 20 秒;现在已发布的目录直接照原样返回,只有插件自己的探针确认变化时才丢弃缓存,重复查找降到 1~3 毫秒、且完全不碰文件系统。\n走查本身也变便宜:同一层并发探测(有上限)、按 frontier 顺序发布以保证目录顺序确定,且只在目录清单里出现 .dsh / .agents 时才探测对应 skills。同机实测命中预算的走查从 20.4 秒降到 4.8 秒。\n修掉了 npm 安装失败:npm 会自动安装缺失的同伴依赖,而 @deepseek-ai/dsh-tool-fs-search 自己又依赖并未公开发布的 @deepseek-ai/dsh-retention,于是 npm install dsh-wsl-workspace 直接 E404 失败(dsh plugin 走 pnpm、只警告,所以一直没暴露)。\n十个宿主同伴依赖现在都标为 optional:包仍声明宿主必须提供什么,但 npm 不再尝试下载它们。\nWSL 世界现在在会话能察觉到的每一处都与宿主一致:Linux 符号链接、会话的访问模式、发行版内的 grep / glob、实时技能目录、有状态的 shell,以及可跟踪的后台任务。\n新增 bash_background:持久 shell 的 schema 只有 command,没有生产者时 job_list 永远说\"没有后台任务\",而传给 bash 的 run_in_background 会被静默忽略;现在后台任务可跟踪,job_output 增量读取、job_kill 取消都照常工作。\nbash 的工具描述写明两件宿主没说的事:cd / export 跨调用保留,命令以 & 结尾会把整条命令后台化(请写成单独一行的 ( 长任务 > log 2>&1 ) &)。0.1.0-rc.7 的宿主缺少 Windows 进程检查器、持久 shell 起不来,插件会在启动时探测并回退到一次性 bash,不再让每次调用都报错。",
|
|
1141
1141
|
"help.usage.title": "用法与特性",
|
|
1142
1142
|
"help.usage.body": "点击侧边栏底部的 W 按钮 → 选发行版 → 输入或浏览 Linux 路径 → 检查 → 创建并打开。\n创建出的会话里,bash 与文件工具都落在该发行版内,模型看到的路径全是 Linux 路径;Windows 盘可从会话内通过 /mnt/<盘符> 访问。\n四种模式(标准 / PTC / 极简 / 创造)各有 WSL 变体,在模式选择器里直接选即可,名字形如 WSL · Standard mode(标准模式)。\n用户名可选,等价于 wsl.exe -u <用户名>,只改变 bash 与 persistent-bash 的运行身份;文件工具走 Windows 侧共享,不受它影响。\n技能目录从会话 cwd 最近的 .git 祖先开始向下扫描 .dsh/skills 与 .agents/skills(含嵌套项目),上限 4 层目录 / 64 个技能目录 / 4096 个已访问目录,并按扫描根缓存 10 秒。9P 解不开的符号链接交给发行版 readlink 解析出真实路径(每次查找最多 32 条)后继续扫描,链接进来的项目与它下面的嵌套项目都能扫到。\n目录不再冻结:对 UNC 关闭文件监视之后,插件每 3 秒重查一次已发布的技能目录、并比对每个技能文件的修改时间与大小,所以新增、删除与改写都会在下一个回合生效;完整重新发现每 30 秒一次,用于找到此前不存在的技能目录(技能正文始终实时读取)。\n文件搜索由发行版内的 grep / glob 提供,源预设没有这两个工具的模式(极简)不会多出来:grep 用 GNU grep -E(\\d、\\w、(?i) 可用,环视与反向引用不支持),跳过隐藏项与 node_modules,不读 .gitignore;glob 用 GNU find 列文件、按 gitignore 风格匹配(* 不跨目录、** 跨、{a,b}、前导 ! 取反),按修改时间从旧到新排序。\n文件工具在链接处按真实路径工作,并按真实路径判策略:链接指向工作区外就等于工作区外。\nbash 由 PTY 承载的持久 shell 提供:登录环境、起始目录就是会话工作区,cd / export / venv / 后台任务跨调用保留(它取代了原先一次性 bash)。\n需要可跟踪的后台任务时用 bash_background:它立刻返回 job id,job_list / job_output(增量读取)/ job_kill 都作用于它;bash 本身没有 run_in_background 参数,传了会被忽略。job id 只在本次 DSH 进程内唯一,重启后会重新编号,引用前先用 job_list 确认。\nbash 与文件工具的 shell 都在发行版内运行、不受 DSH 文件策略约束;文件工具(read/write/edit)受策略约束,工作区内修改模式下只能写工作区内。",
|
|
1143
1143
|
"help.known.title": "已知问题",
|
|
@@ -1180,8 +1180,8 @@ const en = {
|
|
|
1180
1180
|
"help.compat.versionLabel": "Plugin version",
|
|
1181
1181
|
"help.compat.unknown": "Version and compatibility declaration unavailable (the host side did not answer)",
|
|
1182
1182
|
"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 and picks the matching API; a release exposing neither fails loudly instead of leaving an empty workspace.\nA release outside the list usually still works, but is unverified.",
|
|
1183
|
-
"help.news.title": "What's new in 0.7.
|
|
1184
|
-
"help.news.body": "Fixed a failed npm install: npm auto-installs missing peers, and @deepseek-ai/dsh-tool-fs-search peers on @deepseek-ai/dsh-retention, which is not published - so npm install dsh-wsl-workspace died with E404.\nThe ten host peer dependencies are now marked optional, so npm leaves them to the host (dsh plugin add uses pnpm and only warned, which is why this stayed hidden).\nThe WSL world now matches the host everywhere a session can tell the difference: Linux symlinks, the session's access mode, in-distribution grep / glob, a live skill catalog, a stateful shell, and tracked background jobs.\nNew: bash_background. The persistent shell takes `command` only, so without a producer job_list always answered \"no background jobs\" and a run_in_background argument passed to bash was silently ignored; background work is now tracked, with incremental job_output reads and job_kill.\nThe bash tool description now states two things the host does not: cd / exports carry into the next call, and a command ending in & backgrounds the whole command - background the subshell instead, as ( long-job > log 2>&1 ) &.\n0.1.0-rc.7 lacks the Windows process inspector the persistent shell needs, so the plugin probes for it and falls back to a one-shot bash instead of failing every call.",
|
|
1183
|
+
"help.news.title": "What's new in 0.7.2",
|
|
1184
|
+
"help.news.body": "Fixed the WSL skill catalog being re-walked on the request path (issue #25): the host rebuilds the catalog during a request and awaits each provider's list(), while this plugin kept its answer for only 10 s - so every re-collection re-walked the whole workspace.\nA readdir over the WSL 9P share costs 3-16 ms and the walk's budget is 4096 directories, so a large workspace sat 20 s on it. The published catalog is now served as-is until the provider's own watcher sees a change, which takes a repeat lookup to 1-3 ms with no filesystem traffic at all.\nThe walk is cheaper too: one BFS layer is probed concurrently and published in frontier order, and a directory's .dsh/skills is probed only when its own listing showed .dsh. A budget-sized walk measured 20.4 s before and 4.8 s after.\nFixed a failed npm install: npm auto-installs missing peers, and @deepseek-ai/dsh-tool-fs-search peers on @deepseek-ai/dsh-retention, which is not published - so npm install dsh-wsl-workspace died with E404.\nThe ten host peer dependencies are now marked optional, so npm leaves them to the host (dsh plugin add uses pnpm and only warned, which is why this stayed hidden).\nThe WSL world now matches the host everywhere a session can tell the difference: Linux symlinks, the session's access mode, in-distribution grep / glob, a live skill catalog, a stateful shell, and tracked background jobs.\nNew: bash_background. The persistent shell takes `command` only, so without a producer job_list always answered \"no background jobs\" and a run_in_background argument passed to bash was silently ignored; background work is now tracked, with incremental job_output reads and job_kill.\nThe bash tool description now states two things the host does not: cd / exports carry into the next call, and a command ending in & backgrounds the whole command - background the subshell instead, as ( long-job > log 2>&1 ) &.\n0.1.0-rc.7 lacks the Windows process inspector the persistent shell needs, so the plugin probes for it and falls back to a one-shot bash instead of failing every call.",
|
|
1185
1185
|
"help.usage.title": "Usage and features",
|
|
1186
1186
|
"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 bash and persistent-bash; 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.\nA link the share cannot follow is resolved through the distribution (wsl.exe readlink, at most 32 per lookup) and the scan continues at the real path, so a linked-in project and its own nested projects are found too.\nThe catalog is not frozen: published skills directories are re-checked every 3 seconds (skill file mtime + size), so an add, remove or edit appears on the next turn; a 30-second walk finds a skills directory that did not exist before.\nFile search comes from grep / glob inside the distribution, and a mode that mounts no search suite (Minimal) gains none. grep is GNU grep -E (no lookaround or backreferences), skips hidden entries and node_modules, and does not read .gitignore; glob matches gitignore-style patterns here, oldest first.\nThe file tools work at the resolved real path of a link, and the policy is judged there too: a link out of the workspace is an outside write.\n`bash` is a PTY-backed stateful shell: login environment, starting directory the session workspace, and cd / exports / background jobs survive between calls.\nFor a tracked background job use bash_background: it returns a job id immediately, and job_list / job_output (incremental) / job_kill act on it.\n`bash` itself has no run_in_background parameter and ignores one; job ids are unique within this DSH process, so confirm with job_list before acting on one.\nBoth it and the file tools' shell run inside the distribution, outside the DSH file policy; read/write/edit are inside it, and workspace-write only writes inside the workspace.",
|
|
1187
1187
|
"help.known.title": "Known issues",
|
package/lib/client.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"client.js","names":["response: Response","envelope: Envelope<T>","listDir","describe","presetIssue: string | undefined","names: string[]","listDistros","facts: WslPathCheck","check","listScroll: UIEventHandler<HTMLDivElement>","zh: Record<string, string>","en: Record<string, string>","listDistrosApi","describeApi","listDirApi","checkApi","view","registerWindowsApi","setWorkspaceUserApi","defaultPreset: string | undefined","listWorkspacesApi"],"sources":["../src/client/api.ts","../src/shared/paths.ts","../src/client/help.tsx","../src/client/AddWslWorkspace.tsx","../src/client/styles.ts","../src/client/locales.ts","../src/client/index.ts"],"sourcesContent":["/**\n * Thin fetch client for the Host plugin route. The browser calls\n * POST /wsl-workspace/api with a `{ method, params }` envelope and the Host\n * answers `{ ok: true, value }` or `{ ok: false, error }`.\n */\n\n/** Relative route the Host half registers (same-origin with the web server). */\nconst ENDPOINT = '/wsl-workspace/api'\n\n/** One directory entry as the Host lists it. */\nexport interface WslDirEntry {\n name: string\n kind: 'directory' | 'file' | 'other'\n}\n\n/** One directory level plus its breadcrumb ancestry. */\nexport interface WslDirListing {\n /** The listed absolute Linux path. */\n path: string\n /** Parent Linux path, or null at the filesystem root. */\n parent: string | null\n /** The level's children (in name order; the client filters to directories). */\n entries: WslDirEntry[]\n}\n\n/** Existence/directory check result for one Linux path. */\nexport interface WslPathCheck {\n exists: boolean\n isDirectory: boolean\n}\n\n/** Wire envelope the Host route answers with. */\ntype Envelope<T> = { ok: true; value: T } | { ok: false; error: string }\n\n/** Human text for an unknown rejection, reusing the repository's idiom. */\nfunction errorMessage(value: unknown): string {\n return value instanceof Error ? value.message : String(value)\n}\n\n/**\n * Perform one POST call and unwrap the envelope.\n * @param method - the Host method name.\n * @param params - the method payload.\n * @returns the unwrapped value, or throws an Error on network or `ok:false`.\n */\nasync function call<T>(method: string, params: Record<string, unknown> = {}): Promise<T> {\n let response: Response\n try {\n response = await fetch(ENDPOINT, {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({ method, params }),\n })\n } catch (error) {\n // The transport refused before answering (offline, origin mismatch, 404).\n throw new Error(`wsl-workspace request failed: ${errorMessage(error)}`)\n }\n let envelope: Envelope<T>\n try {\n envelope = (await response.json()) as Envelope<T>\n } catch {\n // A non-JSON body means a proxy/loader answered instead of the Host route.\n throw new Error(`wsl-workspace answered non-JSON (${response.status})`)\n }\n if (!envelope.ok) throw new Error(envelope.error)\n return envelope.value\n}\n\n/**\n * List the WSL distros installed on the host.\n * @returns distro names in registry order.\n */\nexport async function listDistros(): Promise<string[]> {\n return call<string[]>('listDistros', {})\n}\n\n/**\n * List one directory level inside a distro.\n * @param distro - distro name.\n * @param path - absolute Linux directory to list.\n * @returns the level's listing with ancestry.\n */\nexport async function listDir(distro: string, path: string): Promise<WslDirListing> {\n return call<WslDirListing>('listDir', { distro, path })\n}\n\n/**\n * Check whether a Linux path exists and is a directory.\n * @param distro - distro name.\n * @param path - absolute Linux path.\n * @returns existence and directory facts.\n */\nexport async function check(distro: string, path: string): Promise<WslPathCheck> {\n return call<WslPathCheck>('check', { distro, path })\n}\n\n/**\n * Store (or clear, with an empty string) the username of one WSL workspace.\n * @param path - the workspace UNC path.\n * @param username - the Linux username; empty string clears the stored value.\n */\nexport async function setWorkspaceUser(path: string, username: string): Promise<void> {\n return call<void>('setUser', { path, username })\n}\n\n/**\n * Register a `/mnt/<drive>` WSL workspace under its Windows drive path,\n * recording the distro (and optional username) for the session env.\n * @param linuxPath - the `/mnt/<drive>/…` Linux path.\n * @param distro - the WSL distribution the workspace belongs to.\n * @param username - optional Linux username.\n */\nexport async function registerWindows(linuxPath: string, distro: string, username: string): Promise<void> {\n return call<void>('registerWindows', { linuxPath, distro, username })\n}\n\n/**\n * List every registered WSL workspace key (canonical UNC and Windows drive\n * spellings). The client uses the drive keys to recognize `/mnt` workspaces\n * across page reloads.\n */\nexport async function listWorkspaces(): Promise<string[]> {\n return call<string[]>('listWorkspaces', {})\n}\n\n/** One declared DSH release and its declared status. */\nexport interface WslDeclaredRelease {\n id: string\n status: string\n}\n\n/** Self-description of the running plugin build (shown by the help panel). */\nexport interface WslSelfDescription {\n version: string\n releases: WslDeclaredRelease[]\n}\n\n/**\n * Read the plugin build version and its declared DSH compatibility matrix,\n * for the dialog help panel.\n * @returns the self-description reported by the host plugin.\n */\nexport async function describe(): Promise<WslSelfDescription> {\n return call<WslSelfDescription>('describe', {})\n}\n","/**\n * WSL path helpers shared by the client and host halves. Pure and\n * dependency-free so both planes can import them without a runtime edge.\n */\n\n/** WSL2 default loopback bridge host: `\\\\wsl.localhost\\<distro>\\...`. */\nconst WSL_LOCALHOST_HOST = 'wsl.localhost'\n/** Legacy WSL interop host: `\\\\wsl$\\<distro>\\...`. */\nconst WSL_LEGACY_HOST = 'wsl$'\n\n/** The two UNC hosts WSL exposes a distribution's filesystem under. */\nconst UNC_HOSTS = [WSL_LOCALHOST_HOST, WSL_LEGACY_HOST]\n\n/** One WSL workspace coordinate parsed out of a UNC path. */\nexport interface WslUncTarget {\n /** Distro name (e.g. `Ubuntu`) as `wsl -l -q` reports it. */\n readonly distro: string\n /** Normalized absolute Linux path (leading `/`; no trailing slash except root). */\n readonly linuxPath: string\n}\n\n/**\n * Parse a WSL UNC path into its distro and Linux path. Accepts the WSL2\n * `\\\\wsl.localhost\\<distro>\\<linux>` form, the legacy `\\\\wsl$\\<distro>\\<linux>`\n * interop form, and forward-slash spellings of either.\n * @param raw - candidate absolute path.\n * @returns the parsed target, or null when the path is not a WSL UNC.\n */\nexport function parseWslUnc(raw: string): WslUncTarget | null {\n const normalized = raw.replace(/\\\\/g, '/').replace(/\\/\\/+/g, '//')\n if (!normalized.startsWith('//')) return null\n const segments = normalized.slice(2).split('/')\n const host = (segments[0] ?? '').toLowerCase()\n if (!UNC_HOSTS.includes(host)) return null\n const distro = segments[1] ?? ''\n if (distro === '') return null\n const rest = segments.slice(2).filter(segment => segment.length > 0)\n return { distro, linuxPath: `/${rest.join('/')}` }\n}\n\n/**\n * Whether a path resolves into a WSL distro through either UNC form.\n * @param raw - candidate absolute path.\n * @returns whether the path parses as a WSL UNC.\n */\nexport function isWslUnc(raw: string): boolean {\n return parseWslUnc(raw) !== null\n}\n\n/**\n * Translate a WSL UNC path to the absolute Linux path a process inside the\n * distribution can open. Throws on non-WSL input: callers rely on this\n * conversion to hand paths to the Linux world, so a silent pass-through\n * would hand a Windows path to bash.\n * @param uncPath - a path {@link parseWslUnc} accepts.\n * @returns the absolute Linux path.\n */\nexport function uncToLinux(uncPath: string): string {\n const parts = parseWslUnc(uncPath)\n if (parts === null) {\n throw new Error(`wsl-workspace: \"${uncPath}\" is not a WSL UNC path`)\n }\n return parts.linuxPath\n}\n\n/**\n * Normalize a Linux absolute path for the Host: collapse repeated slashes and\n * strip a trailing slash (root becomes `/`).\n * @param path - absolute Linux path.\n * @returns the normalized path.\n */\nexport function normalizeLinuxPath(path: string): string {\n const collapsed = path.replace(/\\/+/g, '/')\n return collapsed === '/' ? '/' : collapsed.replace(/\\/$/, '')\n}\n\n/**\n * Whether a path is an absolute, non-empty Linux path.\n * @param path - candidate.\n * @returns whether it starts with `/` and contains no NUL.\n */\nexport function isAbsoluteLinuxPath(path: string): boolean {\n return path.startsWith('/') && !path.includes('\\0')\n}\n\n/**\n * Join a distro and a Linux absolute path into the WSL2 UNC form used as the\n * workspace identity (`\\\\wsl.localhost\\<distro>\\<linux>`, backslash segments).\n * @param distro - distro name.\n * @param linuxPath - absolute Linux path (leading `/`).\n * @returns the UNC path.\n */\nexport function joinUnc(distro: string, linuxPath: string): string {\n if (!isAbsoluteLinuxPath(linuxPath)) {\n throw new Error(`wsl-workspace: cannot map a non-absolute Linux path \"${linuxPath}\" to UNC`)\n }\n // Defense in depth: a distribution name with separators or dot-dirs would\n // escape the `\\\\wsl.localhost\\` share structure (the host route validates\n // wire-supplied names too; every other caller passes through here).\n if (distro === '' || distro === '.' || distro === '..' || /[\\\\/]/.test(distro)) {\n throw new Error(`wsl-workspace: invalid distribution name \"${distro}\"`)\n }\n const normalized = linuxPath.replace(/\\/+/g, '/').replace(/\\/$/, '')\n const withoutLeading = normalized.startsWith('/') ? normalized.slice(1) : normalized\n const windowsSegments = withoutLeading.replace(/\\//g, '\\\\')\n const suffix = windowsSegments === '' ? '' : `\\\\${windowsSegments}`\n return `\\\\\\\\wsl.localhost\\\\${distro}${suffix}`\n}\n\n/**\n * Translate a Windows drive path to the drvfs mount path WSL distributions\n * conventionally expose it at (`C:\\foo` → `/mnt/c/foo`). Only single-letter\n * drives under `/mnt` are mapped; custom mount points are out of scope.\n * @param path - the candidate Windows path.\n * @returns the `/mnt/<drive>/…` path, or `null` for non-drive paths.\n */\nexport function windowsToMntPath(path: string): string | null {\n const match = /^([A-Za-z]):[\\\\/](.*)$/.exec(path)\n if (match === null) return null\n const rest = (match[2] ?? '').replace(/\\\\/g, '/').replace(/\\/+/g, '/').replace(/\\/$/, '')\n return `/mnt/${(match[1] ?? '').toLowerCase()}${rest === '' ? '' : `/${rest}`}`\n}\n\n/**\n * Translate a `/mnt/<drive>/…` path back to its Windows drive path.\n * @param linuxPath - the candidate Linux path.\n * @returns the `X:\\…` drive path, or `null` when the path is not a drvfs mount.\n */\nexport function mntToWindowsPath(linuxPath: string): string | null {\n const match = /^\\/mnt\\/([a-zA-Z])(?:\\/(.*))?$/.exec(linuxPath)\n if (match === null) return null\n const rest = (match[2] ?? '').replace(/\\//g, '\\\\')\n return `${(match[1] ?? '').toUpperCase()}:\\\\${rest}`\n}\n\n/**\n * Canonical Windows drive path for store keys and cross-realm identity:\n * separators unified to `\\`, trailing separator stripped, and the WHOLE path\n * lowercased — Windows paths compare case-insensitively, and the workspace\n * registry may realpath a different casing than the caller spelled (8.3 or\n * on-disk casing), so the store key must collide across casings.\n * @param path - candidate Windows drive path.\n * @returns the canonical form, or `null` when not drive-shaped.\n */\nexport function canonicalWindowsPath(path: string): string | null {\n const match = /^([A-Za-z]):[\\\\/](.*)$/.exec(path)\n if (match === null) return null\n const rest = (match[2] ?? '').replace(/[\\\\/]+/g, '\\\\').replace(/\\\\$/, '').toLowerCase()\n return `${(match[1] ?? '').toLowerCase()}:\\\\${rest}`\n}\n\n/**\n * True when a value is a Windows-shaped path (drive or UNC), which is how\n * the shell executor decides the WSLENV `/p` translation flag: only Windows\n * path values need translation when they cross into the Linux process.\n * @param value - the environment value to classify.\n * @returns whether the value looks like a Windows path.\n */\nexport function isWindowsPathShaped(value: string): boolean {\n return /^[A-Za-z]:[\\\\/]/.test(value) || value.startsWith('\\\\\\\\')\n}\n\n/** Linux username shape for `wsl.exe -u`: starts with a letter or underscore, then letters/digits/`_`/`.`/`-` (max 64). */\nconst WSL_USERNAME_PATTERN = /^[A-Za-z_][A-Za-z0-9_.-]{0,63}$/\n\n/**\n * Whether a value is a safe Linux username for `wsl.exe -u`. The check is\n * strict on purpose: a value starting with `-` could be parsed as a wsl.exe\n * option instead of a username.\n * @param value - candidate username.\n * @returns whether it matches the Linux username shape.\n */\nexport function isValidWslUsername(value: string): boolean {\n return WSL_USERNAME_PATTERN.test(value)\n}\n","/**\n * The dialog's \"?\" help panel: what this build is compatible with, how the\n * plugin is used, and the limits it cannot fix.\n *\n * The compatibility list is not hard-coded: it is read from this plugin's own\n * `package.json` through the host route, so the panel can never advertise a\n * release the build does not declare (and a host that cannot answer simply\n * shows no list).\n */\n\nimport type * as React from 'react'\nimport type { WslSelfDescription } from './api.ts'\n\n/** Props for the help panel. */\nexport interface WslHelpProps {\n /** Translate a `wslWorkspace` dictionary key. */\n t: (key: string, params?: Record<string, unknown>) => string\n /** Host-reported self-description, or null while it is unknown. */\n description: WslSelfDescription | null\n}\n\n/**\n * Split one dictionary entry into its bullet lines.\n * @param value - the multi-line dictionary string.\n * @returns the non-empty lines, trimmed.\n */\nfunction bullets(value: string): string[] {\n return value\n .split('\\n')\n .map(line => line.trim())\n .filter(line => line !== '')\n}\n\n/** One titled section of the panel. */\nfunction Section({ title, children }: { title: string; children: React.ReactNode }): React.ReactElement {\n return (\n <section className=\"dww-help-section\">\n <h3 className=\"dww-help-title\">{title}</h3>\n {children}\n </section>\n )\n}\n\n/**\n * Render the help panel shown behind the dialog's \"?\" button.\n * @param props - translate function plus the host's self-description.\n */\nexport function WslHelp({ t, description }: WslHelpProps): React.ReactElement {\n const releases = description?.releases ?? []\n const version = description === null\n ? t('help.compat.unknown')\n : `${t('help.compat.versionLabel')} v${description.version}`\n return (\n <div className=\"dww-help\" role=\"region\" aria-label={t('help.button')}>\n <p className=\"dww-help-greeting\">\n {t('help.greeting')}{' '}\n <a className=\"dww-help-link\" href=\"https://github.com/6Mikao9/dsh-wsl-workspace\" target=\"_blank\" rel=\"noreferrer\">\n {t('help.greeting.repo')}\n </a>\n </p>\n <Section title={t('help.compat.title')}>\n <div className=\"dww-help-meta\">{version}</div>\n {releases.length > 0\n ? (\n <div className=\"dww-help-chips\">\n {releases.map(entry => <span key={entry.id} className=\"dww-help-chip\">{entry.id}</span>)}\n </div>\n )\n : null}\n <ul className=\"dww-help-list\">\n {bullets(t('help.compat.body')).map(line => <li key={line}>{line}</li>)}\n </ul>\n </Section>\n <Section title={t('help.news.title')}>\n <ul className=\"dww-help-list\">\n {bullets(t('help.news.body')).map(line => <li key={line}>{line}</li>)}\n </ul>\n </Section>\n <Section title={t('help.usage.title')}>\n <ul className=\"dww-help-list\">\n {bullets(t('help.usage.body')).map(line => <li key={line}>{line}</li>)}\n </ul>\n </Section>\n <Section title={t('help.known.title')}>\n <ul className=\"dww-help-list dww-help-list--known\">\n {bullets(t('help.known.body')).map(line => <li key={line}>{line}</li>)}\n </ul>\n </Section>\n <div className=\"dww-help-footer\">\n <a className=\"dww-help-link\" href=\"https://www.npmjs.com/package/dsh-wsl-workspace\" target=\"_blank\" rel=\"noreferrer\">\n {t('help.footer.npm')}\n </a>\n <a className=\"dww-help-link\" href=\"https://github.com/6Mikao9/dsh-wsl-workspace\" target=\"_blank\" rel=\"noreferrer\">\n {t('help.footer.repo')}\n </a>\n </div>\n </div>\n )\n}\n","/**\n * Sidebar footer action that opens the \"Add WSL workspace\" dialog. In the\n * wide sidebar it renders a labeled row; in the 56px rail it collapses to a\n * 36px icon button (both honor the shell's `{ wide }` owner share).\n *\n * Registration omits the typed `locale:` seat (the `wslWorkspace` namespace is\n * not merged into `LocaleNamespaceMap`), so the injected face carries the\n * bound translate function instead.\n */\n\nimport type * as React from 'react'\nimport { useEffect, useRef, useState, type UIEventHandler } from 'react'\nimport { isAbsoluteLinuxPath, isValidWslUsername, normalizeLinuxPath } from '../shared/paths.ts'\nimport type { WslDirListing, WslPathCheck, WslSelfDescription } from './api.ts'\nimport { WslHelp } from './help.tsx'\n\n/** Result-level shape of a browse/check/list call we surface uniformly. */\ninterface ApiCall<T> {\n value: T\n}\n\n/** The inject face: plain data + callbacks the dialog drives. */\nexport interface AddWslWorkspaceInjected {\n /**\n * Confirm the deployment exposes a healthy `wsl` preset.\n * @returns undefined when healthy, else a Chinese/English message to show.\n */\n checkPreset(): Promise<string | undefined>\n /**\n * Read this build's version and its declared DSH compatibility matrix,\n * for the help panel. A host that cannot answer resolves to a fallback\n * description rather than failing the dialog.\n */\n describe(): Promise<WslSelfDescription>\n /** List the WSL distros installed on the host. */\n listDistros(): Promise<string[]>\n /** List one Linux directory level inside a distro. */\n listDir(distro: string, path: string): Promise<WslDirListing>\n /** Check a Linux path's existence/directory facts. */\n check(distro: string, path: string): Promise<WslPathCheck>\n /**\n * Register a WSL workspace and start a session in it. `/mnt/<drive>`\n * paths register under their Windows drive spelling (9P cannot serve\n * drvfs); ext4 paths register as `\\\\wsl.localhost\\<distro>\\...` UNC.\n * @param linuxPath - the absolute Linux workspace path.\n * @param username - optional Linux user for the session (empty = distro default).\n * @param distro - the WSL distribution the path belongs to.\n * @returns undefined on success, else a message to show.\n */\n createWorkspace(linuxPath: string, username: string, distro: string): Promise<string | undefined>\n /** Translate a `wslWorkspace` dictionary key (follows the DeepSeek Harness locale). */\n t: (key: string, params?: Record<string, unknown>) => string\n}\n\n/** Full component props: the owner share plus the injected face. */\nexport interface AddWslWorkspaceProps extends AddWslWorkspaceInjected {\n /** Whether the sidebar renders wide content (false = 56px rail). */\n wide: boolean\n}\n\n/**\n * Build the Linux child path one level below a parent, for the breadcrumb/\n * browse drill.\n * @param parent - the currently listed absolute path (`/` for root).\n * @param name - the child directory name.\n * @returns the child's absolute Linux path.\n */\nexport function dirChildPath(parent: string, name: string): string {\n return parent === '/' ? `/${name}` : `${parent}/${name}`\n}\n\n/** A tiny inline terminal glyph for the dialog's directory rows. */\nfunction WslGlyph({ size = 16 }: { size?: number }): React.ReactElement {\n return (\n <svg width={size} height={size} viewBox=\"0 0 24 24\" fill=\"none\" aria-hidden=\"true\">\n <rect x=\"2.5\" y=\"4.5\" width=\"19\" height=\"15\" rx=\"2.5\" stroke=\"currentColor\" strokeWidth=\"1.6\" />\n <path d=\"M6 9l3.2 2.6L6 14\" stroke=\"currentColor\" strokeWidth=\"1.6\" strokeLinecap=\"round\" strokeLinejoin=\"round\" />\n <path d=\"M12 14h5\" stroke=\"currentColor\" strokeWidth=\"1.6\" strokeLinecap=\"round\" />\n </svg>\n )\n}\n\n/**\n * The \"Add WSL workspace…\" footer action and its dialog.\n * @param props - owner share + injected face.\n */\nexport function AddWslWorkspace({ wide, t, describe, checkPreset, listDistros, listDir, check, createWorkspace }: AddWslWorkspaceProps): React.ReactElement | null {\n const [open, setOpen] = useState(false)\n const [opening, setOpening] = useState(false)\n const [distros, setDistros] = useState<string[]>([])\n const [distro, setDistro] = useState('')\n const [pathInput, setPathInput] = useState('/home/')\n const [username, setUsername] = useState('')\n const [listing, setListing] = useState<WslDirListing | null>(null)\n const [browsePath, setBrowsePath] = useState('/')\n const [browsing, setBrowsing] = useState(false)\n const [error, setError] = useState<string | null>(null)\n const [busy, setBusy] = useState(false)\n // The \"?\" panel: help text plus the compatibility list the host reports.\n const [helpOpen, setHelpOpen] = useState(false)\n const [selfDescription, setSelfDescription] = useState<WslSelfDescription | null>(null)\n // Monotone browse-request sequence: stale responses for a superseded browse are dropped.\n const browseSeq = useRef(0)\n\n const refreshBrowse = async (root: string, targetDistro: string): Promise<void> => {\n const seq = ++browseSeq.current\n setBrowsing(true)\n setBrowsePath(root)\n try {\n const value = await listDir(targetDistro, root)\n if (seq === browseSeq.current) setListing(value)\n } catch {\n // A failed browse (permission, missing dir) is non-fatal: keep old listing.\n if (seq === browseSeq.current) {\n setListing(null)\n setError((previous) => previous ?? t('error.loadDir'))\n }\n } finally {\n if (seq === browseSeq.current) setBrowsing(false)\n }\n }\n\n useEffect(() => {\n if (!open) return\n let cancelled = false\n setError(null)\n // Advisory data for the help panel: never fatal, never blocking the form.\n void describe().then(value => { if (!cancelled) setSelfDescription(value) }).catch(() => { if (!cancelled) setSelfDescription(null) })\n setOpening(true)\n void (async () => {\n let presetIssue: string | undefined\n try {\n presetIssue = await checkPreset()\n } catch {\n presetIssue = t('error.loadDistros')\n }\n let names: string[]\n try {\n names = await listDistros()\n } catch {\n if (cancelled) return\n setOpening(false)\n setError(t('error.loadDistros'))\n return\n }\n if (cancelled) return\n setDistros(names)\n const first = names[0] ?? ''\n setDistro(first)\n // The default browse root walks from `/`; the input defaults to `/home/`.\n setBrowsing(true)\n setOpening(false)\n if (presetIssue !== undefined) setError(presetIssue)\n if (first !== '') void refreshBrowse('/', first)\n })()\n return () => { cancelled = true }\n // eslint-disable-next-line react-hooks/exhaustive-deps -- run once per open against current t.\n }, [open])\n\n useEffect(() => {\n if (!open) return\n const onKey = (event: KeyboardEvent): void => {\n if (event.key === 'Escape' && !busy) setOpen(false)\n }\n window.addEventListener('keydown', onKey)\n return () => window.removeEventListener('keydown', onKey)\n }, [open, busy])\n\n if (!open) {\n // A W-letter action beside Settings at the sidebar foot; the title\n // carries the label in both wide and rail states.\n return (\n <button\n type=\"button\"\n className={wide ? 'dww-action dww-action--wide' : 'dww-action dww-action--rail'}\n title={t('action.title')}\n aria-label={t('action.title')}\n onClick={() => setOpen(true)}\n >\n <span className=\"dww-letter\" aria-hidden=\"true\">W</span>\n </button>\n )\n }\n\n const onDrill = (name: string): void => {\n const next = dirChildPath(listing?.path ?? browsePath, name)\n setPathInput(next)\n void refreshBrowse(next, distro)\n }\n\n const onUp = (): void => {\n const parent = listing?.parent ?? null\n if (parent === null) return\n setPathInput(parent)\n void refreshBrowse(parent, distro)\n }\n\n const onDistroChange = (value: string): void => {\n setDistro(value)\n void refreshBrowse(browsePath, value)\n }\n\n const onCheck = async (): Promise<void> => {\n const path = normalizeLinuxPath(pathInput)\n setError(null)\n if (!isAbsoluteLinuxPath(path) || path === '/') {\n setError(t('error.invalidPath'))\n return\n }\n let facts: WslPathCheck\n try {\n facts = await check(distro, path)\n } catch {\n setError(t('error.pathNotFound'))\n return\n }\n if (!facts.exists || !facts.isDirectory) {\n setError(t('error.pathNotFound'))\n return\n }\n void refreshBrowse(path, distro)\n }\n\n const onConfirm = async (): Promise<void> => {\n const path = normalizeLinuxPath(pathInput)\n setError(null)\n if (!isAbsoluteLinuxPath(path) || path === '/') {\n // A workspace at the distribution root would make every session start\n // at `/`; the check flow rejects it and confirm must agree.\n setError(t('error.invalidPath'))\n return\n }\n const user = username.trim()\n if (user !== '' && !isValidWslUsername(user)) {\n setError(t('error.invalidUsername'))\n return\n }\n setBusy(true)\n try {\n let facts: WslPathCheck\n try {\n facts = await check(distro, path)\n } catch {\n setError(t('error.pathNotFound'))\n return\n }\n if (!facts.exists || !facts.isDirectory) {\n setError(t('error.pathNotFound'))\n return\n }\n const failure = await createWorkspace(path, user, distro)\n if (failure !== undefined) {\n setError(failure)\n return\n }\n setOpen(false)\n } finally {\n setBusy(false)\n }\n }\n\n const children = (listing?.entries.filter(entry => entry.kind === 'directory') ?? []).map(entry => entry.name)\n const maskClick = (): void => { if (!busy) setOpen(false) }\n const listScroll: UIEventHandler<HTMLDivElement> = () => { /* scroll container handles overflow */ }\n\n return (\n <div className=\"dww-overlay\">\n <div className=\"dww-overlay-mask\" onClick={maskClick} />\n <div className=\"dww-card\" role=\"dialog\" aria-modal=\"true\" aria-label={t('dialog.title')}>\n <div className=\"dww-header\">\n <h2 className=\"dww-title\">{t('dialog.title')}</h2>\n <button\n type=\"button\"\n className=\"dww-help-btn\"\n title={t('help.button')}\n aria-label={t('help.button')}\n aria-pressed={helpOpen}\n onClick={() => setHelpOpen(value => !value)}\n >\n ?\n </button>\n <button type=\"button\" className=\"dww-close\" aria-label={t('dialog.cancel')} onClick={maskClick}>\n ✕\n </button>\n </div>\n <div className=\"dww-body\">\n {helpOpen ? (\n <WslHelp t={t} description={selfDescription} />\n ) : (<>\n {error !== null ? (\n <div className=\"dww-error\">\n {error}\n <button type=\"button\" className=\"dww-retry\" onClick={() => setError(null)}>{t('dialog.retry')}</button>\n </div>\n ) : null}\n <div className=\"dww-field\">\n <label className=\"dww-field-label\" htmlFor=\"dww-distro\">{t('dialog.distro')}</label>\n <select\n id=\"dww-distro\"\n className=\"dww-select\"\n value={distro}\n disabled={opening || busy}\n onChange={event => onDistroChange(event.target.value)}\n >\n {distros.length === 0\n ? <option value=\"\">{opening ? t('dialog.loading') : ''}</option>\n : distros.map(name => <option key={name} value={name}>{name}</option>)}\n </select>\n </div>\n <div className=\"dww-field\">\n <label className=\"dww-field-label\" htmlFor=\"dww-path\">{t('dialog.path')}</label>\n <div className=\"dww-input-row\">\n <input\n id=\"dww-path\"\n className=\"dww-input\"\n value={pathInput}\n placeholder={t('dialog.pathPlaceholder')}\n disabled={opening || busy}\n onChange={event => setPathInput(event.target.value)}\n />\n <button type=\"button\" className=\"dww-check-btn\" disabled={opening || busy} onClick={() => void onCheck()}>\n {t('dialog.check')}\n </button>\n </div>\n </div>\n <div className=\"dww-field\">\n <label className=\"dww-field-label\" htmlFor=\"dww-username\">{t('dialog.username')}</label>\n <input\n id=\"dww-username\"\n className=\"dww-input\"\n value={username}\n placeholder={t('dialog.usernamePlaceholder')}\n disabled={opening || busy}\n autoComplete=\"off\"\n spellCheck={false}\n onChange={event => setUsername(event.target.value)}\n />\n </div>\n <div className=\"dww-feedback\">\n <div className=\"dww-breadcrumb\">{browsePath}</div>\n <div className=\"dww-dirlist\" onScroll={listScroll}>\n {browsing ? <div className=\"dww-dir-empty\">{t('dialog.loading')}</div> : (\n listing?.parent !== null && listing !== null\n ? (\n <button type=\"button\" className=\"dww-dir-row dww-dir-row--up\" onClick={onUp}>\n <WslGlyph size={14} />\n <span>{t('dialog.upLevel')}</span>\n </button>\n )\n : null\n )}\n {!browsing && (children.length === 0)\n ? <div className=\"dww-dir-empty\">{t('dialog.browseEmpty')}</div>\n : children.map(name => (\n <button type=\"button\" key={name} className=\"dww-dir-row\" onClick={() => onDrill(name)}>\n <WslGlyph size={14} />\n <span>{name}</span>\n </button>\n ))}\n </div>\n </div>\n </>)}\n </div>\n <div className=\"dww-actions\">\n <button type=\"button\" className=\"dww-btn\" disabled={busy} onClick={maskClick}>{t('dialog.cancel')}</button>\n <button type=\"button\" className=\"dww-btn dww-btn--primary\" disabled={busy || opening} onClick={() => void onConfirm()}>\n {busy ? t('dialog.loading') : t('dialog.confirm')}\n </button>\n </div>\n </div>\n </div>\n )\n}\n\nexport type { ApiCall }\n","/**\n * Third-party stylesheet injection for the WSL workspace UI (the plugin\n * builds no CSS bundle, so styles are injected as one idempotent `<style>`).\n * Colors derive exclusively from the `--dsw-*` design tokens.\n */\n\nconst STYLE_TAG_DATA_ATTRIBUTE = 'data-plugin=\"dsh-wsl-workspace\"'\n\nconst STYLES = `\n/* Sidebar-foot icon action beside Settings (28px round in the wide sidebar,\n 36px round in the rail), matching the shell's icon-button language. */\n.dww-action {\n flex: none;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 28px;\n height: 28px;\n border: none;\n border-radius: 50%;\n padding: 0;\n background: transparent;\n cursor: pointer;\n color: var(--dsw-alias-label-secondary);\n transition:\n background-color 120ms var(--dsw-ease-in-out, ease-in-out),\n color 120ms var(--dsw-ease-in-out, ease-in-out);\n}\n.dww-action:hover:not(:disabled) {\n background: var(--dsw-alias-interactive-bg-hover);\n color: var(--dsw-alias-label-secondary);\n}\n.dww-action:active:not(:disabled) {\n background: var(--dsw-alias-interactive-bg-pressed, var(--dsw-alias-interactive-bg-hover));\n}\n.dww-action:focus-visible {\n outline: 2px solid var(--dsw-alias-state-business-primary);\n outline-offset: 1px;\n}\n.dww-action:disabled { cursor: default; opacity: 0.6; }\n.dww-action--rail {\n width: 36px;\n height: 36px;\n color: var(--dsw-alias-label-primary);\n}\n.dww-action svg { flex: none; }\n\n/* The W letter mark of the sidebar action (sized for wide/rail buttons). */\n.dww-letter {\n font-size: 14px;\n font-weight: 600;\n line-height: 1;\n letter-spacing: 0.02em;\n user-select: none;\n}\n.dww-action--rail .dww-letter { font-size: 17px; }\n\n/* Full-viewport overlay + centered card (mirrors the platform Mask/Dialog). */\n.dww-overlay {\n position: fixed;\n inset: 0;\n z-index: 1000;\n display: flex;\n align-items: center;\n justify-content: center;\n padding: 24px;\n}\n.dww-overlay-mask {\n position: absolute;\n inset: 0;\n background: var(--dsw-alias-bg-mask-1);\n backdrop-filter: var(--dsw-mask-blur);\n}\n.dww-card {\n position: relative;\n z-index: 1;\n box-sizing: border-box;\n display: flex;\n flex-direction: column;\n width: min(440px, 100%);\n max-height: min(640px, 90vh);\n padding: 0 0 20px;\n overflow: hidden;\n border: 1px solid var(--dsw-alias-border-inverted);\n border-radius: 16px;\n background: var(--dsw-alias-bg-layer-2);\n box-shadow: var(--dsw-shadow-lv3);\n font-family: var(--dsw-font-family);\n}\n.dww-header {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 8px;\n padding: 18px 20px 12px;\n}\n.dww-title {\n margin: 0;\n font-size: 16px;\n line-height: 24px;\n font-weight: 500;\n color: var(--dsw-alias-label-primary);\n}\n.dww-close {\n flex: none;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 28px;\n height: 28px;\n border: 0;\n border-radius: 8px;\n background: transparent;\n cursor: pointer;\n color: var(--dsw-alias-label-secondary);\n}\n.dww-close:hover { background: var(--dsw-alias-interactive-bg-hover); }\n.dww-body {\n display: flex;\n flex-direction: column;\n gap: 14px;\n min-width: 0;\n padding: 0 20px;\n overflow: auto;\n}\n.dww-field { display: flex; flex-direction: column; gap: 6px; min-width: 0; }\n.dww-field-label {\n font-size: 12px;\n line-height: 18px;\n color: var(--dsw-alias-label-secondary);\n}\n.dww-select {\n box-sizing: border-box;\n width: 100%;\n height: 36px;\n padding: 0 10px;\n border: 1px solid var(--dsw-alias-border-l2);\n border-radius: 8px;\n background: var(--dsw-alias-bg-layer-3);\n color: var(--dsw-alias-label-primary);\n font-size: 14px;\n}\n.dww-input-row { display: flex; gap: 8px; align-items: center; }\n.dww-input {\n box-sizing: border-box;\n flex: 1;\n height: 36px;\n min-width: 0;\n padding: 0 10px;\n border: 1px solid var(--dsw-alias-border-l2);\n border-radius: 8px;\n background: var(--dsw-alias-bg-layer-3);\n color: var(--dsw-alias-label-primary);\n font-size: 14px;\n}\n.dww-input:focus, .dww-select:focus {\n outline: none;\n border-color: var(--dsw-alias-state-business-primary);\n}\n.dww-check-btn {\n flex: none;\n height: 36px;\n padding: 0 12px;\n border: 1px solid var(--dsw-alias-border-l2);\n border-radius: 8px;\n background: transparent;\n color: var(--dsw-alias-label-primary);\n cursor: pointer;\n font-size: 12px;\n}\n.dww-check-btn:hover:not(:disabled) { background: var(--dsw-alias-interactive-bg-hover); }\n.dww-check-btn:disabled { cursor: default; }\n\n/* Directory browse list. */\n.dww-dirlist {\n display: flex;\n flex-direction: column;\n gap: 2px;\n box-sizing: border-box;\n min-height: 120px;\n max-height: 200px;\n padding: 4px;\n overflow: auto;\n border: 1px solid var(--dsw-alias-border-l2);\n border-radius: 8px;\n background: var(--dsw-alias-bg-layer-3);\n}\n.dww-breadcrumb {\n padding: 0 4px;\n font-size: 12px;\n line-height: 18px;\n color: var(--dsw-alias-label-tertiary);\n overflow: hidden;\n white-space: nowrap;\n text-overflow: ellipsis;\n}\n.dww-dir-row {\n display: flex;\n align-items: center;\n gap: 8px;\n height: 28px;\n padding: 0 8px;\n border: 0;\n border-radius: 6px;\n background: transparent;\n color: var(--dsw-alias-label-primary);\n cursor: pointer;\n font-size: 13px;\n text-align: left;\n}\n.dww-dir-row:hover:not(:disabled) { background: var(--dsw-alias-interactive-bg-hover); }\n.dww-dir-row:disabled { cursor: default; color: var(--dsw-alias-label-tertiary); }\n.dww-dir-row--up { color: var(--dsw-alias-label-secondary); }\n.dww-dir-row svg { flex: none; color: var(--dsw-alias-label-tertiary); }\n.dww-dir-empty {\n padding: 8px;\n font-size: 12px;\n line-height: 18px;\n color: var(--dsw-alias-label-tertiary);\n}\n\n/* Error strip. */\n.dww-error {\n box-sizing: border-box;\n width: 100%;\n padding: 8px 10px;\n border: 1px solid var(--dsw-alias-state-error-primary);\n border-radius: 8px;\n color: var(--dsw-alias-state-error-primary);\n font-size: 12px;\n line-height: 18px;\n}\n.dww-retry {\n margin-left: 6px;\n border: 0;\n background: transparent;\n color: var(--dsw-alias-state-business-primary);\n cursor: pointer;\n font-size: 12px;\n text-decoration: underline;\n}\n\n/* Dialog footer actions. */\n.dww-actions {\n display: flex;\n align-items: center;\n justify-content: flex-end;\n gap: 8px;\n padding: 14px 20px 0;\n}\n.dww-btn {\n height: 36px;\n padding: 0 14px;\n border: 1px solid var(--dsw-alias-border-l2);\n border-radius: 8px;\n background: transparent;\n color: var(--dsw-alias-label-primary);\n cursor: pointer;\n font-size: 14px;\n}\n.dww-btn:hover:not(:disabled) { background: var(--dsw-alias-interactive-bg-hover); }\n.dww-btn--primary {\n border-color: transparent;\n background: var(--dsw-alias-button-primary-fill);\n color: var(--dsw-alias-label-primary-foreground);\n}\n.dww-btn--primary:hover:not(:disabled) { background: var(--dsw-alias-button-primary-hover); }\n.dww-btn:disabled { cursor: default; opacity: 0.6; }\n/* Help panel behind the dialog's \"?\" button. */\n.dww-help-btn {\n flex: none;\n margin-left: auto;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 26px;\n height: 26px;\n border: 1px solid var(--dsw-alias-border-l2);\n border-radius: 50%;\n background: transparent;\n color: var(--dsw-alias-label-secondary);\n cursor: pointer;\n font-size: 13px;\n line-height: 1;\n}\n.dww-help-btn:hover { background: var(--dsw-alias-interactive-bg-hover); }\n.dww-help-btn[aria-pressed='true'] {\n background: var(--dsw-alias-interactive-bg-hover);\n color: var(--dsw-alias-label-primary);\n}\n.dww-help {\n display: flex;\n flex-direction: column;\n gap: 14px;\n max-height: 52vh;\n overflow-y: auto;\n padding-right: 4px;\n}\n.dww-help-section { display: flex; flex-direction: column; gap: 6px; }\n.dww-help-greeting {\n margin: 0;\n padding: 8px 10px;\n border-radius: 8px;\n background: var(--dsw-alias-interactive-bg-hover);\n font-size: 13px;\n line-height: 1.5;\n color: var(--dsw-alias-label-primary);\n}\n.dww-help-title {\n margin: 0;\n font-size: 13px;\n font-weight: 600;\n color: var(--dsw-alias-label-primary);\n}\n.dww-help-meta { font-size: 12px; color: var(--dsw-alias-label-tertiary); }\n.dww-help-chips { display: flex; flex-wrap: wrap; gap: 6px; }\n.dww-help-chip {\n padding: 2px 8px;\n border: 1px solid var(--dsw-alias-border-l2);\n border-radius: 999px;\n font-size: 12px;\n color: var(--dsw-alias-label-secondary);\n}\n.dww-help-list {\n margin: 0;\n padding-left: 18px;\n display: flex;\n flex-direction: column;\n gap: 6px;\n font-size: 13px;\n line-height: 1.6;\n color: var(--dsw-alias-label-secondary);\n}\n.dww-help-list--known { color: var(--dsw-alias-label-primary); }\n.dww-help-footer {\n display: flex;\n gap: 14px;\n padding-top: 8px;\n border-top: 1px solid var(--dsw-alias-border-l2);\n}\n.dww-help-link { font-size: 12px; color: var(--dsw-alias-label-tertiary); text-decoration: underline; }\n.dww-help-link:hover { color: var(--dsw-alias-label-primary); }\n`\n\n/**\n * Idempotently inject the plugin stylesheet. No-op when a tag with the\n * plugin's data attribute already exists.\n */\nexport function ensureStyles(): void {\n if (typeof document === 'undefined') return\n if (document.querySelector(`style[${STYLE_TAG_DATA_ATTRIBUTE}]`) !== null) return\n const style = document.createElement('style')\n style.setAttribute('data-plugin', 'dsh-wsl-workspace')\n style.textContent = STYLES\n document.head.appendChild(style)\n}\n","/**\n * Bilingual dictionaries for the `wslWorkspace` locale namespace. Product copy\n * is Chinese; English is the parallel export for the standalone bundle.\n */\n\n/**\n * The `wslWorkspace` translations (Chinese, the primary product copy).\n */\nexport const zh: Record<string, string> = {\n 'action.add': 'WSL 工作区',\n 'action.title': '添加 WSL 工作区…',\n\n 'dialog.title': '添加 WSL 工作区',\n 'dialog.distro': '发行版',\n 'dialog.path': '路径',\n 'dialog.pathPlaceholder': '/home/',\n 'dialog.username': '用户名',\n 'dialog.usernamePlaceholder': '留空则使用发行版默认用户',\n 'dialog.loading': '正在加载…',\n 'dialog.browseEmpty': '此目录没有子文件夹',\n 'dialog.upLevel': '..(返回上级)',\n 'dialog.browse': '浏览',\n 'dialog.check': '检查',\n 'dialog.confirm': '创建并打开',\n 'dialog.cancel': '取消',\n 'dialog.retry': '重试',\n\n 'error.loadDistros': '无法获取 WSL 发行版列表,请确认已安装 WSL 且插件宿主端可用',\n 'error.rateLimited': '操作过于频繁,请稍后重试',\n 'error.loadDir': '无法浏览该目录',\n 'error.presetMissing': '未找到健康的 wsl preset,请确认插件宿主端已安装并配置该 preset',\n 'error.invalidPath': '请输入以 / 开头的 Linux 绝对路径',\n 'error.invalidUsername': '用户名无效:需以字母或下划线开头,仅含字母、数字、_、.、-',\n 'error.pathNotFound': '该路径不存在或是文件,请选择一个文件夹',\n 'error.createFailed': '创建工作区失败',\n 'help.button': '插件说明',\n 'help.greeting': '当你看到这句话的时候,说明你的插件已经Cia进来llo~(∠・ω< )⌒★,star一下吗?',\n 'help.greeting.repo': 'github.com/6Mikao9/dsh-wsl-workspace',\n\n 'help.compat.title': '兼容性',\n 'help.compat.versionLabel': '插件版本',\n 'help.compat.unknown': '未能读取版本与兼容声明(宿主端没有响应)',\n 'help.compat.body': '上方是这份构建声明兼容的 DSH 版本,每一条都在隔离实例上实测过(独立 DSH_HOME、依赖固定到该版本、跑满十项门禁)。\\n插件在运行时自动识别 DSH 版本并选用对应的 API;两边都不支持时会明确报错,而不是留下一个空工作区。\\n版本不在列表里通常仍然可用,但未经验证。',\n 'help.news.title': '本次更新(0.7.1)',\n 'help.news.body': '修掉了 npm 安装失败:npm 会自动安装缺失的同伴依赖,而 @deepseek-ai/dsh-tool-fs-search 自己又依赖并未公开发布的 @deepseek-ai/dsh-retention,于是 npm install dsh-wsl-workspace 直接 E404 失败(dsh plugin 走 pnpm、只警告,所以一直没暴露)。\\n十个宿主同伴依赖现在都标为 optional:包仍声明宿主必须提供什么,但 npm 不再尝试下载它们。\\nWSL 世界现在在会话能察觉到的每一处都与宿主一致:Linux 符号链接、会话的访问模式、发行版内的 grep / glob、实时技能目录、有状态的 shell,以及可跟踪的后台任务。\\n新增 bash_background:持久 shell 的 schema 只有 command,没有生产者时 job_list 永远说\"没有后台任务\",而传给 bash 的 run_in_background 会被静默忽略;现在后台任务可跟踪,job_output 增量读取、job_kill 取消都照常工作。\\nbash 的工具描述写明两件宿主没说的事:cd / export 跨调用保留,命令以 & 结尾会把整条命令后台化(请写成单独一行的 ( 长任务 > log 2>&1 ) &)。0.1.0-rc.7 的宿主缺少 Windows 进程检查器、持久 shell 起不来,插件会在启动时探测并回退到一次性 bash,不再让每次调用都报错。',\n 'help.usage.title': '用法与特性',\n 'help.usage.body': '点击侧边栏底部的 W 按钮 → 选发行版 → 输入或浏览 Linux 路径 → 检查 → 创建并打开。\\n创建出的会话里,bash 与文件工具都落在该发行版内,模型看到的路径全是 Linux 路径;Windows 盘可从会话内通过 /mnt/<盘符> 访问。\\n四种模式(标准 / PTC / 极简 / 创造)各有 WSL 变体,在模式选择器里直接选即可,名字形如 WSL · Standard mode(标准模式)。\\n用户名可选,等价于 wsl.exe -u <用户名>,只改变 bash 与 persistent-bash 的运行身份;文件工具走 Windows 侧共享,不受它影响。\\n技能目录从会话 cwd 最近的 .git 祖先开始向下扫描 .dsh/skills 与 .agents/skills(含嵌套项目),上限 4 层目录 / 64 个技能目录 / 4096 个已访问目录,并按扫描根缓存 10 秒。9P 解不开的符号链接交给发行版 readlink 解析出真实路径(每次查找最多 32 条)后继续扫描,链接进来的项目与它下面的嵌套项目都能扫到。\\n目录不再冻结:对 UNC 关闭文件监视之后,插件每 3 秒重查一次已发布的技能目录、并比对每个技能文件的修改时间与大小,所以新增、删除与改写都会在下一个回合生效;完整重新发现每 30 秒一次,用于找到此前不存在的技能目录(技能正文始终实时读取)。\\n文件搜索由发行版内的 grep / glob 提供,源预设没有这两个工具的模式(极简)不会多出来:grep 用 GNU grep -E(\\\\d、\\\\w、(?i) 可用,环视与反向引用不支持),跳过隐藏项与 node_modules,不读 .gitignore;glob 用 GNU find 列文件、按 gitignore 风格匹配(* 不跨目录、** 跨、{a,b}、前导 ! 取反),按修改时间从旧到新排序。\\n文件工具在链接处按真实路径工作,并按真实路径判策略:链接指向工作区外就等于工作区外。\\nbash 由 PTY 承载的持久 shell 提供:登录环境、起始目录就是会话工作区,cd / export / venv / 后台任务跨调用保留(它取代了原先一次性 bash)。\\n需要可跟踪的后台任务时用 bash_background:它立刻返回 job id,job_list / job_output(增量读取)/ job_kill 都作用于它;bash 本身没有 run_in_background 参数,传了会被忽略。job id 只在本次 DSH 进程内唯一,重启后会重新编号,引用前先用 job_list 确认。\\nbash 与文件工具的 shell 都在发行版内运行、不受 DSH 文件策略约束;文件工具(read/write/edit)受策略约束,工作区内修改模式下只能写工作区内。',\n 'help.known.title': '已知问题',\n 'help.known.body': 'grep 用的是发行版自带的 GNU grep:方言是 POSIX ERE(环视与反向引用不支持),且不读 .gitignore,被 git 忽略的文件照样会被搜到;只有隐藏项、node_modules 与版本库目录会被跳过。发行版没有 GNU grep(如 Alpine 的 busybox)时会明确报错,而不是给出错位的结果。\\nglob 的\"按修改时间排序\"依赖 GNU find -printf,busybox 会退化成路径排序;含 / 的 include 在插件进程里过滤,因此那种调用会先扫描全部文件再筛。\\n技能目录刷新仍是轮询:已发布目录内的增删改约 3 秒生效,而一个新项目里第一次出现的技能目录要等下一次完整重新发现(最多 30 秒)。\\n0.1.0-rc.7 的宿主没有 Windows 进程检查器,PTY 持久 shell 无法启动(宿主自己也这样),插件回退到一次性 bash:能正常用,但 cd / export 不跨调用保留。\\n极简模式本身不挂 job_* 工具,所以那个模式里也没有 bash_background(与宿主的极简模式一致)。',\n 'help.footer.npm': 'npm 包',\n 'help.footer.repo': 'GitHub 仓库',\n}\n\n/**\n * The `wslWorkspace` translations (English).\n */\nexport const en: Record<string, string> = {\n 'action.add': 'WSL Workspace',\n 'action.title': 'Add WSL workspace…',\n\n 'dialog.title': 'Add WSL workspace',\n 'dialog.distro': 'Distro',\n 'dialog.path': 'Path',\n 'dialog.pathPlaceholder': '/home/',\n 'dialog.username': 'Username',\n 'dialog.usernamePlaceholder': 'Leave empty to use the distro default user',\n 'dialog.loading': 'Loading…',\n 'dialog.browseEmpty': 'No subdirectories here',\n 'dialog.upLevel': '.. (up)',\n 'dialog.browse': 'Browse',\n 'dialog.check': 'Check',\n 'dialog.confirm': 'Create & open',\n 'dialog.cancel': 'Cancel',\n 'dialog.retry': 'Retry',\n\n 'error.loadDistros': 'Could not list WSL distros; confirm WSL is installed and the plugin host side is reachable',\n 'error.rateLimited': 'Too many attempts; retry in a moment',\n 'error.loadDir': 'Could not browse this directory',\n 'error.presetMissing': 'No healthy \"wsl\" preset found; confirm the plugin host side installed and configured it',\n 'error.invalidPath': 'Enter an absolute Linux path starting with /',\n 'error.invalidUsername': 'Invalid username: start with a letter or underscore; only letters, digits, _ . -',\n 'error.pathNotFound': 'The path does not exist or is a file; choose a folder',\n 'error.createFailed': 'Failed to create the workspace',\n 'help.button': 'About this plugin',\n 'help.greeting': 'If you can read this, the plugin has already Cia~llo\\'d its way in~(∠・ω< )⌒★ Care to star the repo?',\n 'help.greeting.repo': 'github.com/6Mikao9/dsh-wsl-workspace',\n\n 'help.compat.title': 'Compatibility',\n 'help.compat.versionLabel': 'Plugin version',\n 'help.compat.unknown': 'Version and compatibility declaration unavailable (the host side did not answer)',\n '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 and picks the matching API; a release exposing neither fails loudly instead of leaving an empty workspace.\\nA release outside the list usually still works, but is unverified.',\n 'help.news.title': \"What's new in 0.7.1\",\n 'help.news.body': 'Fixed a failed npm install: npm auto-installs missing peers, and @deepseek-ai/dsh-tool-fs-search peers on @deepseek-ai/dsh-retention, which is not published - so npm install dsh-wsl-workspace died with E404.\\nThe ten host peer dependencies are now marked optional, so npm leaves them to the host (dsh plugin add uses pnpm and only warned, which is why this stayed hidden).\\nThe WSL world now matches the host everywhere a session can tell the difference: Linux symlinks, the session\\'s access mode, in-distribution grep / glob, a live skill catalog, a stateful shell, and tracked background jobs.\\nNew: bash_background. The persistent shell takes `command` only, so without a producer job_list always answered \"no background jobs\" and a run_in_background argument passed to bash was silently ignored; background work is now tracked, with incremental job_output reads and job_kill.\\nThe bash tool description now states two things the host does not: cd / exports carry into the next call, and a command ending in & backgrounds the whole command - background the subshell instead, as ( long-job > log 2>&1 ) &.\\n0.1.0-rc.7 lacks the Windows process inspector the persistent shell needs, so the plugin probes for it and falls back to a one-shot bash instead of failing every call.',\n 'help.usage.title': 'Usage and features',\n '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 bash and persistent-bash; 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.\\nA link the share cannot follow is resolved through the distribution (wsl.exe readlink, at most 32 per lookup) and the scan continues at the real path, so a linked-in project and its own nested projects are found too.\\nThe catalog is not frozen: published skills directories are re-checked every 3 seconds (skill file mtime + size), so an add, remove or edit appears on the next turn; a 30-second walk finds a skills directory that did not exist before.\\nFile search comes from grep / glob inside the distribution, and a mode that mounts no search suite (Minimal) gains none. grep is GNU grep -E (no lookaround or backreferences), skips hidden entries and node_modules, and does not read .gitignore; glob matches gitignore-style patterns here, oldest first.\\nThe file tools work at the resolved real path of a link, and the policy is judged there too: a link out of the workspace is an outside write.\\n`bash` is a PTY-backed stateful shell: login environment, starting directory the session workspace, and cd / exports / background jobs survive between calls.\\nFor a tracked background job use bash_background: it returns a job id immediately, and job_list / job_output (incremental) / job_kill act on it.\\n`bash` itself has no run_in_background parameter and ignores one; job ids are unique within this DSH process, so confirm with job_list before acting on one.\\nBoth it and the file tools\\' shell run inside the distribution, outside the DSH file policy; read/write/edit are inside it, and workspace-write only writes inside the workspace.',\n 'help.known.title': 'Known issues',\n 'help.known.body': 'grep is the distribution\\'s GNU grep: POSIX ERE (no lookaround or backreferences), and it does not read .gitignore, so git-ignored files are searched too; only hidden entries, node_modules and VCS directories are skipped.\\nglob\\'s modification-time order needs GNU find -printf (busybox falls back to path order), and an include containing \"/\" is filtered in this process, so that call scans every file first.\\nThe catalog refresh is still a poll: an add, remove or edit inside a published skills directory lands within about 3 seconds, while a new project\\'s first skills directory waits for the next full re-discovery (up to 30 seconds).\\n0.1.0-rc.7 has no Windows process inspector, so its PTY persistent shell cannot start (the host has the same gap) and the plugin falls back to a one-shot bash: it works, but cd / exports do not survive.\\nMinimal mode mounts no job_* tools, so it gets no bash_background either - the same as the host\\'s own Minimal mode.',\n 'help.footer.npm': 'npm package',\n 'help.footer.repo': 'GitHub repository',\n}\n","/**\n * Browser half of dsh-wsl-workspace. Registers the \"Add WSL workspace…\"\n * action beside Settings at the sidebar foot (the official\n * `sidebar.footer.action` slot), and keeps every blank session whose\n * workspace is a WSL UNC path composed from the WSL VARIANT of the mode it\n * currently runs (`standard` → `wsl-standard`, PTC → `wsl-code`, …) — so the\n * WSL execution world composes with any mode instead of being a mode itself.\n *\n * The binding is a watching effect rather than a one-shot dialog action so\n * EVERY creation path (this dialog, the workspace row's New Session, the\n * hero picker) converges on the WSL-backed composition automatically.\n */\n\n// Type-only: pulls the locale plugin's Context merge (ctx.locale) and the\n// ui-sidebar SlotMap merge (the 'sidebar.footer.action' entry) into this\n// program. Version-dependent services (`remote`, `connection.api`,\n// `uiWorkspace`) are read dynamically via ctx.get() so both DSH v0.1.1-rc.2\n// and v0.1.2-rc.1+ can load this plugin.\nimport type {} from '@deepseek-ai/dsh-client-locale/client'\nimport type {} from '@deepseek-ai/dsh-client-ui-sidebar/client'\nimport type { ClientContext } from '@deepseek-ai/dsh-client-runtime/client'\nimport { check as checkApi, describe as describeApi, listDir as listDirApi, listDistros as listDistrosApi, listWorkspaces as listWorkspacesApi, registerWindows as registerWindowsApi, setWorkspaceUser as setWorkspaceUserApi } from './api.ts'\nimport { AddWslWorkspace, type AddWslWorkspaceInjected } from './AddWslWorkspace.tsx'\nimport { ensureStyles } from './styles.ts'\nimport { zh, en } from './locales.ts'\nimport { canonicalWindowsPath, isWslUnc, joinUnc, mntToWindowsPath } from '../shared/paths.ts'\n\n/** Required services (cordis fiber inject). */\nexport const inject = ['slots', 'locale', 'sessions', 'workspaces']\n\n/** The legacy standalone WSL preset id (folded into the mode variants). */\nconst LEGACY_WSL_PRESET_ID = 'wsl'\n\n/**\n * Minimal sessions-service face. The renderer-host ctx merge types\n * `ctx.sessions` as its own SessionStore; the service the runtime actually\n * registers under that key satisfies this narrower contract, so the cast is\n * the documented boundary for a third-party plugin.\n */\ninterface WslSessionsFace {\n list: {\n getSnapshot(): {\n ids: string[]\n byId: Record<string, {\n blank: boolean\n cwd?: string\n /** v0.1.1-rc.2 direct field */\n agentPreset?: string\n /** v0.1.2-rc.1+ projection value */\n projectionValues?: Readonly<{ agentPreset?: string | null }>\n }>\n }\n subscribe(fn: () => void): () => void\n }\n /** v0.1.1-rc.2 only — absent in v0.1.2-rc.1+ (projection auto-syncs). */\n noteAgentPreset?(sessionId: string, agentPreset: string): void\n}\n\n/** Minimal workspaces-service face (create only; startSession moved out in v0.1.2-rc.1+). */\ninterface WslWorkspacesFace {\n create(input: { path: string }): Promise<{ workspaceId: string }>\n /** v0.1.1-rc.2 only — removed in v0.1.2-rc.1+. */\n startSession?(workspaceId?: string): void\n}\n\n/** v0.1.2-rc.1+ only — replaces `workspaces.startSession`. */\ninterface WslUiWorkspaceFace {\n startSession(workspaceId?: string): void\n}\n\n/** v0.1.2-rc.1+ agentPresets 命名空间服务(经 ctx.get('remote.agentPresets') 动态取,免 inject、无 associate 陷阱)。 */\ninterface WslAgentPresetsNamespace {\n list(): Promise<{ ok: boolean; value?: { presets: { id: string; broken?: string; isDefault?: boolean }[] }; error?: { message: string } }>\n select(sessionId: string, presetId: string): Promise<{ ok: boolean }>\n}\n\n/** 旧版 connection 服务最小接口(v0.1.1-rc.2 及更早),api 属性承载远程调用。 */\ninterface WslLegacyConnection {\n api?: {\n agentPresets: {\n list(input: Record<string, never>): Promise<{ result: { ok: boolean; value?: { presets: { id: string; broken?: string; isDefault?: boolean }[] }; error?: { message: string } } }>\n select(input: { sessionId: string; agentPreset: string }): Promise<{ result: { ok: boolean } }>\n }\n }\n}\n/**\n * Mount the sidebar action and the auto-binding effect.\n * @param ctx - the browser plugin context.\n */\nexport function apply(ctx: ClientContext): void {\n const workspaces = ctx.get('workspaces') as unknown as WslWorkspacesFace\n const sessions = ctx.get('sessions') as unknown as WslSessionsFace\n // Version-dependent services are resolved ON USE - never through `inject`\n // (a release without the service would refuse to mount the plugin at all)\n // and never once at apply time. This plugin applies before the UI domain\n // that publishes `uiWorkspace` registers its service, so a one-shot read\n // caches `undefined` for the whole page life: on v0.1.2-rc.1+ that silently\n // disabled session creation (see resolveSessionStarter).\n const legacyApi = (): WslLegacyConnection['api'] =>\n (ctx.get('connection') as unknown as WslLegacyConnection | undefined)?.api\n const remoteAgentPresets = (): WslAgentPresetsNamespace | undefined =>\n ctx.get('remote.agentPresets') as unknown as WslAgentPresetsNamespace | undefined\n const uiWorkspaceService = (): WslUiWorkspaceFace | undefined =>\n ctx.get('uiWorkspace') as unknown as WslUiWorkspaceFace | undefined\n const hasNoteAgentPreset = typeof sessions.noteAgentPreset === 'function'\n\n /** Unified agent-preset list: new `remote.agentPresets` namespace (v0.1.2-rc.1+) or legacy `connection.api` (v0.1.1-rc.2). */\n const listAgentPresets = async (): Promise<{ ok: boolean; presets: { id: string; broken?: string; isDefault?: boolean }[]; error?: string }> => {\n const agentPresets = remoteAgentPresets()\n if (agentPresets !== undefined) {\n const r = await agentPresets.list()\n if (!r.ok) return { ok: false, presets: [], error: r.error?.message ?? 'list failed' }\n return { ok: true, presets: r.value?.presets ?? [] }\n }\n const api = legacyApi()\n if (api !== undefined) {\n const r = await api.agentPresets.list({})\n if (!r.result.ok) return { ok: false, presets: [], error: r.result.error?.message ?? 'list failed' }\n return { ok: true, presets: r.result.value?.presets ?? [] }\n }\n return { ok: false, presets: [], error: 'no remote api available' }\n }\n\n /** Unified agent-preset select: new `agentPresets.select(id, preset)` or legacy `connection.api.select({...})`. */\n const selectAgentPreset = async (sessionId: string, presetId: string): Promise<{ ok: boolean }> => {\n const agentPresets = remoteAgentPresets()\n if (agentPresets !== undefined) {\n return agentPresets.select(sessionId, presetId)\n }\n const api = legacyApi()\n if (api !== undefined) {\n const r = await api.agentPresets.select({ sessionId, agentPreset: presetId })\n return { ok: r.result.ok }\n }\n return { ok: false }\n }\n\n /**\n * Resolve how this release opens a session for a workspace - v0.1.2-rc.1+\n * exposes `uiWorkspace.startSession`, v0.1.1-rc.2 keeps it on `workspaces`.\n *\n * Resolved BEFORE the workspace is written. A release that offers neither\n * cannot open a session, and a silent fall-through would leave the workspace\n * behind with an empty `sessionIds` while the dialog still reports success;\n * failing here names the missing capability instead.\n * @returns the starter for the service this release actually exposes.\n * @throws Error naming both candidates when neither service is available.\n */\n const resolveSessionStarter = (): (workspaceId: string) => void | Promise<void> => {\n const ui = uiWorkspaceService()\n if (ui !== undefined) return (workspaceId) => { ui.startSession(workspaceId) }\n const legacyStart = workspaces.startSession\n if (typeof legacyStart === 'function') {\n return (workspaceId) => { Reflect.apply(legacyStart, workspaces, [workspaceId]) }\n }\n throw new Error(\n 'workspace session API unavailable: this DSH release exposes neither '\n + 'uiWorkspace.startSession nor workspaces.startSession',\n )\n }\n\n /** Read agent preset — v0.1.2-rc.1+ uses projectionValues; v0.1.1-rc.2 uses direct field. */\n const getAgentPreset = (summary: { agentPreset?: string; projectionValues?: { agentPreset?: string | null } }): string | undefined => {\n if (summary.projectionValues?.agentPreset !== undefined) {\n const v = summary.projectionValues.agentPreset\n return v === null ? undefined : v\n }\n return summary.agentPreset\n }\n\n /** Note preset change — v0.1.1-rc.2 calls noteAgentPreset; v0.1.2-rc.1+ is auto-synced via projection. */\n const noteAgentPresetCompat = (sessionId: string, presetId: string): void => {\n if (hasNoteAgentPreset && sessions.noteAgentPreset) {\n sessions.noteAgentPreset(sessionId, presetId)\n }\n }\n\n ensureStyles()\n\n ctx.effect(\n () => ctx.locale.register('wslWorkspace' as never, { zh, en }),\n 'dsh-wsl-workspace: locale dictionaries',\n )\n\n // The injected translate function reads the live DeepSeek Harness locale,\n // so the dialog copy follows the app language setting automatically.\n const t = ctx.locale.bind('wslWorkspace' as never) as unknown as (key: string, params?: Record<string, unknown>) => string\n\n // Canonical Windows drive keys of every registered `/mnt/<drive>` workspace\n // (refreshed from the host store; see refreshRoster). A blank session whose\n // cwd is one of these binds to the WSL variant like a UNC-cwd session.\n let wslWindowsPaths = new Set<string>()\n\n const injected = (): AddWslWorkspaceInjected => ({\n t,\n checkPreset: async (): Promise<string | undefined> => {\n let roster\n try {\n roster = await listAgentPresets()\n } catch (error) {\n return error instanceof Error ? error.message : String(error)\n }\n if (!roster.ok) return roster.error\n const healthy = roster.presets.find((entry: { id: string; broken?: string }) =>\n entry.id.startsWith('wsl-') && entry.broken === undefined)\n if (healthy === undefined) return t('error.presetMissing')\n return undefined\n },\n listDistros: () => listDistrosApi(),\n // Advisory data for the help panel: a host that cannot answer reports an\n // unavailable description instead of breaking the dialog.\n describe: () => describeApi(),\n listDir: (distro, path) => listDirApi(distro, path),\n check: (distro, path) => checkApi(distro, path),\n createWorkspace: async (linuxPath, username, distro): Promise<string | undefined> => {\n try {\n // Before any write: without a session starter the workspace below\n // would be created and never opened.\n const startSession = resolveSessionStarter()\n const winPath = mntToWindowsPath(linuxPath)\n if (winPath !== null) {\n // `/mnt/<drive>` workspace: the workspace registry realpath/stats\n // the path and 9P cannot serve drvfs mounts, so register under the\n // drive spelling and store distro/username for the session env.\n // The browser binding below recognizes the drive cwd as WSL.\n const view = await workspaces.create({ path: winPath })\n await registerWindowsApi(linuxPath, distro, username)\n const canonical = canonicalWindowsPath(winPath)\n if (canonical !== null) wslWindowsPaths = new Set(wslWindowsPaths).add(canonical)\n await startSession(view.workspaceId)\n return undefined\n }\n const uncPath = joinUnc(distro, linuxPath)\n const view = await workspaces.create({ path: uncPath })\n await setWorkspaceUserApi(uncPath, username)\n await startSession(view.workspaceId)\n return undefined\n } catch (error) {\n return error instanceof Error ? error.message : String(error)\n }\n },\n })\n\n ctx.effect(\n () => ctx.slots.inject(\n 'sidebar.footer.action',\n () => ctx.slots.register(\n { name: 'sidebar.footer.action', id: 'wsl-workspace', inject: injected },\n AddWslWorkspace,\n ),\n ),\n 'dsh-wsl-workspace: sidebar footer action',\n )\n\n // Mode-variant binding: a blank session whose workspace is a WSL UNC path\n // is recomposed to the WSL variant of the mode it currently runs — plain\n // 标准 becomes `wsl-standard`, PTC becomes `wsl-code`, and so on — so the\n // WSL execution world composes with ANY mode instead of replacing it. The\n // host refuses non-blank sessions (agent-preset-locked), so the swap is\n // attempted at most a few times per session.\n ctx.effect(() => {\n const inFlight = new Set<string>()\n const attempts = new Map<string, number>()\n const MAX_ATTEMPTS = 3\n // Healthy `wsl-<mode>` variant ids plus the roster's default preset id\n // (what a session with no explicit choice gets). Refreshed periodically\n // so variants generated after this page loaded are picked up.\n let variants = new Set<string>()\n let defaultPreset: string | undefined\n const refreshRoster = (): void => {\n void listAgentPresets().then((result: {\n ok: boolean; presets: { id: string; broken?: string; isDefault?: boolean }[]\n }) => {\n if (!result.ok) return\n variants = new Set(result.presets\n .filter((entry: { id: string; broken?: string }) =>\n entry.broken === undefined && entry.id.startsWith('wsl-'))\n .map((entry: { id: string }) => entry.id))\n defaultPreset = result.presets.find(\n (entry: { id: string; isDefault?: boolean }) => entry.isDefault === true,\n )?.id\n // The roster is an INPUT to binding. It can land after the first pass\n // (and after apply()), and nothing else would re-run that pass: the\n // session store only emits when a session changes, so a blank session\n // that was already there would stay unbound until the user acted.\n maybeBind()\n }).catch(() => {\n // A failed roster read leaves the previous mapping; sessions stay on\n // their current composition until the next refresh.\n })\n }\n refreshRoster()\n const refreshWorkspaces = (): void => {\n void listWorkspacesApi().then((keys: string[]) => {\n const next = new Set<string>()\n for (const key of keys) {\n const canonical = canonicalWindowsPath(key)\n if (canonical !== null) next.add(canonical)\n }\n wslWindowsPaths = next\n // Same late-input rule as the roster: the `/mnt/<drive>` key set\n // decides binding for drive-cwd sessions.\n maybeBind()\n }).catch(() => {\n // A failed store read leaves the previous set; sessions stay on\n // their current composition until the next refresh.\n })\n }\n refreshWorkspaces()\n const maybeBind = (): void => {\n const state = sessions.list.getSnapshot()\n for (const id of state.ids) {\n const summary = state.byId[id]\n if (summary === undefined || !summary.blank || summary.cwd === undefined) continue\n // A session belongs to the WSL world when its cwd is a WSL UNC path,\n // or a Windows drive path registered as a `/mnt/<drive>` workspace\n // (9P cannot serve drvfs, so those workspaces carry drive cwds).\n const canonical = canonicalWindowsPath(summary.cwd)\n const isWsl = isWslUnc(summary.cwd)\n || (canonical !== null && wslWindowsPaths.has(canonical))\n if (!isWsl) continue\n const current = getAgentPreset(summary)\n if (current !== undefined && current.startsWith('wsl-')) continue\n // Legacy standalone `wsl` (now folded into the variants): remap it to\n // the default mode's variant, since the standalone preset no longer\n // exists in the roster.\n const base = current === LEGACY_WSL_PRESET_ID\n ? (defaultPreset ?? 'standard')\n : (current ?? defaultPreset)\n if (base === undefined || base === LEGACY_WSL_PRESET_ID || base.startsWith('wsl-')) continue\n const target = `wsl-${base.toLowerCase()}`\n if (!variants.has(target)) continue\n if (inFlight.has(id) || (attempts.get(id) ?? 0) >= MAX_ATTEMPTS) continue\n inFlight.add(id)\n void selectAgentPreset(id, target)\n .then((result: { ok: boolean }) => {\n if (result.ok) noteAgentPresetCompat(id, target)\n })\n .catch(() => {\n // A refused or aborted swap (session already produced output,\n // roster churn, reconnect) leaves the session on its current\n // composition; count the attempt so a stuck session stops\n // retrying after MAX_ATTEMPTS.\n attempts.set(id, (attempts.get(id) ?? 0) + 1)\n })\n .finally(() => {\n inFlight.delete(id)\n })\n }\n }\n maybeBind()\n const unsubscribe = sessions.list.subscribe(() => maybeBind())\n // Variants are generated at host boot; a page loaded before that would\n // never see them without a periodic refresh.\n const timer = window.setInterval(refreshRoster, 60_000)\n return () => {\n unsubscribe()\n window.clearInterval(timer)\n }\n }, 'dsh-wsl-workspace: WSL mode-variant binding')\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"client.js","names":["response: Response","envelope: Envelope<T>","listDir","describe","presetIssue: string | undefined","names: string[]","listDistros","facts: WslPathCheck","check","listScroll: UIEventHandler<HTMLDivElement>","zh: Record<string, string>","en: Record<string, string>","listDistrosApi","describeApi","listDirApi","checkApi","view","registerWindowsApi","setWorkspaceUserApi","defaultPreset: string | undefined","listWorkspacesApi"],"sources":["../src/client/api.ts","../src/shared/paths.ts","../src/client/help.tsx","../src/client/AddWslWorkspace.tsx","../src/client/styles.ts","../src/client/locales.ts","../src/client/index.ts"],"sourcesContent":["/**\n * Thin fetch client for the Host plugin route. The browser calls\n * POST /wsl-workspace/api with a `{ method, params }` envelope and the Host\n * answers `{ ok: true, value }` or `{ ok: false, error }`.\n */\n\n/** Relative route the Host half registers (same-origin with the web server). */\nconst ENDPOINT = '/wsl-workspace/api'\n\n/** One directory entry as the Host lists it. */\nexport interface WslDirEntry {\n name: string\n kind: 'directory' | 'file' | 'other'\n}\n\n/** One directory level plus its breadcrumb ancestry. */\nexport interface WslDirListing {\n /** The listed absolute Linux path. */\n path: string\n /** Parent Linux path, or null at the filesystem root. */\n parent: string | null\n /** The level's children (in name order; the client filters to directories). */\n entries: WslDirEntry[]\n}\n\n/** Existence/directory check result for one Linux path. */\nexport interface WslPathCheck {\n exists: boolean\n isDirectory: boolean\n}\n\n/** Wire envelope the Host route answers with. */\ntype Envelope<T> = { ok: true; value: T } | { ok: false; error: string }\n\n/** Human text for an unknown rejection, reusing the repository's idiom. */\nfunction errorMessage(value: unknown): string {\n return value instanceof Error ? value.message : String(value)\n}\n\n/**\n * Perform one POST call and unwrap the envelope.\n * @param method - the Host method name.\n * @param params - the method payload.\n * @returns the unwrapped value, or throws an Error on network or `ok:false`.\n */\nasync function call<T>(method: string, params: Record<string, unknown> = {}): Promise<T> {\n let response: Response\n try {\n response = await fetch(ENDPOINT, {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({ method, params }),\n })\n } catch (error) {\n // The transport refused before answering (offline, origin mismatch, 404).\n throw new Error(`wsl-workspace request failed: ${errorMessage(error)}`)\n }\n let envelope: Envelope<T>\n try {\n envelope = (await response.json()) as Envelope<T>\n } catch {\n // A non-JSON body means a proxy/loader answered instead of the Host route.\n throw new Error(`wsl-workspace answered non-JSON (${response.status})`)\n }\n if (!envelope.ok) throw new Error(envelope.error)\n return envelope.value\n}\n\n/**\n * List the WSL distros installed on the host.\n * @returns distro names in registry order.\n */\nexport async function listDistros(): Promise<string[]> {\n return call<string[]>('listDistros', {})\n}\n\n/**\n * List one directory level inside a distro.\n * @param distro - distro name.\n * @param path - absolute Linux directory to list.\n * @returns the level's listing with ancestry.\n */\nexport async function listDir(distro: string, path: string): Promise<WslDirListing> {\n return call<WslDirListing>('listDir', { distro, path })\n}\n\n/**\n * Check whether a Linux path exists and is a directory.\n * @param distro - distro name.\n * @param path - absolute Linux path.\n * @returns existence and directory facts.\n */\nexport async function check(distro: string, path: string): Promise<WslPathCheck> {\n return call<WslPathCheck>('check', { distro, path })\n}\n\n/**\n * Store (or clear, with an empty string) the username of one WSL workspace.\n * @param path - the workspace UNC path.\n * @param username - the Linux username; empty string clears the stored value.\n */\nexport async function setWorkspaceUser(path: string, username: string): Promise<void> {\n return call<void>('setUser', { path, username })\n}\n\n/**\n * Register a `/mnt/<drive>` WSL workspace under its Windows drive path,\n * recording the distro (and optional username) for the session env.\n * @param linuxPath - the `/mnt/<drive>/…` Linux path.\n * @param distro - the WSL distribution the workspace belongs to.\n * @param username - optional Linux username.\n */\nexport async function registerWindows(linuxPath: string, distro: string, username: string): Promise<void> {\n return call<void>('registerWindows', { linuxPath, distro, username })\n}\n\n/**\n * List every registered WSL workspace key (canonical UNC and Windows drive\n * spellings). The client uses the drive keys to recognize `/mnt` workspaces\n * across page reloads.\n */\nexport async function listWorkspaces(): Promise<string[]> {\n return call<string[]>('listWorkspaces', {})\n}\n\n/** One declared DSH release and its declared status. */\nexport interface WslDeclaredRelease {\n id: string\n status: string\n}\n\n/** Self-description of the running plugin build (shown by the help panel). */\nexport interface WslSelfDescription {\n version: string\n releases: WslDeclaredRelease[]\n}\n\n/**\n * Read the plugin build version and its declared DSH compatibility matrix,\n * for the dialog help panel.\n * @returns the self-description reported by the host plugin.\n */\nexport async function describe(): Promise<WslSelfDescription> {\n return call<WslSelfDescription>('describe', {})\n}\n","/**\n * WSL path helpers shared by the client and host halves. Pure and\n * dependency-free so both planes can import them without a runtime edge.\n */\n\n/** WSL2 default loopback bridge host: `\\\\wsl.localhost\\<distro>\\...`. */\nconst WSL_LOCALHOST_HOST = 'wsl.localhost'\n/** Legacy WSL interop host: `\\\\wsl$\\<distro>\\...`. */\nconst WSL_LEGACY_HOST = 'wsl$'\n\n/** The two UNC hosts WSL exposes a distribution's filesystem under. */\nconst UNC_HOSTS = [WSL_LOCALHOST_HOST, WSL_LEGACY_HOST]\n\n/** One WSL workspace coordinate parsed out of a UNC path. */\nexport interface WslUncTarget {\n /** Distro name (e.g. `Ubuntu`) as `wsl -l -q` reports it. */\n readonly distro: string\n /** Normalized absolute Linux path (leading `/`; no trailing slash except root). */\n readonly linuxPath: string\n}\n\n/**\n * Parse a WSL UNC path into its distro and Linux path. Accepts the WSL2\n * `\\\\wsl.localhost\\<distro>\\<linux>` form, the legacy `\\\\wsl$\\<distro>\\<linux>`\n * interop form, and forward-slash spellings of either.\n * @param raw - candidate absolute path.\n * @returns the parsed target, or null when the path is not a WSL UNC.\n */\nexport function parseWslUnc(raw: string): WslUncTarget | null {\n const normalized = raw.replace(/\\\\/g, '/').replace(/\\/\\/+/g, '//')\n if (!normalized.startsWith('//')) return null\n const segments = normalized.slice(2).split('/')\n const host = (segments[0] ?? '').toLowerCase()\n if (!UNC_HOSTS.includes(host)) return null\n const distro = segments[1] ?? ''\n if (distro === '') return null\n const rest = segments.slice(2).filter(segment => segment.length > 0)\n return { distro, linuxPath: `/${rest.join('/')}` }\n}\n\n/**\n * Whether a path resolves into a WSL distro through either UNC form.\n * @param raw - candidate absolute path.\n * @returns whether the path parses as a WSL UNC.\n */\nexport function isWslUnc(raw: string): boolean {\n return parseWslUnc(raw) !== null\n}\n\n/**\n * Translate a WSL UNC path to the absolute Linux path a process inside the\n * distribution can open. Throws on non-WSL input: callers rely on this\n * conversion to hand paths to the Linux world, so a silent pass-through\n * would hand a Windows path to bash.\n * @param uncPath - a path {@link parseWslUnc} accepts.\n * @returns the absolute Linux path.\n */\nexport function uncToLinux(uncPath: string): string {\n const parts = parseWslUnc(uncPath)\n if (parts === null) {\n throw new Error(`wsl-workspace: \"${uncPath}\" is not a WSL UNC path`)\n }\n return parts.linuxPath\n}\n\n/**\n * Normalize a Linux absolute path for the Host: collapse repeated slashes and\n * strip a trailing slash (root becomes `/`).\n * @param path - absolute Linux path.\n * @returns the normalized path.\n */\nexport function normalizeLinuxPath(path: string): string {\n const collapsed = path.replace(/\\/+/g, '/')\n return collapsed === '/' ? '/' : collapsed.replace(/\\/$/, '')\n}\n\n/**\n * Whether a path is an absolute, non-empty Linux path.\n * @param path - candidate.\n * @returns whether it starts with `/` and contains no NUL.\n */\nexport function isAbsoluteLinuxPath(path: string): boolean {\n return path.startsWith('/') && !path.includes('\\0')\n}\n\n/**\n * Join a distro and a Linux absolute path into the WSL2 UNC form used as the\n * workspace identity (`\\\\wsl.localhost\\<distro>\\<linux>`, backslash segments).\n * @param distro - distro name.\n * @param linuxPath - absolute Linux path (leading `/`).\n * @returns the UNC path.\n */\nexport function joinUnc(distro: string, linuxPath: string): string {\n if (!isAbsoluteLinuxPath(linuxPath)) {\n throw new Error(`wsl-workspace: cannot map a non-absolute Linux path \"${linuxPath}\" to UNC`)\n }\n // Defense in depth: a distribution name with separators or dot-dirs would\n // escape the `\\\\wsl.localhost\\` share structure (the host route validates\n // wire-supplied names too; every other caller passes through here).\n if (distro === '' || distro === '.' || distro === '..' || /[\\\\/]/.test(distro)) {\n throw new Error(`wsl-workspace: invalid distribution name \"${distro}\"`)\n }\n const normalized = linuxPath.replace(/\\/+/g, '/').replace(/\\/$/, '')\n const withoutLeading = normalized.startsWith('/') ? normalized.slice(1) : normalized\n const windowsSegments = withoutLeading.replace(/\\//g, '\\\\')\n const suffix = windowsSegments === '' ? '' : `\\\\${windowsSegments}`\n return `\\\\\\\\wsl.localhost\\\\${distro}${suffix}`\n}\n\n/**\n * Translate a Windows drive path to the drvfs mount path WSL distributions\n * conventionally expose it at (`C:\\foo` → `/mnt/c/foo`). Only single-letter\n * drives under `/mnt` are mapped; custom mount points are out of scope.\n * @param path - the candidate Windows path.\n * @returns the `/mnt/<drive>/…` path, or `null` for non-drive paths.\n */\nexport function windowsToMntPath(path: string): string | null {\n const match = /^([A-Za-z]):[\\\\/](.*)$/.exec(path)\n if (match === null) return null\n const rest = (match[2] ?? '').replace(/\\\\/g, '/').replace(/\\/+/g, '/').replace(/\\/$/, '')\n return `/mnt/${(match[1] ?? '').toLowerCase()}${rest === '' ? '' : `/${rest}`}`\n}\n\n/**\n * Translate a `/mnt/<drive>/…` path back to its Windows drive path.\n * @param linuxPath - the candidate Linux path.\n * @returns the `X:\\…` drive path, or `null` when the path is not a drvfs mount.\n */\nexport function mntToWindowsPath(linuxPath: string): string | null {\n const match = /^\\/mnt\\/([a-zA-Z])(?:\\/(.*))?$/.exec(linuxPath)\n if (match === null) return null\n const rest = (match[2] ?? '').replace(/\\//g, '\\\\')\n return `${(match[1] ?? '').toUpperCase()}:\\\\${rest}`\n}\n\n/**\n * Canonical Windows drive path for store keys and cross-realm identity:\n * separators unified to `\\`, trailing separator stripped, and the WHOLE path\n * lowercased — Windows paths compare case-insensitively, and the workspace\n * registry may realpath a different casing than the caller spelled (8.3 or\n * on-disk casing), so the store key must collide across casings.\n * @param path - candidate Windows drive path.\n * @returns the canonical form, or `null` when not drive-shaped.\n */\nexport function canonicalWindowsPath(path: string): string | null {\n const match = /^([A-Za-z]):[\\\\/](.*)$/.exec(path)\n if (match === null) return null\n const rest = (match[2] ?? '').replace(/[\\\\/]+/g, '\\\\').replace(/\\\\$/, '').toLowerCase()\n return `${(match[1] ?? '').toLowerCase()}:\\\\${rest}`\n}\n\n/**\n * True when a value is a Windows-shaped path (drive or UNC), which is how\n * the shell executor decides the WSLENV `/p` translation flag: only Windows\n * path values need translation when they cross into the Linux process.\n * @param value - the environment value to classify.\n * @returns whether the value looks like a Windows path.\n */\nexport function isWindowsPathShaped(value: string): boolean {\n return /^[A-Za-z]:[\\\\/]/.test(value) || value.startsWith('\\\\\\\\')\n}\n\n/** Linux username shape for `wsl.exe -u`: starts with a letter or underscore, then letters/digits/`_`/`.`/`-` (max 64). */\nconst WSL_USERNAME_PATTERN = /^[A-Za-z_][A-Za-z0-9_.-]{0,63}$/\n\n/**\n * Whether a value is a safe Linux username for `wsl.exe -u`. The check is\n * strict on purpose: a value starting with `-` could be parsed as a wsl.exe\n * option instead of a username.\n * @param value - candidate username.\n * @returns whether it matches the Linux username shape.\n */\nexport function isValidWslUsername(value: string): boolean {\n return WSL_USERNAME_PATTERN.test(value)\n}\n","/**\n * The dialog's \"?\" help panel: what this build is compatible with, how the\n * plugin is used, and the limits it cannot fix.\n *\n * The compatibility list is not hard-coded: it is read from this plugin's own\n * `package.json` through the host route, so the panel can never advertise a\n * release the build does not declare (and a host that cannot answer simply\n * shows no list).\n */\n\nimport type * as React from 'react'\nimport type { WslSelfDescription } from './api.ts'\n\n/** Props for the help panel. */\nexport interface WslHelpProps {\n /** Translate a `wslWorkspace` dictionary key. */\n t: (key: string, params?: Record<string, unknown>) => string\n /** Host-reported self-description, or null while it is unknown. */\n description: WslSelfDescription | null\n}\n\n/**\n * Split one dictionary entry into its bullet lines.\n * @param value - the multi-line dictionary string.\n * @returns the non-empty lines, trimmed.\n */\nfunction bullets(value: string): string[] {\n return value\n .split('\\n')\n .map(line => line.trim())\n .filter(line => line !== '')\n}\n\n/** One titled section of the panel. */\nfunction Section({ title, children }: { title: string; children: React.ReactNode }): React.ReactElement {\n return (\n <section className=\"dww-help-section\">\n <h3 className=\"dww-help-title\">{title}</h3>\n {children}\n </section>\n )\n}\n\n/**\n * Render the help panel shown behind the dialog's \"?\" button.\n * @param props - translate function plus the host's self-description.\n */\nexport function WslHelp({ t, description }: WslHelpProps): React.ReactElement {\n const releases = description?.releases ?? []\n const version = description === null\n ? t('help.compat.unknown')\n : `${t('help.compat.versionLabel')} v${description.version}`\n return (\n <div className=\"dww-help\" role=\"region\" aria-label={t('help.button')}>\n <p className=\"dww-help-greeting\">\n {t('help.greeting')}{' '}\n <a className=\"dww-help-link\" href=\"https://github.com/6Mikao9/dsh-wsl-workspace\" target=\"_blank\" rel=\"noreferrer\">\n {t('help.greeting.repo')}\n </a>\n </p>\n <Section title={t('help.compat.title')}>\n <div className=\"dww-help-meta\">{version}</div>\n {releases.length > 0\n ? (\n <div className=\"dww-help-chips\">\n {releases.map(entry => <span key={entry.id} className=\"dww-help-chip\">{entry.id}</span>)}\n </div>\n )\n : null}\n <ul className=\"dww-help-list\">\n {bullets(t('help.compat.body')).map(line => <li key={line}>{line}</li>)}\n </ul>\n </Section>\n <Section title={t('help.news.title')}>\n <ul className=\"dww-help-list\">\n {bullets(t('help.news.body')).map(line => <li key={line}>{line}</li>)}\n </ul>\n </Section>\n <Section title={t('help.usage.title')}>\n <ul className=\"dww-help-list\">\n {bullets(t('help.usage.body')).map(line => <li key={line}>{line}</li>)}\n </ul>\n </Section>\n <Section title={t('help.known.title')}>\n <ul className=\"dww-help-list dww-help-list--known\">\n {bullets(t('help.known.body')).map(line => <li key={line}>{line}</li>)}\n </ul>\n </Section>\n <div className=\"dww-help-footer\">\n <a className=\"dww-help-link\" href=\"https://www.npmjs.com/package/dsh-wsl-workspace\" target=\"_blank\" rel=\"noreferrer\">\n {t('help.footer.npm')}\n </a>\n <a className=\"dww-help-link\" href=\"https://github.com/6Mikao9/dsh-wsl-workspace\" target=\"_blank\" rel=\"noreferrer\">\n {t('help.footer.repo')}\n </a>\n </div>\n </div>\n )\n}\n","/**\n * Sidebar footer action that opens the \"Add WSL workspace\" dialog. In the\n * wide sidebar it renders a labeled row; in the 56px rail it collapses to a\n * 36px icon button (both honor the shell's `{ wide }` owner share).\n *\n * Registration omits the typed `locale:` seat (the `wslWorkspace` namespace is\n * not merged into `LocaleNamespaceMap`), so the injected face carries the\n * bound translate function instead.\n */\n\nimport type * as React from 'react'\nimport { useEffect, useRef, useState, type UIEventHandler } from 'react'\nimport { isAbsoluteLinuxPath, isValidWslUsername, normalizeLinuxPath } from '../shared/paths.ts'\nimport type { WslDirListing, WslPathCheck, WslSelfDescription } from './api.ts'\nimport { WslHelp } from './help.tsx'\n\n/** Result-level shape of a browse/check/list call we surface uniformly. */\ninterface ApiCall<T> {\n value: T\n}\n\n/** The inject face: plain data + callbacks the dialog drives. */\nexport interface AddWslWorkspaceInjected {\n /**\n * Confirm the deployment exposes a healthy `wsl` preset.\n * @returns undefined when healthy, else a Chinese/English message to show.\n */\n checkPreset(): Promise<string | undefined>\n /**\n * Read this build's version and its declared DSH compatibility matrix,\n * for the help panel. A host that cannot answer resolves to a fallback\n * description rather than failing the dialog.\n */\n describe(): Promise<WslSelfDescription>\n /** List the WSL distros installed on the host. */\n listDistros(): Promise<string[]>\n /** List one Linux directory level inside a distro. */\n listDir(distro: string, path: string): Promise<WslDirListing>\n /** Check a Linux path's existence/directory facts. */\n check(distro: string, path: string): Promise<WslPathCheck>\n /**\n * Register a WSL workspace and start a session in it. `/mnt/<drive>`\n * paths register under their Windows drive spelling (9P cannot serve\n * drvfs); ext4 paths register as `\\\\wsl.localhost\\<distro>\\...` UNC.\n * @param linuxPath - the absolute Linux workspace path.\n * @param username - optional Linux user for the session (empty = distro default).\n * @param distro - the WSL distribution the path belongs to.\n * @returns undefined on success, else a message to show.\n */\n createWorkspace(linuxPath: string, username: string, distro: string): Promise<string | undefined>\n /** Translate a `wslWorkspace` dictionary key (follows the DeepSeek Harness locale). */\n t: (key: string, params?: Record<string, unknown>) => string\n}\n\n/** Full component props: the owner share plus the injected face. */\nexport interface AddWslWorkspaceProps extends AddWslWorkspaceInjected {\n /** Whether the sidebar renders wide content (false = 56px rail). */\n wide: boolean\n}\n\n/**\n * Build the Linux child path one level below a parent, for the breadcrumb/\n * browse drill.\n * @param parent - the currently listed absolute path (`/` for root).\n * @param name - the child directory name.\n * @returns the child's absolute Linux path.\n */\nexport function dirChildPath(parent: string, name: string): string {\n return parent === '/' ? `/${name}` : `${parent}/${name}`\n}\n\n/** A tiny inline terminal glyph for the dialog's directory rows. */\nfunction WslGlyph({ size = 16 }: { size?: number }): React.ReactElement {\n return (\n <svg width={size} height={size} viewBox=\"0 0 24 24\" fill=\"none\" aria-hidden=\"true\">\n <rect x=\"2.5\" y=\"4.5\" width=\"19\" height=\"15\" rx=\"2.5\" stroke=\"currentColor\" strokeWidth=\"1.6\" />\n <path d=\"M6 9l3.2 2.6L6 14\" stroke=\"currentColor\" strokeWidth=\"1.6\" strokeLinecap=\"round\" strokeLinejoin=\"round\" />\n <path d=\"M12 14h5\" stroke=\"currentColor\" strokeWidth=\"1.6\" strokeLinecap=\"round\" />\n </svg>\n )\n}\n\n/**\n * The \"Add WSL workspace…\" footer action and its dialog.\n * @param props - owner share + injected face.\n */\nexport function AddWslWorkspace({ wide, t, describe, checkPreset, listDistros, listDir, check, createWorkspace }: AddWslWorkspaceProps): React.ReactElement | null {\n const [open, setOpen] = useState(false)\n const [opening, setOpening] = useState(false)\n const [distros, setDistros] = useState<string[]>([])\n const [distro, setDistro] = useState('')\n const [pathInput, setPathInput] = useState('/home/')\n const [username, setUsername] = useState('')\n const [listing, setListing] = useState<WslDirListing | null>(null)\n const [browsePath, setBrowsePath] = useState('/')\n const [browsing, setBrowsing] = useState(false)\n const [error, setError] = useState<string | null>(null)\n const [busy, setBusy] = useState(false)\n // The \"?\" panel: help text plus the compatibility list the host reports.\n const [helpOpen, setHelpOpen] = useState(false)\n const [selfDescription, setSelfDescription] = useState<WslSelfDescription | null>(null)\n // Monotone browse-request sequence: stale responses for a superseded browse are dropped.\n const browseSeq = useRef(0)\n\n const refreshBrowse = async (root: string, targetDistro: string): Promise<void> => {\n const seq = ++browseSeq.current\n setBrowsing(true)\n setBrowsePath(root)\n try {\n const value = await listDir(targetDistro, root)\n if (seq === browseSeq.current) setListing(value)\n } catch {\n // A failed browse (permission, missing dir) is non-fatal: keep old listing.\n if (seq === browseSeq.current) {\n setListing(null)\n setError((previous) => previous ?? t('error.loadDir'))\n }\n } finally {\n if (seq === browseSeq.current) setBrowsing(false)\n }\n }\n\n useEffect(() => {\n if (!open) return\n let cancelled = false\n setError(null)\n // Advisory data for the help panel: never fatal, never blocking the form.\n void describe().then(value => { if (!cancelled) setSelfDescription(value) }).catch(() => { if (!cancelled) setSelfDescription(null) })\n setOpening(true)\n void (async () => {\n let presetIssue: string | undefined\n try {\n presetIssue = await checkPreset()\n } catch {\n presetIssue = t('error.loadDistros')\n }\n let names: string[]\n try {\n names = await listDistros()\n } catch {\n if (cancelled) return\n setOpening(false)\n setError(t('error.loadDistros'))\n return\n }\n if (cancelled) return\n setDistros(names)\n const first = names[0] ?? ''\n setDistro(first)\n // The default browse root walks from `/`; the input defaults to `/home/`.\n setBrowsing(true)\n setOpening(false)\n if (presetIssue !== undefined) setError(presetIssue)\n if (first !== '') void refreshBrowse('/', first)\n })()\n return () => { cancelled = true }\n // eslint-disable-next-line react-hooks/exhaustive-deps -- run once per open against current t.\n }, [open])\n\n useEffect(() => {\n if (!open) return\n const onKey = (event: KeyboardEvent): void => {\n if (event.key === 'Escape' && !busy) setOpen(false)\n }\n window.addEventListener('keydown', onKey)\n return () => window.removeEventListener('keydown', onKey)\n }, [open, busy])\n\n if (!open) {\n // A W-letter action beside Settings at the sidebar foot; the title\n // carries the label in both wide and rail states.\n return (\n <button\n type=\"button\"\n className={wide ? 'dww-action dww-action--wide' : 'dww-action dww-action--rail'}\n title={t('action.title')}\n aria-label={t('action.title')}\n onClick={() => setOpen(true)}\n >\n <span className=\"dww-letter\" aria-hidden=\"true\">W</span>\n </button>\n )\n }\n\n const onDrill = (name: string): void => {\n const next = dirChildPath(listing?.path ?? browsePath, name)\n setPathInput(next)\n void refreshBrowse(next, distro)\n }\n\n const onUp = (): void => {\n const parent = listing?.parent ?? null\n if (parent === null) return\n setPathInput(parent)\n void refreshBrowse(parent, distro)\n }\n\n const onDistroChange = (value: string): void => {\n setDistro(value)\n void refreshBrowse(browsePath, value)\n }\n\n const onCheck = async (): Promise<void> => {\n const path = normalizeLinuxPath(pathInput)\n setError(null)\n if (!isAbsoluteLinuxPath(path) || path === '/') {\n setError(t('error.invalidPath'))\n return\n }\n let facts: WslPathCheck\n try {\n facts = await check(distro, path)\n } catch {\n setError(t('error.pathNotFound'))\n return\n }\n if (!facts.exists || !facts.isDirectory) {\n setError(t('error.pathNotFound'))\n return\n }\n void refreshBrowse(path, distro)\n }\n\n const onConfirm = async (): Promise<void> => {\n const path = normalizeLinuxPath(pathInput)\n setError(null)\n if (!isAbsoluteLinuxPath(path) || path === '/') {\n // A workspace at the distribution root would make every session start\n // at `/`; the check flow rejects it and confirm must agree.\n setError(t('error.invalidPath'))\n return\n }\n const user = username.trim()\n if (user !== '' && !isValidWslUsername(user)) {\n setError(t('error.invalidUsername'))\n return\n }\n setBusy(true)\n try {\n let facts: WslPathCheck\n try {\n facts = await check(distro, path)\n } catch {\n setError(t('error.pathNotFound'))\n return\n }\n if (!facts.exists || !facts.isDirectory) {\n setError(t('error.pathNotFound'))\n return\n }\n const failure = await createWorkspace(path, user, distro)\n if (failure !== undefined) {\n setError(failure)\n return\n }\n setOpen(false)\n } finally {\n setBusy(false)\n }\n }\n\n const children = (listing?.entries.filter(entry => entry.kind === 'directory') ?? []).map(entry => entry.name)\n const maskClick = (): void => { if (!busy) setOpen(false) }\n const listScroll: UIEventHandler<HTMLDivElement> = () => { /* scroll container handles overflow */ }\n\n return (\n <div className=\"dww-overlay\">\n <div className=\"dww-overlay-mask\" onClick={maskClick} />\n <div className=\"dww-card\" role=\"dialog\" aria-modal=\"true\" aria-label={t('dialog.title')}>\n <div className=\"dww-header\">\n <h2 className=\"dww-title\">{t('dialog.title')}</h2>\n <button\n type=\"button\"\n className=\"dww-help-btn\"\n title={t('help.button')}\n aria-label={t('help.button')}\n aria-pressed={helpOpen}\n onClick={() => setHelpOpen(value => !value)}\n >\n ?\n </button>\n <button type=\"button\" className=\"dww-close\" aria-label={t('dialog.cancel')} onClick={maskClick}>\n ✕\n </button>\n </div>\n <div className=\"dww-body\">\n {helpOpen ? (\n <WslHelp t={t} description={selfDescription} />\n ) : (<>\n {error !== null ? (\n <div className=\"dww-error\">\n {error}\n <button type=\"button\" className=\"dww-retry\" onClick={() => setError(null)}>{t('dialog.retry')}</button>\n </div>\n ) : null}\n <div className=\"dww-field\">\n <label className=\"dww-field-label\" htmlFor=\"dww-distro\">{t('dialog.distro')}</label>\n <select\n id=\"dww-distro\"\n className=\"dww-select\"\n value={distro}\n disabled={opening || busy}\n onChange={event => onDistroChange(event.target.value)}\n >\n {distros.length === 0\n ? <option value=\"\">{opening ? t('dialog.loading') : ''}</option>\n : distros.map(name => <option key={name} value={name}>{name}</option>)}\n </select>\n </div>\n <div className=\"dww-field\">\n <label className=\"dww-field-label\" htmlFor=\"dww-path\">{t('dialog.path')}</label>\n <div className=\"dww-input-row\">\n <input\n id=\"dww-path\"\n className=\"dww-input\"\n value={pathInput}\n placeholder={t('dialog.pathPlaceholder')}\n disabled={opening || busy}\n onChange={event => setPathInput(event.target.value)}\n />\n <button type=\"button\" className=\"dww-check-btn\" disabled={opening || busy} onClick={() => void onCheck()}>\n {t('dialog.check')}\n </button>\n </div>\n </div>\n <div className=\"dww-field\">\n <label className=\"dww-field-label\" htmlFor=\"dww-username\">{t('dialog.username')}</label>\n <input\n id=\"dww-username\"\n className=\"dww-input\"\n value={username}\n placeholder={t('dialog.usernamePlaceholder')}\n disabled={opening || busy}\n autoComplete=\"off\"\n spellCheck={false}\n onChange={event => setUsername(event.target.value)}\n />\n </div>\n <div className=\"dww-feedback\">\n <div className=\"dww-breadcrumb\">{browsePath}</div>\n <div className=\"dww-dirlist\" onScroll={listScroll}>\n {browsing ? <div className=\"dww-dir-empty\">{t('dialog.loading')}</div> : (\n listing?.parent !== null && listing !== null\n ? (\n <button type=\"button\" className=\"dww-dir-row dww-dir-row--up\" onClick={onUp}>\n <WslGlyph size={14} />\n <span>{t('dialog.upLevel')}</span>\n </button>\n )\n : null\n )}\n {!browsing && (children.length === 0)\n ? <div className=\"dww-dir-empty\">{t('dialog.browseEmpty')}</div>\n : children.map(name => (\n <button type=\"button\" key={name} className=\"dww-dir-row\" onClick={() => onDrill(name)}>\n <WslGlyph size={14} />\n <span>{name}</span>\n </button>\n ))}\n </div>\n </div>\n </>)}\n </div>\n <div className=\"dww-actions\">\n <button type=\"button\" className=\"dww-btn\" disabled={busy} onClick={maskClick}>{t('dialog.cancel')}</button>\n <button type=\"button\" className=\"dww-btn dww-btn--primary\" disabled={busy || opening} onClick={() => void onConfirm()}>\n {busy ? t('dialog.loading') : t('dialog.confirm')}\n </button>\n </div>\n </div>\n </div>\n )\n}\n\nexport type { ApiCall }\n","/**\n * Third-party stylesheet injection for the WSL workspace UI (the plugin\n * builds no CSS bundle, so styles are injected as one idempotent `<style>`).\n * Colors derive exclusively from the `--dsw-*` design tokens.\n */\n\nconst STYLE_TAG_DATA_ATTRIBUTE = 'data-plugin=\"dsh-wsl-workspace\"'\n\nconst STYLES = `\n/* Sidebar-foot icon action beside Settings (28px round in the wide sidebar,\n 36px round in the rail), matching the shell's icon-button language. */\n.dww-action {\n flex: none;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 28px;\n height: 28px;\n border: none;\n border-radius: 50%;\n padding: 0;\n background: transparent;\n cursor: pointer;\n color: var(--dsw-alias-label-secondary);\n transition:\n background-color 120ms var(--dsw-ease-in-out, ease-in-out),\n color 120ms var(--dsw-ease-in-out, ease-in-out);\n}\n.dww-action:hover:not(:disabled) {\n background: var(--dsw-alias-interactive-bg-hover);\n color: var(--dsw-alias-label-secondary);\n}\n.dww-action:active:not(:disabled) {\n background: var(--dsw-alias-interactive-bg-pressed, var(--dsw-alias-interactive-bg-hover));\n}\n.dww-action:focus-visible {\n outline: 2px solid var(--dsw-alias-state-business-primary);\n outline-offset: 1px;\n}\n.dww-action:disabled { cursor: default; opacity: 0.6; }\n.dww-action--rail {\n width: 36px;\n height: 36px;\n color: var(--dsw-alias-label-primary);\n}\n.dww-action svg { flex: none; }\n\n/* The W letter mark of the sidebar action (sized for wide/rail buttons). */\n.dww-letter {\n font-size: 14px;\n font-weight: 600;\n line-height: 1;\n letter-spacing: 0.02em;\n user-select: none;\n}\n.dww-action--rail .dww-letter { font-size: 17px; }\n\n/* Full-viewport overlay + centered card (mirrors the platform Mask/Dialog). */\n.dww-overlay {\n position: fixed;\n inset: 0;\n z-index: 1000;\n display: flex;\n align-items: center;\n justify-content: center;\n padding: 24px;\n}\n.dww-overlay-mask {\n position: absolute;\n inset: 0;\n background: var(--dsw-alias-bg-mask-1);\n backdrop-filter: var(--dsw-mask-blur);\n}\n.dww-card {\n position: relative;\n z-index: 1;\n box-sizing: border-box;\n display: flex;\n flex-direction: column;\n width: min(440px, 100%);\n max-height: min(640px, 90vh);\n padding: 0 0 20px;\n overflow: hidden;\n border: 1px solid var(--dsw-alias-border-inverted);\n border-radius: 16px;\n background: var(--dsw-alias-bg-layer-2);\n box-shadow: var(--dsw-shadow-lv3);\n font-family: var(--dsw-font-family);\n}\n.dww-header {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 8px;\n padding: 18px 20px 12px;\n}\n.dww-title {\n margin: 0;\n font-size: 16px;\n line-height: 24px;\n font-weight: 500;\n color: var(--dsw-alias-label-primary);\n}\n.dww-close {\n flex: none;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 28px;\n height: 28px;\n border: 0;\n border-radius: 8px;\n background: transparent;\n cursor: pointer;\n color: var(--dsw-alias-label-secondary);\n}\n.dww-close:hover { background: var(--dsw-alias-interactive-bg-hover); }\n.dww-body {\n display: flex;\n flex-direction: column;\n gap: 14px;\n min-width: 0;\n padding: 0 20px;\n overflow: auto;\n}\n.dww-field { display: flex; flex-direction: column; gap: 6px; min-width: 0; }\n.dww-field-label {\n font-size: 12px;\n line-height: 18px;\n color: var(--dsw-alias-label-secondary);\n}\n.dww-select {\n box-sizing: border-box;\n width: 100%;\n height: 36px;\n padding: 0 10px;\n border: 1px solid var(--dsw-alias-border-l2);\n border-radius: 8px;\n background: var(--dsw-alias-bg-layer-3);\n color: var(--dsw-alias-label-primary);\n font-size: 14px;\n}\n.dww-input-row { display: flex; gap: 8px; align-items: center; }\n.dww-input {\n box-sizing: border-box;\n flex: 1;\n height: 36px;\n min-width: 0;\n padding: 0 10px;\n border: 1px solid var(--dsw-alias-border-l2);\n border-radius: 8px;\n background: var(--dsw-alias-bg-layer-3);\n color: var(--dsw-alias-label-primary);\n font-size: 14px;\n}\n.dww-input:focus, .dww-select:focus {\n outline: none;\n border-color: var(--dsw-alias-state-business-primary);\n}\n.dww-check-btn {\n flex: none;\n height: 36px;\n padding: 0 12px;\n border: 1px solid var(--dsw-alias-border-l2);\n border-radius: 8px;\n background: transparent;\n color: var(--dsw-alias-label-primary);\n cursor: pointer;\n font-size: 12px;\n}\n.dww-check-btn:hover:not(:disabled) { background: var(--dsw-alias-interactive-bg-hover); }\n.dww-check-btn:disabled { cursor: default; }\n\n/* Directory browse list. */\n.dww-dirlist {\n display: flex;\n flex-direction: column;\n gap: 2px;\n box-sizing: border-box;\n min-height: 120px;\n max-height: 200px;\n padding: 4px;\n overflow: auto;\n border: 1px solid var(--dsw-alias-border-l2);\n border-radius: 8px;\n background: var(--dsw-alias-bg-layer-3);\n}\n.dww-breadcrumb {\n padding: 0 4px;\n font-size: 12px;\n line-height: 18px;\n color: var(--dsw-alias-label-tertiary);\n overflow: hidden;\n white-space: nowrap;\n text-overflow: ellipsis;\n}\n.dww-dir-row {\n display: flex;\n align-items: center;\n gap: 8px;\n height: 28px;\n padding: 0 8px;\n border: 0;\n border-radius: 6px;\n background: transparent;\n color: var(--dsw-alias-label-primary);\n cursor: pointer;\n font-size: 13px;\n text-align: left;\n}\n.dww-dir-row:hover:not(:disabled) { background: var(--dsw-alias-interactive-bg-hover); }\n.dww-dir-row:disabled { cursor: default; color: var(--dsw-alias-label-tertiary); }\n.dww-dir-row--up { color: var(--dsw-alias-label-secondary); }\n.dww-dir-row svg { flex: none; color: var(--dsw-alias-label-tertiary); }\n.dww-dir-empty {\n padding: 8px;\n font-size: 12px;\n line-height: 18px;\n color: var(--dsw-alias-label-tertiary);\n}\n\n/* Error strip. */\n.dww-error {\n box-sizing: border-box;\n width: 100%;\n padding: 8px 10px;\n border: 1px solid var(--dsw-alias-state-error-primary);\n border-radius: 8px;\n color: var(--dsw-alias-state-error-primary);\n font-size: 12px;\n line-height: 18px;\n}\n.dww-retry {\n margin-left: 6px;\n border: 0;\n background: transparent;\n color: var(--dsw-alias-state-business-primary);\n cursor: pointer;\n font-size: 12px;\n text-decoration: underline;\n}\n\n/* Dialog footer actions. */\n.dww-actions {\n display: flex;\n align-items: center;\n justify-content: flex-end;\n gap: 8px;\n padding: 14px 20px 0;\n}\n.dww-btn {\n height: 36px;\n padding: 0 14px;\n border: 1px solid var(--dsw-alias-border-l2);\n border-radius: 8px;\n background: transparent;\n color: var(--dsw-alias-label-primary);\n cursor: pointer;\n font-size: 14px;\n}\n.dww-btn:hover:not(:disabled) { background: var(--dsw-alias-interactive-bg-hover); }\n.dww-btn--primary {\n border-color: transparent;\n background: var(--dsw-alias-button-primary-fill);\n color: var(--dsw-alias-label-primary-foreground);\n}\n.dww-btn--primary:hover:not(:disabled) { background: var(--dsw-alias-button-primary-hover); }\n.dww-btn:disabled { cursor: default; opacity: 0.6; }\n/* Help panel behind the dialog's \"?\" button. */\n.dww-help-btn {\n flex: none;\n margin-left: auto;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 26px;\n height: 26px;\n border: 1px solid var(--dsw-alias-border-l2);\n border-radius: 50%;\n background: transparent;\n color: var(--dsw-alias-label-secondary);\n cursor: pointer;\n font-size: 13px;\n line-height: 1;\n}\n.dww-help-btn:hover { background: var(--dsw-alias-interactive-bg-hover); }\n.dww-help-btn[aria-pressed='true'] {\n background: var(--dsw-alias-interactive-bg-hover);\n color: var(--dsw-alias-label-primary);\n}\n.dww-help {\n display: flex;\n flex-direction: column;\n gap: 14px;\n max-height: 52vh;\n overflow-y: auto;\n padding-right: 4px;\n}\n.dww-help-section { display: flex; flex-direction: column; gap: 6px; }\n.dww-help-greeting {\n margin: 0;\n padding: 8px 10px;\n border-radius: 8px;\n background: var(--dsw-alias-interactive-bg-hover);\n font-size: 13px;\n line-height: 1.5;\n color: var(--dsw-alias-label-primary);\n}\n.dww-help-title {\n margin: 0;\n font-size: 13px;\n font-weight: 600;\n color: var(--dsw-alias-label-primary);\n}\n.dww-help-meta { font-size: 12px; color: var(--dsw-alias-label-tertiary); }\n.dww-help-chips { display: flex; flex-wrap: wrap; gap: 6px; }\n.dww-help-chip {\n padding: 2px 8px;\n border: 1px solid var(--dsw-alias-border-l2);\n border-radius: 999px;\n font-size: 12px;\n color: var(--dsw-alias-label-secondary);\n}\n.dww-help-list {\n margin: 0;\n padding-left: 18px;\n display: flex;\n flex-direction: column;\n gap: 6px;\n font-size: 13px;\n line-height: 1.6;\n color: var(--dsw-alias-label-secondary);\n}\n.dww-help-list--known { color: var(--dsw-alias-label-primary); }\n.dww-help-footer {\n display: flex;\n gap: 14px;\n padding-top: 8px;\n border-top: 1px solid var(--dsw-alias-border-l2);\n}\n.dww-help-link { font-size: 12px; color: var(--dsw-alias-label-tertiary); text-decoration: underline; }\n.dww-help-link:hover { color: var(--dsw-alias-label-primary); }\n`\n\n/**\n * Idempotently inject the plugin stylesheet. No-op when a tag with the\n * plugin's data attribute already exists.\n */\nexport function ensureStyles(): void {\n if (typeof document === 'undefined') return\n if (document.querySelector(`style[${STYLE_TAG_DATA_ATTRIBUTE}]`) !== null) return\n const style = document.createElement('style')\n style.setAttribute('data-plugin', 'dsh-wsl-workspace')\n style.textContent = STYLES\n document.head.appendChild(style)\n}\n","/**\n * Bilingual dictionaries for the `wslWorkspace` locale namespace. Product copy\n * is Chinese; English is the parallel export for the standalone bundle.\n */\n\n/**\n * The `wslWorkspace` translations (Chinese, the primary product copy).\n */\nexport const zh: Record<string, string> = {\n 'action.add': 'WSL 工作区',\n 'action.title': '添加 WSL 工作区…',\n\n 'dialog.title': '添加 WSL 工作区',\n 'dialog.distro': '发行版',\n 'dialog.path': '路径',\n 'dialog.pathPlaceholder': '/home/',\n 'dialog.username': '用户名',\n 'dialog.usernamePlaceholder': '留空则使用发行版默认用户',\n 'dialog.loading': '正在加载…',\n 'dialog.browseEmpty': '此目录没有子文件夹',\n 'dialog.upLevel': '..(返回上级)',\n 'dialog.browse': '浏览',\n 'dialog.check': '检查',\n 'dialog.confirm': '创建并打开',\n 'dialog.cancel': '取消',\n 'dialog.retry': '重试',\n\n 'error.loadDistros': '无法获取 WSL 发行版列表,请确认已安装 WSL 且插件宿主端可用',\n 'error.rateLimited': '操作过于频繁,请稍后重试',\n 'error.loadDir': '无法浏览该目录',\n 'error.presetMissing': '未找到健康的 wsl preset,请确认插件宿主端已安装并配置该 preset',\n 'error.invalidPath': '请输入以 / 开头的 Linux 绝对路径',\n 'error.invalidUsername': '用户名无效:需以字母或下划线开头,仅含字母、数字、_、.、-',\n 'error.pathNotFound': '该路径不存在或是文件,请选择一个文件夹',\n 'error.createFailed': '创建工作区失败',\n 'help.button': '插件说明',\n 'help.greeting': '当你看到这句话的时候,说明你的插件已经Cia进来llo~(∠・ω< )⌒★,star一下吗?',\n 'help.greeting.repo': 'github.com/6Mikao9/dsh-wsl-workspace',\n\n 'help.compat.title': '兼容性',\n 'help.compat.versionLabel': '插件版本',\n 'help.compat.unknown': '未能读取版本与兼容声明(宿主端没有响应)',\n 'help.compat.body': '上方是这份构建声明兼容的 DSH 版本,每一条都在隔离实例上实测过(独立 DSH_HOME、依赖固定到该版本、跑满十项门禁)。\\n插件在运行时自动识别 DSH 版本并选用对应的 API;两边都不支持时会明确报错,而不是留下一个空工作区。\\n版本不在列表里通常仍然可用,但未经验证。',\n 'help.news.title': '本次更新(0.7.2)',\n 'help.news.body': '修掉了 WSL 技能目录在请求路径上重走的问题(issue #25):宿主会在请求期间重建技能目录并等待各 provider 的 list(),而插件只把答案保留 10 秒,于是每次重新收集都得重走整棵工作区,工作区一大就正好卡在走查的 4096 目录预算上。\\n9P 共享上单次 readdir 实测 3~16 毫秒,所以命中预算的工作区那次走查要 20 秒;现在已发布的目录直接照原样返回,只有插件自己的探针确认变化时才丢弃缓存,重复查找降到 1~3 毫秒、且完全不碰文件系统。\\n走查本身也变便宜:同一层并发探测(有上限)、按 frontier 顺序发布以保证目录顺序确定,且只在目录清单里出现 .dsh / .agents 时才探测对应 skills。同机实测命中预算的走查从 20.4 秒降到 4.8 秒。\\n修掉了 npm 安装失败:npm 会自动安装缺失的同伴依赖,而 @deepseek-ai/dsh-tool-fs-search 自己又依赖并未公开发布的 @deepseek-ai/dsh-retention,于是 npm install dsh-wsl-workspace 直接 E404 失败(dsh plugin 走 pnpm、只警告,所以一直没暴露)。\\n十个宿主同伴依赖现在都标为 optional:包仍声明宿主必须提供什么,但 npm 不再尝试下载它们。\\nWSL 世界现在在会话能察觉到的每一处都与宿主一致:Linux 符号链接、会话的访问模式、发行版内的 grep / glob、实时技能目录、有状态的 shell,以及可跟踪的后台任务。\\n新增 bash_background:持久 shell 的 schema 只有 command,没有生产者时 job_list 永远说\"没有后台任务\",而传给 bash 的 run_in_background 会被静默忽略;现在后台任务可跟踪,job_output 增量读取、job_kill 取消都照常工作。\\nbash 的工具描述写明两件宿主没说的事:cd / export 跨调用保留,命令以 & 结尾会把整条命令后台化(请写成单独一行的 ( 长任务 > log 2>&1 ) &)。0.1.0-rc.7 的宿主缺少 Windows 进程检查器、持久 shell 起不来,插件会在启动时探测并回退到一次性 bash,不再让每次调用都报错。',\n 'help.usage.title': '用法与特性',\n 'help.usage.body': '点击侧边栏底部的 W 按钮 → 选发行版 → 输入或浏览 Linux 路径 → 检查 → 创建并打开。\\n创建出的会话里,bash 与文件工具都落在该发行版内,模型看到的路径全是 Linux 路径;Windows 盘可从会话内通过 /mnt/<盘符> 访问。\\n四种模式(标准 / PTC / 极简 / 创造)各有 WSL 变体,在模式选择器里直接选即可,名字形如 WSL · Standard mode(标准模式)。\\n用户名可选,等价于 wsl.exe -u <用户名>,只改变 bash 与 persistent-bash 的运行身份;文件工具走 Windows 侧共享,不受它影响。\\n技能目录从会话 cwd 最近的 .git 祖先开始向下扫描 .dsh/skills 与 .agents/skills(含嵌套项目),上限 4 层目录 / 64 个技能目录 / 4096 个已访问目录,并按扫描根缓存 10 秒。9P 解不开的符号链接交给发行版 readlink 解析出真实路径(每次查找最多 32 条)后继续扫描,链接进来的项目与它下面的嵌套项目都能扫到。\\n目录不再冻结:对 UNC 关闭文件监视之后,插件每 3 秒重查一次已发布的技能目录、并比对每个技能文件的修改时间与大小,所以新增、删除与改写都会在下一个回合生效;完整重新发现每 30 秒一次,用于找到此前不存在的技能目录(技能正文始终实时读取)。\\n文件搜索由发行版内的 grep / glob 提供,源预设没有这两个工具的模式(极简)不会多出来:grep 用 GNU grep -E(\\\\d、\\\\w、(?i) 可用,环视与反向引用不支持),跳过隐藏项与 node_modules,不读 .gitignore;glob 用 GNU find 列文件、按 gitignore 风格匹配(* 不跨目录、** 跨、{a,b}、前导 ! 取反),按修改时间从旧到新排序。\\n文件工具在链接处按真实路径工作,并按真实路径判策略:链接指向工作区外就等于工作区外。\\nbash 由 PTY 承载的持久 shell 提供:登录环境、起始目录就是会话工作区,cd / export / venv / 后台任务跨调用保留(它取代了原先一次性 bash)。\\n需要可跟踪的后台任务时用 bash_background:它立刻返回 job id,job_list / job_output(增量读取)/ job_kill 都作用于它;bash 本身没有 run_in_background 参数,传了会被忽略。job id 只在本次 DSH 进程内唯一,重启后会重新编号,引用前先用 job_list 确认。\\nbash 与文件工具的 shell 都在发行版内运行、不受 DSH 文件策略约束;文件工具(read/write/edit)受策略约束,工作区内修改模式下只能写工作区内。',\n 'help.known.title': '已知问题',\n 'help.known.body': 'grep 用的是发行版自带的 GNU grep:方言是 POSIX ERE(环视与反向引用不支持),且不读 .gitignore,被 git 忽略的文件照样会被搜到;只有隐藏项、node_modules 与版本库目录会被跳过。发行版没有 GNU grep(如 Alpine 的 busybox)时会明确报错,而不是给出错位的结果。\\nglob 的\"按修改时间排序\"依赖 GNU find -printf,busybox 会退化成路径排序;含 / 的 include 在插件进程里过滤,因此那种调用会先扫描全部文件再筛。\\n技能目录刷新仍是轮询:已发布目录内的增删改约 3 秒生效,而一个新项目里第一次出现的技能目录要等下一次完整重新发现(最多 30 秒)。\\n0.1.0-rc.7 的宿主没有 Windows 进程检查器,PTY 持久 shell 无法启动(宿主自己也这样),插件回退到一次性 bash:能正常用,但 cd / export 不跨调用保留。\\n极简模式本身不挂 job_* 工具,所以那个模式里也没有 bash_background(与宿主的极简模式一致)。',\n 'help.footer.npm': 'npm 包',\n 'help.footer.repo': 'GitHub 仓库',\n}\n\n/**\n * The `wslWorkspace` translations (English).\n */\nexport const en: Record<string, string> = {\n 'action.add': 'WSL Workspace',\n 'action.title': 'Add WSL workspace…',\n\n 'dialog.title': 'Add WSL workspace',\n 'dialog.distro': 'Distro',\n 'dialog.path': 'Path',\n 'dialog.pathPlaceholder': '/home/',\n 'dialog.username': 'Username',\n 'dialog.usernamePlaceholder': 'Leave empty to use the distro default user',\n 'dialog.loading': 'Loading…',\n 'dialog.browseEmpty': 'No subdirectories here',\n 'dialog.upLevel': '.. (up)',\n 'dialog.browse': 'Browse',\n 'dialog.check': 'Check',\n 'dialog.confirm': 'Create & open',\n 'dialog.cancel': 'Cancel',\n 'dialog.retry': 'Retry',\n\n 'error.loadDistros': 'Could not list WSL distros; confirm WSL is installed and the plugin host side is reachable',\n 'error.rateLimited': 'Too many attempts; retry in a moment',\n 'error.loadDir': 'Could not browse this directory',\n 'error.presetMissing': 'No healthy \"wsl\" preset found; confirm the plugin host side installed and configured it',\n 'error.invalidPath': 'Enter an absolute Linux path starting with /',\n 'error.invalidUsername': 'Invalid username: start with a letter or underscore; only letters, digits, _ . -',\n 'error.pathNotFound': 'The path does not exist or is a file; choose a folder',\n 'error.createFailed': 'Failed to create the workspace',\n 'help.button': 'About this plugin',\n 'help.greeting': 'If you can read this, the plugin has already Cia~llo\\'d its way in~(∠・ω< )⌒★ Care to star the repo?',\n 'help.greeting.repo': 'github.com/6Mikao9/dsh-wsl-workspace',\n\n 'help.compat.title': 'Compatibility',\n 'help.compat.versionLabel': 'Plugin version',\n 'help.compat.unknown': 'Version and compatibility declaration unavailable (the host side did not answer)',\n '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 and picks the matching API; a release exposing neither fails loudly instead of leaving an empty workspace.\\nA release outside the list usually still works, but is unverified.',\n 'help.news.title': \"What's new in 0.7.2\",\n 'help.news.body': 'Fixed the WSL skill catalog being re-walked on the request path (issue #25): the host rebuilds the catalog during a request and awaits each provider\\'s list(), while this plugin kept its answer for only 10 s - so every re-collection re-walked the whole workspace.\\nA readdir over the WSL 9P share costs 3-16 ms and the walk\\'s budget is 4096 directories, so a large workspace sat 20 s on it. The published catalog is now served as-is until the provider\\'s own watcher sees a change, which takes a repeat lookup to 1-3 ms with no filesystem traffic at all.\\nThe walk is cheaper too: one BFS layer is probed concurrently and published in frontier order, and a directory\\'s .dsh/skills is probed only when its own listing showed .dsh. A budget-sized walk measured 20.4 s before and 4.8 s after.\\nFixed a failed npm install: npm auto-installs missing peers, and @deepseek-ai/dsh-tool-fs-search peers on @deepseek-ai/dsh-retention, which is not published - so npm install dsh-wsl-workspace died with E404.\\nThe ten host peer dependencies are now marked optional, so npm leaves them to the host (dsh plugin add uses pnpm and only warned, which is why this stayed hidden).\\nThe WSL world now matches the host everywhere a session can tell the difference: Linux symlinks, the session\\'s access mode, in-distribution grep / glob, a live skill catalog, a stateful shell, and tracked background jobs.\\nNew: bash_background. The persistent shell takes `command` only, so without a producer job_list always answered \"no background jobs\" and a run_in_background argument passed to bash was silently ignored; background work is now tracked, with incremental job_output reads and job_kill.\\nThe bash tool description now states two things the host does not: cd / exports carry into the next call, and a command ending in & backgrounds the whole command - background the subshell instead, as ( long-job > log 2>&1 ) &.\\n0.1.0-rc.7 lacks the Windows process inspector the persistent shell needs, so the plugin probes for it and falls back to a one-shot bash instead of failing every call.',\n 'help.usage.title': 'Usage and features',\n '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 bash and persistent-bash; 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.\\nA link the share cannot follow is resolved through the distribution (wsl.exe readlink, at most 32 per lookup) and the scan continues at the real path, so a linked-in project and its own nested projects are found too.\\nThe catalog is not frozen: published skills directories are re-checked every 3 seconds (skill file mtime + size), so an add, remove or edit appears on the next turn; a 30-second walk finds a skills directory that did not exist before.\\nFile search comes from grep / glob inside the distribution, and a mode that mounts no search suite (Minimal) gains none. grep is GNU grep -E (no lookaround or backreferences), skips hidden entries and node_modules, and does not read .gitignore; glob matches gitignore-style patterns here, oldest first.\\nThe file tools work at the resolved real path of a link, and the policy is judged there too: a link out of the workspace is an outside write.\\n`bash` is a PTY-backed stateful shell: login environment, starting directory the session workspace, and cd / exports / background jobs survive between calls.\\nFor a tracked background job use bash_background: it returns a job id immediately, and job_list / job_output (incremental) / job_kill act on it.\\n`bash` itself has no run_in_background parameter and ignores one; job ids are unique within this DSH process, so confirm with job_list before acting on one.\\nBoth it and the file tools\\' shell run inside the distribution, outside the DSH file policy; read/write/edit are inside it, and workspace-write only writes inside the workspace.',\n 'help.known.title': 'Known issues',\n 'help.known.body': 'grep is the distribution\\'s GNU grep: POSIX ERE (no lookaround or backreferences), and it does not read .gitignore, so git-ignored files are searched too; only hidden entries, node_modules and VCS directories are skipped.\\nglob\\'s modification-time order needs GNU find -printf (busybox falls back to path order), and an include containing \"/\" is filtered in this process, so that call scans every file first.\\nThe catalog refresh is still a poll: an add, remove or edit inside a published skills directory lands within about 3 seconds, while a new project\\'s first skills directory waits for the next full re-discovery (up to 30 seconds).\\n0.1.0-rc.7 has no Windows process inspector, so its PTY persistent shell cannot start (the host has the same gap) and the plugin falls back to a one-shot bash: it works, but cd / exports do not survive.\\nMinimal mode mounts no job_* tools, so it gets no bash_background either - the same as the host\\'s own Minimal mode.',\n 'help.footer.npm': 'npm package',\n 'help.footer.repo': 'GitHub repository',\n}\n","/**\n * Browser half of dsh-wsl-workspace. Registers the \"Add WSL workspace…\"\n * action beside Settings at the sidebar foot (the official\n * `sidebar.footer.action` slot), and keeps every blank session whose\n * workspace is a WSL UNC path composed from the WSL VARIANT of the mode it\n * currently runs (`standard` → `wsl-standard`, PTC → `wsl-code`, …) — so the\n * WSL execution world composes with any mode instead of being a mode itself.\n *\n * The binding is a watching effect rather than a one-shot dialog action so\n * EVERY creation path (this dialog, the workspace row's New Session, the\n * hero picker) converges on the WSL-backed composition automatically.\n */\n\n// Type-only: pulls the locale plugin's Context merge (ctx.locale) and the\n// ui-sidebar SlotMap merge (the 'sidebar.footer.action' entry) into this\n// program. Version-dependent services (`remote`, `connection.api`,\n// `uiWorkspace`) are read dynamically via ctx.get() so both DSH v0.1.1-rc.2\n// and v0.1.2-rc.1+ can load this plugin.\nimport type {} from '@deepseek-ai/dsh-client-locale/client'\nimport type {} from '@deepseek-ai/dsh-client-ui-sidebar/client'\nimport type { ClientContext } from '@deepseek-ai/dsh-client-runtime/client'\nimport { check as checkApi, describe as describeApi, listDir as listDirApi, listDistros as listDistrosApi, listWorkspaces as listWorkspacesApi, registerWindows as registerWindowsApi, setWorkspaceUser as setWorkspaceUserApi } from './api.ts'\nimport { AddWslWorkspace, type AddWslWorkspaceInjected } from './AddWslWorkspace.tsx'\nimport { ensureStyles } from './styles.ts'\nimport { zh, en } from './locales.ts'\nimport { canonicalWindowsPath, isWslUnc, joinUnc, mntToWindowsPath } from '../shared/paths.ts'\n\n/** Required services (cordis fiber inject). */\nexport const inject = ['slots', 'locale', 'sessions', 'workspaces']\n\n/** The legacy standalone WSL preset id (folded into the mode variants). */\nconst LEGACY_WSL_PRESET_ID = 'wsl'\n\n/**\n * Minimal sessions-service face. The renderer-host ctx merge types\n * `ctx.sessions` as its own SessionStore; the service the runtime actually\n * registers under that key satisfies this narrower contract, so the cast is\n * the documented boundary for a third-party plugin.\n */\ninterface WslSessionsFace {\n list: {\n getSnapshot(): {\n ids: string[]\n byId: Record<string, {\n blank: boolean\n cwd?: string\n /** v0.1.1-rc.2 direct field */\n agentPreset?: string\n /** v0.1.2-rc.1+ projection value */\n projectionValues?: Readonly<{ agentPreset?: string | null }>\n }>\n }\n subscribe(fn: () => void): () => void\n }\n /** v0.1.1-rc.2 only — absent in v0.1.2-rc.1+ (projection auto-syncs). */\n noteAgentPreset?(sessionId: string, agentPreset: string): void\n}\n\n/** Minimal workspaces-service face (create only; startSession moved out in v0.1.2-rc.1+). */\ninterface WslWorkspacesFace {\n create(input: { path: string }): Promise<{ workspaceId: string }>\n /** v0.1.1-rc.2 only — removed in v0.1.2-rc.1+. */\n startSession?(workspaceId?: string): void\n}\n\n/** v0.1.2-rc.1+ only — replaces `workspaces.startSession`. */\ninterface WslUiWorkspaceFace {\n startSession(workspaceId?: string): void\n}\n\n/** v0.1.2-rc.1+ agentPresets 命名空间服务(经 ctx.get('remote.agentPresets') 动态取,免 inject、无 associate 陷阱)。 */\ninterface WslAgentPresetsNamespace {\n list(): Promise<{ ok: boolean; value?: { presets: { id: string; broken?: string; isDefault?: boolean }[] }; error?: { message: string } }>\n select(sessionId: string, presetId: string): Promise<{ ok: boolean }>\n}\n\n/** 旧版 connection 服务最小接口(v0.1.1-rc.2 及更早),api 属性承载远程调用。 */\ninterface WslLegacyConnection {\n api?: {\n agentPresets: {\n list(input: Record<string, never>): Promise<{ result: { ok: boolean; value?: { presets: { id: string; broken?: string; isDefault?: boolean }[] }; error?: { message: string } } }>\n select(input: { sessionId: string; agentPreset: string }): Promise<{ result: { ok: boolean } }>\n }\n }\n}\n/**\n * Mount the sidebar action and the auto-binding effect.\n * @param ctx - the browser plugin context.\n */\nexport function apply(ctx: ClientContext): void {\n const workspaces = ctx.get('workspaces') as unknown as WslWorkspacesFace\n const sessions = ctx.get('sessions') as unknown as WslSessionsFace\n // Version-dependent services are resolved ON USE - never through `inject`\n // (a release without the service would refuse to mount the plugin at all)\n // and never once at apply time. This plugin applies before the UI domain\n // that publishes `uiWorkspace` registers its service, so a one-shot read\n // caches `undefined` for the whole page life: on v0.1.2-rc.1+ that silently\n // disabled session creation (see resolveSessionStarter).\n const legacyApi = (): WslLegacyConnection['api'] =>\n (ctx.get('connection') as unknown as WslLegacyConnection | undefined)?.api\n const remoteAgentPresets = (): WslAgentPresetsNamespace | undefined =>\n ctx.get('remote.agentPresets') as unknown as WslAgentPresetsNamespace | undefined\n const uiWorkspaceService = (): WslUiWorkspaceFace | undefined =>\n ctx.get('uiWorkspace') as unknown as WslUiWorkspaceFace | undefined\n const hasNoteAgentPreset = typeof sessions.noteAgentPreset === 'function'\n\n /** Unified agent-preset list: new `remote.agentPresets` namespace (v0.1.2-rc.1+) or legacy `connection.api` (v0.1.1-rc.2). */\n const listAgentPresets = async (): Promise<{ ok: boolean; presets: { id: string; broken?: string; isDefault?: boolean }[]; error?: string }> => {\n const agentPresets = remoteAgentPresets()\n if (agentPresets !== undefined) {\n const r = await agentPresets.list()\n if (!r.ok) return { ok: false, presets: [], error: r.error?.message ?? 'list failed' }\n return { ok: true, presets: r.value?.presets ?? [] }\n }\n const api = legacyApi()\n if (api !== undefined) {\n const r = await api.agentPresets.list({})\n if (!r.result.ok) return { ok: false, presets: [], error: r.result.error?.message ?? 'list failed' }\n return { ok: true, presets: r.result.value?.presets ?? [] }\n }\n return { ok: false, presets: [], error: 'no remote api available' }\n }\n\n /** Unified agent-preset select: new `agentPresets.select(id, preset)` or legacy `connection.api.select({...})`. */\n const selectAgentPreset = async (sessionId: string, presetId: string): Promise<{ ok: boolean }> => {\n const agentPresets = remoteAgentPresets()\n if (agentPresets !== undefined) {\n return agentPresets.select(sessionId, presetId)\n }\n const api = legacyApi()\n if (api !== undefined) {\n const r = await api.agentPresets.select({ sessionId, agentPreset: presetId })\n return { ok: r.result.ok }\n }\n return { ok: false }\n }\n\n /**\n * Resolve how this release opens a session for a workspace - v0.1.2-rc.1+\n * exposes `uiWorkspace.startSession`, v0.1.1-rc.2 keeps it on `workspaces`.\n *\n * Resolved BEFORE the workspace is written. A release that offers neither\n * cannot open a session, and a silent fall-through would leave the workspace\n * behind with an empty `sessionIds` while the dialog still reports success;\n * failing here names the missing capability instead.\n * @returns the starter for the service this release actually exposes.\n * @throws Error naming both candidates when neither service is available.\n */\n const resolveSessionStarter = (): (workspaceId: string) => void | Promise<void> => {\n const ui = uiWorkspaceService()\n if (ui !== undefined) return (workspaceId) => { ui.startSession(workspaceId) }\n const legacyStart = workspaces.startSession\n if (typeof legacyStart === 'function') {\n return (workspaceId) => { Reflect.apply(legacyStart, workspaces, [workspaceId]) }\n }\n throw new Error(\n 'workspace session API unavailable: this DSH release exposes neither '\n + 'uiWorkspace.startSession nor workspaces.startSession',\n )\n }\n\n /** Read agent preset — v0.1.2-rc.1+ uses projectionValues; v0.1.1-rc.2 uses direct field. */\n const getAgentPreset = (summary: { agentPreset?: string; projectionValues?: { agentPreset?: string | null } }): string | undefined => {\n if (summary.projectionValues?.agentPreset !== undefined) {\n const v = summary.projectionValues.agentPreset\n return v === null ? undefined : v\n }\n return summary.agentPreset\n }\n\n /** Note preset change — v0.1.1-rc.2 calls noteAgentPreset; v0.1.2-rc.1+ is auto-synced via projection. */\n const noteAgentPresetCompat = (sessionId: string, presetId: string): void => {\n if (hasNoteAgentPreset && sessions.noteAgentPreset) {\n sessions.noteAgentPreset(sessionId, presetId)\n }\n }\n\n ensureStyles()\n\n ctx.effect(\n () => ctx.locale.register('wslWorkspace' as never, { zh, en }),\n 'dsh-wsl-workspace: locale dictionaries',\n )\n\n // The injected translate function reads the live DeepSeek Harness locale,\n // so the dialog copy follows the app language setting automatically.\n const t = ctx.locale.bind('wslWorkspace' as never) as unknown as (key: string, params?: Record<string, unknown>) => string\n\n // Canonical Windows drive keys of every registered `/mnt/<drive>` workspace\n // (refreshed from the host store; see refreshRoster). A blank session whose\n // cwd is one of these binds to the WSL variant like a UNC-cwd session.\n let wslWindowsPaths = new Set<string>()\n\n const injected = (): AddWslWorkspaceInjected => ({\n t,\n checkPreset: async (): Promise<string | undefined> => {\n let roster\n try {\n roster = await listAgentPresets()\n } catch (error) {\n return error instanceof Error ? error.message : String(error)\n }\n if (!roster.ok) return roster.error\n const healthy = roster.presets.find((entry: { id: string; broken?: string }) =>\n entry.id.startsWith('wsl-') && entry.broken === undefined)\n if (healthy === undefined) return t('error.presetMissing')\n return undefined\n },\n listDistros: () => listDistrosApi(),\n // Advisory data for the help panel: a host that cannot answer reports an\n // unavailable description instead of breaking the dialog.\n describe: () => describeApi(),\n listDir: (distro, path) => listDirApi(distro, path),\n check: (distro, path) => checkApi(distro, path),\n createWorkspace: async (linuxPath, username, distro): Promise<string | undefined> => {\n try {\n // Before any write: without a session starter the workspace below\n // would be created and never opened.\n const startSession = resolveSessionStarter()\n const winPath = mntToWindowsPath(linuxPath)\n if (winPath !== null) {\n // `/mnt/<drive>` workspace: the workspace registry realpath/stats\n // the path and 9P cannot serve drvfs mounts, so register under the\n // drive spelling and store distro/username for the session env.\n // The browser binding below recognizes the drive cwd as WSL.\n const view = await workspaces.create({ path: winPath })\n await registerWindowsApi(linuxPath, distro, username)\n const canonical = canonicalWindowsPath(winPath)\n if (canonical !== null) wslWindowsPaths = new Set(wslWindowsPaths).add(canonical)\n await startSession(view.workspaceId)\n return undefined\n }\n const uncPath = joinUnc(distro, linuxPath)\n const view = await workspaces.create({ path: uncPath })\n await setWorkspaceUserApi(uncPath, username)\n await startSession(view.workspaceId)\n return undefined\n } catch (error) {\n return error instanceof Error ? error.message : String(error)\n }\n },\n })\n\n ctx.effect(\n () => ctx.slots.inject(\n 'sidebar.footer.action',\n () => ctx.slots.register(\n { name: 'sidebar.footer.action', id: 'wsl-workspace', inject: injected },\n AddWslWorkspace,\n ),\n ),\n 'dsh-wsl-workspace: sidebar footer action',\n )\n\n // Mode-variant binding: a blank session whose workspace is a WSL UNC path\n // is recomposed to the WSL variant of the mode it currently runs — plain\n // 标准 becomes `wsl-standard`, PTC becomes `wsl-code`, and so on — so the\n // WSL execution world composes with ANY mode instead of replacing it. The\n // host refuses non-blank sessions (agent-preset-locked), so the swap is\n // attempted at most a few times per session.\n ctx.effect(() => {\n const inFlight = new Set<string>()\n const attempts = new Map<string, number>()\n const MAX_ATTEMPTS = 3\n // Healthy `wsl-<mode>` variant ids plus the roster's default preset id\n // (what a session with no explicit choice gets). Refreshed periodically\n // so variants generated after this page loaded are picked up.\n let variants = new Set<string>()\n let defaultPreset: string | undefined\n const refreshRoster = (): void => {\n void listAgentPresets().then((result: {\n ok: boolean; presets: { id: string; broken?: string; isDefault?: boolean }[]\n }) => {\n if (!result.ok) return\n variants = new Set(result.presets\n .filter((entry: { id: string; broken?: string }) =>\n entry.broken === undefined && entry.id.startsWith('wsl-'))\n .map((entry: { id: string }) => entry.id))\n defaultPreset = result.presets.find(\n (entry: { id: string; isDefault?: boolean }) => entry.isDefault === true,\n )?.id\n // The roster is an INPUT to binding. It can land after the first pass\n // (and after apply()), and nothing else would re-run that pass: the\n // session store only emits when a session changes, so a blank session\n // that was already there would stay unbound until the user acted.\n maybeBind()\n }).catch(() => {\n // A failed roster read leaves the previous mapping; sessions stay on\n // their current composition until the next refresh.\n })\n }\n refreshRoster()\n const refreshWorkspaces = (): void => {\n void listWorkspacesApi().then((keys: string[]) => {\n const next = new Set<string>()\n for (const key of keys) {\n const canonical = canonicalWindowsPath(key)\n if (canonical !== null) next.add(canonical)\n }\n wslWindowsPaths = next\n // Same late-input rule as the roster: the `/mnt/<drive>` key set\n // decides binding for drive-cwd sessions.\n maybeBind()\n }).catch(() => {\n // A failed store read leaves the previous set; sessions stay on\n // their current composition until the next refresh.\n })\n }\n refreshWorkspaces()\n const maybeBind = (): void => {\n const state = sessions.list.getSnapshot()\n for (const id of state.ids) {\n const summary = state.byId[id]\n if (summary === undefined || !summary.blank || summary.cwd === undefined) continue\n // A session belongs to the WSL world when its cwd is a WSL UNC path,\n // or a Windows drive path registered as a `/mnt/<drive>` workspace\n // (9P cannot serve drvfs, so those workspaces carry drive cwds).\n const canonical = canonicalWindowsPath(summary.cwd)\n const isWsl = isWslUnc(summary.cwd)\n || (canonical !== null && wslWindowsPaths.has(canonical))\n if (!isWsl) continue\n const current = getAgentPreset(summary)\n if (current !== undefined && current.startsWith('wsl-')) continue\n // Legacy standalone `wsl` (now folded into the variants): remap it to\n // the default mode's variant, since the standalone preset no longer\n // exists in the roster.\n const base = current === LEGACY_WSL_PRESET_ID\n ? (defaultPreset ?? 'standard')\n : (current ?? defaultPreset)\n if (base === undefined || base === LEGACY_WSL_PRESET_ID || base.startsWith('wsl-')) continue\n const target = `wsl-${base.toLowerCase()}`\n if (!variants.has(target)) continue\n if (inFlight.has(id) || (attempts.get(id) ?? 0) >= MAX_ATTEMPTS) continue\n inFlight.add(id)\n void selectAgentPreset(id, target)\n .then((result: { ok: boolean }) => {\n if (result.ok) noteAgentPresetCompat(id, target)\n })\n .catch(() => {\n // A refused or aborted swap (session already produced output,\n // roster churn, reconnect) leaves the session on its current\n // composition; count the attempt so a stuck session stops\n // retrying after MAX_ATTEMPTS.\n attempts.set(id, (attempts.get(id) ?? 0) + 1)\n })\n .finally(() => {\n inFlight.delete(id)\n })\n }\n }\n maybeBind()\n const unsubscribe = sessions.list.subscribe(() => maybeBind())\n // Variants are generated at host boot; a page loaded before that would\n // never see them without a periodic refresh.\n const timer = window.setInterval(refreshRoster, 60_000)\n return () => {\n unsubscribe()\n window.clearInterval(timer)\n }\n }, 'dsh-wsl-workspace: WSL mode-variant binding')\n}\n"],"mappings":"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"}
|