dsh-wsl-workspace 0.4.2 → 0.4.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.de.md +52 -52
- package/README.es.md +52 -52
- package/README.fr.md +52 -52
- package/README.ja.md +52 -52
- package/README.ko.md +52 -52
- package/README.md +100 -90
- package/README.pt.md +52 -52
- package/README.ru.md +52 -52
- package/README.zh.md +50 -49
- package/TESTING.md +108 -108
- package/lib/client.js +220 -16
- package/lib/client.js.map +1 -1
- package/lib/fs.js.map +1 -1
- package/lib/index.js +164 -24
- package/lib/index.js.map +1 -1
- package/lib/shell.js.map +1 -1
- package/lib/wsl-6VcgOvF9.js.map +1 -1
- package/lib/wsl-credentials-B6wIXsKt.js.map +1 -1
- package/package.json +92 -90
- package/src/client/AddWslWorkspace.tsx +375 -349
- package/src/client/api.ts +145 -124
- package/src/client/help.tsx +88 -0
- package/src/client/index.ts +361 -358
- package/src/client/locales.ts +92 -68
- package/src/client/styles.ts +65 -0
- package/src/fs.ts +247 -247
- package/src/host/variants.ts +292 -164
- package/src/host/wsl-skills.ts +533 -533
- package/src/index.ts +571 -537
- package/src/shared/paths.ts +175 -175
- package/src/shared/wsl-credentials.ts +135 -135
- package/src/shell.ts +451 -451
package/lib/client.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"client.js","names":["response: Response","envelope: Envelope<T>","listDir","presetIssue: string | undefined","names: string[]","listDistros","facts: WslPathCheck","check","listScroll: UIEventHandler<HTMLDivElement>","zh: Record<string, string>","en: Record<string, string>","listDistrosApi","listDirApi","checkApi","view","registerWindowsApi","setWorkspaceUserApi","defaultPreset: string | undefined","listWorkspacesApi"],"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":["/**\r\n * Thin fetch client for the Host plugin route. The browser calls\r\n * POST /wsl-workspace/api with a `{ method, params }` envelope and the Host\r\n * answers `{ ok: true, value }` or `{ ok: false, error }`.\r\n */\r\n\r\n/** Relative route the Host half registers (same-origin with the web server). */\r\nconst ENDPOINT = '/wsl-workspace/api'\r\n\r\n/** One directory entry as the Host lists it. */\r\nexport interface WslDirEntry {\r\n name: string\r\n kind: 'directory' | 'file' | 'other'\r\n}\r\n\r\n/** One directory level plus its breadcrumb ancestry. */\r\nexport interface WslDirListing {\r\n /** The listed absolute Linux path. */\r\n path: string\r\n /** Parent Linux path, or null at the filesystem root. */\r\n parent: string | null\r\n /** The level's children (in name order; the client filters to directories). */\r\n entries: WslDirEntry[]\r\n}\r\n\r\n/** Existence/directory check result for one Linux path. */\r\nexport interface WslPathCheck {\r\n exists: boolean\r\n isDirectory: boolean\r\n}\r\n\r\n/** Wire envelope the Host route answers with. */\r\ntype Envelope<T> = { ok: true; value: T } | { ok: false; error: string }\r\n\r\n/** Human text for an unknown rejection, reusing the repository's idiom. */\r\nfunction errorMessage(value: unknown): string {\r\n return value instanceof Error ? value.message : String(value)\r\n}\r\n\r\n/**\r\n * Perform one POST call and unwrap the envelope.\r\n * @param method - the Host method name.\r\n * @param params - the method payload.\r\n * @returns the unwrapped value, or throws an Error on network or `ok:false`.\r\n */\r\nasync function call<T>(method: string, params: Record<string, unknown> = {}): Promise<T> {\r\n let response: Response\r\n try {\r\n response = await fetch(ENDPOINT, {\r\n method: 'POST',\r\n headers: { 'content-type': 'application/json' },\r\n body: JSON.stringify({ method, params }),\r\n })\r\n } catch (error) {\r\n // The transport refused before answering (offline, origin mismatch, 404).\r\n throw new Error(`wsl-workspace request failed: ${errorMessage(error)}`)\r\n }\r\n let envelope: Envelope<T>\r\n try {\r\n envelope = (await response.json()) as Envelope<T>\r\n } catch {\r\n // A non-JSON body means a proxy/loader answered instead of the Host route.\r\n throw new Error(`wsl-workspace answered non-JSON (${response.status})`)\r\n }\r\n if (!envelope.ok) throw new Error(envelope.error)\r\n return envelope.value\r\n}\r\n\r\n/**\r\n * List the WSL distros installed on the host.\r\n * @returns distro names in registry order.\r\n */\r\nexport async function listDistros(): Promise<string[]> {\r\n return call<string[]>('listDistros', {})\r\n}\r\n\r\n/**\r\n * List one directory level inside a distro.\r\n * @param distro - distro name.\r\n * @param path - absolute Linux directory to list.\r\n * @returns the level's listing with ancestry.\r\n */\r\nexport async function listDir(distro: string, path: string): Promise<WslDirListing> {\r\n return call<WslDirListing>('listDir', { distro, path })\r\n}\r\n\r\n/**\r\n * Check whether a Linux path exists and is a directory.\r\n * @param distro - distro name.\r\n * @param path - absolute Linux path.\r\n * @returns existence and directory facts.\r\n */\r\nexport async function check(distro: string, path: string): Promise<WslPathCheck> {\r\n return call<WslPathCheck>('check', { distro, path })\r\n}\r\n\r\n/**\r\n * Store (or clear, with an empty string) the username of one WSL workspace.\r\n * @param path - the workspace UNC path.\r\n * @param username - the Linux username; empty string clears the stored value.\r\n */\r\nexport async function setWorkspaceUser(path: string, username: string): Promise<void> {\r\n return call<void>('setUser', { path, username })\r\n}\r\n\r\n/**\r\n * Register a `/mnt/<drive>` WSL workspace under its Windows drive path,\r\n * recording the distro (and optional username) for the session env.\r\n * @param linuxPath - the `/mnt/<drive>/…` Linux path.\r\n * @param distro - the WSL distribution the workspace belongs to.\r\n * @param username - optional Linux username.\r\n */\r\nexport async function registerWindows(linuxPath: string, distro: string, username: string): Promise<void> {\r\n return call<void>('registerWindows', { linuxPath, distro, username })\r\n}\r\n\r\n/**\r\n * List every registered WSL workspace key (canonical UNC and Windows drive\r\n * spellings). The client uses the drive keys to recognize `/mnt` workspaces\r\n * across page reloads.\r\n */\r\nexport async function listWorkspaces(): Promise<string[]> {\r\n return call<string[]>('listWorkspaces', {})\r\n}\r\n","/**\r\n * WSL path helpers shared by the client and host halves. Pure and\r\n * dependency-free so both planes can import them without a runtime edge.\r\n */\r\n\r\n/** WSL2 default loopback bridge host: `\\\\wsl.localhost\\<distro>\\...`. */\r\nconst WSL_LOCALHOST_HOST = 'wsl.localhost'\r\n/** Legacy WSL interop host: `\\\\wsl$\\<distro>\\...`. */\r\nconst WSL_LEGACY_HOST = 'wsl$'\r\n\r\n/** The two UNC hosts WSL exposes a distribution's filesystem under. */\r\nconst UNC_HOSTS = [WSL_LOCALHOST_HOST, WSL_LEGACY_HOST]\r\n\r\n/** One WSL workspace coordinate parsed out of a UNC path. */\r\nexport interface WslUncTarget {\r\n /** Distro name (e.g. `Ubuntu`) as `wsl -l -q` reports it. */\r\n readonly distro: string\r\n /** Normalized absolute Linux path (leading `/`; no trailing slash except root). */\r\n readonly linuxPath: string\r\n}\r\n\r\n/**\r\n * Parse a WSL UNC path into its distro and Linux path. Accepts the WSL2\r\n * `\\\\wsl.localhost\\<distro>\\<linux>` form, the legacy `\\\\wsl$\\<distro>\\<linux>`\r\n * interop form, and forward-slash spellings of either.\r\n * @param raw - candidate absolute path.\r\n * @returns the parsed target, or null when the path is not a WSL UNC.\r\n */\r\nexport function parseWslUnc(raw: string): WslUncTarget | null {\r\n const normalized = raw.replace(/\\\\/g, '/').replace(/\\/\\/+/g, '//')\r\n if (!normalized.startsWith('//')) return null\r\n const segments = normalized.slice(2).split('/')\r\n const host = (segments[0] ?? '').toLowerCase()\r\n if (!UNC_HOSTS.includes(host)) return null\r\n const distro = segments[1] ?? ''\r\n if (distro === '') return null\r\n const rest = segments.slice(2).filter(segment => segment.length > 0)\r\n return { distro, linuxPath: `/${rest.join('/')}` }\r\n}\r\n\r\n/**\r\n * Whether a path resolves into a WSL distro through either UNC form.\r\n * @param raw - candidate absolute path.\r\n * @returns whether the path parses as a WSL UNC.\r\n */\r\nexport function isWslUnc(raw: string): boolean {\r\n return parseWslUnc(raw) !== null\r\n}\r\n\r\n/**\r\n * Translate a WSL UNC path to the absolute Linux path a process inside the\r\n * distribution can open. Throws on non-WSL input: callers rely on this\r\n * conversion to hand paths to the Linux world, so a silent pass-through\r\n * would hand a Windows path to bash.\r\n * @param uncPath - a path {@link parseWslUnc} accepts.\r\n * @returns the absolute Linux path.\r\n */\r\nexport function uncToLinux(uncPath: string): string {\r\n const parts = parseWslUnc(uncPath)\r\n if (parts === null) {\r\n throw new Error(`wsl-workspace: \"${uncPath}\" is not a WSL UNC path`)\r\n }\r\n return parts.linuxPath\r\n}\r\n\r\n/**\r\n * Normalize a Linux absolute path for the Host: collapse repeated slashes and\r\n * strip a trailing slash (root becomes `/`).\r\n * @param path - absolute Linux path.\r\n * @returns the normalized path.\r\n */\r\nexport function normalizeLinuxPath(path: string): string {\r\n const collapsed = path.replace(/\\/+/g, '/')\r\n return collapsed === '/' ? '/' : collapsed.replace(/\\/$/, '')\r\n}\r\n\r\n/**\r\n * Whether a path is an absolute, non-empty Linux path.\r\n * @param path - candidate.\r\n * @returns whether it starts with `/` and contains no NUL.\r\n */\r\nexport function isAbsoluteLinuxPath(path: string): boolean {\r\n return path.startsWith('/') && !path.includes('\\0')\r\n}\r\n\r\n/**\r\n * Join a distro and a Linux absolute path into the WSL2 UNC form used as the\r\n * workspace identity (`\\\\wsl.localhost\\<distro>\\<linux>`, backslash segments).\r\n * @param distro - distro name.\r\n * @param linuxPath - absolute Linux path (leading `/`).\r\n * @returns the UNC path.\r\n */\r\nexport function joinUnc(distro: string, linuxPath: string): string {\r\n if (!isAbsoluteLinuxPath(linuxPath)) {\r\n throw new Error(`wsl-workspace: cannot map a non-absolute Linux path \"${linuxPath}\" to UNC`)\r\n }\r\n // Defense in depth: a distribution name with separators or dot-dirs would\r\n // escape the `\\\\wsl.localhost\\` share structure (the host route validates\r\n // wire-supplied names too; every other caller passes through here).\r\n if (distro === '' || distro === '.' || distro === '..' || /[\\\\/]/.test(distro)) {\r\n throw new Error(`wsl-workspace: invalid distribution name \"${distro}\"`)\r\n }\r\n const normalized = linuxPath.replace(/\\/+/g, '/').replace(/\\/$/, '')\r\n const withoutLeading = normalized.startsWith('/') ? normalized.slice(1) : normalized\r\n const windowsSegments = withoutLeading.replace(/\\//g, '\\\\')\r\n const suffix = windowsSegments === '' ? '' : `\\\\${windowsSegments}`\r\n return `\\\\\\\\wsl.localhost\\\\${distro}${suffix}`\r\n}\r\n\r\n/**\r\n * Translate a Windows drive path to the drvfs mount path WSL distributions\r\n * conventionally expose it at (`C:\\foo` → `/mnt/c/foo`). Only single-letter\r\n * drives under `/mnt` are mapped; custom mount points are out of scope.\r\n * @param path - the candidate Windows path.\r\n * @returns the `/mnt/<drive>/…` path, or `null` for non-drive paths.\r\n */\r\nexport function windowsToMntPath(path: string): string | null {\r\n const match = /^([A-Za-z]):[\\\\/](.*)$/.exec(path)\r\n if (match === null) return null\r\n const rest = (match[2] ?? '').replace(/\\\\/g, '/').replace(/\\/+/g, '/').replace(/\\/$/, '')\r\n return `/mnt/${(match[1] ?? '').toLowerCase()}${rest === '' ? '' : `/${rest}`}`\r\n}\r\n\r\n/**\r\n * Translate a `/mnt/<drive>/…` path back to its Windows drive path.\r\n * @param linuxPath - the candidate Linux path.\r\n * @returns the `X:\\…` drive path, or `null` when the path is not a drvfs mount.\r\n */\r\nexport function mntToWindowsPath(linuxPath: string): string | null {\r\n const match = /^\\/mnt\\/([a-zA-Z])(?:\\/(.*))?$/.exec(linuxPath)\r\n if (match === null) return null\r\n const rest = (match[2] ?? '').replace(/\\//g, '\\\\')\r\n return `${(match[1] ?? '').toUpperCase()}:\\\\${rest}`\r\n}\r\n\r\n/**\r\n * Canonical Windows drive path for store keys and cross-realm identity:\r\n * separators unified to `\\`, trailing separator stripped, and the WHOLE path\r\n * lowercased — Windows paths compare case-insensitively, and the workspace\r\n * registry may realpath a different casing than the caller spelled (8.3 or\r\n * on-disk casing), so the store key must collide across casings.\r\n * @param path - candidate Windows drive path.\r\n * @returns the canonical form, or `null` when not drive-shaped.\r\n */\r\nexport function canonicalWindowsPath(path: string): string | null {\r\n const match = /^([A-Za-z]):[\\\\/](.*)$/.exec(path)\r\n if (match === null) return null\r\n const rest = (match[2] ?? '').replace(/[\\\\/]+/g, '\\\\').replace(/\\\\$/, '').toLowerCase()\r\n return `${(match[1] ?? '').toLowerCase()}:\\\\${rest}`\r\n}\r\n\r\n/**\r\n * True when a value is a Windows-shaped path (drive or UNC), which is how\r\n * the shell executor decides the WSLENV `/p` translation flag: only Windows\r\n * path values need translation when they cross into the Linux process.\r\n * @param value - the environment value to classify.\r\n * @returns whether the value looks like a Windows path.\r\n */\r\nexport function isWindowsPathShaped(value: string): boolean {\r\n return /^[A-Za-z]:[\\\\/]/.test(value) || value.startsWith('\\\\\\\\')\r\n}\r\n\r\n/** Linux username shape for `wsl.exe -u`: starts with a letter or underscore, then letters/digits/`_`/`.`/`-` (max 64). */\r\nconst WSL_USERNAME_PATTERN = /^[A-Za-z_][A-Za-z0-9_.-]{0,63}$/\r\n\r\n/**\r\n * Whether a value is a safe Linux username for `wsl.exe -u`. The check is\r\n * strict on purpose: a value starting with `-` could be parsed as a wsl.exe\r\n * option instead of a username.\r\n * @param value - candidate username.\r\n * @returns whether it matches the Linux username shape.\r\n */\r\nexport function isValidWslUsername(value: string): boolean {\r\n return WSL_USERNAME_PATTERN.test(value)\r\n}\r\n","/**\r\n * Sidebar footer action that opens the \"Add WSL workspace\" dialog. In the\r\n * wide sidebar it renders a labeled row; in the 56px rail it collapses to a\r\n * 36px icon button (both honor the shell's `{ wide }` owner share).\r\n *\r\n * Registration omits the typed `locale:` seat (the `wslWorkspace` namespace is\r\n * not merged into `LocaleNamespaceMap`), so the injected face carries the\r\n * bound translate function instead.\r\n */\r\n\r\nimport type * as React from 'react'\r\nimport { useEffect, useRef, useState, type UIEventHandler } from 'react'\r\nimport { isAbsoluteLinuxPath, isValidWslUsername, normalizeLinuxPath } from '../shared/paths.ts'\r\nimport type { WslDirListing, WslPathCheck } from './api.ts'\r\n\r\n/** Result-level shape of a browse/check/list call we surface uniformly. */\r\ninterface ApiCall<T> {\r\n value: T\r\n}\r\n\r\n/** The inject face: plain data + callbacks the dialog drives. */\r\nexport interface AddWslWorkspaceInjected {\r\n /**\r\n * Confirm the deployment exposes a healthy `wsl` preset.\r\n * @returns undefined when healthy, else a Chinese/English message to show.\r\n */\r\n checkPreset(): Promise<string | undefined>\r\n /** List the WSL distros installed on the host. */\r\n listDistros(): Promise<string[]>\r\n /** List one Linux directory level inside a distro. */\r\n listDir(distro: string, path: string): Promise<WslDirListing>\r\n /** Check a Linux path's existence/directory facts. */\r\n check(distro: string, path: string): Promise<WslPathCheck>\r\n /**\r\n * Register a WSL workspace and start a session in it. `/mnt/<drive>`\r\n * paths register under their Windows drive spelling (9P cannot serve\r\n * drvfs); ext4 paths register as `\\\\wsl.localhost\\<distro>\\...` UNC.\r\n * @param linuxPath - the absolute Linux workspace path.\r\n * @param username - optional Linux user for the session (empty = distro default).\r\n * @param distro - the WSL distribution the path belongs to.\r\n * @returns undefined on success, else a message to show.\r\n */\r\n createWorkspace(linuxPath: string, username: string, distro: string): Promise<string | undefined>\r\n /** Translate a `wslWorkspace` dictionary key (follows the DeepSeek Harness locale). */\r\n t: (key: string, params?: Record<string, unknown>) => string\r\n}\r\n\r\n/** Full component props: the owner share plus the injected face. */\r\nexport interface AddWslWorkspaceProps extends AddWslWorkspaceInjected {\r\n /** Whether the sidebar renders wide content (false = 56px rail). */\r\n wide: boolean\r\n}\r\n\r\n/**\r\n * Build the Linux child path one level below a parent, for the breadcrumb/\r\n * browse drill.\r\n * @param parent - the currently listed absolute path (`/` for root).\r\n * @param name - the child directory name.\r\n * @returns the child's absolute Linux path.\r\n */\r\nexport function dirChildPath(parent: string, name: string): string {\r\n return parent === '/' ? `/${name}` : `${parent}/${name}`\r\n}\r\n\r\n/** A tiny inline terminal glyph for the dialog's directory rows. */\r\nfunction WslGlyph({ size = 16 }: { size?: number }): React.ReactElement {\r\n return (\r\n <svg width={size} height={size} viewBox=\"0 0 24 24\" fill=\"none\" aria-hidden=\"true\">\r\n <rect x=\"2.5\" y=\"4.5\" width=\"19\" height=\"15\" rx=\"2.5\" stroke=\"currentColor\" strokeWidth=\"1.6\" />\r\n <path d=\"M6 9l3.2 2.6L6 14\" stroke=\"currentColor\" strokeWidth=\"1.6\" strokeLinecap=\"round\" strokeLinejoin=\"round\" />\r\n <path d=\"M12 14h5\" stroke=\"currentColor\" strokeWidth=\"1.6\" strokeLinecap=\"round\" />\r\n </svg>\r\n )\r\n}\r\n\r\n/**\r\n * The \"Add WSL workspace…\" footer action and its dialog.\r\n * @param props - owner share + injected face.\r\n */\r\nexport function AddWslWorkspace({ wide, t, checkPreset, listDistros, listDir, check, createWorkspace }: AddWslWorkspaceProps): React.ReactElement | null {\r\n const [open, setOpen] = useState(false)\r\n const [opening, setOpening] = useState(false)\r\n const [distros, setDistros] = useState<string[]>([])\r\n const [distro, setDistro] = useState('')\r\n const [pathInput, setPathInput] = useState('/home/')\r\n const [username, setUsername] = useState('')\r\n const [listing, setListing] = useState<WslDirListing | null>(null)\r\n const [browsePath, setBrowsePath] = useState('/')\r\n const [browsing, setBrowsing] = useState(false)\r\n const [error, setError] = useState<string | null>(null)\r\n const [busy, setBusy] = useState(false)\r\n // Monotone browse-request sequence: stale responses for a superseded browse are dropped.\r\n const browseSeq = useRef(0)\r\n\r\n const refreshBrowse = async (root: string, targetDistro: string): Promise<void> => {\r\n const seq = ++browseSeq.current\r\n setBrowsing(true)\r\n setBrowsePath(root)\r\n try {\r\n const value = await listDir(targetDistro, root)\r\n if (seq === browseSeq.current) setListing(value)\r\n } catch {\r\n // A failed browse (permission, missing dir) is non-fatal: keep old listing.\r\n if (seq === browseSeq.current) {\r\n setListing(null)\r\n setError((previous) => previous ?? t('error.loadDir'))\r\n }\r\n } finally {\r\n if (seq === browseSeq.current) setBrowsing(false)\r\n }\r\n }\r\n\r\n useEffect(() => {\r\n if (!open) return\r\n let cancelled = false\r\n setError(null)\r\n setOpening(true)\r\n void (async () => {\r\n let presetIssue: string | undefined\r\n try {\r\n presetIssue = await checkPreset()\r\n } catch {\r\n presetIssue = t('error.loadDistros')\r\n }\r\n let names: string[]\r\n try {\r\n names = await listDistros()\r\n } catch {\r\n if (cancelled) return\r\n setOpening(false)\r\n setError(t('error.loadDistros'))\r\n return\r\n }\r\n if (cancelled) return\r\n setDistros(names)\r\n const first = names[0] ?? ''\r\n setDistro(first)\r\n // The default browse root walks from `/`; the input defaults to `/home/`.\r\n setBrowsing(true)\r\n setOpening(false)\r\n if (presetIssue !== undefined) setError(presetIssue)\r\n if (first !== '') void refreshBrowse('/', first)\r\n })()\r\n return () => { cancelled = true }\r\n // eslint-disable-next-line react-hooks/exhaustive-deps -- run once per open against current t.\r\n }, [open])\r\n\r\n useEffect(() => {\r\n if (!open) return\r\n const onKey = (event: KeyboardEvent): void => {\r\n if (event.key === 'Escape' && !busy) setOpen(false)\r\n }\r\n window.addEventListener('keydown', onKey)\r\n return () => window.removeEventListener('keydown', onKey)\r\n }, [open, busy])\r\n\r\n if (!open) {\r\n // A W-letter action beside Settings at the sidebar foot; the title\r\n // carries the label in both wide and rail states.\r\n return (\r\n <button\r\n type=\"button\"\r\n className={wide ? 'dww-action dww-action--wide' : 'dww-action dww-action--rail'}\r\n title={t('action.title')}\r\n aria-label={t('action.title')}\r\n onClick={() => setOpen(true)}\r\n >\r\n <span className=\"dww-letter\" aria-hidden=\"true\">W</span>\r\n </button>\r\n )\r\n }\r\n\r\n const onDrill = (name: string): void => {\r\n const next = dirChildPath(listing?.path ?? browsePath, name)\r\n setPathInput(next)\r\n void refreshBrowse(next, distro)\r\n }\r\n\r\n const onUp = (): void => {\r\n const parent = listing?.parent ?? null\r\n if (parent === null) return\r\n setPathInput(parent)\r\n void refreshBrowse(parent, distro)\r\n }\r\n\r\n const onDistroChange = (value: string): void => {\r\n setDistro(value)\r\n void refreshBrowse(browsePath, value)\r\n }\r\n\r\n const onCheck = async (): Promise<void> => {\r\n const path = normalizeLinuxPath(pathInput)\r\n setError(null)\r\n if (!isAbsoluteLinuxPath(path) || path === '/') {\r\n setError(t('error.invalidPath'))\r\n return\r\n }\r\n let facts: WslPathCheck\r\n try {\r\n facts = await check(distro, path)\r\n } catch {\r\n setError(t('error.pathNotFound'))\r\n return\r\n }\r\n if (!facts.exists || !facts.isDirectory) {\r\n setError(t('error.pathNotFound'))\r\n return\r\n }\r\n void refreshBrowse(path, distro)\r\n }\r\n\r\n const onConfirm = async (): Promise<void> => {\r\n const path = normalizeLinuxPath(pathInput)\r\n setError(null)\r\n if (!isAbsoluteLinuxPath(path) || path === '/') {\r\n // A workspace at the distribution root would make every session start\r\n // at `/`; the check flow rejects it and confirm must agree.\r\n setError(t('error.invalidPath'))\r\n return\r\n }\r\n const user = username.trim()\r\n if (user !== '' && !isValidWslUsername(user)) {\r\n setError(t('error.invalidUsername'))\r\n return\r\n }\r\n setBusy(true)\r\n try {\r\n let facts: WslPathCheck\r\n try {\r\n facts = await check(distro, path)\r\n } catch {\r\n setError(t('error.pathNotFound'))\r\n return\r\n }\r\n if (!facts.exists || !facts.isDirectory) {\r\n setError(t('error.pathNotFound'))\r\n return\r\n }\r\n const failure = await createWorkspace(path, user, distro)\r\n if (failure !== undefined) {\r\n setError(failure)\r\n return\r\n }\r\n setOpen(false)\r\n } finally {\r\n setBusy(false)\r\n }\r\n }\r\n\r\n const children = (listing?.entries.filter(entry => entry.kind === 'directory') ?? []).map(entry => entry.name)\r\n const maskClick = (): void => { if (!busy) setOpen(false) }\r\n const listScroll: UIEventHandler<HTMLDivElement> = () => { /* scroll container handles overflow */ }\r\n\r\n return (\r\n <div className=\"dww-overlay\">\r\n <div className=\"dww-overlay-mask\" onClick={maskClick} />\r\n <div className=\"dww-card\" role=\"dialog\" aria-modal=\"true\" aria-label={t('dialog.title')}>\r\n <div className=\"dww-header\">\r\n <h2 className=\"dww-title\">{t('dialog.title')}</h2>\r\n <button type=\"button\" className=\"dww-close\" aria-label={t('dialog.cancel')} onClick={maskClick}>\r\n ✕\r\n </button>\r\n </div>\r\n <div className=\"dww-body\">\r\n {error !== null ? (\r\n <div className=\"dww-error\">\r\n {error}\r\n <button type=\"button\" className=\"dww-retry\" onClick={() => setError(null)}>{t('dialog.retry')}</button>\r\n </div>\r\n ) : null}\r\n <div className=\"dww-field\">\r\n <label className=\"dww-field-label\" htmlFor=\"dww-distro\">{t('dialog.distro')}</label>\r\n <select\r\n id=\"dww-distro\"\r\n className=\"dww-select\"\r\n value={distro}\r\n disabled={opening || busy}\r\n onChange={event => onDistroChange(event.target.value)}\r\n >\r\n {distros.length === 0\r\n ? <option value=\"\">{opening ? t('dialog.loading') : ''}</option>\r\n : distros.map(name => <option key={name} value={name}>{name}</option>)}\r\n </select>\r\n </div>\r\n <div className=\"dww-field\">\r\n <label className=\"dww-field-label\" htmlFor=\"dww-path\">{t('dialog.path')}</label>\r\n <div className=\"dww-input-row\">\r\n <input\r\n id=\"dww-path\"\r\n className=\"dww-input\"\r\n value={pathInput}\r\n placeholder={t('dialog.pathPlaceholder')}\r\n disabled={opening || busy}\r\n onChange={event => setPathInput(event.target.value)}\r\n />\r\n <button type=\"button\" className=\"dww-check-btn\" disabled={opening || busy} onClick={() => void onCheck()}>\r\n {t('dialog.check')}\r\n </button>\r\n </div>\r\n </div>\r\n <div className=\"dww-field\">\r\n <label className=\"dww-field-label\" htmlFor=\"dww-username\">{t('dialog.username')}</label>\r\n <input\r\n id=\"dww-username\"\r\n className=\"dww-input\"\r\n value={username}\r\n placeholder={t('dialog.usernamePlaceholder')}\r\n disabled={opening || busy}\r\n autoComplete=\"off\"\r\n spellCheck={false}\r\n onChange={event => setUsername(event.target.value)}\r\n />\r\n </div>\r\n <div className=\"dww-feedback\">\r\n <div className=\"dww-breadcrumb\">{browsePath}</div>\r\n <div className=\"dww-dirlist\" onScroll={listScroll}>\r\n {browsing ? <div className=\"dww-dir-empty\">{t('dialog.loading')}</div> : (\r\n listing?.parent !== null && listing !== null\r\n ? (\r\n <button type=\"button\" className=\"dww-dir-row dww-dir-row--up\" onClick={onUp}>\r\n <WslGlyph size={14} />\r\n <span>{t('dialog.upLevel')}</span>\r\n </button>\r\n )\r\n : null\r\n )}\r\n {!browsing && (children.length === 0)\r\n ? <div className=\"dww-dir-empty\">{t('dialog.browseEmpty')}</div>\r\n : children.map(name => (\r\n <button type=\"button\" key={name} className=\"dww-dir-row\" onClick={() => onDrill(name)}>\r\n <WslGlyph size={14} />\r\n <span>{name}</span>\r\n </button>\r\n ))}\r\n </div>\r\n </div>\r\n </div>\r\n <div className=\"dww-actions\">\r\n <button type=\"button\" className=\"dww-btn\" disabled={busy} onClick={maskClick}>{t('dialog.cancel')}</button>\r\n <button type=\"button\" className=\"dww-btn dww-btn--primary\" disabled={busy || opening} onClick={() => void onConfirm()}>\r\n {busy ? t('dialog.loading') : t('dialog.confirm')}\r\n </button>\r\n </div>\r\n </div>\r\n </div>\r\n )\r\n}\r\n\r\nexport type { ApiCall }\r\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","/**\r\n * Bilingual dictionaries for the `wslWorkspace` locale namespace. Product copy\r\n * is Chinese; English is the parallel export for the standalone bundle.\r\n */\r\n\r\n/**\r\n * The `wslWorkspace` translations (Chinese, the primary product copy).\r\n */\r\nexport const zh: Record<string, string> = {\r\n 'action.add': 'WSL 工作区',\r\n 'action.title': '添加 WSL 工作区…',\r\n\r\n 'dialog.title': '添加 WSL 工作区',\r\n 'dialog.distro': '发行版',\r\n 'dialog.path': '路径',\r\n 'dialog.pathPlaceholder': '/home/',\r\n 'dialog.username': '用户名',\r\n 'dialog.usernamePlaceholder': '留空则使用发行版默认用户',\r\n 'dialog.loading': '正在加载…',\r\n 'dialog.browseEmpty': '此目录没有子文件夹',\r\n 'dialog.upLevel': '..(返回上级)',\r\n 'dialog.browse': '浏览',\r\n 'dialog.check': '检查',\r\n 'dialog.confirm': '创建并打开',\r\n 'dialog.cancel': '取消',\r\n 'dialog.retry': '重试',\r\n\r\n 'error.loadDistros': '无法获取 WSL 发行版列表,请确认已安装 WSL 且插件宿主端可用',\r\n 'error.rateLimited': '操作过于频繁,请稍后重试',\r\n 'error.loadDir': '无法浏览该目录',\r\n 'error.presetMissing': '未找到健康的 wsl preset,请确认插件宿主端已安装并配置该 preset',\r\n 'error.invalidPath': '请输入以 / 开头的 Linux 绝对路径',\r\n 'error.invalidUsername': '用户名无效:需以字母或下划线开头,仅含字母、数字、_、.、-',\r\n 'error.pathNotFound': '该路径不存在或是文件,请选择一个文件夹',\r\n 'error.createFailed': '创建工作区失败',\r\n}\r\n\r\n/**\r\n * The `wslWorkspace` translations (English).\r\n */\r\nexport const en: Record<string, string> = {\r\n 'action.add': 'WSL Workspace',\r\n 'action.title': 'Add WSL workspace…',\r\n\r\n 'dialog.title': 'Add WSL workspace',\r\n 'dialog.distro': 'Distro',\r\n 'dialog.path': 'Path',\r\n 'dialog.pathPlaceholder': '/home/',\r\n 'dialog.username': 'Username',\r\n 'dialog.usernamePlaceholder': 'Leave empty to use the distro default user',\r\n 'dialog.loading': 'Loading…',\r\n 'dialog.browseEmpty': 'No subdirectories here',\r\n 'dialog.upLevel': '.. (up)',\r\n 'dialog.browse': 'Browse',\r\n 'dialog.check': 'Check',\r\n 'dialog.confirm': 'Create & open',\r\n 'dialog.cancel': 'Cancel',\r\n 'dialog.retry': 'Retry',\r\n\r\n 'error.loadDistros': 'Could not list WSL distros; confirm WSL is installed and the plugin host side is reachable',\r\n 'error.rateLimited': 'Too many attempts; retry in a moment',\r\n 'error.loadDir': 'Could not browse this directory',\r\n 'error.presetMissing': 'No healthy \"wsl\" preset found; confirm the plugin host side installed and configured it',\r\n 'error.invalidPath': 'Enter an absolute Linux path starting with /',\r\n 'error.invalidUsername': 'Invalid username: start with a letter or underscore; only letters, digits, _ . -',\r\n 'error.pathNotFound': 'The path does not exist or is a file; choose a folder',\r\n 'error.createFailed': 'Failed to create the workspace',\r\n}\r\n","/**\r\n * Browser half of dsh-wsl-workspace. Registers the \"Add WSL workspace…\"\r\n * action beside Settings at the sidebar foot (the official\r\n * `sidebar.footer.action` slot), and keeps every blank session whose\r\n * workspace is a WSL UNC path composed from the WSL VARIANT of the mode it\r\n * currently runs (`standard` → `wsl-standard`, PTC → `wsl-code`, …) — so the\r\n * WSL execution world composes with any mode instead of being a mode itself.\r\n *\r\n * The binding is a watching effect rather than a one-shot dialog action so\r\n * EVERY creation path (this dialog, the workspace row's New Session, the\r\n * hero picker) converges on the WSL-backed composition automatically.\r\n */\r\n\r\n// Type-only: pulls the locale plugin's Context merge (ctx.locale) and the\r\n// ui-sidebar SlotMap merge (the 'sidebar.footer.action' entry) into this\r\n// program. Version-dependent services (`remote`, `connection.api`,\r\n// `uiWorkspace`) are read dynamically via ctx.get() so both DSH v0.1.1-rc.2\r\n// and v0.1.2-rc.1+ can load this plugin.\r\nimport type {} from '@deepseek-ai/dsh-client-locale/client'\r\nimport type {} from '@deepseek-ai/dsh-client-ui-sidebar/client'\r\nimport type { ClientContext } from '@deepseek-ai/dsh-client-runtime/client'\r\nimport { check as checkApi, listDir as listDirApi, listDistros as listDistrosApi, listWorkspaces as listWorkspacesApi, registerWindows as registerWindowsApi, setWorkspaceUser as setWorkspaceUserApi } from './api.ts'\r\nimport { AddWslWorkspace, type AddWslWorkspaceInjected } from './AddWslWorkspace.tsx'\r\nimport { ensureStyles } from './styles.ts'\r\nimport { zh, en } from './locales.ts'\r\nimport { canonicalWindowsPath, isWslUnc, joinUnc, mntToWindowsPath } from '../shared/paths.ts'\r\n\r\n/** Required services (cordis fiber inject). */\r\nexport const inject = ['slots', 'locale', 'sessions', 'workspaces']\r\n\r\n/** The legacy standalone WSL preset id (folded into the mode variants). */\r\nconst LEGACY_WSL_PRESET_ID = 'wsl'\r\n\r\n/**\r\n * Minimal sessions-service face. The renderer-host ctx merge types\r\n * `ctx.sessions` as its own SessionStore; the service the runtime actually\r\n * registers under that key satisfies this narrower contract, so the cast is\r\n * the documented boundary for a third-party plugin.\r\n */\r\ninterface WslSessionsFace {\r\n list: {\r\n getSnapshot(): {\r\n ids: string[]\r\n byId: Record<string, {\r\n blank: boolean\r\n cwd?: string\r\n /** v0.1.1-rc.2 direct field */\r\n agentPreset?: string\r\n /** v0.1.2-rc.1+ projection value */\r\n projectionValues?: Readonly<{ agentPreset?: string | null }>\r\n }>\r\n }\r\n subscribe(fn: () => void): () => void\r\n }\r\n /** v0.1.1-rc.2 only — absent in v0.1.2-rc.1+ (projection auto-syncs). */\r\n noteAgentPreset?(sessionId: string, agentPreset: string): void\r\n}\r\n\r\n/** Minimal workspaces-service face (create only; startSession moved out in v0.1.2-rc.1+). */\r\ninterface WslWorkspacesFace {\r\n create(input: { path: string }): Promise<{ workspaceId: string }>\r\n /** v0.1.1-rc.2 only — removed in v0.1.2-rc.1+. */\r\n startSession?(workspaceId?: string): void\r\n}\r\n\r\n/** v0.1.2-rc.1+ only — replaces `workspaces.startSession`. */\r\ninterface WslUiWorkspaceFace {\r\n startSession(workspaceId?: string): void\r\n}\r\n\r\n/** v0.1.2-rc.1+ agentPresets 命名空间服务(经 ctx.get('remote.agentPresets') 动态取,免 inject、无 associate 陷阱)。 */\r\ninterface WslAgentPresetsNamespace {\r\n list(): Promise<{ ok: boolean; value?: { presets: { id: string; broken?: string; isDefault?: boolean }[] }; error?: { message: string } }>\r\n select(sessionId: string, presetId: string): Promise<{ ok: boolean }>\r\n}\r\n\r\n/** 旧版 connection 服务最小接口(v0.1.1-rc.2 及更早),api 属性承载远程调用。 */\r\ninterface WslLegacyConnection {\r\n api?: {\r\n agentPresets: {\r\n list(input: Record<string, never>): Promise<{ result: { ok: boolean; value?: { presets: { id: string; broken?: string; isDefault?: boolean }[] }; error?: { message: string } } }>\r\n select(input: { sessionId: string; agentPreset: string }): Promise<{ result: { ok: boolean } }>\r\n }\r\n }\r\n}\r\n/**\r\n * Mount the sidebar action and the auto-binding effect.\r\n * @param ctx - the browser plugin context.\r\n */\r\nexport function apply(ctx: ClientContext): void {\r\n const workspaces = ctx.get('workspaces') as unknown as WslWorkspacesFace\r\n const sessions = ctx.get('sessions') as unknown as WslSessionsFace\r\n // Version-dependent services are resolved ON USE - never through `inject`\r\n // (a release without the service would refuse to mount the plugin at all)\r\n // and never once at apply time. This plugin applies before the UI domain\r\n // that publishes `uiWorkspace` registers its service, so a one-shot read\r\n // caches `undefined` for the whole page life: on v0.1.2-rc.1+ that silently\r\n // disabled session creation (see resolveSessionStarter).\r\n const legacyApi = (): WslLegacyConnection['api'] =>\r\n (ctx.get('connection') as unknown as WslLegacyConnection | undefined)?.api\r\n const remoteAgentPresets = (): WslAgentPresetsNamespace | undefined =>\r\n ctx.get('remote.agentPresets') as unknown as WslAgentPresetsNamespace | undefined\r\n const uiWorkspaceService = (): WslUiWorkspaceFace | undefined =>\r\n ctx.get('uiWorkspace') as unknown as WslUiWorkspaceFace | undefined\r\n const hasNoteAgentPreset = typeof sessions.noteAgentPreset === 'function'\r\n\r\n /** Unified agent-preset list: new `remote.agentPresets` namespace (v0.1.2-rc.1+) or legacy `connection.api` (v0.1.1-rc.2). */\r\n const listAgentPresets = async (): Promise<{ ok: boolean; presets: { id: string; broken?: string; isDefault?: boolean }[]; error?: string }> => {\r\n const agentPresets = remoteAgentPresets()\r\n if (agentPresets !== undefined) {\r\n const r = await agentPresets.list()\r\n if (!r.ok) return { ok: false, presets: [], error: r.error?.message ?? 'list failed' }\r\n return { ok: true, presets: r.value?.presets ?? [] }\r\n }\r\n const api = legacyApi()\r\n if (api !== undefined) {\r\n const r = await api.agentPresets.list({})\r\n if (!r.result.ok) return { ok: false, presets: [], error: r.result.error?.message ?? 'list failed' }\r\n return { ok: true, presets: r.result.value?.presets ?? [] }\r\n }\r\n return { ok: false, presets: [], error: 'no remote api available' }\r\n }\r\n\r\n /** Unified agent-preset select: new `agentPresets.select(id, preset)` or legacy `connection.api.select({...})`. */\r\n const selectAgentPreset = async (sessionId: string, presetId: string): Promise<{ ok: boolean }> => {\r\n const agentPresets = remoteAgentPresets()\r\n if (agentPresets !== undefined) {\r\n return agentPresets.select(sessionId, presetId)\r\n }\r\n const api = legacyApi()\r\n if (api !== undefined) {\r\n const r = await api.agentPresets.select({ sessionId, agentPreset: presetId })\r\n return { ok: r.result.ok }\r\n }\r\n return { ok: false }\r\n }\r\n\r\n /**\r\n * Resolve how this release opens a session for a workspace - v0.1.2-rc.1+\r\n * exposes `uiWorkspace.startSession`, v0.1.1-rc.2 keeps it on `workspaces`.\r\n *\r\n * Resolved BEFORE the workspace is written. A release that offers neither\r\n * cannot open a session, and a silent fall-through would leave the workspace\r\n * behind with an empty `sessionIds` while the dialog still reports success;\r\n * failing here names the missing capability instead.\r\n * @returns the starter for the service this release actually exposes.\r\n * @throws Error naming both candidates when neither service is available.\r\n */\r\n const resolveSessionStarter = (): (workspaceId: string) => void | Promise<void> => {\r\n const ui = uiWorkspaceService()\r\n if (ui !== undefined) return (workspaceId) => { ui.startSession(workspaceId) }\r\n const legacyStart = workspaces.startSession\r\n if (typeof legacyStart === 'function') {\r\n return (workspaceId) => { Reflect.apply(legacyStart, workspaces, [workspaceId]) }\r\n }\r\n throw new Error(\r\n 'workspace session API unavailable: this DSH release exposes neither '\r\n + 'uiWorkspace.startSession nor workspaces.startSession',\r\n )\r\n }\r\n\r\n /** Read agent preset — v0.1.2-rc.1+ uses projectionValues; v0.1.1-rc.2 uses direct field. */\r\n const getAgentPreset = (summary: { agentPreset?: string; projectionValues?: { agentPreset?: string | null } }): string | undefined => {\r\n if (summary.projectionValues?.agentPreset !== undefined) {\r\n const v = summary.projectionValues.agentPreset\r\n return v === null ? undefined : v\r\n }\r\n return summary.agentPreset\r\n }\r\n\r\n /** Note preset change — v0.1.1-rc.2 calls noteAgentPreset; v0.1.2-rc.1+ is auto-synced via projection. */\r\n const noteAgentPresetCompat = (sessionId: string, presetId: string): void => {\r\n if (hasNoteAgentPreset && sessions.noteAgentPreset) {\r\n sessions.noteAgentPreset(sessionId, presetId)\r\n }\r\n }\r\n\r\n ensureStyles()\r\n\r\n ctx.effect(\r\n () => ctx.locale.register('wslWorkspace' as never, { zh, en }),\r\n 'dsh-wsl-workspace: locale dictionaries',\r\n )\r\n\r\n // The injected translate function reads the live DeepSeek Harness locale,\r\n // so the dialog copy follows the app language setting automatically.\r\n const t = ctx.locale.bind('wslWorkspace' as never) as unknown as (key: string, params?: Record<string, unknown>) => string\r\n\r\n // Canonical Windows drive keys of every registered `/mnt/<drive>` workspace\r\n // (refreshed from the host store; see refreshRoster). A blank session whose\r\n // cwd is one of these binds to the WSL variant like a UNC-cwd session.\r\n let wslWindowsPaths = new Set<string>()\r\n\r\n const injected = (): AddWslWorkspaceInjected => ({\r\n t,\r\n checkPreset: async (): Promise<string | undefined> => {\r\n let roster\r\n try {\r\n roster = await listAgentPresets()\r\n } catch (error) {\r\n return error instanceof Error ? error.message : String(error)\r\n }\r\n if (!roster.ok) return roster.error\r\n const healthy = roster.presets.find((entry: { id: string; broken?: string }) =>\r\n entry.id.startsWith('wsl-') && entry.broken === undefined)\r\n if (healthy === undefined) return t('error.presetMissing')\r\n return undefined\r\n },\r\n listDistros: () => listDistrosApi(),\r\n listDir: (distro, path) => listDirApi(distro, path),\r\n check: (distro, path) => checkApi(distro, path),\r\n createWorkspace: async (linuxPath, username, distro): Promise<string | undefined> => {\r\n try {\r\n // Before any write: without a session starter the workspace below\r\n // would be created and never opened.\r\n const startSession = resolveSessionStarter()\r\n const winPath = mntToWindowsPath(linuxPath)\r\n if (winPath !== null) {\r\n // `/mnt/<drive>` workspace: the workspace registry realpath/stats\r\n // the path and 9P cannot serve drvfs mounts, so register under the\r\n // drive spelling and store distro/username for the session env.\r\n // The browser binding below recognizes the drive cwd as WSL.\r\n const view = await workspaces.create({ path: winPath })\r\n await registerWindowsApi(linuxPath, distro, username)\r\n const canonical = canonicalWindowsPath(winPath)\r\n if (canonical !== null) wslWindowsPaths = new Set(wslWindowsPaths).add(canonical)\r\n await startSession(view.workspaceId)\r\n return undefined\r\n }\r\n const uncPath = joinUnc(distro, linuxPath)\r\n const view = await workspaces.create({ path: uncPath })\r\n await setWorkspaceUserApi(uncPath, username)\r\n await startSession(view.workspaceId)\r\n return undefined\r\n } catch (error) {\r\n return error instanceof Error ? error.message : String(error)\r\n }\r\n },\r\n })\r\n\r\n ctx.effect(\r\n () => ctx.slots.inject(\r\n 'sidebar.footer.action',\r\n () => ctx.slots.register(\r\n { name: 'sidebar.footer.action', id: 'wsl-workspace', inject: injected },\r\n AddWslWorkspace,\r\n ),\r\n ),\r\n 'dsh-wsl-workspace: sidebar footer action',\r\n )\r\n\r\n // Mode-variant binding: a blank session whose workspace is a WSL UNC path\r\n // is recomposed to the WSL variant of the mode it currently runs — plain\r\n // 标准 becomes `wsl-standard`, PTC becomes `wsl-code`, and so on — so the\r\n // WSL execution world composes with ANY mode instead of replacing it. The\r\n // host refuses non-blank sessions (agent-preset-locked), so the swap is\r\n // attempted at most a few times per session.\r\n ctx.effect(() => {\r\n const inFlight = new Set<string>()\r\n const attempts = new Map<string, number>()\r\n const MAX_ATTEMPTS = 3\r\n // Healthy `wsl-<mode>` variant ids plus the roster's default preset id\r\n // (what a session with no explicit choice gets). Refreshed periodically\r\n // so variants generated after this page loaded are picked up.\r\n let variants = new Set<string>()\r\n let defaultPreset: string | undefined\r\n const refreshRoster = (): void => {\r\n void listAgentPresets().then((result: {\r\n ok: boolean; presets: { id: string; broken?: string; isDefault?: boolean }[]\r\n }) => {\r\n if (!result.ok) return\r\n variants = new Set(result.presets\r\n .filter((entry: { id: string; broken?: string }) =>\r\n entry.broken === undefined && entry.id.startsWith('wsl-'))\r\n .map((entry: { id: string }) => entry.id))\r\n defaultPreset = result.presets.find(\r\n (entry: { id: string; isDefault?: boolean }) => entry.isDefault === true,\r\n )?.id\r\n // The roster is an INPUT to binding. It can land after the first pass\r\n // (and after apply()), and nothing else would re-run that pass: the\r\n // session store only emits when a session changes, so a blank session\r\n // that was already there would stay unbound until the user acted.\r\n maybeBind()\r\n }).catch(() => {\r\n // A failed roster read leaves the previous mapping; sessions stay on\r\n // their current composition until the next refresh.\r\n })\r\n }\r\n refreshRoster()\r\n const refreshWorkspaces = (): void => {\r\n void listWorkspacesApi().then((keys: string[]) => {\r\n const next = new Set<string>()\r\n for (const key of keys) {\r\n const canonical = canonicalWindowsPath(key)\r\n if (canonical !== null) next.add(canonical)\r\n }\r\n wslWindowsPaths = next\r\n // Same late-input rule as the roster: the `/mnt/<drive>` key set\r\n // decides binding for drive-cwd sessions.\r\n maybeBind()\r\n }).catch(() => {\r\n // A failed store read leaves the previous set; sessions stay on\r\n // their current composition until the next refresh.\r\n })\r\n }\r\n refreshWorkspaces()\r\n const maybeBind = (): void => {\r\n const state = sessions.list.getSnapshot()\r\n for (const id of state.ids) {\r\n const summary = state.byId[id]\r\n if (summary === undefined || !summary.blank || summary.cwd === undefined) continue\r\n // A session belongs to the WSL world when its cwd is a WSL UNC path,\r\n // or a Windows drive path registered as a `/mnt/<drive>` workspace\r\n // (9P cannot serve drvfs, so those workspaces carry drive cwds).\r\n const canonical = canonicalWindowsPath(summary.cwd)\r\n const isWsl = isWslUnc(summary.cwd)\r\n || (canonical !== null && wslWindowsPaths.has(canonical))\r\n if (!isWsl) continue\r\n const current = getAgentPreset(summary)\r\n if (current !== undefined && current.startsWith('wsl-')) continue\r\n // Legacy standalone `wsl` (now folded into the variants): remap it to\r\n // the default mode's variant, since the standalone preset no longer\r\n // exists in the roster.\r\n const base = current === LEGACY_WSL_PRESET_ID\r\n ? (defaultPreset ?? 'standard')\r\n : (current ?? defaultPreset)\r\n if (base === undefined || base === LEGACY_WSL_PRESET_ID || base.startsWith('wsl-')) continue\r\n const target = `wsl-${base.toLowerCase()}`\r\n if (!variants.has(target)) continue\r\n if (inFlight.has(id) || (attempts.get(id) ?? 0) >= MAX_ATTEMPTS) continue\r\n inFlight.add(id)\r\n void selectAgentPreset(id, target)\r\n .then((result: { ok: boolean }) => {\r\n if (result.ok) noteAgentPresetCompat(id, target)\r\n })\r\n .catch(() => {\r\n // A refused or aborted swap (session already produced output,\r\n // roster churn, reconnect) leaves the session on its current\r\n // composition; count the attempt so a stuck session stops\r\n // retrying after MAX_ATTEMPTS.\r\n attempts.set(id, (attempts.get(id) ?? 0) + 1)\r\n })\r\n .finally(() => {\r\n inFlight.delete(id)\r\n })\r\n }\r\n }\r\n maybeBind()\r\n const unsubscribe = sessions.list.subscribe(() => maybeBind())\r\n // Variants are generated at host boot; a page loaded before that would\r\n // never see them without a periodic refresh.\r\n const timer = window.setInterval(refreshRoster, 60_000)\r\n return () => {\r\n unsubscribe()\r\n window.clearInterval(timer)\r\n }\r\n }, 'dsh-wsl-workspace: WSL mode-variant binding')\r\n}\r\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"client.js","names":["response: Response","envelope: Envelope<T>","listDir","describe","presetIssue: string | undefined","names: string[]","listDistros","facts: WslPathCheck","check","listScroll: UIEventHandler<HTMLDivElement>","zh: Record<string, string>","en: Record<string, string>","listDistrosApi","describeApi","listDirApi","checkApi","view","registerWindowsApi","setWorkspaceUserApi","defaultPreset: string | undefined","listWorkspacesApi"],"sources":["../src/client/api.ts","../src/shared/paths.ts","../src/client/help.tsx","../src/client/AddWslWorkspace.tsx","../src/client/styles.ts","../src/client/locales.ts","../src/client/index.ts"],"sourcesContent":["/**\n * Thin fetch client for the Host plugin route. The browser calls\n * POST /wsl-workspace/api with a `{ method, params }` envelope and the Host\n * answers `{ ok: true, value }` or `{ ok: false, error }`.\n */\n\n/** Relative route the Host half registers (same-origin with the web server). */\nconst ENDPOINT = '/wsl-workspace/api'\n\n/** One directory entry as the Host lists it. */\nexport interface WslDirEntry {\n name: string\n kind: 'directory' | 'file' | 'other'\n}\n\n/** One directory level plus its breadcrumb ancestry. */\nexport interface WslDirListing {\n /** The listed absolute Linux path. */\n path: string\n /** Parent Linux path, or null at the filesystem root. */\n parent: string | null\n /** The level's children (in name order; the client filters to directories). */\n entries: WslDirEntry[]\n}\n\n/** Existence/directory check result for one Linux path. */\nexport interface WslPathCheck {\n exists: boolean\n isDirectory: boolean\n}\n\n/** Wire envelope the Host route answers with. */\ntype Envelope<T> = { ok: true; value: T } | { ok: false; error: string }\n\n/** Human text for an unknown rejection, reusing the repository's idiom. */\nfunction errorMessage(value: unknown): string {\n return value instanceof Error ? value.message : String(value)\n}\n\n/**\n * Perform one POST call and unwrap the envelope.\n * @param method - the Host method name.\n * @param params - the method payload.\n * @returns the unwrapped value, or throws an Error on network or `ok:false`.\n */\nasync function call<T>(method: string, params: Record<string, unknown> = {}): Promise<T> {\n let response: Response\n try {\n response = await fetch(ENDPOINT, {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({ method, params }),\n })\n } catch (error) {\n // The transport refused before answering (offline, origin mismatch, 404).\n throw new Error(`wsl-workspace request failed: ${errorMessage(error)}`)\n }\n let envelope: Envelope<T>\n try {\n envelope = (await response.json()) as Envelope<T>\n } catch {\n // A non-JSON body means a proxy/loader answered instead of the Host route.\n throw new Error(`wsl-workspace answered non-JSON (${response.status})`)\n }\n if (!envelope.ok) throw new Error(envelope.error)\n return envelope.value\n}\n\n/**\n * List the WSL distros installed on the host.\n * @returns distro names in registry order.\n */\nexport async function listDistros(): Promise<string[]> {\n return call<string[]>('listDistros', {})\n}\n\n/**\n * List one directory level inside a distro.\n * @param distro - distro name.\n * @param path - absolute Linux directory to list.\n * @returns the level's listing with ancestry.\n */\nexport async function listDir(distro: string, path: string): Promise<WslDirListing> {\n return call<WslDirListing>('listDir', { distro, path })\n}\n\n/**\n * Check whether a Linux path exists and is a directory.\n * @param distro - distro name.\n * @param path - absolute Linux path.\n * @returns existence and directory facts.\n */\nexport async function check(distro: string, path: string): Promise<WslPathCheck> {\n return call<WslPathCheck>('check', { distro, path })\n}\n\n/**\n * Store (or clear, with an empty string) the username of one WSL workspace.\n * @param path - the workspace UNC path.\n * @param username - the Linux username; empty string clears the stored value.\n */\nexport async function setWorkspaceUser(path: string, username: string): Promise<void> {\n return call<void>('setUser', { path, username })\n}\n\n/**\n * Register a `/mnt/<drive>` WSL workspace under its Windows drive path,\n * recording the distro (and optional username) for the session env.\n * @param linuxPath - the `/mnt/<drive>/…` Linux path.\n * @param distro - the WSL distribution the workspace belongs to.\n * @param username - optional Linux username.\n */\nexport async function registerWindows(linuxPath: string, distro: string, username: string): Promise<void> {\n return call<void>('registerWindows', { linuxPath, distro, username })\n}\n\n/**\n * List every registered WSL workspace key (canonical UNC and Windows drive\n * spellings). The client uses the drive keys to recognize `/mnt` workspaces\n * across page reloads.\n */\nexport async function listWorkspaces(): Promise<string[]> {\n return call<string[]>('listWorkspaces', {})\n}\n\n/** One declared DSH release and its declared status. */\nexport interface WslDeclaredRelease {\n id: string\n status: string\n}\n\n/** Self-description of the running plugin build (shown by the help panel). */\nexport interface WslSelfDescription {\n version: string\n releases: WslDeclaredRelease[]\n}\n\n/**\n * Read the plugin build version and its declared DSH compatibility matrix,\n * for the dialog help panel.\n * @returns the self-description reported by the host plugin.\n */\nexport async function describe(): Promise<WslSelfDescription> {\n return call<WslSelfDescription>('describe', {})\n}\n","/**\n * WSL path helpers shared by the client and host halves. Pure and\n * dependency-free so both planes can import them without a runtime edge.\n */\n\n/** WSL2 default loopback bridge host: `\\\\wsl.localhost\\<distro>\\...`. */\nconst WSL_LOCALHOST_HOST = 'wsl.localhost'\n/** Legacy WSL interop host: `\\\\wsl$\\<distro>\\...`. */\nconst WSL_LEGACY_HOST = 'wsl$'\n\n/** The two UNC hosts WSL exposes a distribution's filesystem under. */\nconst UNC_HOSTS = [WSL_LOCALHOST_HOST, WSL_LEGACY_HOST]\n\n/** One WSL workspace coordinate parsed out of a UNC path. */\nexport interface WslUncTarget {\n /** Distro name (e.g. `Ubuntu`) as `wsl -l -q` reports it. */\n readonly distro: string\n /** Normalized absolute Linux path (leading `/`; no trailing slash except root). */\n readonly linuxPath: string\n}\n\n/**\n * Parse a WSL UNC path into its distro and Linux path. Accepts the WSL2\n * `\\\\wsl.localhost\\<distro>\\<linux>` form, the legacy `\\\\wsl$\\<distro>\\<linux>`\n * interop form, and forward-slash spellings of either.\n * @param raw - candidate absolute path.\n * @returns the parsed target, or null when the path is not a WSL UNC.\n */\nexport function parseWslUnc(raw: string): WslUncTarget | null {\n const normalized = raw.replace(/\\\\/g, '/').replace(/\\/\\/+/g, '//')\n if (!normalized.startsWith('//')) return null\n const segments = normalized.slice(2).split('/')\n const host = (segments[0] ?? '').toLowerCase()\n if (!UNC_HOSTS.includes(host)) return null\n const distro = segments[1] ?? ''\n if (distro === '') return null\n const rest = segments.slice(2).filter(segment => segment.length > 0)\n return { distro, linuxPath: `/${rest.join('/')}` }\n}\n\n/**\n * Whether a path resolves into a WSL distro through either UNC form.\n * @param raw - candidate absolute path.\n * @returns whether the path parses as a WSL UNC.\n */\nexport function isWslUnc(raw: string): boolean {\n return parseWslUnc(raw) !== null\n}\n\n/**\n * Translate a WSL UNC path to the absolute Linux path a process inside the\n * distribution can open. Throws on non-WSL input: callers rely on this\n * conversion to hand paths to the Linux world, so a silent pass-through\n * would hand a Windows path to bash.\n * @param uncPath - a path {@link parseWslUnc} accepts.\n * @returns the absolute Linux path.\n */\nexport function uncToLinux(uncPath: string): string {\n const parts = parseWslUnc(uncPath)\n if (parts === null) {\n throw new Error(`wsl-workspace: \"${uncPath}\" is not a WSL UNC path`)\n }\n return parts.linuxPath\n}\n\n/**\n * Normalize a Linux absolute path for the Host: collapse repeated slashes and\n * strip a trailing slash (root becomes `/`).\n * @param path - absolute Linux path.\n * @returns the normalized path.\n */\nexport function normalizeLinuxPath(path: string): string {\n const collapsed = path.replace(/\\/+/g, '/')\n return collapsed === '/' ? '/' : collapsed.replace(/\\/$/, '')\n}\n\n/**\n * Whether a path is an absolute, non-empty Linux path.\n * @param path - candidate.\n * @returns whether it starts with `/` and contains no NUL.\n */\nexport function isAbsoluteLinuxPath(path: string): boolean {\n return path.startsWith('/') && !path.includes('\\0')\n}\n\n/**\n * Join a distro and a Linux absolute path into the WSL2 UNC form used as the\n * workspace identity (`\\\\wsl.localhost\\<distro>\\<linux>`, backslash segments).\n * @param distro - distro name.\n * @param linuxPath - absolute Linux path (leading `/`).\n * @returns the UNC path.\n */\nexport function joinUnc(distro: string, linuxPath: string): string {\n if (!isAbsoluteLinuxPath(linuxPath)) {\n throw new Error(`wsl-workspace: cannot map a non-absolute Linux path \"${linuxPath}\" to UNC`)\n }\n // Defense in depth: a distribution name with separators or dot-dirs would\n // escape the `\\\\wsl.localhost\\` share structure (the host route validates\n // wire-supplied names too; every other caller passes through here).\n if (distro === '' || distro === '.' || distro === '..' || /[\\\\/]/.test(distro)) {\n throw new Error(`wsl-workspace: invalid distribution name \"${distro}\"`)\n }\n const normalized = linuxPath.replace(/\\/+/g, '/').replace(/\\/$/, '')\n const withoutLeading = normalized.startsWith('/') ? normalized.slice(1) : normalized\n const windowsSegments = withoutLeading.replace(/\\//g, '\\\\')\n const suffix = windowsSegments === '' ? '' : `\\\\${windowsSegments}`\n return `\\\\\\\\wsl.localhost\\\\${distro}${suffix}`\n}\n\n/**\n * Translate a Windows drive path to the drvfs mount path WSL distributions\n * conventionally expose it at (`C:\\foo` → `/mnt/c/foo`). Only single-letter\n * drives under `/mnt` are mapped; custom mount points are out of scope.\n * @param path - the candidate Windows path.\n * @returns the `/mnt/<drive>/…` path, or `null` for non-drive paths.\n */\nexport function windowsToMntPath(path: string): string | null {\n const match = /^([A-Za-z]):[\\\\/](.*)$/.exec(path)\n if (match === null) return null\n const rest = (match[2] ?? '').replace(/\\\\/g, '/').replace(/\\/+/g, '/').replace(/\\/$/, '')\n return `/mnt/${(match[1] ?? '').toLowerCase()}${rest === '' ? '' : `/${rest}`}`\n}\n\n/**\n * Translate a `/mnt/<drive>/…` path back to its Windows drive path.\n * @param linuxPath - the candidate Linux path.\n * @returns the `X:\\…` drive path, or `null` when the path is not a drvfs mount.\n */\nexport function mntToWindowsPath(linuxPath: string): string | null {\n const match = /^\\/mnt\\/([a-zA-Z])(?:\\/(.*))?$/.exec(linuxPath)\n if (match === null) return null\n const rest = (match[2] ?? '').replace(/\\//g, '\\\\')\n return `${(match[1] ?? '').toUpperCase()}:\\\\${rest}`\n}\n\n/**\n * Canonical Windows drive path for store keys and cross-realm identity:\n * separators unified to `\\`, trailing separator stripped, and the WHOLE path\n * lowercased — Windows paths compare case-insensitively, and the workspace\n * registry may realpath a different casing than the caller spelled (8.3 or\n * on-disk casing), so the store key must collide across casings.\n * @param path - candidate Windows drive path.\n * @returns the canonical form, or `null` when not drive-shaped.\n */\nexport function canonicalWindowsPath(path: string): string | null {\n const match = /^([A-Za-z]):[\\\\/](.*)$/.exec(path)\n if (match === null) return null\n const rest = (match[2] ?? '').replace(/[\\\\/]+/g, '\\\\').replace(/\\\\$/, '').toLowerCase()\n return `${(match[1] ?? '').toLowerCase()}:\\\\${rest}`\n}\n\n/**\n * True when a value is a Windows-shaped path (drive or UNC), which is how\n * the shell executor decides the WSLENV `/p` translation flag: only Windows\n * path values need translation when they cross into the Linux process.\n * @param value - the environment value to classify.\n * @returns whether the value looks like a Windows path.\n */\nexport function isWindowsPathShaped(value: string): boolean {\n return /^[A-Za-z]:[\\\\/]/.test(value) || value.startsWith('\\\\\\\\')\n}\n\n/** Linux username shape for `wsl.exe -u`: starts with a letter or underscore, then letters/digits/`_`/`.`/`-` (max 64). */\nconst WSL_USERNAME_PATTERN = /^[A-Za-z_][A-Za-z0-9_.-]{0,63}$/\n\n/**\n * Whether a value is a safe Linux username for `wsl.exe -u`. The check is\n * strict on purpose: a value starting with `-` could be parsed as a wsl.exe\n * option instead of a username.\n * @param value - candidate username.\n * @returns whether it matches the Linux username shape.\n */\nexport function isValidWslUsername(value: string): boolean {\n return WSL_USERNAME_PATTERN.test(value)\n}\n","/**\n * The dialog's \"?\" help panel: what this build is compatible with, how the\n * plugin is used, and the limits it cannot fix.\n *\n * The compatibility list is not hard-coded: it is read from this plugin's own\n * `package.json` through the host route, so the panel can never advertise a\n * release the build does not declare (and a host that cannot answer simply\n * shows no list).\n */\n\nimport type * as React from 'react'\nimport type { WslSelfDescription } from './api.ts'\n\n/** Props for the help panel. */\nexport interface WslHelpProps {\n /** Translate a `wslWorkspace` dictionary key. */\n t: (key: string, params?: Record<string, unknown>) => string\n /** Host-reported self-description, or null while it is unknown. */\n description: WslSelfDescription | null\n}\n\n/**\n * Split one dictionary entry into its bullet lines.\n * @param value - the multi-line dictionary string.\n * @returns the non-empty lines, trimmed.\n */\nfunction bullets(value: string): string[] {\n return value\n .split('\\n')\n .map(line => line.trim())\n .filter(line => line !== '')\n}\n\n/** One titled section of the panel. */\nfunction Section({ title, children }: { title: string; children: React.ReactNode }): React.ReactElement {\n return (\n <section className=\"dww-help-section\">\n <h3 className=\"dww-help-title\">{title}</h3>\n {children}\n </section>\n )\n}\n\n/**\n * Render the help panel shown behind the dialog's \"?\" button.\n * @param props - translate function plus the host's self-description.\n */\nexport function WslHelp({ t, description }: WslHelpProps): React.ReactElement {\n const releases = description?.releases ?? []\n const version = description === null\n ? t('help.compat.unknown')\n : `${t('help.compat.versionLabel')} v${description.version}`\n return (\n <div className=\"dww-help\" role=\"region\" aria-label={t('help.button')}>\n <Section title={t('help.compat.title')}>\n <div className=\"dww-help-meta\">{version}</div>\n {releases.length > 0\n ? (\n <div className=\"dww-help-chips\">\n {releases.map(entry => <span key={entry.id} className=\"dww-help-chip\">{entry.id}</span>)}\n </div>\n )\n : null}\n <ul className=\"dww-help-list\">\n {bullets(t('help.compat.body')).map(line => <li key={line}>{line}</li>)}\n </ul>\n </Section>\n <Section title={t('help.usage.title')}>\n <ul className=\"dww-help-list\">\n {bullets(t('help.usage.body')).map(line => <li key={line}>{line}</li>)}\n </ul>\n </Section>\n <Section title={t('help.known.title')}>\n <ul className=\"dww-help-list dww-help-list--known\">\n {bullets(t('help.known.body')).map(line => <li key={line}>{line}</li>)}\n </ul>\n </Section>\n <div className=\"dww-help-footer\">\n <a className=\"dww-help-link\" href=\"https://www.npmjs.com/package/dsh-wsl-workspace\" target=\"_blank\" rel=\"noreferrer\">\n {t('help.footer.npm')}\n </a>\n <a className=\"dww-help-link\" href=\"https://github.com/6Mikao9/dsh-wsl-workspace\" target=\"_blank\" rel=\"noreferrer\">\n {t('help.footer.repo')}\n </a>\n </div>\n </div>\n )\n}\n","/**\n * Sidebar footer action that opens the \"Add WSL workspace\" dialog. In the\n * wide sidebar it renders a labeled row; in the 56px rail it collapses to a\n * 36px icon button (both honor the shell's `{ wide }` owner share).\n *\n * Registration omits the typed `locale:` seat (the `wslWorkspace` namespace is\n * not merged into `LocaleNamespaceMap`), so the injected face carries the\n * bound translate function instead.\n */\n\nimport type * as React from 'react'\nimport { useEffect, useRef, useState, type UIEventHandler } from 'react'\nimport { isAbsoluteLinuxPath, isValidWslUsername, normalizeLinuxPath } from '../shared/paths.ts'\nimport type { WslDirListing, WslPathCheck, WslSelfDescription } from './api.ts'\nimport { WslHelp } from './help.tsx'\n\n/** Result-level shape of a browse/check/list call we surface uniformly. */\ninterface ApiCall<T> {\n value: T\n}\n\n/** The inject face: plain data + callbacks the dialog drives. */\nexport interface AddWslWorkspaceInjected {\n /**\n * Confirm the deployment exposes a healthy `wsl` preset.\n * @returns undefined when healthy, else a Chinese/English message to show.\n */\n checkPreset(): Promise<string | undefined>\n /**\n * Read this build's version and its declared DSH compatibility matrix,\n * for the help panel. A host that cannot answer resolves to a fallback\n * description rather than failing the dialog.\n */\n describe(): Promise<WslSelfDescription>\n /** List the WSL distros installed on the host. */\n listDistros(): Promise<string[]>\n /** List one Linux directory level inside a distro. */\n listDir(distro: string, path: string): Promise<WslDirListing>\n /** Check a Linux path's existence/directory facts. */\n check(distro: string, path: string): Promise<WslPathCheck>\n /**\n * Register a WSL workspace and start a session in it. `/mnt/<drive>`\n * paths register under their Windows drive spelling (9P cannot serve\n * drvfs); ext4 paths register as `\\\\wsl.localhost\\<distro>\\...` UNC.\n * @param linuxPath - the absolute Linux workspace path.\n * @param username - optional Linux user for the session (empty = distro default).\n * @param distro - the WSL distribution the path belongs to.\n * @returns undefined on success, else a message to show.\n */\n createWorkspace(linuxPath: string, username: string, distro: string): Promise<string | undefined>\n /** Translate a `wslWorkspace` dictionary key (follows the DeepSeek Harness locale). */\n t: (key: string, params?: Record<string, unknown>) => string\n}\n\n/** Full component props: the owner share plus the injected face. */\nexport interface AddWslWorkspaceProps extends AddWslWorkspaceInjected {\n /** Whether the sidebar renders wide content (false = 56px rail). */\n wide: boolean\n}\n\n/**\n * Build the Linux child path one level below a parent, for the breadcrumb/\n * browse drill.\n * @param parent - the currently listed absolute path (`/` for root).\n * @param name - the child directory name.\n * @returns the child's absolute Linux path.\n */\nexport function dirChildPath(parent: string, name: string): string {\n return parent === '/' ? `/${name}` : `${parent}/${name}`\n}\n\n/** A tiny inline terminal glyph for the dialog's directory rows. */\nfunction WslGlyph({ size = 16 }: { size?: number }): React.ReactElement {\n return (\n <svg width={size} height={size} viewBox=\"0 0 24 24\" fill=\"none\" aria-hidden=\"true\">\n <rect x=\"2.5\" y=\"4.5\" width=\"19\" height=\"15\" rx=\"2.5\" stroke=\"currentColor\" strokeWidth=\"1.6\" />\n <path d=\"M6 9l3.2 2.6L6 14\" stroke=\"currentColor\" strokeWidth=\"1.6\" strokeLinecap=\"round\" strokeLinejoin=\"round\" />\n <path d=\"M12 14h5\" stroke=\"currentColor\" strokeWidth=\"1.6\" strokeLinecap=\"round\" />\n </svg>\n )\n}\n\n/**\n * The \"Add WSL workspace…\" footer action and its dialog.\n * @param props - owner share + injected face.\n */\nexport function AddWslWorkspace({ wide, t, describe, checkPreset, listDistros, listDir, check, createWorkspace }: AddWslWorkspaceProps): React.ReactElement | null {\n const [open, setOpen] = useState(false)\n const [opening, setOpening] = useState(false)\n const [distros, setDistros] = useState<string[]>([])\n const [distro, setDistro] = useState('')\n const [pathInput, setPathInput] = useState('/home/')\n const [username, setUsername] = useState('')\n const [listing, setListing] = useState<WslDirListing | null>(null)\n const [browsePath, setBrowsePath] = useState('/')\n const [browsing, setBrowsing] = useState(false)\n const [error, setError] = useState<string | null>(null)\n const [busy, setBusy] = useState(false)\n // The \"?\" panel: help text plus the compatibility list the host reports.\n const [helpOpen, setHelpOpen] = useState(false)\n const [selfDescription, setSelfDescription] = useState<WslSelfDescription | null>(null)\n // Monotone browse-request sequence: stale responses for a superseded browse are dropped.\n const browseSeq = useRef(0)\n\n const refreshBrowse = async (root: string, targetDistro: string): Promise<void> => {\n const seq = ++browseSeq.current\n setBrowsing(true)\n setBrowsePath(root)\n try {\n const value = await listDir(targetDistro, root)\n if (seq === browseSeq.current) setListing(value)\n } catch {\n // A failed browse (permission, missing dir) is non-fatal: keep old listing.\n if (seq === browseSeq.current) {\n setListing(null)\n setError((previous) => previous ?? t('error.loadDir'))\n }\n } finally {\n if (seq === browseSeq.current) setBrowsing(false)\n }\n }\n\n useEffect(() => {\n if (!open) return\n let cancelled = false\n setError(null)\n // Advisory data for the help panel: never fatal, never blocking the form.\n void describe().then(value => { if (!cancelled) setSelfDescription(value) }).catch(() => { if (!cancelled) setSelfDescription(null) })\n setOpening(true)\n void (async () => {\n let presetIssue: string | undefined\n try {\n presetIssue = await checkPreset()\n } catch {\n presetIssue = t('error.loadDistros')\n }\n let names: string[]\n try {\n names = await listDistros()\n } catch {\n if (cancelled) return\n setOpening(false)\n setError(t('error.loadDistros'))\n return\n }\n if (cancelled) return\n setDistros(names)\n const first = names[0] ?? ''\n setDistro(first)\n // The default browse root walks from `/`; the input defaults to `/home/`.\n setBrowsing(true)\n setOpening(false)\n if (presetIssue !== undefined) setError(presetIssue)\n if (first !== '') void refreshBrowse('/', first)\n })()\n return () => { cancelled = true }\n // eslint-disable-next-line react-hooks/exhaustive-deps -- run once per open against current t.\n }, [open])\n\n useEffect(() => {\n if (!open) return\n const onKey = (event: KeyboardEvent): void => {\n if (event.key === 'Escape' && !busy) setOpen(false)\n }\n window.addEventListener('keydown', onKey)\n return () => window.removeEventListener('keydown', onKey)\n }, [open, busy])\n\n if (!open) {\n // A W-letter action beside Settings at the sidebar foot; the title\n // carries the label in both wide and rail states.\n return (\n <button\n type=\"button\"\n className={wide ? 'dww-action dww-action--wide' : 'dww-action dww-action--rail'}\n title={t('action.title')}\n aria-label={t('action.title')}\n onClick={() => setOpen(true)}\n >\n <span className=\"dww-letter\" aria-hidden=\"true\">W</span>\n </button>\n )\n }\n\n const onDrill = (name: string): void => {\n const next = dirChildPath(listing?.path ?? browsePath, name)\n setPathInput(next)\n void refreshBrowse(next, distro)\n }\n\n const onUp = (): void => {\n const parent = listing?.parent ?? null\n if (parent === null) return\n setPathInput(parent)\n void refreshBrowse(parent, distro)\n }\n\n const onDistroChange = (value: string): void => {\n setDistro(value)\n void refreshBrowse(browsePath, value)\n }\n\n const onCheck = async (): Promise<void> => {\n const path = normalizeLinuxPath(pathInput)\n setError(null)\n if (!isAbsoluteLinuxPath(path) || path === '/') {\n setError(t('error.invalidPath'))\n return\n }\n let facts: WslPathCheck\n try {\n facts = await check(distro, path)\n } catch {\n setError(t('error.pathNotFound'))\n return\n }\n if (!facts.exists || !facts.isDirectory) {\n setError(t('error.pathNotFound'))\n return\n }\n void refreshBrowse(path, distro)\n }\n\n const onConfirm = async (): Promise<void> => {\n const path = normalizeLinuxPath(pathInput)\n setError(null)\n if (!isAbsoluteLinuxPath(path) || path === '/') {\n // A workspace at the distribution root would make every session start\n // at `/`; the check flow rejects it and confirm must agree.\n setError(t('error.invalidPath'))\n return\n }\n const user = username.trim()\n if (user !== '' && !isValidWslUsername(user)) {\n setError(t('error.invalidUsername'))\n return\n }\n setBusy(true)\n try {\n let facts: WslPathCheck\n try {\n facts = await check(distro, path)\n } catch {\n setError(t('error.pathNotFound'))\n return\n }\n if (!facts.exists || !facts.isDirectory) {\n setError(t('error.pathNotFound'))\n return\n }\n const failure = await createWorkspace(path, user, distro)\n if (failure !== undefined) {\n setError(failure)\n return\n }\n setOpen(false)\n } finally {\n setBusy(false)\n }\n }\n\n const children = (listing?.entries.filter(entry => entry.kind === 'directory') ?? []).map(entry => entry.name)\n const maskClick = (): void => { if (!busy) setOpen(false) }\n const listScroll: UIEventHandler<HTMLDivElement> = () => { /* scroll container handles overflow */ }\n\n return (\n <div className=\"dww-overlay\">\n <div className=\"dww-overlay-mask\" onClick={maskClick} />\n <div className=\"dww-card\" role=\"dialog\" aria-modal=\"true\" aria-label={t('dialog.title')}>\n <div className=\"dww-header\">\n <h2 className=\"dww-title\">{t('dialog.title')}</h2>\n <button\n type=\"button\"\n className=\"dww-help-btn\"\n title={t('help.button')}\n aria-label={t('help.button')}\n aria-pressed={helpOpen}\n onClick={() => setHelpOpen(value => !value)}\n >\n ?\n </button>\n <button type=\"button\" className=\"dww-close\" aria-label={t('dialog.cancel')} onClick={maskClick}>\n ✕\n </button>\n </div>\n <div className=\"dww-body\">\n {helpOpen ? (\n <WslHelp t={t} description={selfDescription} />\n ) : (<>\n {error !== null ? (\n <div className=\"dww-error\">\n {error}\n <button type=\"button\" className=\"dww-retry\" onClick={() => setError(null)}>{t('dialog.retry')}</button>\n </div>\n ) : null}\n <div className=\"dww-field\">\n <label className=\"dww-field-label\" htmlFor=\"dww-distro\">{t('dialog.distro')}</label>\n <select\n id=\"dww-distro\"\n className=\"dww-select\"\n value={distro}\n disabled={opening || busy}\n onChange={event => onDistroChange(event.target.value)}\n >\n {distros.length === 0\n ? <option value=\"\">{opening ? t('dialog.loading') : ''}</option>\n : distros.map(name => <option key={name} value={name}>{name}</option>)}\n </select>\n </div>\n <div className=\"dww-field\">\n <label className=\"dww-field-label\" htmlFor=\"dww-path\">{t('dialog.path')}</label>\n <div className=\"dww-input-row\">\n <input\n id=\"dww-path\"\n className=\"dww-input\"\n value={pathInput}\n placeholder={t('dialog.pathPlaceholder')}\n disabled={opening || busy}\n onChange={event => setPathInput(event.target.value)}\n />\n <button type=\"button\" className=\"dww-check-btn\" disabled={opening || busy} onClick={() => void onCheck()}>\n {t('dialog.check')}\n </button>\n </div>\n </div>\n <div className=\"dww-field\">\n <label className=\"dww-field-label\" htmlFor=\"dww-username\">{t('dialog.username')}</label>\n <input\n id=\"dww-username\"\n className=\"dww-input\"\n value={username}\n placeholder={t('dialog.usernamePlaceholder')}\n disabled={opening || busy}\n autoComplete=\"off\"\n spellCheck={false}\n onChange={event => setUsername(event.target.value)}\n />\n </div>\n <div className=\"dww-feedback\">\n <div className=\"dww-breadcrumb\">{browsePath}</div>\n <div className=\"dww-dirlist\" onScroll={listScroll}>\n {browsing ? <div className=\"dww-dir-empty\">{t('dialog.loading')}</div> : (\n listing?.parent !== null && listing !== null\n ? (\n <button type=\"button\" className=\"dww-dir-row dww-dir-row--up\" onClick={onUp}>\n <WslGlyph size={14} />\n <span>{t('dialog.upLevel')}</span>\n </button>\n )\n : null\n )}\n {!browsing && (children.length === 0)\n ? <div className=\"dww-dir-empty\">{t('dialog.browseEmpty')}</div>\n : children.map(name => (\n <button type=\"button\" key={name} className=\"dww-dir-row\" onClick={() => onDrill(name)}>\n <WslGlyph size={14} />\n <span>{name}</span>\n </button>\n ))}\n </div>\n </div>\n </>)}\n </div>\n <div className=\"dww-actions\">\n <button type=\"button\" className=\"dww-btn\" disabled={busy} onClick={maskClick}>{t('dialog.cancel')}</button>\n <button type=\"button\" className=\"dww-btn dww-btn--primary\" disabled={busy || opening} onClick={() => void onConfirm()}>\n {busy ? t('dialog.loading') : t('dialog.confirm')}\n </button>\n </div>\n </div>\n </div>\n )\n}\n\nexport type { ApiCall }\n","/**\n * Third-party stylesheet injection for the WSL workspace UI (the plugin\n * builds no CSS bundle, so styles are injected as one idempotent `<style>`).\n * Colors derive exclusively from the `--dsw-*` design tokens.\n */\n\nconst STYLE_TAG_DATA_ATTRIBUTE = 'data-plugin=\"dsh-wsl-workspace\"'\n\nconst STYLES = `\n/* Sidebar-foot icon action beside Settings (28px round in the wide sidebar,\n 36px round in the rail), matching the shell's icon-button language. */\n.dww-action {\n flex: none;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 28px;\n height: 28px;\n border: none;\n border-radius: 50%;\n padding: 0;\n background: transparent;\n cursor: pointer;\n color: var(--dsw-alias-label-secondary);\n transition:\n background-color 120ms var(--dsw-ease-in-out, ease-in-out),\n color 120ms var(--dsw-ease-in-out, ease-in-out);\n}\n.dww-action:hover:not(:disabled) {\n background: var(--dsw-alias-interactive-bg-hover);\n color: var(--dsw-alias-label-secondary);\n}\n.dww-action:active:not(:disabled) {\n background: var(--dsw-alias-interactive-bg-pressed, var(--dsw-alias-interactive-bg-hover));\n}\n.dww-action:focus-visible {\n outline: 2px solid var(--dsw-alias-state-business-primary);\n outline-offset: 1px;\n}\n.dww-action:disabled { cursor: default; opacity: 0.6; }\n.dww-action--rail {\n width: 36px;\n height: 36px;\n color: var(--dsw-alias-label-primary);\n}\n.dww-action svg { flex: none; }\n\n/* The W letter mark of the sidebar action (sized for wide/rail buttons). */\n.dww-letter {\n font-size: 14px;\n font-weight: 600;\n line-height: 1;\n letter-spacing: 0.02em;\n user-select: none;\n}\n.dww-action--rail .dww-letter { font-size: 17px; }\n\n/* Full-viewport overlay + centered card (mirrors the platform Mask/Dialog). */\n.dww-overlay {\n position: fixed;\n inset: 0;\n z-index: 1000;\n display: flex;\n align-items: center;\n justify-content: center;\n padding: 24px;\n}\n.dww-overlay-mask {\n position: absolute;\n inset: 0;\n background: var(--dsw-alias-bg-mask-1);\n backdrop-filter: var(--dsw-mask-blur);\n}\n.dww-card {\n position: relative;\n z-index: 1;\n box-sizing: border-box;\n display: flex;\n flex-direction: column;\n width: min(440px, 100%);\n max-height: min(640px, 90vh);\n padding: 0 0 20px;\n overflow: hidden;\n border: 1px solid var(--dsw-alias-border-inverted);\n border-radius: 16px;\n background: var(--dsw-alias-bg-layer-2);\n box-shadow: var(--dsw-shadow-lv3);\n font-family: var(--dsw-font-family);\n}\n.dww-header {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 8px;\n padding: 18px 20px 12px;\n}\n.dww-title {\n margin: 0;\n font-size: 16px;\n line-height: 24px;\n font-weight: 500;\n color: var(--dsw-alias-label-primary);\n}\n.dww-close {\n flex: none;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 28px;\n height: 28px;\n border: 0;\n border-radius: 8px;\n background: transparent;\n cursor: pointer;\n color: var(--dsw-alias-label-secondary);\n}\n.dww-close:hover { background: var(--dsw-alias-interactive-bg-hover); }\n.dww-body {\n display: flex;\n flex-direction: column;\n gap: 14px;\n min-width: 0;\n padding: 0 20px;\n overflow: auto;\n}\n.dww-field { display: flex; flex-direction: column; gap: 6px; min-width: 0; }\n.dww-field-label {\n font-size: 12px;\n line-height: 18px;\n color: var(--dsw-alias-label-secondary);\n}\n.dww-select {\n box-sizing: border-box;\n width: 100%;\n height: 36px;\n padding: 0 10px;\n border: 1px solid var(--dsw-alias-border-l2);\n border-radius: 8px;\n background: var(--dsw-alias-bg-layer-3);\n color: var(--dsw-alias-label-primary);\n font-size: 14px;\n}\n.dww-input-row { display: flex; gap: 8px; align-items: center; }\n.dww-input {\n box-sizing: border-box;\n flex: 1;\n height: 36px;\n min-width: 0;\n padding: 0 10px;\n border: 1px solid var(--dsw-alias-border-l2);\n border-radius: 8px;\n background: var(--dsw-alias-bg-layer-3);\n color: var(--dsw-alias-label-primary);\n font-size: 14px;\n}\n.dww-input:focus, .dww-select:focus {\n outline: none;\n border-color: var(--dsw-alias-state-business-primary);\n}\n.dww-check-btn {\n flex: none;\n height: 36px;\n padding: 0 12px;\n border: 1px solid var(--dsw-alias-border-l2);\n border-radius: 8px;\n background: transparent;\n color: var(--dsw-alias-label-primary);\n cursor: pointer;\n font-size: 12px;\n}\n.dww-check-btn:hover:not(:disabled) { background: var(--dsw-alias-interactive-bg-hover); }\n.dww-check-btn:disabled { cursor: default; }\n\n/* Directory browse list. */\n.dww-dirlist {\n display: flex;\n flex-direction: column;\n gap: 2px;\n box-sizing: border-box;\n min-height: 120px;\n max-height: 200px;\n padding: 4px;\n overflow: auto;\n border: 1px solid var(--dsw-alias-border-l2);\n border-radius: 8px;\n background: var(--dsw-alias-bg-layer-3);\n}\n.dww-breadcrumb {\n padding: 0 4px;\n font-size: 12px;\n line-height: 18px;\n color: var(--dsw-alias-label-tertiary);\n overflow: hidden;\n white-space: nowrap;\n text-overflow: ellipsis;\n}\n.dww-dir-row {\n display: flex;\n align-items: center;\n gap: 8px;\n height: 28px;\n padding: 0 8px;\n border: 0;\n border-radius: 6px;\n background: transparent;\n color: var(--dsw-alias-label-primary);\n cursor: pointer;\n font-size: 13px;\n text-align: left;\n}\n.dww-dir-row:hover:not(:disabled) { background: var(--dsw-alias-interactive-bg-hover); }\n.dww-dir-row:disabled { cursor: default; color: var(--dsw-alias-label-tertiary); }\n.dww-dir-row--up { color: var(--dsw-alias-label-secondary); }\n.dww-dir-row svg { flex: none; color: var(--dsw-alias-label-tertiary); }\n.dww-dir-empty {\n padding: 8px;\n font-size: 12px;\n line-height: 18px;\n color: var(--dsw-alias-label-tertiary);\n}\n\n/* Error strip. */\n.dww-error {\n box-sizing: border-box;\n width: 100%;\n padding: 8px 10px;\n border: 1px solid var(--dsw-alias-state-error-primary);\n border-radius: 8px;\n color: var(--dsw-alias-state-error-primary);\n font-size: 12px;\n line-height: 18px;\n}\n.dww-retry {\n margin-left: 6px;\n border: 0;\n background: transparent;\n color: var(--dsw-alias-state-business-primary);\n cursor: pointer;\n font-size: 12px;\n text-decoration: underline;\n}\n\n/* Dialog footer actions. */\n.dww-actions {\n display: flex;\n align-items: center;\n justify-content: flex-end;\n gap: 8px;\n padding: 14px 20px 0;\n}\n.dww-btn {\n height: 36px;\n padding: 0 14px;\n border: 1px solid var(--dsw-alias-border-l2);\n border-radius: 8px;\n background: transparent;\n color: var(--dsw-alias-label-primary);\n cursor: pointer;\n font-size: 14px;\n}\n.dww-btn:hover:not(:disabled) { background: var(--dsw-alias-interactive-bg-hover); }\n.dww-btn--primary {\n border-color: transparent;\n background: var(--dsw-alias-button-primary-fill);\n color: var(--dsw-alias-label-primary-foreground);\n}\n.dww-btn--primary:hover:not(:disabled) { background: var(--dsw-alias-button-primary-hover); }\n.dww-btn:disabled { cursor: default; opacity: 0.6; }\n/* Help panel behind the dialog's \"?\" button. */\n.dww-help-btn {\n flex: none;\n margin-left: auto;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 26px;\n height: 26px;\n border: 1px solid var(--dsw-alias-border-l2);\n border-radius: 50%;\n background: transparent;\n color: var(--dsw-alias-label-secondary);\n cursor: pointer;\n font-size: 13px;\n line-height: 1;\n}\n.dww-help-btn:hover { background: var(--dsw-alias-interactive-bg-hover); }\n.dww-help-btn[aria-pressed='true'] {\n background: var(--dsw-alias-interactive-bg-hover);\n color: var(--dsw-alias-label-primary);\n}\n.dww-help {\n display: flex;\n flex-direction: column;\n gap: 14px;\n max-height: 52vh;\n overflow-y: auto;\n padding-right: 4px;\n}\n.dww-help-section { display: flex; flex-direction: column; gap: 6px; }\n.dww-help-title {\n margin: 0;\n font-size: 13px;\n font-weight: 600;\n color: var(--dsw-alias-label-primary);\n}\n.dww-help-meta { font-size: 12px; color: var(--dsw-alias-label-tertiary); }\n.dww-help-chips { display: flex; flex-wrap: wrap; gap: 6px; }\n.dww-help-chip {\n padding: 2px 8px;\n border: 1px solid var(--dsw-alias-border-l2);\n border-radius: 999px;\n font-size: 12px;\n color: var(--dsw-alias-label-secondary);\n}\n.dww-help-list {\n margin: 0;\n padding-left: 18px;\n display: flex;\n flex-direction: column;\n gap: 6px;\n font-size: 13px;\n line-height: 1.6;\n color: var(--dsw-alias-label-secondary);\n}\n.dww-help-list--known { color: var(--dsw-alias-label-primary); }\n.dww-help-footer {\n display: flex;\n gap: 14px;\n padding-top: 8px;\n border-top: 1px solid var(--dsw-alias-border-l2);\n}\n.dww-help-link { font-size: 12px; color: var(--dsw-alias-label-tertiary); text-decoration: underline; }\n.dww-help-link:hover { color: var(--dsw-alias-label-primary); }\n`\n\n/**\n * Idempotently inject the plugin stylesheet. No-op when a tag with the\n * plugin's data attribute already exists.\n */\nexport function ensureStyles(): void {\n if (typeof document === 'undefined') return\n if (document.querySelector(`style[${STYLE_TAG_DATA_ATTRIBUTE}]`) !== null) return\n const style = document.createElement('style')\n style.setAttribute('data-plugin', 'dsh-wsl-workspace')\n style.textContent = STYLES\n document.head.appendChild(style)\n}\n","/**\n * Bilingual dictionaries for the `wslWorkspace` locale namespace. Product copy\n * is Chinese; English is the parallel export for the standalone bundle.\n */\n\n/**\n * The `wslWorkspace` translations (Chinese, the primary product copy).\n */\nexport const zh: Record<string, string> = {\n 'action.add': 'WSL 工作区',\n 'action.title': '添加 WSL 工作区…',\n\n 'dialog.title': '添加 WSL 工作区',\n 'dialog.distro': '发行版',\n 'dialog.path': '路径',\n 'dialog.pathPlaceholder': '/home/',\n 'dialog.username': '用户名',\n 'dialog.usernamePlaceholder': '留空则使用发行版默认用户',\n 'dialog.loading': '正在加载…',\n 'dialog.browseEmpty': '此目录没有子文件夹',\n 'dialog.upLevel': '..(返回上级)',\n 'dialog.browse': '浏览',\n 'dialog.check': '检查',\n 'dialog.confirm': '创建并打开',\n 'dialog.cancel': '取消',\n 'dialog.retry': '重试',\n\n 'error.loadDistros': '无法获取 WSL 发行版列表,请确认已安装 WSL 且插件宿主端可用',\n 'error.rateLimited': '操作过于频繁,请稍后重试',\n 'error.loadDir': '无法浏览该目录',\n 'error.presetMissing': '未找到健康的 wsl preset,请确认插件宿主端已安装并配置该 preset',\n 'error.invalidPath': '请输入以 / 开头的 Linux 绝对路径',\n 'error.invalidUsername': '用户名无效:需以字母或下划线开头,仅含字母、数字、_、.、-',\n 'error.pathNotFound': '该路径不存在或是文件,请选择一个文件夹',\n 'error.createFailed': '创建工作区失败',\n 'help.button': '插件说明',\n\n 'help.compat.title': '兼容性',\n 'help.compat.versionLabel': '插件版本',\n 'help.compat.unknown': '未能读取版本与兼容声明(宿主端没有响应)',\n 'help.compat.body': '上方是这份构建声明兼容的 DSH 版本,每一条都在隔离实例上实测过(独立 DSH_HOME、依赖固定到该版本、跑满十项门禁)。\\n插件在运行时会识别 DSH 版本:0.1.2 线用 uiWorkspace + remote.agentPresets,更早的版本用 workspaces.startSession + connection.api;两边都没有时会明确报错,而不是留下一个空工作区。\\n版本不在列表里通常仍然可用,但未经验证。',\n 'help.usage.title': '用法与特性',\n 'help.usage.body': '点击侧边栏底部的 W 按钮 → 选发行版 → 输入或浏览 Linux 路径 → 检查 → 创建并打开。\\n创建出的会话里,bash 与文件工具都落在该发行版内,模型看到的路径全是 Linux 路径;Windows 盘可从会话内通过 /mnt/<盘符> 访问。\\n四种模式(标准 / PTC / 极简 / 创造)各有 WSL 变体,在模式选择器里直接选即可,名字形如 WSL · Standard mode(标准模式)。\\n用户名可选,等价于 wsl.exe -u <用户名>,只改变 bash 的运行身份;文件工具走 Windows 侧共享,不受它影响。\\n技能目录从会话 cwd 最近的 .git 祖先开始向下扫描 .dsh/skills 与 .agents/skills(含嵌套项目),上限 4 层目录 / 64 个技能目录 / 4096 个已访问目录,并按扫描根缓存 10 秒。\\n为了让 UNC 工作区也能拿到目录,插件生成预设时会把技能行的 watch 关掉,目录改由会话启动时扫描一次:会话中途新加入的技能要等下一个会话才出现,而技能内容(get)始终是实时读取的。\\nbash 在发行版内运行,不受 DSH 文件策略约束;文件工具(read/write/edit)受策略约束,工作区内修改模式下只能写工作区内。',\n 'help.known.title': '已知问题',\n 'help.known.body': 'Windows 侧共享无法解析 Linux 符号链接:用 ln -s 链进工作区的项目发现不了(扫描会跳过、不报错),请把工作区注册在真实项目目录所在的层级。\\n会话运行中途新加入的技能不会即时出现在目录里(因为对 UNC 关闭了文件监视,见「用法与特性」);重开一个会话即可看到。技能内容本身始终实时读取。\\n只有经本插件注册的 WSL 工作区才会默认用 WSL 变体;在其它工作区里手动新建的会话用宿主默认预设,bash/文件工具就是 Windows 侧的。\\n0.4.3 已修复:UNC 工作区下模型收不到技能目录(宿主 fs.watch 在 \\\\\\\\wsl.localhost\\\\… 上失败导致整条目录消息被丢弃),现在生成预设时会关闭该监视,目录照常注入。',\n 'help.footer.npm': 'npm 包',\n 'help.footer.repo': 'GitHub 仓库',\n}\n\n/**\n * The `wslWorkspace` translations (English).\n */\nexport const en: Record<string, string> = {\n 'action.add': 'WSL Workspace',\n 'action.title': 'Add WSL workspace…',\n\n 'dialog.title': 'Add WSL workspace',\n 'dialog.distro': 'Distro',\n 'dialog.path': 'Path',\n 'dialog.pathPlaceholder': '/home/',\n 'dialog.username': 'Username',\n 'dialog.usernamePlaceholder': 'Leave empty to use the distro default user',\n 'dialog.loading': 'Loading…',\n 'dialog.browseEmpty': 'No subdirectories here',\n 'dialog.upLevel': '.. (up)',\n 'dialog.browse': 'Browse',\n 'dialog.check': 'Check',\n 'dialog.confirm': 'Create & open',\n 'dialog.cancel': 'Cancel',\n 'dialog.retry': 'Retry',\n\n 'error.loadDistros': 'Could not list WSL distros; confirm WSL is installed and the plugin host side is reachable',\n 'error.rateLimited': 'Too many attempts; retry in a moment',\n 'error.loadDir': 'Could not browse this directory',\n 'error.presetMissing': 'No healthy \"wsl\" preset found; confirm the plugin host side installed and configured it',\n 'error.invalidPath': 'Enter an absolute Linux path starting with /',\n 'error.invalidUsername': 'Invalid username: start with a letter or underscore; only letters, digits, _ . -',\n 'error.pathNotFound': 'The path does not exist or is a file; choose a folder',\n 'error.createFailed': 'Failed to create the workspace',\n 'help.button': 'About this plugin',\n\n 'help.compat.title': 'Compatibility',\n 'help.compat.versionLabel': 'Plugin version',\n 'help.compat.unknown': 'Version and compatibility declaration unavailable (the host side did not answer)',\n 'help.compat.body': 'The chips above are the DSH releases this build declares, each verified on an isolated instance (own DSH_HOME, dependencies pinned to that release, full check suite).\\nThe plugin detects the DSH generation at runtime: the 0.1.2 line uses uiWorkspace + remote.agentPresets, older releases use workspaces.startSession + connection.api, and a release exposing neither fails loudly instead of leaving an empty workspace.\\nA release outside the list usually still works, but is unverified.',\n 'help.usage.title': 'Usage and features',\n 'help.usage.body': 'Click the W button at the sidebar foot, pick a distribution, type or browse to a Linux path, press Check, then Create & open.\\nIn that session the bash tool and the file tools run inside the distribution, so every path the model sees is a Linux path; Windows drives stay reachable as /mnt/<drive>.\\nEach mode (Standard / PTC / Minimal / Creator) has a WSL variant in the mode picker, named like WSL · Standard mode.\\nThe optional username behaves like wsl.exe -u <user> for the bash tool only; the file tools go through the Windows-side share and are unaffected.\\nThe skill catalog is discovered from the nearest .git ancestor of the session cwd downwards (.dsh/skills and .agents/skills, nested projects included), bounded to 4 levels / 64 skill directories / 4096 visited directories, and cached per scan root for 10 seconds.\\nSo that a UNC workspace still receives a catalog, the generated preset turns that row\\'s `watch` off and the catalog is scanned once at session start: a skill added mid-session appears in the next session, while skill bodies are always read live.\\nbash runs inside the distribution and is not wrapped by the DSH file policy; read/write/edit are, and workspace-write only writes inside the workspace.',\n 'help.known.title': 'Known issues',\n 'help.known.body': 'The Windows-side share cannot resolve Linux symlinks, so a project linked in with ln -s is not discoverable (the scan skips it without failing); register the workspace at a level that holds the real project directories.\\nA skill added while a session is running does not show up in that session\\'s catalog (file watching is off for UNC workspaces, see Usage and features); start a new session to pick it up. Skill bodies are always read live.\\nOnly workspaces registered through this plugin default to a WSL variant; a session started by hand in another workspace keeps the host default preset, so its bash and file tools run on the Windows side.\\nFixed in 0.4.3: on a UNC workspace the model did not receive the skill catalog at all (the host fs.watch call fails on \\\\\\\\wsl.localhost\\\\... and the whole catalog message was dropped). The generated preset now disables that watcher, and the catalog is injected as usual.',\n 'help.footer.npm': 'npm package',\n 'help.footer.repo': 'GitHub repository',\n}\n","/**\n * Browser half of dsh-wsl-workspace. Registers the \"Add WSL workspace…\"\n * action beside Settings at the sidebar foot (the official\n * `sidebar.footer.action` slot), and keeps every blank session whose\n * workspace is a WSL UNC path composed from the WSL VARIANT of the mode it\n * currently runs (`standard` → `wsl-standard`, PTC → `wsl-code`, …) — so the\n * WSL execution world composes with any mode instead of being a mode itself.\n *\n * The binding is a watching effect rather than a one-shot dialog action so\n * EVERY creation path (this dialog, the workspace row's New Session, the\n * hero picker) converges on the WSL-backed composition automatically.\n */\n\n// Type-only: pulls the locale plugin's Context merge (ctx.locale) and the\n// ui-sidebar SlotMap merge (the 'sidebar.footer.action' entry) into this\n// program. Version-dependent services (`remote`, `connection.api`,\n// `uiWorkspace`) are read dynamically via ctx.get() so both DSH v0.1.1-rc.2\n// and v0.1.2-rc.1+ can load this plugin.\nimport type {} from '@deepseek-ai/dsh-client-locale/client'\nimport type {} from '@deepseek-ai/dsh-client-ui-sidebar/client'\nimport type { ClientContext } from '@deepseek-ai/dsh-client-runtime/client'\nimport { check as checkApi, describe as describeApi, listDir as listDirApi, listDistros as listDistrosApi, listWorkspaces as listWorkspacesApi, registerWindows as registerWindowsApi, setWorkspaceUser as setWorkspaceUserApi } from './api.ts'\nimport { AddWslWorkspace, type AddWslWorkspaceInjected } from './AddWslWorkspace.tsx'\nimport { ensureStyles } from './styles.ts'\nimport { zh, en } from './locales.ts'\nimport { canonicalWindowsPath, isWslUnc, joinUnc, mntToWindowsPath } from '../shared/paths.ts'\n\n/** Required services (cordis fiber inject). */\nexport const inject = ['slots', 'locale', 'sessions', 'workspaces']\n\n/** The legacy standalone WSL preset id (folded into the mode variants). */\nconst LEGACY_WSL_PRESET_ID = 'wsl'\n\n/**\n * Minimal sessions-service face. The renderer-host ctx merge types\n * `ctx.sessions` as its own SessionStore; the service the runtime actually\n * registers under that key satisfies this narrower contract, so the cast is\n * the documented boundary for a third-party plugin.\n */\ninterface WslSessionsFace {\n list: {\n getSnapshot(): {\n ids: string[]\n byId: Record<string, {\n blank: boolean\n cwd?: string\n /** v0.1.1-rc.2 direct field */\n agentPreset?: string\n /** v0.1.2-rc.1+ projection value */\n projectionValues?: Readonly<{ agentPreset?: string | null }>\n }>\n }\n subscribe(fn: () => void): () => void\n }\n /** v0.1.1-rc.2 only — absent in v0.1.2-rc.1+ (projection auto-syncs). */\n noteAgentPreset?(sessionId: string, agentPreset: string): void\n}\n\n/** Minimal workspaces-service face (create only; startSession moved out in v0.1.2-rc.1+). */\ninterface WslWorkspacesFace {\n create(input: { path: string }): Promise<{ workspaceId: string }>\n /** v0.1.1-rc.2 only — removed in v0.1.2-rc.1+. */\n startSession?(workspaceId?: string): void\n}\n\n/** v0.1.2-rc.1+ only — replaces `workspaces.startSession`. */\ninterface WslUiWorkspaceFace {\n startSession(workspaceId?: string): void\n}\n\n/** v0.1.2-rc.1+ agentPresets 命名空间服务(经 ctx.get('remote.agentPresets') 动态取,免 inject、无 associate 陷阱)。 */\ninterface WslAgentPresetsNamespace {\n list(): Promise<{ ok: boolean; value?: { presets: { id: string; broken?: string; isDefault?: boolean }[] }; error?: { message: string } }>\n select(sessionId: string, presetId: string): Promise<{ ok: boolean }>\n}\n\n/** 旧版 connection 服务最小接口(v0.1.1-rc.2 及更早),api 属性承载远程调用。 */\ninterface WslLegacyConnection {\n api?: {\n agentPresets: {\n list(input: Record<string, never>): Promise<{ result: { ok: boolean; value?: { presets: { id: string; broken?: string; isDefault?: boolean }[] }; error?: { message: string } } }>\n select(input: { sessionId: string; agentPreset: string }): Promise<{ result: { ok: boolean } }>\n }\n }\n}\n/**\n * Mount the sidebar action and the auto-binding effect.\n * @param ctx - the browser plugin context.\n */\nexport function apply(ctx: ClientContext): void {\n const workspaces = ctx.get('workspaces') as unknown as WslWorkspacesFace\n const sessions = ctx.get('sessions') as unknown as WslSessionsFace\n // Version-dependent services are resolved ON USE - never through `inject`\n // (a release without the service would refuse to mount the plugin at all)\n // and never once at apply time. This plugin applies before the UI domain\n // that publishes `uiWorkspace` registers its service, so a one-shot read\n // caches `undefined` for the whole page life: on v0.1.2-rc.1+ that silently\n // disabled session creation (see resolveSessionStarter).\n const legacyApi = (): WslLegacyConnection['api'] =>\n (ctx.get('connection') as unknown as WslLegacyConnection | undefined)?.api\n const remoteAgentPresets = (): WslAgentPresetsNamespace | undefined =>\n ctx.get('remote.agentPresets') as unknown as WslAgentPresetsNamespace | undefined\n const uiWorkspaceService = (): WslUiWorkspaceFace | undefined =>\n ctx.get('uiWorkspace') as unknown as WslUiWorkspaceFace | undefined\n const hasNoteAgentPreset = typeof sessions.noteAgentPreset === 'function'\n\n /** Unified agent-preset list: new `remote.agentPresets` namespace (v0.1.2-rc.1+) or legacy `connection.api` (v0.1.1-rc.2). */\n const listAgentPresets = async (): Promise<{ ok: boolean; presets: { id: string; broken?: string; isDefault?: boolean }[]; error?: string }> => {\n const agentPresets = remoteAgentPresets()\n if (agentPresets !== undefined) {\n const r = await agentPresets.list()\n if (!r.ok) return { ok: false, presets: [], error: r.error?.message ?? 'list failed' }\n return { ok: true, presets: r.value?.presets ?? [] }\n }\n const api = legacyApi()\n if (api !== undefined) {\n const r = await api.agentPresets.list({})\n if (!r.result.ok) return { ok: false, presets: [], error: r.result.error?.message ?? 'list failed' }\n return { ok: true, presets: r.result.value?.presets ?? [] }\n }\n return { ok: false, presets: [], error: 'no remote api available' }\n }\n\n /** Unified agent-preset select: new `agentPresets.select(id, preset)` or legacy `connection.api.select({...})`. */\n const selectAgentPreset = async (sessionId: string, presetId: string): Promise<{ ok: boolean }> => {\n const agentPresets = remoteAgentPresets()\n if (agentPresets !== undefined) {\n return agentPresets.select(sessionId, presetId)\n }\n const api = legacyApi()\n if (api !== undefined) {\n const r = await api.agentPresets.select({ sessionId, agentPreset: presetId })\n return { ok: r.result.ok }\n }\n return { ok: false }\n }\n\n /**\n * Resolve how this release opens a session for a workspace - v0.1.2-rc.1+\n * exposes `uiWorkspace.startSession`, v0.1.1-rc.2 keeps it on `workspaces`.\n *\n * Resolved BEFORE the workspace is written. A release that offers neither\n * cannot open a session, and a silent fall-through would leave the workspace\n * behind with an empty `sessionIds` while the dialog still reports success;\n * failing here names the missing capability instead.\n * @returns the starter for the service this release actually exposes.\n * @throws Error naming both candidates when neither service is available.\n */\n const resolveSessionStarter = (): (workspaceId: string) => void | Promise<void> => {\n const ui = uiWorkspaceService()\n if (ui !== undefined) return (workspaceId) => { ui.startSession(workspaceId) }\n const legacyStart = workspaces.startSession\n if (typeof legacyStart === 'function') {\n return (workspaceId) => { Reflect.apply(legacyStart, workspaces, [workspaceId]) }\n }\n throw new Error(\n 'workspace session API unavailable: this DSH release exposes neither '\n + 'uiWorkspace.startSession nor workspaces.startSession',\n )\n }\n\n /** Read agent preset — v0.1.2-rc.1+ uses projectionValues; v0.1.1-rc.2 uses direct field. */\n const getAgentPreset = (summary: { agentPreset?: string; projectionValues?: { agentPreset?: string | null } }): string | undefined => {\n if (summary.projectionValues?.agentPreset !== undefined) {\n const v = summary.projectionValues.agentPreset\n return v === null ? undefined : v\n }\n return summary.agentPreset\n }\n\n /** Note preset change — v0.1.1-rc.2 calls noteAgentPreset; v0.1.2-rc.1+ is auto-synced via projection. */\n const noteAgentPresetCompat = (sessionId: string, presetId: string): void => {\n if (hasNoteAgentPreset && sessions.noteAgentPreset) {\n sessions.noteAgentPreset(sessionId, presetId)\n }\n }\n\n ensureStyles()\n\n ctx.effect(\n () => ctx.locale.register('wslWorkspace' as never, { zh, en }),\n 'dsh-wsl-workspace: locale dictionaries',\n )\n\n // The injected translate function reads the live DeepSeek Harness locale,\n // so the dialog copy follows the app language setting automatically.\n const t = ctx.locale.bind('wslWorkspace' as never) as unknown as (key: string, params?: Record<string, unknown>) => string\n\n // Canonical Windows drive keys of every registered `/mnt/<drive>` workspace\n // (refreshed from the host store; see refreshRoster). A blank session whose\n // cwd is one of these binds to the WSL variant like a UNC-cwd session.\n let wslWindowsPaths = new Set<string>()\n\n const injected = (): AddWslWorkspaceInjected => ({\n t,\n checkPreset: async (): Promise<string | undefined> => {\n let roster\n try {\n roster = await listAgentPresets()\n } catch (error) {\n return error instanceof Error ? error.message : String(error)\n }\n if (!roster.ok) return roster.error\n const healthy = roster.presets.find((entry: { id: string; broken?: string }) =>\n entry.id.startsWith('wsl-') && entry.broken === undefined)\n if (healthy === undefined) return t('error.presetMissing')\n return undefined\n },\n listDistros: () => listDistrosApi(),\n // Advisory data for the help panel: a host that cannot answer reports an\n // unavailable description instead of breaking the dialog.\n describe: () => describeApi(),\n listDir: (distro, path) => listDirApi(distro, path),\n check: (distro, path) => checkApi(distro, path),\n createWorkspace: async (linuxPath, username, distro): Promise<string | undefined> => {\n try {\n // Before any write: without a session starter the workspace below\n // would be created and never opened.\n const startSession = resolveSessionStarter()\n const winPath = mntToWindowsPath(linuxPath)\n if (winPath !== null) {\n // `/mnt/<drive>` workspace: the workspace registry realpath/stats\n // the path and 9P cannot serve drvfs mounts, so register under the\n // drive spelling and store distro/username for the session env.\n // The browser binding below recognizes the drive cwd as WSL.\n const view = await workspaces.create({ path: winPath })\n await registerWindowsApi(linuxPath, distro, username)\n const canonical = canonicalWindowsPath(winPath)\n if (canonical !== null) wslWindowsPaths = new Set(wslWindowsPaths).add(canonical)\n await startSession(view.workspaceId)\n return undefined\n }\n const uncPath = joinUnc(distro, linuxPath)\n const view = await workspaces.create({ path: uncPath })\n await setWorkspaceUserApi(uncPath, username)\n await startSession(view.workspaceId)\n return undefined\n } catch (error) {\n return error instanceof Error ? error.message : String(error)\n }\n },\n })\n\n ctx.effect(\n () => ctx.slots.inject(\n 'sidebar.footer.action',\n () => ctx.slots.register(\n { name: 'sidebar.footer.action', id: 'wsl-workspace', inject: injected },\n AddWslWorkspace,\n ),\n ),\n 'dsh-wsl-workspace: sidebar footer action',\n )\n\n // Mode-variant binding: a blank session whose workspace is a WSL UNC path\n // is recomposed to the WSL variant of the mode it currently runs — plain\n // 标准 becomes `wsl-standard`, PTC becomes `wsl-code`, and so on — so the\n // WSL execution world composes with ANY mode instead of replacing it. The\n // host refuses non-blank sessions (agent-preset-locked), so the swap is\n // attempted at most a few times per session.\n ctx.effect(() => {\n const inFlight = new Set<string>()\n const attempts = new Map<string, number>()\n const MAX_ATTEMPTS = 3\n // Healthy `wsl-<mode>` variant ids plus the roster's default preset id\n // (what a session with no explicit choice gets). Refreshed periodically\n // so variants generated after this page loaded are picked up.\n let variants = new Set<string>()\n let defaultPreset: string | undefined\n const refreshRoster = (): void => {\n void listAgentPresets().then((result: {\n ok: boolean; presets: { id: string; broken?: string; isDefault?: boolean }[]\n }) => {\n if (!result.ok) return\n variants = new Set(result.presets\n .filter((entry: { id: string; broken?: string }) =>\n entry.broken === undefined && entry.id.startsWith('wsl-'))\n .map((entry: { id: string }) => entry.id))\n defaultPreset = result.presets.find(\n (entry: { id: string; isDefault?: boolean }) => entry.isDefault === true,\n )?.id\n // The roster is an INPUT to binding. It can land after the first pass\n // (and after apply()), and nothing else would re-run that pass: the\n // session store only emits when a session changes, so a blank session\n // that was already there would stay unbound until the user acted.\n maybeBind()\n }).catch(() => {\n // A failed roster read leaves the previous mapping; sessions stay on\n // their current composition until the next refresh.\n })\n }\n refreshRoster()\n const refreshWorkspaces = (): void => {\n void listWorkspacesApi().then((keys: string[]) => {\n const next = new Set<string>()\n for (const key of keys) {\n const canonical = canonicalWindowsPath(key)\n if (canonical !== null) next.add(canonical)\n }\n wslWindowsPaths = next\n // Same late-input rule as the roster: the `/mnt/<drive>` key set\n // decides binding for drive-cwd sessions.\n maybeBind()\n }).catch(() => {\n // A failed store read leaves the previous set; sessions stay on\n // their current composition until the next refresh.\n })\n }\n refreshWorkspaces()\n const maybeBind = (): void => {\n const state = sessions.list.getSnapshot()\n for (const id of state.ids) {\n const summary = state.byId[id]\n if (summary === undefined || !summary.blank || summary.cwd === undefined) continue\n // A session belongs to the WSL world when its cwd is a WSL UNC path,\n // or a Windows drive path registered as a `/mnt/<drive>` workspace\n // (9P cannot serve drvfs, so those workspaces carry drive cwds).\n const canonical = canonicalWindowsPath(summary.cwd)\n const isWsl = isWslUnc(summary.cwd)\n || (canonical !== null && wslWindowsPaths.has(canonical))\n if (!isWsl) continue\n const current = getAgentPreset(summary)\n if (current !== undefined && current.startsWith('wsl-')) continue\n // Legacy standalone `wsl` (now folded into the variants): remap it to\n // the default mode's variant, since the standalone preset no longer\n // exists in the roster.\n const base = current === LEGACY_WSL_PRESET_ID\n ? (defaultPreset ?? 'standard')\n : (current ?? defaultPreset)\n if (base === undefined || base === LEGACY_WSL_PRESET_ID || base.startsWith('wsl-')) continue\n const target = `wsl-${base.toLowerCase()}`\n if (!variants.has(target)) continue\n if (inFlight.has(id) || (attempts.get(id) ?? 0) >= MAX_ATTEMPTS) continue\n inFlight.add(id)\n void selectAgentPreset(id, target)\n .then((result: { ok: boolean }) => {\n if (result.ok) noteAgentPresetCompat(id, target)\n })\n .catch(() => {\n // A refused or aborted swap (session already produced output,\n // roster churn, reconnect) leaves the session on its current\n // composition; count the attempt so a stuck session stops\n // retrying after MAX_ATTEMPTS.\n attempts.set(id, (attempts.get(id) ?? 0) + 1)\n })\n .finally(() => {\n inFlight.delete(id)\n })\n }\n }\n maybeBind()\n const unsubscribe = sessions.list.subscribe(() => maybeBind())\n // Variants are generated at host boot; a page loaded before that would\n // never see them without a periodic refresh.\n const timer = window.setInterval(refreshRoster, 60_000)\n return () => {\n unsubscribe()\n window.clearInterval(timer)\n }\n }, 'dsh-wsl-workspace: WSL mode-variant binding')\n}\n"],"mappings":"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"}
|
package/lib/fs.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"fs.js","names":[],"sources":["../src/fs.ts"],"sourcesContent":["/**\r\n * WSL Service Provider for the `ctx.fs` capability seam. Backed by the host\r\n * filesystem over the `\\\\wsl.localhost\\<distro>\\…` 9P share — zero install\r\n * inside the distribution — while every model/UI-facing path is the Linux\r\n * path a WSL process would open (`processPath`, `displayPath`, `fileUrl`).\r\n * Reuses `LocalFileSystem`'s mechanics (realpath identity, atomic writes,\r\n * per-target locks, version guards) unchanged, because those operate on the\r\n * UNC path Node can open directly.\r\n *\r\n * Both UNC paths and Linux absolute paths resolve; Windows drive paths\r\n * resolve through their `/mnt/<drive>` form, so a WSL-composed session can\r\n * still touch the Windows filesystem coherently.\r\n * @module dsh-wsl-workspace/fs\r\n */\r\n\r\nimport { Context } from '@deepseek-ai/cordis'\r\nimport z from '@deepseek-ai/schemastery'\r\nimport { AsyncLocalStorage } from 'node:async_hooks'\r\nimport { link, lstat, rename } from 'node:fs/promises'\r\nimport { FsError } from '@deepseek-ai/dsh-fs'\r\nimport type { FsPathInfo, FsTarget } from '@deepseek-ai/dsh-fs'\r\nimport { LocalFileSystem } from '@deepseek-ai/dsh-fs-local'\r\nimport {\r\n isAbsoluteLinuxPath,\r\n joinUnc,\r\n mntToWindowsPath,\r\n parseWslUnc,\r\n windowsToMntPath,\r\n} from './shared/paths.ts'\r\nimport { defaultDistroSync } from './shared/wsl.ts'\r\n\r\n/** Plugin config. `cwd`/`distro` are optional because UNC workdirs carry both. */\r\nexport interface Config {\r\n /** Base directory for relative paths without a per-call cwd (UNC or Linux). */\r\n cwd?: string\r\n /** Default distribution for Linux-absolute paths without a UNC cwd. */\r\n distro?: string\r\n /** Exclusive UTF-8 byte limit on each overwrite-diff side (see fs-local). */\r\n diffBasisMaxBytes?: number\r\n}\r\n\r\n/** One translated coordinate: the input the local backend opens plus its cwd. */\r\ninterface Translated {\r\n /** Absolute path to hand to the local backend (UNC or Windows drive). */\r\n input: string\r\n /** Absolute Windows-side base for relative inputs (UNC or Windows drive). */\r\n cwd: string\r\n}\r\n\r\ninterface ToolExecution {\r\n agent?: { session: { header: { cwd?: string } } }\r\n}\r\n\r\n/**\r\n * The WSL filesystem backend. Identity keys are canonical UNC paths; the\r\n * Linux form is derived on demand, so both worlds stay in sync across\r\n * aliases and symlinks.\r\n */\r\nexport class WslFileSystem extends LocalFileSystem {\r\n static override Config: z<Config> = z.object({\r\n cwd: z.string(),\r\n distro: z.string(),\r\n diffBasisMaxBytes: z.number().default(10 * 1024 * 1024),\r\n })\r\n\r\n private readonly distro: string | undefined\r\n private readonly executionCwd = new AsyncLocalStorage<string | undefined>()\r\n\r\n constructor(ctx: Context, config: Config) {\r\n // schemastery fills the defaults before construction; the parent validates\r\n // `diffBasisMaxBytes` and stores the resolved shape.\r\n super(ctx, config)\r\n this.distro = config.distro\r\n ctx.on('tools/execute', (exec: ToolExecution, next: () => unknown) => (\r\n this.executionCwd.run(exec.agent?.session.header.cwd, next)\r\n ))\r\n // The 9P/drvfs substrate has no hard links and no Win32 security semantics:\r\n // replace the atomic-publication boundaries the parent's fsio defaults to.\r\n this.internals = {\r\n linkFile: WslFileSystem.publishNoReplace,\r\n replaceFile: WslFileSystem.replaceOverWrite,\r\n copyFileDacl: WslFileSystem.skipDaclCopy,\r\n }\r\n }\r\n\r\n /**\r\n * No-replace publication for filesystems without hard links. A real\r\n * collision (a concurrent external creator won) must still surface as the\r\n * original EEXIST so the guarded-create failure path classifies it; an\r\n * absent target falls back to rename, which on Windows publishes without\r\n * replacing anything. Safe against this backend's own writers because the\r\n * per-target lock serializes them.\r\n * @param tempPath - the staged file.\r\n * @param destPath - the destination to create.\r\n */\r\n private static async publishNoReplace(tempPath: string, destPath: string): Promise<void> {\r\n try {\r\n await link(tempPath, destPath)\r\n return\r\n } catch (error) {\r\n let exists = false\r\n try {\r\n await lstat(destPath)\r\n exists = true\r\n } catch {\r\n // Absent destination: rename publishes the staged file.\r\n }\r\n if (exists) throw error\r\n await rename(tempPath, destPath)\r\n }\r\n }\r\n\r\n /**\r\n * Security-preserving replacement boundary: Windows rename replaces an\r\n * existing destination atomically; no DACL preservation is needed over 9P.\r\n * @param destPath - the file being replaced.\r\n * @param tempPath - the staged replacement.\r\n */\r\n private static async replaceOverWrite(destPath: string, tempPath: string): Promise<void> {\r\n await rename(tempPath, destPath)\r\n }\r\n\r\n /** 9P files inherit their directory's DACL; nothing to preserve. */\r\n private static async skipDaclCopy(): Promise<void> {}\r\n\r\n /** Translate a model/plugin path into Windows-side coordinates. */\r\n private translate(path: string, cwd?: string): Translated {\r\n const unc = parseWslUnc(path)\r\n if (unc !== null) {\r\n return { input: joinUnc(unc.distro, unc.linuxPath), cwd: this.cwdOr(cwd) }\r\n }\r\n if (isAbsoluteLinuxPath(path)) {\r\n // /mnt/<drive>/… names the Windows filesystem inside the Linux world\r\n // (the dual-access path for migration): open the drive path directly so\r\n // both worlds stay coherent — the display stays the /mnt form.\r\n const win = mntToWindowsPath(path)\r\n if (win !== null) return { input: win, cwd: this.cwdOr(cwd) }\r\n return { input: joinUnc(this.distroFor(cwd), path), cwd: this.cwdOr(cwd) }\r\n }\r\n if (windowsToMntPath(path) !== null) {\r\n // Windows drive paths open directly; the Linux world reaches them via /mnt.\r\n return { input: path, cwd: this.cwdOr(cwd) }\r\n }\r\n // Relative: resolve against the caller cwd (or the configured base).\r\n const base = this.uncCwd(cwd)\r\n return { input: path, cwd: base }\r\n }\r\n\r\n /** A base for absolute inputs (unused by resolution, but the parent needs one). */\r\n private cwdOr(cwd?: string): string {\r\n return cwd ?? this.executionCwd.getStore() ?? this.config.cwd ?? process.cwd()\r\n }\r\n\r\n private uncCwd(cwd?: string): string {\r\n const base = cwd ?? this.executionCwd.getStore() ?? this.config.cwd\r\n if (base === undefined || base === '') {\r\n throw new FsError('wsl-fs: no cwd and no configured base for relative resolution', 'FS_IO_ERROR')\r\n }\r\n const unc = parseWslUnc(base)\r\n if (unc !== null) return joinUnc(unc.distro, unc.linuxPath)\r\n if (isAbsoluteLinuxPath(base)) return joinUnc(this.distroFor(base), base)\r\n if (windowsToMntPath(base) !== null) return base\r\n throw new FsError(`wsl-fs: cwd \"${base}\" is not in the WSL execution world`, 'FS_IO_ERROR')\r\n }\r\n\r\n /**\r\n * Resolve the distribution an absolute Linux path opens inside. The chain:\r\n * the caller cwd when it is a WSL UNC path, then the current tool\r\n * execution's session cwd, then the configured `distro`,\r\n * then the host's default distribution from the Lxss registry. The registry\r\n * fallback is reserved for calls that genuinely have no session.\r\n */\r\n private distroFor(cwd?: string): string {\r\n const fromCwd = parseWslUnc(cwd ?? '')\r\n if (fromCwd !== null) return fromCwd.distro\r\n const fromExecution = parseWslUnc(this.executionCwd.getStore() ?? '')\r\n if (fromExecution !== null) return fromExecution.distro\r\n const distro = this.distro\r\n if (distro !== undefined && distro !== '') return distro\r\n const fallback = defaultDistroSync()\r\n if (fallback !== undefined) return fallback\r\n throw new FsError('wsl-fs: Linux path carries no distribution and none is configured', 'FS_IO_ERROR')\r\n }\r\n\r\n /** The Linux display path for a resolved Windows-side path. */\r\n private linuxDisplay(raw: string): string {\r\n const unc = parseWslUnc(raw)\r\n if (unc !== null) return unc.linuxPath\r\n const mnt = windowsToMntPath(raw)\r\n if (mnt !== null) return mnt\r\n throw new FsError(`wsl-fs: resolved path \"${raw}\" is outside the WSL execution world`, 'FS_IO_ERROR')\r\n }\r\n\r\n override async resolve(path: string, opts?: { cwd?: string; signal?: AbortSignal }): Promise<FsTarget> {\r\n if (opts?.signal?.aborted) throw new FsError('resolve aborted', 'FS_ABORTED')\r\n const { input, cwd } = this.translate(path, opts?.cwd)\r\n const local = await super.resolve(input, {\r\n cwd,\r\n ...opts?.signal !== undefined ? { signal: opts.signal } : {},\r\n })\r\n return { targetKey: local.targetKey, displayPath: this.linuxDisplay(String(local.displayPath)) }\r\n }\r\n\r\n override processPath(target: FsTarget): string {\r\n const key = String(target.targetKey)\r\n const unc = parseWslUnc(key)\r\n if (unc !== null) return unc.linuxPath\r\n const mnt = windowsToMntPath(key)\r\n if (mnt !== null) return mnt\r\n throw new FsError(`wsl-fs: target \"${target.displayPath}\" is outside the WSL execution world`, 'FS_IO_ERROR')\r\n }\r\n\r\n override fileUrl(target: FsTarget): string {\r\n const linux = this.processPath(target)\r\n const encoded = linux.split('/').map(encodeURIComponent).join('/')\r\n return `file://${encoded}`\r\n }\r\n\r\n override contains(parent: FsTarget, child: FsTarget): boolean {\r\n const parentWorld = this.worldPath(parent)\r\n const childWorld = this.worldPath(child)\r\n if (parentWorld.distro !== childWorld.distro) return false\r\n const parentPath = parentWorld.linuxPath\r\n const childPath = childWorld.linuxPath\r\n if (childPath === parentPath) return true\r\n return parentPath === '/' ? true : childPath.startsWith(`${parentPath}/`)\r\n }\r\n\r\n /** One target's (distro, linuxPath) pair for containment; `undefined` distro = Windows world. */\r\n private worldPath(target: FsTarget): { distro: string | undefined; linuxPath: string } {\r\n const key = String(target.targetKey)\r\n const unc = parseWslUnc(key)\r\n if (unc !== null) return { distro: unc.distro, linuxPath: unc.linuxPath }\r\n const mnt = windowsToMntPath(key)\r\n if (mnt !== null) return { distro: undefined, linuxPath: mnt }\r\n throw new FsError(`wsl-fs: target \"${target.displayPath}\" is outside the WSL execution world`, 'FS_IO_ERROR')\r\n }\r\n\r\n override async lstat(path: string, opts?: { cwd?: string }, signal?: AbortSignal): Promise<FsPathInfo | undefined> {\r\n if (signal?.aborted) throw new FsError('lstat aborted', 'FS_ABORTED')\r\n if (path.trim().length === 0) throw new FsError('file_path must be a non-empty string', 'FS_NOT_FOUND')\r\n const { input, cwd } = this.translate(path, opts?.cwd)\r\n return super.lstat(input, { cwd }, signal)\r\n }\r\n}\r\n\r\nexport default WslFileSystem\r\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"fs.js","names":[],"sources":["../src/fs.ts"],"sourcesContent":["/**\n * WSL Service Provider for the `ctx.fs` capability seam. Backed by the host\n * filesystem over the `\\\\wsl.localhost\\<distro>\\…` 9P share — zero install\n * inside the distribution — while every model/UI-facing path is the Linux\n * path a WSL process would open (`processPath`, `displayPath`, `fileUrl`).\n * Reuses `LocalFileSystem`'s mechanics (realpath identity, atomic writes,\n * per-target locks, version guards) unchanged, because those operate on the\n * UNC path Node can open directly.\n *\n * Both UNC paths and Linux absolute paths resolve; Windows drive paths\n * resolve through their `/mnt/<drive>` form, so a WSL-composed session can\n * still touch the Windows filesystem coherently.\n * @module dsh-wsl-workspace/fs\n */\n\nimport { Context } from '@deepseek-ai/cordis'\nimport z from '@deepseek-ai/schemastery'\nimport { AsyncLocalStorage } from 'node:async_hooks'\nimport { link, lstat, rename } from 'node:fs/promises'\nimport { FsError } from '@deepseek-ai/dsh-fs'\nimport type { FsPathInfo, FsTarget } from '@deepseek-ai/dsh-fs'\nimport { LocalFileSystem } from '@deepseek-ai/dsh-fs-local'\nimport {\n isAbsoluteLinuxPath,\n joinUnc,\n mntToWindowsPath,\n parseWslUnc,\n windowsToMntPath,\n} from './shared/paths.ts'\nimport { defaultDistroSync } from './shared/wsl.ts'\n\n/** Plugin config. `cwd`/`distro` are optional because UNC workdirs carry both. */\nexport interface Config {\n /** Base directory for relative paths without a per-call cwd (UNC or Linux). */\n cwd?: string\n /** Default distribution for Linux-absolute paths without a UNC cwd. */\n distro?: string\n /** Exclusive UTF-8 byte limit on each overwrite-diff side (see fs-local). */\n diffBasisMaxBytes?: number\n}\n\n/** One translated coordinate: the input the local backend opens plus its cwd. */\ninterface Translated {\n /** Absolute path to hand to the local backend (UNC or Windows drive). */\n input: string\n /** Absolute Windows-side base for relative inputs (UNC or Windows drive). */\n cwd: string\n}\n\ninterface ToolExecution {\n agent?: { session: { header: { cwd?: string } } }\n}\n\n/**\n * The WSL filesystem backend. Identity keys are canonical UNC paths; the\n * Linux form is derived on demand, so both worlds stay in sync across\n * aliases and symlinks.\n */\nexport class WslFileSystem extends LocalFileSystem {\n static override Config: z<Config> = z.object({\n cwd: z.string(),\n distro: z.string(),\n diffBasisMaxBytes: z.number().default(10 * 1024 * 1024),\n })\n\n private readonly distro: string | undefined\n private readonly executionCwd = new AsyncLocalStorage<string | undefined>()\n\n constructor(ctx: Context, config: Config) {\n // schemastery fills the defaults before construction; the parent validates\n // `diffBasisMaxBytes` and stores the resolved shape.\n super(ctx, config)\n this.distro = config.distro\n ctx.on('tools/execute', (exec: ToolExecution, next: () => unknown) => (\n this.executionCwd.run(exec.agent?.session.header.cwd, next)\n ))\n // The 9P/drvfs substrate has no hard links and no Win32 security semantics:\n // replace the atomic-publication boundaries the parent's fsio defaults to.\n this.internals = {\n linkFile: WslFileSystem.publishNoReplace,\n replaceFile: WslFileSystem.replaceOverWrite,\n copyFileDacl: WslFileSystem.skipDaclCopy,\n }\n }\n\n /**\n * No-replace publication for filesystems without hard links. A real\n * collision (a concurrent external creator won) must still surface as the\n * original EEXIST so the guarded-create failure path classifies it; an\n * absent target falls back to rename, which on Windows publishes without\n * replacing anything. Safe against this backend's own writers because the\n * per-target lock serializes them.\n * @param tempPath - the staged file.\n * @param destPath - the destination to create.\n */\n private static async publishNoReplace(tempPath: string, destPath: string): Promise<void> {\n try {\n await link(tempPath, destPath)\n return\n } catch (error) {\n let exists = false\n try {\n await lstat(destPath)\n exists = true\n } catch {\n // Absent destination: rename publishes the staged file.\n }\n if (exists) throw error\n await rename(tempPath, destPath)\n }\n }\n\n /**\n * Security-preserving replacement boundary: Windows rename replaces an\n * existing destination atomically; no DACL preservation is needed over 9P.\n * @param destPath - the file being replaced.\n * @param tempPath - the staged replacement.\n */\n private static async replaceOverWrite(destPath: string, tempPath: string): Promise<void> {\n await rename(tempPath, destPath)\n }\n\n /** 9P files inherit their directory's DACL; nothing to preserve. */\n private static async skipDaclCopy(): Promise<void> {}\n\n /** Translate a model/plugin path into Windows-side coordinates. */\n private translate(path: string, cwd?: string): Translated {\n const unc = parseWslUnc(path)\n if (unc !== null) {\n return { input: joinUnc(unc.distro, unc.linuxPath), cwd: this.cwdOr(cwd) }\n }\n if (isAbsoluteLinuxPath(path)) {\n // /mnt/<drive>/… names the Windows filesystem inside the Linux world\n // (the dual-access path for migration): open the drive path directly so\n // both worlds stay coherent — the display stays the /mnt form.\n const win = mntToWindowsPath(path)\n if (win !== null) return { input: win, cwd: this.cwdOr(cwd) }\n return { input: joinUnc(this.distroFor(cwd), path), cwd: this.cwdOr(cwd) }\n }\n if (windowsToMntPath(path) !== null) {\n // Windows drive paths open directly; the Linux world reaches them via /mnt.\n return { input: path, cwd: this.cwdOr(cwd) }\n }\n // Relative: resolve against the caller cwd (or the configured base).\n const base = this.uncCwd(cwd)\n return { input: path, cwd: base }\n }\n\n /** A base for absolute inputs (unused by resolution, but the parent needs one). */\n private cwdOr(cwd?: string): string {\n return cwd ?? this.executionCwd.getStore() ?? this.config.cwd ?? process.cwd()\n }\n\n private uncCwd(cwd?: string): string {\n const base = cwd ?? this.executionCwd.getStore() ?? this.config.cwd\n if (base === undefined || base === '') {\n throw new FsError('wsl-fs: no cwd and no configured base for relative resolution', 'FS_IO_ERROR')\n }\n const unc = parseWslUnc(base)\n if (unc !== null) return joinUnc(unc.distro, unc.linuxPath)\n if (isAbsoluteLinuxPath(base)) return joinUnc(this.distroFor(base), base)\n if (windowsToMntPath(base) !== null) return base\n throw new FsError(`wsl-fs: cwd \"${base}\" is not in the WSL execution world`, 'FS_IO_ERROR')\n }\n\n /**\n * Resolve the distribution an absolute Linux path opens inside. The chain:\n * the caller cwd when it is a WSL UNC path, then the current tool\n * execution's session cwd, then the configured `distro`,\n * then the host's default distribution from the Lxss registry. The registry\n * fallback is reserved for calls that genuinely have no session.\n */\n private distroFor(cwd?: string): string {\n const fromCwd = parseWslUnc(cwd ?? '')\n if (fromCwd !== null) return fromCwd.distro\n const fromExecution = parseWslUnc(this.executionCwd.getStore() ?? '')\n if (fromExecution !== null) return fromExecution.distro\n const distro = this.distro\n if (distro !== undefined && distro !== '') return distro\n const fallback = defaultDistroSync()\n if (fallback !== undefined) return fallback\n throw new FsError('wsl-fs: Linux path carries no distribution and none is configured', 'FS_IO_ERROR')\n }\n\n /** The Linux display path for a resolved Windows-side path. */\n private linuxDisplay(raw: string): string {\n const unc = parseWslUnc(raw)\n if (unc !== null) return unc.linuxPath\n const mnt = windowsToMntPath(raw)\n if (mnt !== null) return mnt\n throw new FsError(`wsl-fs: resolved path \"${raw}\" is outside the WSL execution world`, 'FS_IO_ERROR')\n }\n\n override async resolve(path: string, opts?: { cwd?: string; signal?: AbortSignal }): Promise<FsTarget> {\n if (opts?.signal?.aborted) throw new FsError('resolve aborted', 'FS_ABORTED')\n const { input, cwd } = this.translate(path, opts?.cwd)\n const local = await super.resolve(input, {\n cwd,\n ...opts?.signal !== undefined ? { signal: opts.signal } : {},\n })\n return { targetKey: local.targetKey, displayPath: this.linuxDisplay(String(local.displayPath)) }\n }\n\n override processPath(target: FsTarget): string {\n const key = String(target.targetKey)\n const unc = parseWslUnc(key)\n if (unc !== null) return unc.linuxPath\n const mnt = windowsToMntPath(key)\n if (mnt !== null) return mnt\n throw new FsError(`wsl-fs: target \"${target.displayPath}\" is outside the WSL execution world`, 'FS_IO_ERROR')\n }\n\n override fileUrl(target: FsTarget): string {\n const linux = this.processPath(target)\n const encoded = linux.split('/').map(encodeURIComponent).join('/')\n return `file://${encoded}`\n }\n\n override contains(parent: FsTarget, child: FsTarget): boolean {\n const parentWorld = this.worldPath(parent)\n const childWorld = this.worldPath(child)\n if (parentWorld.distro !== childWorld.distro) return false\n const parentPath = parentWorld.linuxPath\n const childPath = childWorld.linuxPath\n if (childPath === parentPath) return true\n return parentPath === '/' ? true : childPath.startsWith(`${parentPath}/`)\n }\n\n /** One target's (distro, linuxPath) pair for containment; `undefined` distro = Windows world. */\n private worldPath(target: FsTarget): { distro: string | undefined; linuxPath: string } {\n const key = String(target.targetKey)\n const unc = parseWslUnc(key)\n if (unc !== null) return { distro: unc.distro, linuxPath: unc.linuxPath }\n const mnt = windowsToMntPath(key)\n if (mnt !== null) return { distro: undefined, linuxPath: mnt }\n throw new FsError(`wsl-fs: target \"${target.displayPath}\" is outside the WSL execution world`, 'FS_IO_ERROR')\n }\n\n override async lstat(path: string, opts?: { cwd?: string }, signal?: AbortSignal): Promise<FsPathInfo | undefined> {\n if (signal?.aborted) throw new FsError('lstat aborted', 'FS_ABORTED')\n if (path.trim().length === 0) throw new FsError('file_path must be a non-empty string', 'FS_NOT_FOUND')\n const { input, cwd } = this.translate(path, opts?.cwd)\n return super.lstat(input, { cwd }, signal)\n }\n}\n\nexport default WslFileSystem\n"],"mappings":"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"}
|