dsh-wsl-workspace 0.1.2 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -23,10 +23,10 @@ After restarting `dsh web`, a W button appears beside Settings at the sidebar fo
23
23
 
24
24
  ## Usage
25
25
 
26
- Click the W button beside Settings at the sidebar foot to open the "Add WSL workspace" dialog. Pick a distribution from the list, then browse the directory tree or type an absolute Linux path (for example `/home/me/proj`) — use the Check button to verify the path exists before creating the workspace. The dialog carries a built-in language switch (中文 / EN), defaulting to Chinese. The username field is optional: leave it empty to run commands as the distribution's default user, or name a Linux user of that distribution to run the session as that user instead (equivalent to `wsl.exe -u <username>`). The username only changes the bash tool's run identity — the file tools go through the Windows-side WSL share and are unaffected. Each workspace's username is kept in `<dshHome>/wsl-workspaces.json`; delete the entry (or recreate the workspace from the dialog) to return to the default user.
27
-
28
- Click "Create & open" to start a new session in the workspace. In the new session the bash tool executes commands inside the chosen distribution and `read`/`write`/`edit` operate on WSL files, so every path the model sees is a Linux path. The mode picker keeps working as usual: Standard, PTC, Minimal and Creative each land on their WSL variant automatically, and Windows files stay reachable from inside the session under `/mnt/<drive>` (for example `/mnt/c/Users/...`).
26
+ Click the W button beside Settings at the sidebar foot to open the "Add WSL workspace" dialog. Pick a distribution from the list, then browse the directory tree or type an absolute Linux path (for example `/home/me/proj`) — use the Check button to verify the path exists before creating the workspace. The dialog follows the DeepSeek Harness UI language. The username field is optional: leave it empty to run commands as the distribution's default user, or name a Linux user of that distribution to run the session as that user instead (equivalent to `wsl.exe -u <username>`). The username only changes the bash tool's run identity — the file tools go through the Windows-side WSL share and are unaffected. Each workspace's username is kept in `<dshHome>/wsl-workspaces.json`; delete the entry (or recreate the workspace from the dialog) to return to the default user.
29
27
 
28
+ Click "Create & open" to start a new session in the workspace. In the new session the bash tool executes commands inside the chosen distribution and `read`/`write`/`edit` operate on WSL files, so every path the model sees is a Linux path. The mode picker keeps working as usual: Standard, PTC, Minimal and Creative each land on their WSL variant automatically (the WSL variant entries in the picker are in English), and Windows files stay reachable from inside the session under `/mnt/<drive>` (for example `/mnt/c/Users/...`).
29
+ ![alt text](image-2.png)
30
30
  ## Behavior notes
31
31
 
32
32
  - **bash tool**: runs inside the WSL distribution as the configured username (empty = the distro default user, often `root`), so it can read and write anywhere in the distro. The Windows ACL sandbox cannot wrap `wsl.exe` — its children run on the Linux kernel side — so WSL itself is the isolation boundary and the DSH file policy does not apply to bash.
package/README.zh.md CHANGED
@@ -1,7 +1,7 @@
1
1
  # dsh-wsl-workspace
2
2
 
3
3
  [English](README.md)
4
-
4
+ ![alt text](0a981213b4d304673ed6dbe368ab6334.png)
5
5
  在 DeepSeek Harness Web GUI 中「添加 WSL 工作区」:让 agent 会话的 bash 命令与文件读写都运行在本机 WSL 发行版里,路径均为 Linux 形式,WSL 内无需安装任何工具链。
6
6
 
7
7
  ## 安装
@@ -22,9 +22,9 @@ dsh plugin --profile web add D:\path\to\dsh-wsl-workspace
22
22
  重启 `dsh web` 后,侧栏底部 Settings 旁出现 W 按钮。
23
23
 
24
24
  ## 使用
25
- 点侧栏底部 Settings 旁的 W 按钮,打开「添加 WSL 工作区」对话框。先从下拉框选择一个发行版,再浏览目录树或直接输入 Linux 绝对路径(如 `/home/me/proj`),可以点「检查」确认路径存在。对话框自带 中文/EN 语言切换,默认中文。用户名是可选项:留空则以该发行版的默认用户运行,填写该发行版里的某个 Linux 用户名则以该用户运行(等价于 `wsl.exe -u <用户名>`)。用户名只影响 bash 命令的运行身份,文件工具通过 Windows 侧的 WSL 共享访问、不受其影响;每个工作区填写的用户名保存在 `<dshHome>/wsl-workspaces.json`,删除对应条目(或重开对话框重建工作区)即可恢复默认用户。
25
+ 点侧栏底部 Settings 旁的 W 按钮,打开「添加 WSL 工作区」对话框。先从下拉框选择一个发行版,再浏览目录树或直接输入 Linux 绝对路径(如 `/home/me/proj`),可以点「检查」确认路径存在。对话框文案跟随 DSH 界面语言。用户名是可选项:留空则以该发行版的默认用户运行,填写该发行版里的某个 Linux 用户名则以该用户运行(等价于 `wsl.exe -u <用户名>`)。用户名只影响 bash 命令的运行身份,文件工具通过 Windows 侧的 WSL 共享访问、不受其影响;每个工作区填写的用户名保存在 `<dshHome>/wsl-workspaces.json`,删除对应条目(或重开对话框重建工作区)即可恢复默认用户。
26
26
 
27
- 点「创建并打开」后,新会话随即运行在 WSL:`bash` 工具在所选发行版内执行命令,`read`/`write`/`edit` 读写 WSL 文件,模型看到的所有路径都是 Linux 形式。模式选择器照常可用——标准、PTC、极简、创造都会自动落到对应的 WSL 变体;会话内仍可通过 `/mnt/<drive>`(如 `/mnt/c/Users/...`)访问 Windows 文件。
27
+ 点「创建并打开」后,新会话随即运行在 WSL:`bash` 工具在所选发行版内执行命令,`read`/`write`/`edit` 读写 WSL 文件,模型看到的所有路径都是 Linux 形式。模式选择器照常可用——标准、PTC、极简、创造都会自动落到对应的 WSL 变体(选择器里的 WSL 变体条目为英文);会话内仍可通过 `/mnt/<drive>`(如 `/mnt/c/Users/...`)访问 Windows 文件。
28
28
  ![alt text](image-1.png)
29
29
  ## 行为与权限说明
30
30
 
package/image-2.png ADDED
Binary file
package/lib/client.js CHANGED
@@ -215,10 +215,9 @@ window.__ModuleLoader__.load({
215
215
  * The "Add WSL workspace…" footer action and its dialog.
216
216
  * @param props - owner share + injected face.
217
217
  */
218
- function AddWslWorkspace({ wide, language, setLanguage, t, checkPreset, listDistros, listDir, check, createWorkspace }) {
218
+ function AddWslWorkspace({ wide, t, checkPreset, listDistros, listDir, check, createWorkspace }) {
219
219
  const [open, setOpen] = (0, react.useState)(false);
220
220
  const [opening, setOpening] = (0, react.useState)(false);
221
- const [lang, setLang] = (0, react.useState)(language);
222
221
  const [distros, setDistros] = (0, react.useState)([]);
223
222
  const [distro, setDistro] = (0, react.useState)("");
224
223
  const [pathInput, setPathInput] = (0, react.useState)("/home/");
@@ -373,10 +372,6 @@ window.__ModuleLoader__.load({
373
372
  const maskClick = () => {
374
373
  if (!busy) setOpen(false);
375
374
  };
376
- const onLanguageChange = (next) => {
377
- setLang(next);
378
- setLanguage(next);
379
- };
380
375
  const listScroll = () => {};
381
376
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
382
377
  className: "dww-overlay",
@@ -394,32 +389,12 @@ window.__ModuleLoader__.load({
394
389
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h2", {
395
390
  className: "dww-title",
396
391
  children: t("dialog.title")
397
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
398
- className: "dww-header-actions",
399
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
400
- className: "dww-lang",
401
- role: "group",
402
- "aria-label": "Language",
403
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
404
- type: "button",
405
- className: lang === "zh" ? "dww-lang-btn dww-lang-btn--active" : "dww-lang-btn",
406
- disabled: busy,
407
- onClick: () => onLanguageChange("zh"),
408
- children: "中文"
409
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
410
- type: "button",
411
- className: lang === "en" ? "dww-lang-btn dww-lang-btn--active" : "dww-lang-btn",
412
- disabled: busy,
413
- onClick: () => onLanguageChange("en"),
414
- children: "EN"
415
- })]
416
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
417
- type: "button",
418
- className: "dww-close",
419
- "aria-label": t("dialog.cancel"),
420
- onClick: maskClick,
421
- children: "✕"
422
- })]
392
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
393
+ type: "button",
394
+ className: "dww-close",
395
+ "aria-label": t("dialog.cancel"),
396
+ onClick: maskClick,
397
+ children: ""
423
398
  })]
424
399
  }),
425
400
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
@@ -641,33 +616,6 @@ window.__ModuleLoader__.load({
641
616
  gap: 8px;
642
617
  padding: 18px 20px 12px;
643
618
  }
644
- .dww-header-actions {
645
- display: flex;
646
- align-items: center;
647
- gap: 8px;
648
- }
649
- /* Built-in dialog language switch (中文 / EN), independent of the app locale. */
650
- .dww-lang {
651
- display: inline-flex;
652
- overflow: hidden;
653
- border: 1px solid var(--dsw-alias-border-l2);
654
- border-radius: 8px;
655
- }
656
- .dww-lang-btn {
657
- height: 28px;
658
- padding: 0 10px;
659
- border: 0;
660
- background: transparent;
661
- color: var(--dsw-alias-label-secondary);
662
- cursor: pointer;
663
- font-size: 12px;
664
- }
665
- .dww-lang-btn:hover:not(:disabled) { background: var(--dsw-alias-interactive-bg-hover); }
666
- .dww-lang-btn--active {
667
- background: var(--dsw-alias-button-primary-fill);
668
- color: var(--dsw-alias-label-primary-foreground);
669
- }
670
- .dww-lang-btn:disabled { cursor: default; opacity: 0.6; }
671
619
  .dww-title {
672
620
  margin: 0;
673
621
  font-size: 16px;
@@ -942,22 +890,8 @@ window.__ModuleLoader__.load({
942
890
  zh,
943
891
  en
944
892
  }), "dsh-wsl-workspace: locale dictionaries");
945
- let language = "zh";
946
- const dictionaries = {
947
- zh,
948
- en
949
- };
950
- const t = (key, params) => {
951
- let text = dictionaries[language][key] ?? key;
952
- if (params !== void 0) for (const [name, value] of Object.entries(params)) text = text.replaceAll(`{${name}}`, String(value));
953
- return text;
954
- };
955
- const setLanguage = (next) => {
956
- language = next;
957
- };
893
+ const t = ctx.locale.bind("wslWorkspace");
958
894
  const injected = () => ({
959
- language,
960
- setLanguage,
961
895
  t,
962
896
  checkPreset: async () => {
963
897
  let roster;
package/lib/client.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"client.js","names":["listDistrosApi","listDirApi","checkApi","setWorkspaceUserApi"],"sources":["../src/client/api.ts","../src/shared/paths.ts","../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 * 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 * 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 * 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, joinUnc, normalizeLinuxPath } from '../shared/paths.ts'\nimport type { WslDirListing, WslPathCheck } from './api.ts'\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 /** 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 workspace over a WSL UNC path and start a session in it.\n * @param path - the `\\\\wsl.localhost\\<distro>\\...` UNC path.\n * @param username - optional Linux user for the session (empty = distro default).\n * @returns undefined on success, else a message to show.\n */\n createWorkspace(path: string, username: string): Promise<string | undefined>\n /** Translate a `wslWorkspace` dictionary key in the dialog's current language. */\n t: (key: string, params?: Record<string, unknown>) => string\n /** The dialog language at injection time; the component mirrors it as its initial state. */\n language: 'zh' | 'en'\n /** Switch the dialog language for every injected helper (defaults to zh). */\n setLanguage(language: 'zh' | 'en'): void\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, language, setLanguage, t, checkPreset, listDistros, listDir, check, createWorkspace }: AddWslWorkspaceProps): React.ReactElement | null {\n const [open, setOpen] = useState(false)\n const [opening, setOpening] = useState(false)\n const [lang, setLang] = useState<'zh' | 'en'>(language)\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 // 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 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(joinUnc(distro, path), user)\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 onLanguageChange = (next: 'zh' | 'en'): void => {\n setLang(next)\n setLanguage(next)\n }\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 <div className=\"dww-header-actions\">\n <div className=\"dww-lang\" role=\"group\" aria-label=\"Language\">\n <button\n type=\"button\"\n className={lang === 'zh' ? 'dww-lang-btn dww-lang-btn--active' : 'dww-lang-btn'}\n disabled={busy}\n onClick={() => onLanguageChange('zh')}\n >\n 中文\n </button>\n <button\n type=\"button\"\n className={lang === 'en' ? 'dww-lang-btn dww-lang-btn--active' : 'dww-lang-btn'}\n disabled={busy}\n onClick={() => onLanguageChange('en')}\n >\n EN\n </button>\n </div>\n <button type=\"button\" className=\"dww-close\" aria-label={t('dialog.cancel')} onClick={maskClick}>\n ✕\n </button>\n </div>\n </div>\n <div className=\"dww-body\">\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 </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-header-actions {\n display: flex;\n align-items: center;\n gap: 8px;\n}\n/* Built-in dialog language switch (中文 / EN), independent of the app locale. */\n.dww-lang {\n display: inline-flex;\n overflow: hidden;\n border: 1px solid var(--dsw-alias-border-l2);\n border-radius: 8px;\n}\n.dww-lang-btn {\n height: 28px;\n padding: 0 10px;\n border: 0;\n background: transparent;\n color: var(--dsw-alias-label-secondary);\n cursor: pointer;\n font-size: 12px;\n}\n.dww-lang-btn:hover:not(:disabled) { background: var(--dsw-alias-interactive-bg-hover); }\n.dww-lang-btn--active {\n background: var(--dsw-alias-button-primary-fill);\n color: var(--dsw-alias-label-primary-foreground);\n}\n.dww-lang-btn:disabled { cursor: default; opacity: 0.6; }\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`\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}\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}\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\nimport type { ConnectionHandle } from '@deepseek-ai/dsh-api-remotes/client'\n// Type-only: pulls the locale plugin's Context merge (ctx.locale), the\n// runtime's ClientContext, and the ui-sidebar SlotMap merge (the\n// 'sidebar.footer.action' entry) into this program.\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, listDir as listDirApi, listDistros as listDistrosApi, 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 { isWslUnc } from '../shared/paths.ts'\n\n/** Required services (cordis fiber inject). */\nexport const inject = ['slots', 'locale', 'connection', '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(): { ids: string[]; byId: Record<string, { blank: boolean; cwd?: string; agentPreset?: string }> }\n subscribe(fn: () => void): () => void\n }\n noteAgentPreset(sessionId: string, agentPreset: string): void\n}\n\n/** Minimal workspaces-service face (create + start-session only). */\ninterface WslWorkspacesFace {\n create(input: { path: string }): Promise<{ workspaceId: string }>\n startSession(workspaceId?: string): void\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 { api } = ctx.get('connection') as ConnectionHandle\n const workspaces = ctx.get('workspaces') as unknown as WslWorkspacesFace\n const sessions = ctx.get('sessions') as unknown as WslSessionsFace\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 dialog owns a built-in language switch (default zh) independent of\n // the app locale. Injected helpers read the current choice through this\n // mutable holder, so their messages (checkPreset etc.) follow the dialog's\n // language too.\n let language: 'zh' | 'en' = 'zh'\n const dictionaries = { zh, en }\n const t = (key: string, params?: Record<string, unknown>): string => {\n let text = dictionaries[language][key] ?? key\n if (params !== undefined) {\n for (const [name, value] of Object.entries(params)) {\n text = text.replaceAll(`{${name}}`, String(value))\n }\n }\n return text\n }\n const setLanguage = (next: 'zh' | 'en'): void => { language = next }\n\n const injected = (): AddWslWorkspaceInjected => ({\n language,\n setLanguage,\n t,\n checkPreset: async (): Promise<string | undefined> => {\n let roster\n try {\n const response = await api.agentPresets.list({})\n roster = response.result\n } catch (error) {\n return error instanceof Error ? error.message : String(error)\n }\n if (!roster.ok) return roster.error.message\n const healthy = roster.value.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 listDir: (distro, path) => listDirApi(distro, path),\n check: (distro, path) => checkApi(distro, path),\n createWorkspace: async (path, username): Promise<string | undefined> => {\n try {\n const view = await workspaces.create({ path })\n await setWorkspaceUserApi(path, username)\n workspaces.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 api.agentPresets.list({}).then((response: {\n result: { ok: boolean; value: { presets: { id: string; broken?: string; isDefault?: boolean }[] } }\n }) => {\n const result = response.result\n if (!result.ok) return\n variants = new Set(result.value.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.value.presets.find(\n (entry: { id: string; isDefault?: boolean }) => entry.isDefault === true,\n )?.id\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 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 if (!isWslUnc(summary.cwd)) continue\n const current = summary.agentPreset\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 api.agentPresets.select({ sessionId: id, agentPreset: target })\n .then((response: { result: { ok: boolean } }) => {\n if (response.result.ok) sessions.noteAgentPreset(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":["listDistrosApi","listDirApi","checkApi","setWorkspaceUserApi"],"sources":["../src/client/api.ts","../src/shared/paths.ts","../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 * 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 * 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 * 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, joinUnc, normalizeLinuxPath } from '../shared/paths.ts'\nimport type { WslDirListing, WslPathCheck } from './api.ts'\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 /** 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 workspace over a WSL UNC path and start a session in it.\n * @param path - the `\\\\wsl.localhost\\<distro>\\...` UNC path.\n * @param username - optional Linux user for the session (empty = distro default).\n * @returns undefined on success, else a message to show.\n */\n createWorkspace(path: string, username: 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, 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 // 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 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(joinUnc(distro, path), user)\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 type=\"button\" className=\"dww-close\" aria-label={t('dialog.cancel')} onClick={maskClick}>\n ✕\n </button>\n </div>\n <div className=\"dww-body\">\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 </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`\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}\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}\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\nimport type { ConnectionHandle } from '@deepseek-ai/dsh-api-remotes/client'\n// Type-only: pulls the locale plugin's Context merge (ctx.locale), the\n// runtime's ClientContext, and the ui-sidebar SlotMap merge (the\n// 'sidebar.footer.action' entry) into this program.\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, listDir as listDirApi, listDistros as listDistrosApi, 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 { isWslUnc } from '../shared/paths.ts'\n\n/** Required services (cordis fiber inject). */\nexport const inject = ['slots', 'locale', 'connection', '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(): { ids: string[]; byId: Record<string, { blank: boolean; cwd?: string; agentPreset?: string }> }\n subscribe(fn: () => void): () => void\n }\n noteAgentPreset(sessionId: string, agentPreset: string): void\n}\n\n/** Minimal workspaces-service face (create + start-session only). */\ninterface WslWorkspacesFace {\n create(input: { path: string }): Promise<{ workspaceId: string }>\n startSession(workspaceId?: string): void\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 { api } = ctx.get('connection') as ConnectionHandle\n const workspaces = ctx.get('workspaces') as unknown as WslWorkspacesFace\n const sessions = ctx.get('sessions') as unknown as WslSessionsFace\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 const injected = (): AddWslWorkspaceInjected => ({\n t,\n checkPreset: async (): Promise<string | undefined> => {\n let roster\n try {\n const response = await api.agentPresets.list({})\n roster = response.result\n } catch (error) {\n return error instanceof Error ? error.message : String(error)\n }\n if (!roster.ok) return roster.error.message\n const healthy = roster.value.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 listDir: (distro, path) => listDirApi(distro, path),\n check: (distro, path) => checkApi(distro, path),\n createWorkspace: async (path, username): Promise<string | undefined> => {\n try {\n const view = await workspaces.create({ path })\n await setWorkspaceUserApi(path, username)\n workspaces.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 api.agentPresets.list({}).then((response: {\n result: { ok: boolean; value: { presets: { id: string; broken?: string; isDefault?: boolean }[] } }\n }) => {\n const result = response.result\n if (!result.ok) return\n variants = new Set(result.value.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.value.presets.find(\n (entry: { id: string; isDefault?: boolean }) => entry.isDefault === true,\n )?.id\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 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 if (!isWslUnc(summary.cwd)) continue\n const current = summary.agentPreset\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 api.agentPresets.select({ sessionId: id, agentPreset: target })\n .then((response: { result: { ok: boolean } }) => {\n if (response.result.ok) sessions.noteAgentPreset(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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"}
package/lib/index.js CHANGED
@@ -206,6 +206,23 @@ function variantIdFor(sourceId) {
206
206
  //#region src/index.ts
207
207
  /** The HTTP route this plugin serves (a relative, same-origin path). */
208
208
  const DEFAULT_ROUTE = "/wsl-workspace/api";
209
+ /**
210
+ * English display names for the shipped source modes, matching the app's own
211
+ * built-in copy. The DSH picker localizes only the four built-in ids itself;
212
+ * `wsl-*` variant ids render the preset.yml text verbatim, so the plugin
213
+ * writes English names for the four shipped modes (custom presets keep
214
+ * theirs).
215
+ */
216
+ const MODE_DISPLAY_NAMES = {
217
+ standard: "Standard mode",
218
+ code: "Code mode",
219
+ minimal: "Minimal mode",
220
+ cordis: "Creator mode"
221
+ };
222
+ /** One English sentence describing a WSL variant's execution world. */
223
+ function variantDescription(display) {
224
+ return `WSL execution world for ${display}: bash and file tools run inside the WSL distribution.`;
225
+ }
209
226
  const MAX_BODY_BYTES = 1024 * 1024;
210
227
  /** Valid WSL distribution names: one path-safe segment (no separators, no dot-dirs). */
211
228
  const DISTRO_PATTERN = /^[A-Za-z0-9][A-Za-z0-9 ._-]*$/;
@@ -366,11 +383,13 @@ async function materializeVariants(agentPresets, dshHome, shellPath, fsPath) {
366
383
  try {
367
384
  const meta = readFileSync(join(dirname(preset.path), "preset.yml"), "utf8");
368
385
  const match = /^name:\s*(.+)$/m.exec(meta);
369
- if (match?.[1] !== void 0 && match[1].trim() !== "") name = `WSL · ${match[1].trim()}`;
386
+ const displayName = MODE_DISPLAY_NAMES[preset.id];
387
+ if (displayName !== void 0) name = `WSL · ${displayName}`;
388
+ else if (match?.[1] !== void 0 && match[1].trim() !== "") name = `WSL · ${match[1].trim()}`;
370
389
  const orderMatch = /^order:\s*(\d+)\s*$/m.exec(meta);
371
390
  if (orderMatch?.[1] !== void 0) orderLine = `order: ${orderMatch[1]}\n`;
372
391
  } catch {}
373
- writeFileSync(join(dir, "preset.yml"), `name: ${name}\n` + orderLine + `description: ${preset.id} 模式叠加 WSL 执行世界:bash 与文件工具运行在 WSL 发行版内。\n`, "utf8");
392
+ writeFileSync(join(dir, "preset.yml"), `name: ${name}\n` + orderLine + `description: ${variantDescription(MODE_DISPLAY_NAMES[preset.id] ?? preset.id)}\n`, "utf8");
374
393
  generated.add(variantId);
375
394
  }
376
395
  for (const entry of readdirSync(userRoot, { withFileTypes: true })) {
package/lib/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":[],"sources":["../src/host/variants.ts","../src/index.ts"],"sourcesContent":["/**\n * WSL preset-variant generator. For every healthy source preset the roster\n * supplies, a `wsl-<id>` variant is materialized under the roster's user\n * root: the source composition with its shell/filesystem world replaced by\n * the WSL providers, so any mode (standard, minimal, code, cordis, user\n * presets) can run on top of a WSL execution world. The execution world is\n * therefore orthogonal to the mode instead of a mode itself.\n *\n * The transformation is text-level on the top-level rows of the composition\n * (the shape all shipped presets share), with surgical edits for the known\n * special groups; unknown shapes are kept verbatim where possible.\n * @module dsh-wsl-workspace/host/variants\n */\n\n/** Top-level rows that name the execution world and are replaced by the variant's own. */\nconst WORLD_ROWS = new Set(['tool-bash', 'tool-pwsh', 'tool-fs', 'tool-fs-search', 'filesystem', 'persistent-shell'])\n\n/** The injected WSL world group: providers + the bash/fs consumers, entry-local. */\nfunction wslWorldGroup(shellPath: string, fsPath: string, includeEditor: boolean): string {\n return [\n '# ── WSL execution world (dsh-wsl-workspace variant) ─────────────────────',\n '# The shell and fs services are provided entry-locally (the isolate',\n '# realm); host services (tools registry, shell-env, jobs) fall through.',\n '# tool-fs-search is intentionally absent: the packaged ripgrep runs on',\n '# the Windows host and cannot open Linux paths; WSL sessions search with',\n '# shell tools instead.',\n '- id: wsl-world',\n \" name: cordis:group\",\n ' group: true',\n ' isolate:',\n ' shell: true',\n ' fs: true',\n ' config:',\n ` - id: shell-wsl`,\n ` name: '${shellPath.replace(/'/g, \"''\")}'`,\n ' - id: fs-wsl',\n ` name: '${fsPath.replace(/'/g, \"''\")}'`,\n ' - id: tool-bash',\n \" name: '@deepseek-ai/dsh-tool-bash'\",\n ' - id: tool-fs',\n \" name: '@deepseek-ai/dsh-tool-fs'\",\n ...(includeEditor\n ? [\n ' - id: str-replace-editor',\n \" name: '@deepseek-ai/dsh-tool-str-replace-editor'\",\n ' config:',\n ' maxOutputChars: 16000',\n ]\n : []),\n '',\n ].join('\\n')\n}\n\n/**\n * The persistent-shell group re-pointed at WSL: the PTY spawns wsl.exe's\n * bash instead of a host `bash` (which does not exist on Windows).\n */\nfunction persistentShellGroup(): string {\n return [\n '# ── persistent shell over WSL (variant) ─────────────────────────────────',\n '- id: persistent-shell',\n ' name: cordis:group',\n ' group: true',\n ' isolate:',\n ' terminals: true',\n ' config:',\n ' - id: pty',\n \" name: '@deepseek-ai/dsh-terminal'\",\n '',\n ' - id: terminal-bash',\n \" name: '@deepseek-ai/dsh-terminal-bash'\",\n ' config:',\n ' timeoutMs: 300000',\n \" shellPath: 'wsl.exe'\",\n \" shellArgs: ['-e', 'bash', '-l']\",\n '',\n ' - id: persistent-bash',\n \" name: '@deepseek-ai/dsh-tool-bash-persistent'\",\n ' config:',\n ' timeoutMs: 300000',\n ' description: |-',\n ' Run commands in a bash shell inside the WSL distribution',\n ' * When invoking this tool, the contents of the \"command\" parameter does NOT need to be XML-escaped.',\n \" * You don't have access to the internet via this tool.\",\n ' * You do have access to a mirror of common linux and python packages via apt and pip.',\n ' * State is persistent across command calls and discussions with the user.',\n \" * To inspect a particular line range of a file, e.g. lines 10-25, try 'sed -n 10,25p /path/to/the/file'.\",\n ' * Please avoid commands that may produce a very large amount of output.',\n \" * Please run long lived commands in the background, e.g. 'sleep 10 &' or start a server in the background.\",\n '',\n ].join('\\n')\n}\n\n/** The sentence appended to a standard-like persona when the variant runs in WSL. */\nconst PERSONA_APPEND = ' Your working directory {{cwd}} is inside a WSL (Windows Subsystem for Linux) distribution: the bash tool and the file read/write/edit tools use Linux paths, and the Windows filesystem is reachable as /mnt/<drive> for file migration.'\n\n/** The top-level rows of one composition, as (startLine, endLineExclusive) spans. */\nfunction topLevelSpans(lines: readonly string[]): { start: number; end: number }[] {\n const spans: { start: number; end: number }[] = []\n let start = -1\n for (let index = 0; index < lines.length; index++) {\n if (lines[index]?.startsWith('- id: ') === true) {\n if (start >= 0) spans.push({ start, end: index })\n start = index\n }\n }\n if (start >= 0) spans.push({ start, end: lines.length })\n return spans\n}\n\n/** The row id of a top-level span, or undefined when the first line is malformed. */\nfunction spanId(lines: readonly string[], span: { start: number; end: number }): string | undefined {\n return /^- id: ([A-Za-z0-9_.-]+)/.exec(lines[span.start] ?? '')?.[1]\n}\n\n/** Whether a top-level span is a `persona` row with an appendable folded text. */\nfunction appendablePersona(lines: readonly string[], span: { start: number; end: number }): boolean {\n const block = lines.slice(span.start, span.end).join('\\n')\n if (!block.includes('complete: true') && /text: [>|-]/.test(block)) {\n // Append only when the folded text actually has content lines.\n const textLine = block.split('\\n').find(line => /^(\\s*)text: [>|-]/.test(line))\n if (textLine !== undefined) {\n const indent = /^(\\s*)/.exec(textLine)?.[1]?.length ?? 0\n return block.split('\\n').some(line => line.length > indent && /^\\s+/.test(line) && !line.includes(':'))\n }\n }\n return false\n}\n\n/** Append the WSL sentence to a persona row's folded text (in place of its last text line). */\nfunction appendPersona(lines: readonly string[], span: { start: number; end: number }): string[] {\n const block = lines.slice(span.start, span.end)\n const textIndex = block.findIndex(line => /^(\\s*)text: [>|-]/.test(line))\n if (textIndex < 0) return [...block]\n const indent = /^(\\s*)/.exec(block[textIndex] ?? '')?.[1]?.length ?? 0\n let lastText = -1\n for (let index = textIndex + 1; index < block.length; index++) {\n const line = block[index] ?? ''\n if (line.trim() === '') continue\n if (line.length > indent && /^\\s+/.test(line)) lastText = index\n }\n if (lastText < 0) return [...block]\n const updated = [...block]\n const textIndent = /^(\\s*)/.exec(block[lastText] ?? '')?.[1] ?? ' '\n updated.splice(lastText + 1, 0, `${textIndent}${PERSONA_APPEND}`)\n return updated\n}\n\n/**\n * Transform one source preset composition into its WSL variant: drop the\n * execution-world rows, keep everything else verbatim, and append the WSL\n * world group (plus the persistent-shell group when the source had one).\n * @param source - the source composition text.\n * @param shellPath - absolute path of the plugin's built WSL shell provider.\n * @param fsPath - absolute path of the plugin's built WSL fs provider.\n * @returns the variant composition text.\n */\nexport function transformPresetForWsl(source: string, shellPath: string, fsPath: string): string {\n const lines = source.split('\\n')\n const spans = topLevelSpans(lines)\n const kept: string[] = []\n let sawEditor = false\n let sawPersistent = false\n let personaAppended = false\n for (const span of spans) {\n const id = spanId(lines, span)\n if (id === undefined) {\n kept.push(...lines.slice(span.start, span.end))\n continue\n }\n if (WORLD_ROWS.has(id)) {\n if (id === 'persistent-shell') sawPersistent = true\n continue\n }\n if (id === 'persona' && !personaAppended && appendablePersona(lines, span)) {\n kept.push(...appendPersona(lines, span))\n personaAppended = true\n continue\n }\n kept.push(...lines.slice(span.start, span.end))\n if (id === 'str-replace-editor') sawEditor = true\n }\n if (source.includes('str-replace-editor')) sawEditor = true\n const result = [...kept]\n if (result.length > 0 && result[result.length - 1] !== '') result.push('')\n result.push(wslWorldGroup(shellPath, fsPath, sawEditor))\n if (sawPersistent) result.push(persistentShellGroup())\n return result.join('\\n').replace(/\\n{3,}/g, '\\n\\n').replace(/\\n+$/, '\\n')\n}\n\n/** Whether an id is one of this plugin's own preset directories. */\nexport function isWslVariantId(id: string): boolean {\n return id === 'wsl' || /^wsl-[a-z0-9-]+$/.test(id)\n}\n\n/** The variant id for one source preset id. */\nexport function variantIdFor(sourceId: string): string {\n return `wsl-${sourceId.toLowerCase()}`\n}\n","/**\r\n * Host half of dsh-wsl-workspace. Three responsibilities:\r\n *\r\n * 1. Materialize a `wsl-<mode>` variant for every healthy roster preset\r\n * under `<dshHome>/.agent-presets/` (the roster's auto-scanned user\r\n * root), so the WSL execution world — `shell-wsl` + `fs-wsl` behind one\r\n * entry-local realm, with `tool-bash`/`tool-fs` consumers — composes with\r\n * ANY mode instead of being a mode itself; the legacy standalone `wsl`\r\n * preset directory and stale variants are removed on boot. The preset\r\n * rows name THIS package's built lib files by absolute path, which the\r\n * preset mount resolves to `file:` URLs without relying on bare specifier\r\n * resolution from the preset's home directory.\r\n *\r\n * 2. Serve the browser dialog's data route (`/wsl-workspace/api`):\r\n * distribution discovery, one-level directory listing, path checks, and\r\n * the per-workspace username store — all over the 9P UNC share.\r\n * Loopback-only, matching the sensitivity of the privileged configuration\r\n * surface.\r\n *\r\n * 3. Contribute the per-session `DSH_WSL_DISTRO` managed-env fact so the WSL\r\n * shell executor can resolve a plain Linux `workdir` to the calling\r\n * session's distribution.\r\n * @module dsh-wsl-workspace\r\n */\r\n\r\nimport { Context } from '@deepseek-ai/cordis'\r\nimport z from '@deepseek-ai/schemastery'\r\nimport { mkdirSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync } from 'node:fs'\r\nimport { fileURLToPath } from 'node:url'\r\nimport { dirname, join } from 'node:path'\r\nimport type { IncomingMessage, ServerResponse } from 'node:http'\r\nimport { homedir } from 'node:os'\r\nimport { joinUnc, normalizeLinuxPath, isAbsoluteLinuxPath, isValidWslUsername, parseWslUnc } from './shared/paths.ts'\r\nimport { canonicalWslUnc, getWorkspaceUsername, setWorkspaceUsername } from './shared/wsl-credentials.ts'\r\nimport { defaultDistro, listDistros } from './shared/wsl.ts'\r\nimport { isWslVariantId, transformPresetForWsl, variantIdFor } from './host/variants.ts'\r\n\r\n/** The HTTP route this plugin serves (a relative, same-origin path). */\r\nexport const DEFAULT_ROUTE = '/wsl-workspace/api'\r\n\r\n/** Plugin config. */\r\nexport interface Config {\r\n /** The route under which the dialog data API is served. */\r\n route?: string\r\n}\r\n\r\n/** The shape after schemastery applied the defaults. */\r\ntype ResolvedConfig = Required<Config>\r\n\r\n/** The `webServer.register` route contract this plugin consumes. */\r\ninterface WebServerRoute {\r\n kind: 'exact'\r\n path: string\r\n handler(req: IncomingMessage, res: ServerResponse): Promise<void>\r\n}\r\n\r\ninterface WebServerService {\r\n register(route: WebServerRoute): () => void\r\n}\r\n\r\n/** The `ctx.shellEnv` registry face this plugin consumes (optional service). */\r\ninterface ShellEnvService {\r\n register(contributor: {\r\n name: string\r\n variables: Readonly<Record<string, { description: string }>>\r\n resolve(execution: {\r\n agent?: { session: { header: { cwd?: string } } }\r\n }): Readonly<Partial<Record<string, string>>>\r\n }): () => void\r\n}\r\n\r\n/** One directory entry the dialog lists. */\r\ninterface WslDirEntryWire {\r\n name: string\r\n kind: 'directory' | 'file' | 'other'\r\n}\r\n\r\n/** One directory level plus its breadcrumb ancestry. */\r\ninterface WslDirListingWire {\r\n path: string\r\n parent: string | null\r\n entries: WslDirEntryWire[]\r\n}\r\n\r\n/** The wire envelope every method answers with. */\r\ntype Envelope<T> = { ok: true; value: T } | { ok: false; error: string }\r\n\r\nconst MAX_BODY_BYTES = 1024 * 1024\r\n\r\n/** Valid WSL distribution names: one path-safe segment (no separators, no dot-dirs). */\r\nconst DISTRO_PATTERN = /^[A-Za-z0-9][A-Za-z0-9 ._-]*$/\r\n\r\n/** The loopback hostnames the data route answers to (DNS-rebinding fence). */\r\nconst LOOPBACK_HOSTNAMES = new Set(['localhost', '127.0.0.1', '::1', '::ffff:127.0.0.1'])\r\n\r\n/** True when a socket address is loopback (any IPv4/IPv6 spelling). */\r\nfunction isLoopback(address: string | undefined): boolean {\r\n return address === '127.0.0.1' || address === '::1' || address === '::ffff:127.0.0.1'\r\n}\r\n\r\n/** The hostname part of a `Host` header value (port and IPv6 brackets stripped). */\r\nfunction hostNameOf(host: string): string {\r\n if (host.startsWith('[')) {\r\n const end = host.indexOf(']')\r\n return end >= 0 ? host.slice(1, end) : host\r\n }\r\n return host.split(':')[0] ?? ''\r\n}\r\n\r\n/** True when the request's `Host` header names a loopback host. */\r\nfunction isLoopbackHost(host: string | undefined): boolean {\r\n return host !== undefined && LOOPBACK_HOSTNAMES.has(hostNameOf(host).toLowerCase())\r\n}\r\n\r\n/**\r\n * Validate a wire-supplied distribution name before it becomes a UNC segment:\r\n * an attacker-controlled segment containing separators or `..` would escape\r\n * the `\\\\wsl.localhost\\` share structure into arbitrary UNC paths.\r\n * @param value - the raw wire value.\r\n * @returns the validated distro name.\r\n */\r\nfunction requireDistro(value: unknown): string {\r\n if (typeof value !== 'string' || !DISTRO_PATTERN.test(value) || value === '.' || value === '..') {\r\n throw new Error('distro must be a valid WSL distribution name')\r\n }\r\n return value\r\n}\r\n\r\n/** Human text for an unknown rejection. */\r\nfunction messageOf(value: unknown): string {\r\n return value instanceof Error ? value.message : String(value)\r\n}\r\n\r\n/** Write one JSON envelope. */\r\nfunction json(res: ServerResponse, status: number, body: Envelope<unknown>): void {\r\n res.writeHead(status, {\r\n 'content-type': 'application/json; charset=utf-8',\r\n 'cache-control': 'no-store',\r\n 'x-content-type-options': 'nosniff',\r\n })\r\n res.end(JSON.stringify(body))\r\n}\r\n\r\n/** Collect and parse the request body, bounded. */\r\nasync function readBody(req: IncomingMessage): Promise<Record<string, unknown>> {\r\n const chunks: Buffer[] = []\r\n let size = 0\r\n for await (const chunk of req) {\r\n const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)\r\n size += buffer.length\r\n if (size > MAX_BODY_BYTES) throw new Error('request body is too large')\r\n chunks.push(buffer)\r\n }\r\n const parsed = JSON.parse(Buffer.concat(chunks).toString('utf8')) as unknown\r\n if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {\r\n throw new Error('request body must be a JSON object')\r\n }\r\n return parsed as Record<string, unknown>\r\n}\r\n\r\n/** Normalize a Linux path for the wire (rejecting non-absolute input). */\r\nfunction requireLinuxPath(value: unknown, label: string): string {\r\n if (typeof value !== 'string' || !isAbsoluteLinuxPath(value)) {\r\n throw new Error(`${label} must be an absolute Linux path`)\r\n }\r\n return normalizeLinuxPath(value)\r\n}\r\n\r\n/** Validate a wire-supplied workspace path and return its canonical UNC form. */\r\nfunction requireWslUnc(value: unknown): string {\r\n if (typeof value !== 'string') throw new Error('path must be a string')\r\n const canonical = canonicalWslUnc(value)\r\n if (canonical === null) throw new Error('path must be a WSL UNC workspace path')\r\n return canonical\r\n}\r\n\r\n/** Resolve one directory listing over the 9P share. */\r\nfunction listWslDir(distro: string, linuxPath: string): WslDirListingWire {\r\n const unc = joinUnc(distro, linuxPath)\r\n const dirents = readdirSync(unc, { withFileTypes: true })\r\n const entries: WslDirEntryWire[] = dirents\r\n .slice(0, 1000)\r\n .map((dirent): WslDirEntryWire => {\r\n const kind: WslDirEntryWire['kind'] = dirent.isDirectory()\r\n ? 'directory'\r\n : dirent.isFile() ? 'file' : 'other'\r\n return { name: dirent.name, kind }\r\n })\r\n .sort((a, b) => {\r\n if (a.kind === 'directory' && b.kind !== 'directory') return -1\r\n if (a.kind !== 'directory' && b.kind === 'directory') return 1\r\n return a.name.localeCompare(b.name)\r\n })\r\n const parent = linuxPath === '/' ? null : linuxPath.split('/').slice(0, -1).join('/') || '/'\r\n return { path: linuxPath, parent, entries }\r\n}\r\n\r\n/** Route one method dispatch. */\r\nasync function dispatch(method: string, params: Record<string, unknown>): Promise<unknown> {\r\n switch (method) {\r\n case 'listDistros': {\r\n const distros = await listDistros()\r\n const fallback = await defaultDistro()\r\n if (fallback !== undefined && distros.includes(fallback)) {\r\n return [fallback, ...distros.filter(name => name !== fallback)]\r\n }\r\n return distros\r\n }\r\n case 'listDir': {\r\n const distro = requireDistro(params.distro)\r\n const path = requireLinuxPath(params.path, 'path')\r\n return listWslDir(distro, path)\r\n }\r\n case 'check': {\r\n const distro = requireDistro(params.distro)\r\n const path = requireLinuxPath(params.path, 'path')\r\n const unc = joinUnc(distro, path)\r\n try {\r\n const info = statSync(unc)\r\n return { exists: true, isDirectory: info.isDirectory() }\r\n } catch {\r\n return { exists: false, isDirectory: false }\r\n }\r\n }\r\n case 'setUser': {\r\n const path = requireWslUnc(params.path)\r\n const username = params.username\r\n if (username === undefined || username === '') {\r\n setWorkspaceUsername(path, undefined)\r\n } else {\r\n if (typeof username !== 'string' || !isValidWslUsername(username)) {\r\n throw new Error('username must match the Linux username pattern [A-Za-z_][A-Za-z0-9_.-]*')\r\n }\r\n setWorkspaceUsername(path, username)\r\n }\r\n return null\r\n }\r\n default:\r\n throw new Error(`unknown method \"${method}\"`)\r\n }\r\n}\r\n\r\n/** The `ctx.agentPresets` roster face this plugin consumes (optional service). */\r\ninterface AgentPresetsService {\r\n list(): Promise<{ id: string; broken?: string; path: string }[]>\r\n read(id: string): Promise<string>\r\n}\r\n\r\n/**\r\n * Materialize a `wsl-<mode>` variant for every healthy source preset, and\r\n * remove this plugin's managed residue: stale variants whose source\r\n * disappeared, plus the legacy standalone `wsl` preset directory (the\r\n * execution world now composes with modes; a standalone WSL mode no longer\r\n * exists). Managed files: rewritten on every boot.\r\n * @param agentPresets - the roster service.\r\n * @param dshHome - the harness home (user preset root parent).\r\n * @param shellPath - absolute path of the plugin's built WSL shell provider.\r\n * @param fsPath - absolute path of the plugin's built WSL fs provider.\r\n */\r\nasync function materializeVariants(\r\n agentPresets: AgentPresetsService,\r\n dshHome: string,\r\n shellPath: string,\r\n fsPath: string,\r\n): Promise<void> {\r\n const presets = await agentPresets.list()\r\n const userRoot = join(dshHome, '.agent-presets')\r\n const generated = new Set<string>()\r\n for (const preset of presets) {\r\n if (preset.broken !== undefined) continue\r\n if (isWslVariantId(preset.id)) continue\r\n const variantId = variantIdFor(preset.id)\r\n const source = await agentPresets.read(preset.id)\r\n const transformed = transformPresetForWsl(source, shellPath, fsPath)\r\n const dir = join(userRoot, variantId)\r\n mkdirSync(dir, { recursive: true })\r\n writeFileSync(join(dir, 'agent.cordis.yml'), transformed, 'utf8')\r\n let name = `WSL · ${preset.id}`\r\n let orderLine = ''\r\n try {\r\n const meta = readFileSync(join(dirname(preset.path), 'preset.yml'), 'utf8')\r\n const match = /^name:\\s*(.+)$/m.exec(meta)\r\n if (match?.[1] !== undefined && match[1].trim() !== '') name = `WSL · ${match[1].trim()}`\r\n // Inherit the source's declared order so the WSL variants line up with\r\n // the local modes in the roster (standard, PTC, minimal, cordis).\r\n const orderMatch = /^order:\\s*(\\d+)\\s*$/m.exec(meta)\r\n if (orderMatch?.[1] !== undefined) orderLine = `order: ${orderMatch[1]}\\n`\r\n } catch {\r\n // Absent or unreadable display metadata falls back to the id-based name.\r\n }\r\n writeFileSync(\r\n join(dir, 'preset.yml'),\r\n `name: ${name}\\n`\r\n + orderLine\r\n + `description: ${preset.id} 模式叠加 WSL 执行世界:bash 与文件工具运行在 WSL 发行版内。\\n`,\r\n 'utf8',\r\n )\r\n generated.add(variantId)\r\n }\r\n for (const entry of readdirSync(userRoot, { withFileTypes: true })) {\r\n if (!entry.isDirectory()) continue\r\n if (entry.name === 'wsl') {\r\n // The legacy standalone WSL mode: folded into the variants above.\r\n rmSync(join(userRoot, entry.name), { recursive: true, force: true })\r\n continue\r\n }\r\n if (!/^wsl-[a-z0-9-]+$/.test(entry.name)) continue\r\n if (!generated.has(entry.name)) rmSync(join(userRoot, entry.name), { recursive: true, force: true })\r\n }\r\n}\r\n\r\n/** Function-plugin plugin contract. */\r\nexport const name = 'dsh-wsl-workspace'\r\n\r\n/** Required services. */\r\nexport const inject = ['webServer']\r\n\r\n/** Validated plugin config (schemastery applied the defaults). */\r\nexport const Config: z<Config> = z.object({\r\n route: z.string().default(DEFAULT_ROUTE),\r\n})\r\n\r\n/**\r\n * Apply the host half: materialize the `wsl` preset plus a `wsl-<mode>`\r\n * variant for every healthy roster preset, register the data route, and\r\n * contribute the per-session `DSH_WSL_DISTRO` managed-env fact so the WSL\r\n * shell executor can resolve a plain Linux `workdir` to the calling\r\n * session's distribution.\r\n * @param ctx - the host plugin context.\r\n * @param config - the validated configuration.\r\n */\r\nexport function apply(ctx: Context, config: Config): void {\r\n const resolved = config as ResolvedConfig\r\n const dshHome = process.env.DSH_HOME ?? join(homedir(), '.dsh')\r\n const packageRoot = fileURLToPath(new URL('..', import.meta.url))\r\n const shellPath = join(packageRoot, 'lib', 'shell.js').replace(/\\\\/g, '/')\r\n const fsPath = join(packageRoot, 'lib', 'fs.js').replace(/\\\\/g, '/')\r\n\r\n const agentPresets = ctx.get('agentPresets') as unknown as AgentPresetsService | undefined\r\n if (agentPresets !== undefined) {\r\n ctx.effect(() => {\r\n void materializeVariants(agentPresets, dshHome, shellPath, fsPath).catch((error) => {\r\n // Variant generation is best-effort over a live roster: a missing or\r\n // unreadable source preset must not take the whole plugin down, but\r\n // the failure is surfaced loudly rather than hidden.\r\n console.error(`dsh-wsl-workspace: WSL preset-variant generation failed: ${messageOf(error)}`)\r\n })\r\n return () => {}\r\n }, 'dsh-wsl-workspace: WSL preset variants')\r\n }\r\n\r\n const shellEnv = ctx.get('shellEnv') as unknown as ShellEnvService | undefined\r\n if (shellEnv !== undefined) {\r\n ctx.effect(() => shellEnv.register({\r\n name: 'wsl-workspace-distro',\r\n variables: {\r\n DSH_WSL_DISTRO: {\r\n description: 'The WSL distribution of the calling session workspace, when the session cwd is a WSL UNC path.',\r\n },\r\n DSH_WSL_USER: {\r\n description: 'The Linux user of the calling session workspace, when the workspace has one configured.',\r\n },\r\n },\r\n resolve(execution) {\r\n const cwd = execution.agent?.session.header.cwd\r\n const unc = cwd === undefined ? null : parseWslUnc(cwd)\r\n if (unc === null) return {}\r\n const username = getWorkspaceUsername(joinUnc(unc.distro, unc.linuxPath))\r\n return username === undefined || username === ''\r\n ? { DSH_WSL_DISTRO: unc.distro }\r\n : { DSH_WSL_DISTRO: unc.distro, DSH_WSL_USER: username }\r\n },\r\n }), 'dsh-wsl-workspace: per-session distro env fact')\r\n }\r\n\r\n const webServer = ctx.get('webServer') as unknown as WebServerService\r\n ctx.effect(() => webServer.register({\r\n kind: 'exact',\r\n path: resolved.route,\r\n handler: async (req, res) => {\r\n if (!isLoopback(req.socket.remoteAddress) || !isLoopbackHost(req.headers.host)) {\r\n json(res, 403, { ok: false, error: 'loopback-only' })\r\n return\r\n }\r\n if (req.method !== 'POST') {\r\n json(res, 405, { ok: false, error: 'method not allowed' })\r\n return\r\n }\r\n let body: Record<string, unknown>\r\n try {\r\n body = await readBody(req)\r\n } catch (error) {\r\n json(res, 400, { ok: false, error: messageOf(error) })\r\n return\r\n }\r\n const method = typeof body.method === 'string' ? body.method : ''\r\n const params = body.params === undefined ? {} : body.params\r\n if (params === null || typeof params !== 'object' || Array.isArray(params)) {\r\n json(res, 400, { ok: false, error: 'params must be an object' })\r\n return\r\n }\r\n try {\r\n const value = await dispatch(method, params as Record<string, unknown>)\r\n json(res, 200, { ok: true, value })\r\n } catch (error) {\r\n json(res, 200, { ok: false, error: messageOf(error) })\r\n }\r\n },\r\n }), 'dsh-wsl-workspace: dialog data route')\r\n}\r\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.js","names":[],"sources":["../src/host/variants.ts","../src/index.ts"],"sourcesContent":["/**\n * WSL preset-variant generator. For every healthy source preset the roster\n * supplies, a `wsl-<id>` variant is materialized under the roster's user\n * root: the source composition with its shell/filesystem world replaced by\n * the WSL providers, so any mode (standard, minimal, code, cordis, user\n * presets) can run on top of a WSL execution world. The execution world is\n * therefore orthogonal to the mode instead of a mode itself.\n *\n * The transformation is text-level on the top-level rows of the composition\n * (the shape all shipped presets share), with surgical edits for the known\n * special groups; unknown shapes are kept verbatim where possible.\n * @module dsh-wsl-workspace/host/variants\n */\n\n/** Top-level rows that name the execution world and are replaced by the variant's own. */\nconst WORLD_ROWS = new Set(['tool-bash', 'tool-pwsh', 'tool-fs', 'tool-fs-search', 'filesystem', 'persistent-shell'])\n\n/** The injected WSL world group: providers + the bash/fs consumers, entry-local. */\nfunction wslWorldGroup(shellPath: string, fsPath: string, includeEditor: boolean): string {\n return [\n '# ── WSL execution world (dsh-wsl-workspace variant) ─────────────────────',\n '# The shell and fs services are provided entry-locally (the isolate',\n '# realm); host services (tools registry, shell-env, jobs) fall through.',\n '# tool-fs-search is intentionally absent: the packaged ripgrep runs on',\n '# the Windows host and cannot open Linux paths; WSL sessions search with',\n '# shell tools instead.',\n '- id: wsl-world',\n \" name: cordis:group\",\n ' group: true',\n ' isolate:',\n ' shell: true',\n ' fs: true',\n ' config:',\n ` - id: shell-wsl`,\n ` name: '${shellPath.replace(/'/g, \"''\")}'`,\n ' - id: fs-wsl',\n ` name: '${fsPath.replace(/'/g, \"''\")}'`,\n ' - id: tool-bash',\n \" name: '@deepseek-ai/dsh-tool-bash'\",\n ' - id: tool-fs',\n \" name: '@deepseek-ai/dsh-tool-fs'\",\n ...(includeEditor\n ? [\n ' - id: str-replace-editor',\n \" name: '@deepseek-ai/dsh-tool-str-replace-editor'\",\n ' config:',\n ' maxOutputChars: 16000',\n ]\n : []),\n '',\n ].join('\\n')\n}\n\n/**\n * The persistent-shell group re-pointed at WSL: the PTY spawns wsl.exe's\n * bash instead of a host `bash` (which does not exist on Windows).\n */\nfunction persistentShellGroup(): string {\n return [\n '# ── persistent shell over WSL (variant) ─────────────────────────────────',\n '- id: persistent-shell',\n ' name: cordis:group',\n ' group: true',\n ' isolate:',\n ' terminals: true',\n ' config:',\n ' - id: pty',\n \" name: '@deepseek-ai/dsh-terminal'\",\n '',\n ' - id: terminal-bash',\n \" name: '@deepseek-ai/dsh-terminal-bash'\",\n ' config:',\n ' timeoutMs: 300000',\n \" shellPath: 'wsl.exe'\",\n \" shellArgs: ['-e', 'bash', '-l']\",\n '',\n ' - id: persistent-bash',\n \" name: '@deepseek-ai/dsh-tool-bash-persistent'\",\n ' config:',\n ' timeoutMs: 300000',\n ' description: |-',\n ' Run commands in a bash shell inside the WSL distribution',\n ' * When invoking this tool, the contents of the \"command\" parameter does NOT need to be XML-escaped.',\n \" * You don't have access to the internet via this tool.\",\n ' * You do have access to a mirror of common linux and python packages via apt and pip.',\n ' * State is persistent across command calls and discussions with the user.',\n \" * To inspect a particular line range of a file, e.g. lines 10-25, try 'sed -n 10,25p /path/to/the/file'.\",\n ' * Please avoid commands that may produce a very large amount of output.',\n \" * Please run long lived commands in the background, e.g. 'sleep 10 &' or start a server in the background.\",\n '',\n ].join('\\n')\n}\n\n/** The sentence appended to a standard-like persona when the variant runs in WSL. */\nconst PERSONA_APPEND = ' Your working directory {{cwd}} is inside a WSL (Windows Subsystem for Linux) distribution: the bash tool and the file read/write/edit tools use Linux paths, and the Windows filesystem is reachable as /mnt/<drive> for file migration.'\n\n/** The top-level rows of one composition, as (startLine, endLineExclusive) spans. */\nfunction topLevelSpans(lines: readonly string[]): { start: number; end: number }[] {\n const spans: { start: number; end: number }[] = []\n let start = -1\n for (let index = 0; index < lines.length; index++) {\n if (lines[index]?.startsWith('- id: ') === true) {\n if (start >= 0) spans.push({ start, end: index })\n start = index\n }\n }\n if (start >= 0) spans.push({ start, end: lines.length })\n return spans\n}\n\n/** The row id of a top-level span, or undefined when the first line is malformed. */\nfunction spanId(lines: readonly string[], span: { start: number; end: number }): string | undefined {\n return /^- id: ([A-Za-z0-9_.-]+)/.exec(lines[span.start] ?? '')?.[1]\n}\n\n/** Whether a top-level span is a `persona` row with an appendable folded text. */\nfunction appendablePersona(lines: readonly string[], span: { start: number; end: number }): boolean {\n const block = lines.slice(span.start, span.end).join('\\n')\n if (!block.includes('complete: true') && /text: [>|-]/.test(block)) {\n // Append only when the folded text actually has content lines.\n const textLine = block.split('\\n').find(line => /^(\\s*)text: [>|-]/.test(line))\n if (textLine !== undefined) {\n const indent = /^(\\s*)/.exec(textLine)?.[1]?.length ?? 0\n return block.split('\\n').some(line => line.length > indent && /^\\s+/.test(line) && !line.includes(':'))\n }\n }\n return false\n}\n\n/** Append the WSL sentence to a persona row's folded text (in place of its last text line). */\nfunction appendPersona(lines: readonly string[], span: { start: number; end: number }): string[] {\n const block = lines.slice(span.start, span.end)\n const textIndex = block.findIndex(line => /^(\\s*)text: [>|-]/.test(line))\n if (textIndex < 0) return [...block]\n const indent = /^(\\s*)/.exec(block[textIndex] ?? '')?.[1]?.length ?? 0\n let lastText = -1\n for (let index = textIndex + 1; index < block.length; index++) {\n const line = block[index] ?? ''\n if (line.trim() === '') continue\n if (line.length > indent && /^\\s+/.test(line)) lastText = index\n }\n if (lastText < 0) return [...block]\n const updated = [...block]\n const textIndent = /^(\\s*)/.exec(block[lastText] ?? '')?.[1] ?? ' '\n updated.splice(lastText + 1, 0, `${textIndent}${PERSONA_APPEND}`)\n return updated\n}\n\n/**\n * Transform one source preset composition into its WSL variant: drop the\n * execution-world rows, keep everything else verbatim, and append the WSL\n * world group (plus the persistent-shell group when the source had one).\n * @param source - the source composition text.\n * @param shellPath - absolute path of the plugin's built WSL shell provider.\n * @param fsPath - absolute path of the plugin's built WSL fs provider.\n * @returns the variant composition text.\n */\nexport function transformPresetForWsl(source: string, shellPath: string, fsPath: string): string {\n const lines = source.split('\\n')\n const spans = topLevelSpans(lines)\n const kept: string[] = []\n let sawEditor = false\n let sawPersistent = false\n let personaAppended = false\n for (const span of spans) {\n const id = spanId(lines, span)\n if (id === undefined) {\n kept.push(...lines.slice(span.start, span.end))\n continue\n }\n if (WORLD_ROWS.has(id)) {\n if (id === 'persistent-shell') sawPersistent = true\n continue\n }\n if (id === 'persona' && !personaAppended && appendablePersona(lines, span)) {\n kept.push(...appendPersona(lines, span))\n personaAppended = true\n continue\n }\n kept.push(...lines.slice(span.start, span.end))\n if (id === 'str-replace-editor') sawEditor = true\n }\n if (source.includes('str-replace-editor')) sawEditor = true\n const result = [...kept]\n if (result.length > 0 && result[result.length - 1] !== '') result.push('')\n result.push(wslWorldGroup(shellPath, fsPath, sawEditor))\n if (sawPersistent) result.push(persistentShellGroup())\n return result.join('\\n').replace(/\\n{3,}/g, '\\n\\n').replace(/\\n+$/, '\\n')\n}\n\n/** Whether an id is one of this plugin's own preset directories. */\nexport function isWslVariantId(id: string): boolean {\n return id === 'wsl' || /^wsl-[a-z0-9-]+$/.test(id)\n}\n\n/** The variant id for one source preset id. */\nexport function variantIdFor(sourceId: string): string {\n return `wsl-${sourceId.toLowerCase()}`\n}\n","/**\r\n * Host half of dsh-wsl-workspace. Three responsibilities:\r\n *\r\n * 1. Materialize a `wsl-<mode>` variant for every healthy roster preset\r\n * under `<dshHome>/.agent-presets/` (the roster's auto-scanned user\r\n * root), so the WSL execution world — `shell-wsl` + `fs-wsl` behind one\r\n * entry-local realm, with `tool-bash`/`tool-fs` consumers — composes with\r\n * ANY mode instead of being a mode itself; the legacy standalone `wsl`\r\n * preset directory and stale variants are removed on boot. The preset\r\n * rows name THIS package's built lib files by absolute path, which the\r\n * preset mount resolves to `file:` URLs without relying on bare specifier\r\n * resolution from the preset's home directory.\r\n *\r\n * 2. Serve the browser dialog's data route (`/wsl-workspace/api`):\r\n * distribution discovery, one-level directory listing, path checks, and\r\n * the per-workspace username store — all over the 9P UNC share.\r\n * Loopback-only, matching the sensitivity of the privileged configuration\r\n * surface.\r\n *\r\n * 3. Contribute the per-session `DSH_WSL_DISTRO` managed-env fact so the WSL\r\n * shell executor can resolve a plain Linux `workdir` to the calling\r\n * session's distribution.\r\n * @module dsh-wsl-workspace\r\n */\r\n\r\nimport { Context } from '@deepseek-ai/cordis'\r\nimport z from '@deepseek-ai/schemastery'\r\nimport { mkdirSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync } from 'node:fs'\r\nimport { fileURLToPath } from 'node:url'\r\nimport { dirname, join } from 'node:path'\r\nimport type { IncomingMessage, ServerResponse } from 'node:http'\r\nimport { homedir } from 'node:os'\r\nimport { joinUnc, normalizeLinuxPath, isAbsoluteLinuxPath, isValidWslUsername, parseWslUnc } from './shared/paths.ts'\r\nimport { canonicalWslUnc, getWorkspaceUsername, setWorkspaceUsername } from './shared/wsl-credentials.ts'\r\nimport { defaultDistro, listDistros } from './shared/wsl.ts'\r\nimport { isWslVariantId, transformPresetForWsl, variantIdFor } from './host/variants.ts'\r\n\r\n/** The HTTP route this plugin serves (a relative, same-origin path). */\r\nexport const DEFAULT_ROUTE = '/wsl-workspace/api'\r\n\r\n/**\r\n * English display names for the shipped source modes, matching the app's own\r\n * built-in copy. The DSH picker localizes only the four built-in ids itself;\r\n * `wsl-*` variant ids render the preset.yml text verbatim, so the plugin\r\n * writes English names for the four shipped modes (custom presets keep\r\n * theirs).\r\n */\r\nconst MODE_DISPLAY_NAMES: Readonly<Record<string, string>> = {\r\n standard: 'Standard mode',\r\n code: 'Code mode',\r\n minimal: 'Minimal mode',\r\n cordis: 'Creator mode',\r\n}\r\n\r\n/** One English sentence describing a WSL variant's execution world. */\r\nfunction variantDescription(display: string): string {\r\n return `WSL execution world for ${display}: bash and file tools run inside the WSL distribution.`\r\n}\r\n\r\n/** Plugin config. */\r\nexport interface Config {\r\n /** The route under which the dialog data API is served. */\r\n route?: string\r\n}\r\n\r\n/** The shape after schemastery applied the defaults. */\r\ntype ResolvedConfig = Required<Config>\r\n\r\n/** The `webServer.register` route contract this plugin consumes. */\r\ninterface WebServerRoute {\r\n kind: 'exact'\r\n path: string\r\n handler(req: IncomingMessage, res: ServerResponse): Promise<void>\r\n}\r\n\r\ninterface WebServerService {\r\n register(route: WebServerRoute): () => void\r\n}\r\n\r\n/** The `ctx.shellEnv` registry face this plugin consumes (optional service). */\r\ninterface ShellEnvService {\r\n register(contributor: {\r\n name: string\r\n variables: Readonly<Record<string, { description: string }>>\r\n resolve(execution: {\r\n agent?: { session: { header: { cwd?: string } } }\r\n }): Readonly<Partial<Record<string, string>>>\r\n }): () => void\r\n}\r\n\r\n/** One directory entry the dialog lists. */\r\ninterface WslDirEntryWire {\r\n name: string\r\n kind: 'directory' | 'file' | 'other'\r\n}\r\n\r\n/** One directory level plus its breadcrumb ancestry. */\r\ninterface WslDirListingWire {\r\n path: string\r\n parent: string | null\r\n entries: WslDirEntryWire[]\r\n}\r\n\r\n/** The wire envelope every method answers with. */\r\ntype Envelope<T> = { ok: true; value: T } | { ok: false; error: string }\r\n\r\nconst MAX_BODY_BYTES = 1024 * 1024\r\n\r\n/** Valid WSL distribution names: one path-safe segment (no separators, no dot-dirs). */\r\nconst DISTRO_PATTERN = /^[A-Za-z0-9][A-Za-z0-9 ._-]*$/\r\n\r\n/** The loopback hostnames the data route answers to (DNS-rebinding fence). */\r\nconst LOOPBACK_HOSTNAMES = new Set(['localhost', '127.0.0.1', '::1', '::ffff:127.0.0.1'])\r\n\r\n/** True when a socket address is loopback (any IPv4/IPv6 spelling). */\r\nfunction isLoopback(address: string | undefined): boolean {\r\n return address === '127.0.0.1' || address === '::1' || address === '::ffff:127.0.0.1'\r\n}\r\n\r\n/** The hostname part of a `Host` header value (port and IPv6 brackets stripped). */\r\nfunction hostNameOf(host: string): string {\r\n if (host.startsWith('[')) {\r\n const end = host.indexOf(']')\r\n return end >= 0 ? host.slice(1, end) : host\r\n }\r\n return host.split(':')[0] ?? ''\r\n}\r\n\r\n/** True when the request's `Host` header names a loopback host. */\r\nfunction isLoopbackHost(host: string | undefined): boolean {\r\n return host !== undefined && LOOPBACK_HOSTNAMES.has(hostNameOf(host).toLowerCase())\r\n}\r\n\r\n/**\r\n * Validate a wire-supplied distribution name before it becomes a UNC segment:\r\n * an attacker-controlled segment containing separators or `..` would escape\r\n * the `\\\\wsl.localhost\\` share structure into arbitrary UNC paths.\r\n * @param value - the raw wire value.\r\n * @returns the validated distro name.\r\n */\r\nfunction requireDistro(value: unknown): string {\r\n if (typeof value !== 'string' || !DISTRO_PATTERN.test(value) || value === '.' || value === '..') {\r\n throw new Error('distro must be a valid WSL distribution name')\r\n }\r\n return value\r\n}\r\n\r\n/** Human text for an unknown rejection. */\r\nfunction messageOf(value: unknown): string {\r\n return value instanceof Error ? value.message : String(value)\r\n}\r\n\r\n/** Write one JSON envelope. */\r\nfunction json(res: ServerResponse, status: number, body: Envelope<unknown>): void {\r\n res.writeHead(status, {\r\n 'content-type': 'application/json; charset=utf-8',\r\n 'cache-control': 'no-store',\r\n 'x-content-type-options': 'nosniff',\r\n })\r\n res.end(JSON.stringify(body))\r\n}\r\n\r\n/** Collect and parse the request body, bounded. */\r\nasync function readBody(req: IncomingMessage): Promise<Record<string, unknown>> {\r\n const chunks: Buffer[] = []\r\n let size = 0\r\n for await (const chunk of req) {\r\n const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)\r\n size += buffer.length\r\n if (size > MAX_BODY_BYTES) throw new Error('request body is too large')\r\n chunks.push(buffer)\r\n }\r\n const parsed = JSON.parse(Buffer.concat(chunks).toString('utf8')) as unknown\r\n if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {\r\n throw new Error('request body must be a JSON object')\r\n }\r\n return parsed as Record<string, unknown>\r\n}\r\n\r\n/** Normalize a Linux path for the wire (rejecting non-absolute input). */\r\nfunction requireLinuxPath(value: unknown, label: string): string {\r\n if (typeof value !== 'string' || !isAbsoluteLinuxPath(value)) {\r\n throw new Error(`${label} must be an absolute Linux path`)\r\n }\r\n return normalizeLinuxPath(value)\r\n}\r\n\r\n/** Validate a wire-supplied workspace path and return its canonical UNC form. */\r\nfunction requireWslUnc(value: unknown): string {\r\n if (typeof value !== 'string') throw new Error('path must be a string')\r\n const canonical = canonicalWslUnc(value)\r\n if (canonical === null) throw new Error('path must be a WSL UNC workspace path')\r\n return canonical\r\n}\r\n\r\n/** Resolve one directory listing over the 9P share. */\r\nfunction listWslDir(distro: string, linuxPath: string): WslDirListingWire {\r\n const unc = joinUnc(distro, linuxPath)\r\n const dirents = readdirSync(unc, { withFileTypes: true })\r\n const entries: WslDirEntryWire[] = dirents\r\n .slice(0, 1000)\r\n .map((dirent): WslDirEntryWire => {\r\n const kind: WslDirEntryWire['kind'] = dirent.isDirectory()\r\n ? 'directory'\r\n : dirent.isFile() ? 'file' : 'other'\r\n return { name: dirent.name, kind }\r\n })\r\n .sort((a, b) => {\r\n if (a.kind === 'directory' && b.kind !== 'directory') return -1\r\n if (a.kind !== 'directory' && b.kind === 'directory') return 1\r\n return a.name.localeCompare(b.name)\r\n })\r\n const parent = linuxPath === '/' ? null : linuxPath.split('/').slice(0, -1).join('/') || '/'\r\n return { path: linuxPath, parent, entries }\r\n}\r\n\r\n/** Route one method dispatch. */\r\nasync function dispatch(method: string, params: Record<string, unknown>): Promise<unknown> {\r\n switch (method) {\r\n case 'listDistros': {\r\n const distros = await listDistros()\r\n const fallback = await defaultDistro()\r\n if (fallback !== undefined && distros.includes(fallback)) {\r\n return [fallback, ...distros.filter(name => name !== fallback)]\r\n }\r\n return distros\r\n }\r\n case 'listDir': {\r\n const distro = requireDistro(params.distro)\r\n const path = requireLinuxPath(params.path, 'path')\r\n return listWslDir(distro, path)\r\n }\r\n case 'check': {\r\n const distro = requireDistro(params.distro)\r\n const path = requireLinuxPath(params.path, 'path')\r\n const unc = joinUnc(distro, path)\r\n try {\r\n const info = statSync(unc)\r\n return { exists: true, isDirectory: info.isDirectory() }\r\n } catch {\r\n return { exists: false, isDirectory: false }\r\n }\r\n }\r\n case 'setUser': {\r\n const path = requireWslUnc(params.path)\r\n const username = params.username\r\n if (username === undefined || username === '') {\r\n setWorkspaceUsername(path, undefined)\r\n } else {\r\n if (typeof username !== 'string' || !isValidWslUsername(username)) {\r\n throw new Error('username must match the Linux username pattern [A-Za-z_][A-Za-z0-9_.-]*')\r\n }\r\n setWorkspaceUsername(path, username)\r\n }\r\n return null\r\n }\r\n default:\r\n throw new Error(`unknown method \"${method}\"`)\r\n }\r\n}\r\n\r\n/** The `ctx.agentPresets` roster face this plugin consumes (optional service). */\r\ninterface AgentPresetsService {\r\n list(): Promise<{ id: string; broken?: string; path: string }[]>\r\n read(id: string): Promise<string>\r\n}\r\n\r\n/**\r\n * Materialize a `wsl-<mode>` variant for every healthy source preset, and\r\n * remove this plugin's managed residue: stale variants whose source\r\n * disappeared, plus the legacy standalone `wsl` preset directory (the\r\n * execution world now composes with modes; a standalone WSL mode no longer\r\n * exists). Managed files: rewritten on every boot.\r\n * @param agentPresets - the roster service.\r\n * @param dshHome - the harness home (user preset root parent).\r\n * @param shellPath - absolute path of the plugin's built WSL shell provider.\r\n * @param fsPath - absolute path of the plugin's built WSL fs provider.\r\n */\r\nasync function materializeVariants(\r\n agentPresets: AgentPresetsService,\r\n dshHome: string,\r\n shellPath: string,\r\n fsPath: string,\r\n): Promise<void> {\r\n const presets = await agentPresets.list()\r\n const userRoot = join(dshHome, '.agent-presets')\r\n const generated = new Set<string>()\r\n for (const preset of presets) {\r\n if (preset.broken !== undefined) continue\r\n if (isWslVariantId(preset.id)) continue\r\n const variantId = variantIdFor(preset.id)\r\n const source = await agentPresets.read(preset.id)\r\n const transformed = transformPresetForWsl(source, shellPath, fsPath)\r\n const dir = join(userRoot, variantId)\r\n mkdirSync(dir, { recursive: true })\r\n writeFileSync(join(dir, 'agent.cordis.yml'), transformed, 'utf8')\r\n let name = `WSL · ${preset.id}`\r\n let orderLine = ''\r\n try {\r\n const meta = readFileSync(join(dirname(preset.path), 'preset.yml'), 'utf8')\r\n const match = /^name:\\s*(.+)$/m.exec(meta)\r\n // Shipped modes show English display names (the picker reads the file\r\n // text verbatim for wsl-* ids); custom presets keep their own name.\r\n const displayName = MODE_DISPLAY_NAMES[preset.id]\r\n if (displayName !== undefined) {\r\n name = `WSL · ${displayName}`\r\n } else if (match?.[1] !== undefined && match[1].trim() !== '') {\r\n name = `WSL · ${match[1].trim()}`\r\n }\r\n // Inherit the source's declared order so the WSL variants line up with\r\n // the local modes in the roster (standard, PTC, minimal, cordis).\r\n const orderMatch = /^order:\\s*(\\d+)\\s*$/m.exec(meta)\r\n if (orderMatch?.[1] !== undefined) orderLine = `order: ${orderMatch[1]}\\n`\r\n } catch {\r\n // Absent or unreadable display metadata falls back to the id-based name.\r\n }\r\n writeFileSync(\r\n join(dir, 'preset.yml'),\r\n `name: ${name}\\n`\r\n + orderLine\r\n + `description: ${variantDescription(MODE_DISPLAY_NAMES[preset.id] ?? preset.id)}\\n`,\r\n 'utf8',\r\n )\r\n generated.add(variantId)\r\n }\r\n for (const entry of readdirSync(userRoot, { withFileTypes: true })) {\r\n if (!entry.isDirectory()) continue\r\n if (entry.name === 'wsl') {\r\n // The legacy standalone WSL mode: folded into the variants above.\r\n rmSync(join(userRoot, entry.name), { recursive: true, force: true })\r\n continue\r\n }\r\n if (!/^wsl-[a-z0-9-]+$/.test(entry.name)) continue\r\n if (!generated.has(entry.name)) rmSync(join(userRoot, entry.name), { recursive: true, force: true })\r\n }\r\n}\r\n\r\n/** Function-plugin plugin contract. */\r\nexport const name = 'dsh-wsl-workspace'\r\n\r\n/** Required services. */\r\nexport const inject = ['webServer']\r\n\r\n/** Validated plugin config (schemastery applied the defaults). */\r\nexport const Config: z<Config> = z.object({\r\n route: z.string().default(DEFAULT_ROUTE),\r\n})\r\n\r\n/**\r\n * Apply the host half: materialize the `wsl` preset plus a `wsl-<mode>`\r\n * variant for every healthy roster preset, register the data route, and\r\n * contribute the per-session `DSH_WSL_DISTRO` managed-env fact so the WSL\r\n * shell executor can resolve a plain Linux `workdir` to the calling\r\n * session's distribution.\r\n * @param ctx - the host plugin context.\r\n * @param config - the validated configuration.\r\n */\r\nexport function apply(ctx: Context, config: Config): void {\r\n const resolved = config as ResolvedConfig\r\n const dshHome = process.env.DSH_HOME ?? join(homedir(), '.dsh')\r\n const packageRoot = fileURLToPath(new URL('..', import.meta.url))\r\n const shellPath = join(packageRoot, 'lib', 'shell.js').replace(/\\\\/g, '/')\r\n const fsPath = join(packageRoot, 'lib', 'fs.js').replace(/\\\\/g, '/')\r\n\r\n const agentPresets = ctx.get('agentPresets') as unknown as AgentPresetsService | undefined\r\n if (agentPresets !== undefined) {\r\n ctx.effect(() => {\r\n void materializeVariants(agentPresets, dshHome, shellPath, fsPath).catch((error) => {\r\n // Variant generation is best-effort over a live roster: a missing or\r\n // unreadable source preset must not take the whole plugin down, but\r\n // the failure is surfaced loudly rather than hidden.\r\n console.error(`dsh-wsl-workspace: WSL preset-variant generation failed: ${messageOf(error)}`)\r\n })\r\n return () => {}\r\n }, 'dsh-wsl-workspace: WSL preset variants')\r\n }\r\n\r\n const shellEnv = ctx.get('shellEnv') as unknown as ShellEnvService | undefined\r\n if (shellEnv !== undefined) {\r\n ctx.effect(() => shellEnv.register({\r\n name: 'wsl-workspace-distro',\r\n variables: {\r\n DSH_WSL_DISTRO: {\r\n description: 'The WSL distribution of the calling session workspace, when the session cwd is a WSL UNC path.',\r\n },\r\n DSH_WSL_USER: {\r\n description: 'The Linux user of the calling session workspace, when the workspace has one configured.',\r\n },\r\n },\r\n resolve(execution) {\r\n const cwd = execution.agent?.session.header.cwd\r\n const unc = cwd === undefined ? null : parseWslUnc(cwd)\r\n if (unc === null) return {}\r\n const username = getWorkspaceUsername(joinUnc(unc.distro, unc.linuxPath))\r\n return username === undefined || username === ''\r\n ? { DSH_WSL_DISTRO: unc.distro }\r\n : { DSH_WSL_DISTRO: unc.distro, DSH_WSL_USER: username }\r\n },\r\n }), 'dsh-wsl-workspace: per-session distro env fact')\r\n }\r\n\r\n const webServer = ctx.get('webServer') as unknown as WebServerService\r\n ctx.effect(() => webServer.register({\r\n kind: 'exact',\r\n path: resolved.route,\r\n handler: async (req, res) => {\r\n if (!isLoopback(req.socket.remoteAddress) || !isLoopbackHost(req.headers.host)) {\r\n json(res, 403, { ok: false, error: 'loopback-only' })\r\n return\r\n }\r\n if (req.method !== 'POST') {\r\n json(res, 405, { ok: false, error: 'method not allowed' })\r\n return\r\n }\r\n let body: Record<string, unknown>\r\n try {\r\n body = await readBody(req)\r\n } catch (error) {\r\n json(res, 400, { ok: false, error: messageOf(error) })\r\n return\r\n }\r\n const method = typeof body.method === 'string' ? body.method : ''\r\n const params = body.params === undefined ? {} : body.params\r\n if (params === null || typeof params !== 'object' || Array.isArray(params)) {\r\n json(res, 400, { ok: false, error: 'params must be an object' })\r\n return\r\n }\r\n try {\r\n const value = await dispatch(method, params as Record<string, unknown>)\r\n json(res, 200, { ok: true, value })\r\n } catch (error) {\r\n json(res, 200, { ok: false, error: messageOf(error) })\r\n }\r\n },\r\n }), 'dsh-wsl-workspace: dialog data route')\r\n}\r\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "dsh-wsl-workspace",
3
3
  "description": "WSL workspace support for DeepSeek Harness: add a WSL workspace from the web GUI and run the whole agent session (bash + file tools) inside the WSL distribution, VS Code Remote-WSL style. No toolchain install inside WSL required.",
4
- "version": "0.1.2",
4
+ "version": "0.2.0",
5
5
  "type": "module",
6
6
  "repository": {
7
7
  "type": "git",
@@ -54,6 +54,7 @@
54
54
  "README.md",
55
55
  "README.zh.md",
56
56
  "image.png",
57
- "image-1.png"
57
+ "image-1.png",
58
+ "image-2.png"
58
59
  ]
59
60
  }
@@ -38,12 +38,8 @@ export interface AddWslWorkspaceInjected {
38
38
  * @returns undefined on success, else a message to show.
39
39
  */
40
40
  createWorkspace(path: string, username: string): Promise<string | undefined>
41
- /** Translate a `wslWorkspace` dictionary key in the dialog's current language. */
41
+ /** Translate a `wslWorkspace` dictionary key (follows the DeepSeek Harness locale). */
42
42
  t: (key: string, params?: Record<string, unknown>) => string
43
- /** The dialog language at injection time; the component mirrors it as its initial state. */
44
- language: 'zh' | 'en'
45
- /** Switch the dialog language for every injected helper (defaults to zh). */
46
- setLanguage(language: 'zh' | 'en'): void
47
43
  }
48
44
 
49
45
  /** Full component props: the owner share plus the injected face. */
@@ -78,10 +74,9 @@ function WslGlyph({ size = 16 }: { size?: number }): React.ReactElement {
78
74
  * The "Add WSL workspace…" footer action and its dialog.
79
75
  * @param props - owner share + injected face.
80
76
  */
81
- export function AddWslWorkspace({ wide, language, setLanguage, t, checkPreset, listDistros, listDir, check, createWorkspace }: AddWslWorkspaceProps): React.ReactElement | null {
77
+ export function AddWslWorkspace({ wide, t, checkPreset, listDistros, listDir, check, createWorkspace }: AddWslWorkspaceProps): React.ReactElement | null {
82
78
  const [open, setOpen] = useState(false)
83
79
  const [opening, setOpening] = useState(false)
84
- const [lang, setLang] = useState<'zh' | 'en'>(language)
85
80
  const [distros, setDistros] = useState<string[]>([])
86
81
  const [distro, setDistro] = useState('')
87
82
  const [pathInput, setPathInput] = useState('/home/')
@@ -251,10 +246,6 @@ export function AddWslWorkspace({ wide, language, setLanguage, t, checkPreset, l
251
246
 
252
247
  const children = (listing?.entries.filter(entry => entry.kind === 'directory') ?? []).map(entry => entry.name)
253
248
  const maskClick = (): void => { if (!busy) setOpen(false) }
254
- const onLanguageChange = (next: 'zh' | 'en'): void => {
255
- setLang(next)
256
- setLanguage(next)
257
- }
258
249
  const listScroll: UIEventHandler<HTMLDivElement> = () => { /* scroll container handles overflow */ }
259
250
 
260
251
  return (
@@ -263,29 +254,9 @@ export function AddWslWorkspace({ wide, language, setLanguage, t, checkPreset, l
263
254
  <div className="dww-card" role="dialog" aria-modal="true" aria-label={t('dialog.title')}>
264
255
  <div className="dww-header">
265
256
  <h2 className="dww-title">{t('dialog.title')}</h2>
266
- <div className="dww-header-actions">
267
- <div className="dww-lang" role="group" aria-label="Language">
268
- <button
269
- type="button"
270
- className={lang === 'zh' ? 'dww-lang-btn dww-lang-btn--active' : 'dww-lang-btn'}
271
- disabled={busy}
272
- onClick={() => onLanguageChange('zh')}
273
- >
274
- 中文
275
- </button>
276
- <button
277
- type="button"
278
- className={lang === 'en' ? 'dww-lang-btn dww-lang-btn--active' : 'dww-lang-btn'}
279
- disabled={busy}
280
- onClick={() => onLanguageChange('en')}
281
- >
282
- EN
283
- </button>
284
- </div>
285
- <button type="button" className="dww-close" aria-label={t('dialog.cancel')} onClick={maskClick}>
286
-
287
- </button>
288
- </div>
257
+ <button type="button" className="dww-close" aria-label={t('dialog.cancel')} onClick={maskClick}>
258
+
259
+ </button>
289
260
  </div>
290
261
  <div className="dww-body">
291
262
  {error !== null ? (
@@ -66,26 +66,11 @@ export function apply(ctx: ClientContext): void {
66
66
  'dsh-wsl-workspace: locale dictionaries',
67
67
  )
68
68
 
69
- // The dialog owns a built-in language switch (default zh) independent of
70
- // the app locale. Injected helpers read the current choice through this
71
- // mutable holder, so their messages (checkPreset etc.) follow the dialog's
72
- // language too.
73
- let language: 'zh' | 'en' = 'zh'
74
- const dictionaries = { zh, en }
75
- const t = (key: string, params?: Record<string, unknown>): string => {
76
- let text = dictionaries[language][key] ?? key
77
- if (params !== undefined) {
78
- for (const [name, value] of Object.entries(params)) {
79
- text = text.replaceAll(`{${name}}`, String(value))
80
- }
81
- }
82
- return text
83
- }
84
- const setLanguage = (next: 'zh' | 'en'): void => { language = next }
69
+ // The injected translate function reads the live DeepSeek Harness locale,
70
+ // so the dialog copy follows the app language setting automatically.
71
+ const t = ctx.locale.bind('wslWorkspace' as never) as unknown as (key: string, params?: Record<string, unknown>) => string
85
72
 
86
73
  const injected = (): AddWslWorkspaceInjected => ({
87
- language,
88
- setLanguage,
89
74
  t,
90
75
  checkPreset: async (): Promise<string | undefined> => {
91
76
  let roster
@@ -94,33 +94,6 @@ const STYLES = `
94
94
  gap: 8px;
95
95
  padding: 18px 20px 12px;
96
96
  }
97
- .dww-header-actions {
98
- display: flex;
99
- align-items: center;
100
- gap: 8px;
101
- }
102
- /* Built-in dialog language switch (中文 / EN), independent of the app locale. */
103
- .dww-lang {
104
- display: inline-flex;
105
- overflow: hidden;
106
- border: 1px solid var(--dsw-alias-border-l2);
107
- border-radius: 8px;
108
- }
109
- .dww-lang-btn {
110
- height: 28px;
111
- padding: 0 10px;
112
- border: 0;
113
- background: transparent;
114
- color: var(--dsw-alias-label-secondary);
115
- cursor: pointer;
116
- font-size: 12px;
117
- }
118
- .dww-lang-btn:hover:not(:disabled) { background: var(--dsw-alias-interactive-bg-hover); }
119
- .dww-lang-btn--active {
120
- background: var(--dsw-alias-button-primary-fill);
121
- color: var(--dsw-alias-label-primary-foreground);
122
- }
123
- .dww-lang-btn:disabled { cursor: default; opacity: 0.6; }
124
97
  .dww-title {
125
98
  margin: 0;
126
99
  font-size: 16px;
package/src/index.ts CHANGED
@@ -38,6 +38,25 @@ import { isWslVariantId, transformPresetForWsl, variantIdFor } from './host/vari
38
38
  /** The HTTP route this plugin serves (a relative, same-origin path). */
39
39
  export const DEFAULT_ROUTE = '/wsl-workspace/api'
40
40
 
41
+ /**
42
+ * English display names for the shipped source modes, matching the app's own
43
+ * built-in copy. The DSH picker localizes only the four built-in ids itself;
44
+ * `wsl-*` variant ids render the preset.yml text verbatim, so the plugin
45
+ * writes English names for the four shipped modes (custom presets keep
46
+ * theirs).
47
+ */
48
+ const MODE_DISPLAY_NAMES: Readonly<Record<string, string>> = {
49
+ standard: 'Standard mode',
50
+ code: 'Code mode',
51
+ minimal: 'Minimal mode',
52
+ cordis: 'Creator mode',
53
+ }
54
+
55
+ /** One English sentence describing a WSL variant's execution world. */
56
+ function variantDescription(display: string): string {
57
+ return `WSL execution world for ${display}: bash and file tools run inside the WSL distribution.`
58
+ }
59
+
41
60
  /** Plugin config. */
42
61
  export interface Config {
43
62
  /** The route under which the dialog data API is served. */
@@ -280,7 +299,14 @@ async function materializeVariants(
280
299
  try {
281
300
  const meta = readFileSync(join(dirname(preset.path), 'preset.yml'), 'utf8')
282
301
  const match = /^name:\s*(.+)$/m.exec(meta)
283
- if (match?.[1] !== undefined && match[1].trim() !== '') name = `WSL · ${match[1].trim()}`
302
+ // Shipped modes show English display names (the picker reads the file
303
+ // text verbatim for wsl-* ids); custom presets keep their own name.
304
+ const displayName = MODE_DISPLAY_NAMES[preset.id]
305
+ if (displayName !== undefined) {
306
+ name = `WSL · ${displayName}`
307
+ } else if (match?.[1] !== undefined && match[1].trim() !== '') {
308
+ name = `WSL · ${match[1].trim()}`
309
+ }
284
310
  // Inherit the source's declared order so the WSL variants line up with
285
311
  // the local modes in the roster (standard, PTC, minimal, cordis).
286
312
  const orderMatch = /^order:\s*(\d+)\s*$/m.exec(meta)
@@ -292,7 +318,7 @@ async function materializeVariants(
292
318
  join(dir, 'preset.yml'),
293
319
  `name: ${name}\n`
294
320
  + orderLine
295
- + `description: ${preset.id} 模式叠加 WSL 执行世界:bash 与文件工具运行在 WSL 发行版内。\n`,
321
+ + `description: ${variantDescription(MODE_DISPLAY_NAMES[preset.id] ?? preset.id)}\n`,
296
322
  'utf8',
297
323
  )
298
324
  generated.add(variantId)