dsh-wsl-workspace 0.3.2 → 0.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/client.js CHANGED
@@ -1,582 +1,616 @@
1
- window.__ModuleLoader__.load({
2
- id: "dsh-wsl-workspace",
3
- factory: (require) => {
4
- var module = { exports: {} };
5
- var exports = module.exports;
6
- Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
7
- let react = require("react");
8
- let react_jsx_runtime = require("react/jsx-runtime");
9
- //#region src/client/api.ts
10
- /**
11
- * Thin fetch client for the Host plugin route. The browser calls
12
- * POST /wsl-workspace/api with a `{ method, params }` envelope and the Host
13
- * answers `{ ok: true, value }` or `{ ok: false, error }`.
14
- */
15
- /** Relative route the Host half registers (same-origin with the web server). */
16
- const ENDPOINT = "/wsl-workspace/api";
17
- /** Human text for an unknown rejection, reusing the repository's idiom. */
18
- function errorMessage(value) {
19
- return value instanceof Error ? value.message : String(value);
1
+ window.__ModuleLoader__.load({ id: "dsh-wsl-workspace", factory: (require) => {
2
+ var module = { exports: {} }; var exports = module.exports;
3
+ //#region rolldown:runtime
4
+ var __create = Object.create;
5
+ var __defProp = Object.defineProperty;
6
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
7
+ var __getOwnPropNames = Object.getOwnPropertyNames;
8
+ var __getProtoOf = Object.getPrototypeOf;
9
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
12
+ key = keys[i];
13
+ if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
14
+ get: ((k) => from[k]).bind(null, key),
15
+ enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
16
+ });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
21
+ value: mod,
22
+ enumerable: true
23
+ }) : target, mod));
24
+
25
+ //#endregion
26
+ const react = __toESM(require("react"));
27
+ const react_jsx_runtime = __toESM(require("react/jsx-runtime"));
28
+
29
+ //#region src/client/api.ts
30
+ /**
31
+ * Thin fetch client for the Host plugin route. The browser calls
32
+ * POST /wsl-workspace/api with a `{ method, params }` envelope and the Host
33
+ * answers `{ ok: true, value }` or `{ ok: false, error }`.
34
+ */
35
+ /** Relative route the Host half registers (same-origin with the web server). */
36
+ const ENDPOINT = "/wsl-workspace/api";
37
+ /** Human text for an unknown rejection, reusing the repository's idiom. */
38
+ function errorMessage(value) {
39
+ return value instanceof Error ? value.message : String(value);
40
+ }
41
+ /**
42
+ * Perform one POST call and unwrap the envelope.
43
+ * @param method - the Host method name.
44
+ * @param params - the method payload.
45
+ * @returns the unwrapped value, or throws an Error on network or `ok:false`.
46
+ */
47
+ async function call(method, params = {}) {
48
+ let response;
49
+ try {
50
+ response = await fetch(ENDPOINT, {
51
+ method: "POST",
52
+ headers: { "content-type": "application/json" },
53
+ body: JSON.stringify({
54
+ method,
55
+ params
56
+ })
57
+ });
58
+ } catch (error) {
59
+ throw new Error(`wsl-workspace request failed: ${errorMessage(error)}`);
60
+ }
61
+ let envelope;
62
+ try {
63
+ envelope = await response.json();
64
+ } catch {
65
+ throw new Error(`wsl-workspace answered non-JSON (${response.status})`);
66
+ }
67
+ if (!envelope.ok) throw new Error(envelope.error);
68
+ return envelope.value;
69
+ }
70
+ /**
71
+ * List the WSL distros installed on the host.
72
+ * @returns distro names in registry order.
73
+ */
74
+ async function listDistros() {
75
+ return call("listDistros", {});
76
+ }
77
+ /**
78
+ * List one directory level inside a distro.
79
+ * @param distro - distro name.
80
+ * @param path - absolute Linux directory to list.
81
+ * @returns the level's listing with ancestry.
82
+ */
83
+ async function listDir(distro, path) {
84
+ return call("listDir", {
85
+ distro,
86
+ path
87
+ });
88
+ }
89
+ /**
90
+ * Check whether a Linux path exists and is a directory.
91
+ * @param distro - distro name.
92
+ * @param path - absolute Linux path.
93
+ * @returns existence and directory facts.
94
+ */
95
+ async function check(distro, path) {
96
+ return call("check", {
97
+ distro,
98
+ path
99
+ });
100
+ }
101
+ /**
102
+ * Store (or clear, with an empty string) the username of one WSL workspace.
103
+ * @param path - the workspace UNC path.
104
+ * @param username - the Linux username; empty string clears the stored value.
105
+ */
106
+ async function setWorkspaceUser(path, username) {
107
+ return call("setUser", {
108
+ path,
109
+ username
110
+ });
111
+ }
112
+ /**
113
+ * Register a `/mnt/<drive>` WSL workspace under its Windows drive path,
114
+ * recording the distro (and optional username) for the session env.
115
+ * @param linuxPath - the `/mnt/<drive>/…` Linux path.
116
+ * @param distro - the WSL distribution the workspace belongs to.
117
+ * @param username - optional Linux username.
118
+ */
119
+ async function registerWindows(linuxPath, distro, username) {
120
+ return call("registerWindows", {
121
+ linuxPath,
122
+ distro,
123
+ username
124
+ });
125
+ }
126
+ /**
127
+ * List every registered WSL workspace key (canonical UNC and Windows drive
128
+ * spellings). The client uses the drive keys to recognize `/mnt` workspaces
129
+ * across page reloads.
130
+ */
131
+ async function listWorkspaces() {
132
+ return call("listWorkspaces", {});
133
+ }
134
+
135
+ //#endregion
136
+ //#region src/shared/paths.ts
137
+ /**
138
+ * WSL path helpers shared by the client and host halves. Pure and
139
+ * dependency-free so both planes can import them without a runtime edge.
140
+ */
141
+ /** WSL2 default loopback bridge host: `\\wsl.localhost\<distro>\...`. */
142
+ const WSL_LOCALHOST_HOST = "wsl.localhost";
143
+ /** Legacy WSL interop host: `\\wsl$\<distro>\...`. */
144
+ const WSL_LEGACY_HOST = "wsl$";
145
+ /** The two UNC hosts WSL exposes a distribution's filesystem under. */
146
+ const UNC_HOSTS = [WSL_LOCALHOST_HOST, WSL_LEGACY_HOST];
147
+ /**
148
+ * Parse a WSL UNC path into its distro and Linux path. Accepts the WSL2
149
+ * `\\wsl.localhost\<distro>\<linux>` form, the legacy `\\wsl$\<distro>\<linux>`
150
+ * interop form, and forward-slash spellings of either.
151
+ * @param raw - candidate absolute path.
152
+ * @returns the parsed target, or null when the path is not a WSL UNC.
153
+ */
154
+ function parseWslUnc(raw) {
155
+ const normalized = raw.replace(/\\/g, "/").replace(/\/\/+/g, "//");
156
+ if (!normalized.startsWith("//")) return null;
157
+ const segments = normalized.slice(2).split("/");
158
+ const host = (segments[0] ?? "").toLowerCase();
159
+ if (!UNC_HOSTS.includes(host)) return null;
160
+ const distro = segments[1] ?? "";
161
+ if (distro === "") return null;
162
+ const rest = segments.slice(2).filter((segment) => segment.length > 0);
163
+ return {
164
+ distro,
165
+ linuxPath: `/${rest.join("/")}`
166
+ };
167
+ }
168
+ /**
169
+ * Whether a path resolves into a WSL distro through either UNC form.
170
+ * @param raw - candidate absolute path.
171
+ * @returns whether the path parses as a WSL UNC.
172
+ */
173
+ function isWslUnc(raw) {
174
+ return parseWslUnc(raw) !== null;
175
+ }
176
+ /**
177
+ * Normalize a Linux absolute path for the Host: collapse repeated slashes and
178
+ * strip a trailing slash (root becomes `/`).
179
+ * @param path - absolute Linux path.
180
+ * @returns the normalized path.
181
+ */
182
+ function normalizeLinuxPath(path) {
183
+ const collapsed = path.replace(/\/+/g, "/");
184
+ return collapsed === "/" ? "/" : collapsed.replace(/\/$/, "");
185
+ }
186
+ /**
187
+ * Whether a path is an absolute, non-empty Linux path.
188
+ * @param path - candidate.
189
+ * @returns whether it starts with `/` and contains no NUL.
190
+ */
191
+ function isAbsoluteLinuxPath(path) {
192
+ return path.startsWith("/") && !path.includes("\0");
193
+ }
194
+ /**
195
+ * Join a distro and a Linux absolute path into the WSL2 UNC form used as the
196
+ * workspace identity (`\\wsl.localhost\<distro>\<linux>`, backslash segments).
197
+ * @param distro - distro name.
198
+ * @param linuxPath - absolute Linux path (leading `/`).
199
+ * @returns the UNC path.
200
+ */
201
+ function joinUnc(distro, linuxPath) {
202
+ if (!isAbsoluteLinuxPath(linuxPath)) throw new Error(`wsl-workspace: cannot map a non-absolute Linux path "${linuxPath}" to UNC`);
203
+ if (distro === "" || distro === "." || distro === ".." || /[\\/]/.test(distro)) throw new Error(`wsl-workspace: invalid distribution name "${distro}"`);
204
+ const normalized = linuxPath.replace(/\/+/g, "/").replace(/\/$/, "");
205
+ const withoutLeading = normalized.startsWith("/") ? normalized.slice(1) : normalized;
206
+ const windowsSegments = withoutLeading.replace(/\//g, "\\");
207
+ const suffix = windowsSegments === "" ? "" : `\\${windowsSegments}`;
208
+ return `\\\\wsl.localhost\\${distro}${suffix}`;
209
+ }
210
+ /**
211
+ * Translate a `/mnt/<drive>/…` path back to its Windows drive path.
212
+ * @param linuxPath - the candidate Linux path.
213
+ * @returns the `X:\…` drive path, or `null` when the path is not a drvfs mount.
214
+ */
215
+ function mntToWindowsPath(linuxPath) {
216
+ const match = /^\/mnt\/([a-zA-Z])(?:\/(.*))?$/.exec(linuxPath);
217
+ if (match === null) return null;
218
+ const rest = (match[2] ?? "").replace(/\//g, "\\");
219
+ return `${(match[1] ?? "").toUpperCase()}:\\${rest}`;
220
+ }
221
+ /**
222
+ * Canonical Windows drive path for store keys and cross-realm identity:
223
+ * separators unified to `\`, trailing separator stripped, and the WHOLE path
224
+ * lowercased — Windows paths compare case-insensitively, and the workspace
225
+ * registry may realpath a different casing than the caller spelled (8.3 or
226
+ * on-disk casing), so the store key must collide across casings.
227
+ * @param path - candidate Windows drive path.
228
+ * @returns the canonical form, or `null` when not drive-shaped.
229
+ */
230
+ function canonicalWindowsPath(path) {
231
+ const match = /^([A-Za-z]):[\\/](.*)$/.exec(path);
232
+ if (match === null) return null;
233
+ const rest = (match[2] ?? "").replace(/[\\/]+/g, "\\").replace(/\\$/, "").toLowerCase();
234
+ return `${(match[1] ?? "").toLowerCase()}:\\${rest}`;
235
+ }
236
+ /** Linux username shape for `wsl.exe -u`: starts with a letter or underscore, then letters/digits/`_`/`.`/`-` (max 64). */
237
+ const WSL_USERNAME_PATTERN = /^[A-Za-z_][A-Za-z0-9_.-]{0,63}$/;
238
+ /**
239
+ * Whether a value is a safe Linux username for `wsl.exe -u`. The check is
240
+ * strict on purpose: a value starting with `-` could be parsed as a wsl.exe
241
+ * option instead of a username.
242
+ * @param value - candidate username.
243
+ * @returns whether it matches the Linux username shape.
244
+ */
245
+ function isValidWslUsername(value) {
246
+ return WSL_USERNAME_PATTERN.test(value);
247
+ }
248
+
249
+ //#endregion
250
+ //#region src/client/AddWslWorkspace.tsx
251
+ /**
252
+ * Build the Linux child path one level below a parent, for the breadcrumb/
253
+ * browse drill.
254
+ * @param parent - the currently listed absolute path (`/` for root).
255
+ * @param name - the child directory name.
256
+ * @returns the child's absolute Linux path.
257
+ */
258
+ function dirChildPath(parent, name) {
259
+ return parent === "/" ? `/${name}` : `${parent}/${name}`;
260
+ }
261
+ /** A tiny inline terminal glyph for the dialog's directory rows. */
262
+ function WslGlyph({ size = 16 }) {
263
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("svg", {
264
+ width: size,
265
+ height: size,
266
+ viewBox: "0 0 24 24",
267
+ fill: "none",
268
+ "aria-hidden": "true",
269
+ children: [
270
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("rect", {
271
+ x: "2.5",
272
+ y: "4.5",
273
+ width: "19",
274
+ height: "15",
275
+ rx: "2.5",
276
+ stroke: "currentColor",
277
+ strokeWidth: "1.6"
278
+ }),
279
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
280
+ d: "M6 9l3.2 2.6L6 14",
281
+ stroke: "currentColor",
282
+ strokeWidth: "1.6",
283
+ strokeLinecap: "round",
284
+ strokeLinejoin: "round"
285
+ }),
286
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
287
+ d: "M12 14h5",
288
+ stroke: "currentColor",
289
+ strokeWidth: "1.6",
290
+ strokeLinecap: "round"
291
+ })
292
+ ]
293
+ });
294
+ }
295
+ /**
296
+ * The "Add WSL workspace…" footer action and its dialog.
297
+ * @param props - owner share + injected face.
298
+ */
299
+ function AddWslWorkspace({ wide, t, checkPreset, listDistros: listDistros$1, listDir: listDir$1, check: check$1, createWorkspace }) {
300
+ const [open, setOpen] = (0, react.useState)(false);
301
+ const [opening, setOpening] = (0, react.useState)(false);
302
+ const [distros, setDistros] = (0, react.useState)([]);
303
+ const [distro, setDistro] = (0, react.useState)("");
304
+ const [pathInput, setPathInput] = (0, react.useState)("/home/");
305
+ const [username, setUsername] = (0, react.useState)("");
306
+ const [listing, setListing] = (0, react.useState)(null);
307
+ const [browsePath, setBrowsePath] = (0, react.useState)("/");
308
+ const [browsing, setBrowsing] = (0, react.useState)(false);
309
+ const [error, setError] = (0, react.useState)(null);
310
+ const [busy, setBusy] = (0, react.useState)(false);
311
+ const browseSeq = (0, react.useRef)(0);
312
+ const refreshBrowse = async (root, targetDistro) => {
313
+ const seq = ++browseSeq.current;
314
+ setBrowsing(true);
315
+ setBrowsePath(root);
316
+ try {
317
+ const value = await listDir$1(targetDistro, root);
318
+ if (seq === browseSeq.current) setListing(value);
319
+ } catch {
320
+ if (seq === browseSeq.current) {
321
+ setListing(null);
322
+ setError((previous) => previous ?? t("error.loadDir"));
323
+ }
324
+ } finally {
325
+ if (seq === browseSeq.current) setBrowsing(false);
20
326
  }
21
- /**
22
- * Perform one POST call and unwrap the envelope.
23
- * @param method - the Host method name.
24
- * @param params - the method payload.
25
- * @returns the unwrapped value, or throws an Error on network or `ok:false`.
26
- */
27
- async function call(method, params = {}) {
28
- let response;
327
+ };
328
+ (0, react.useEffect)(() => {
329
+ if (!open) return;
330
+ let cancelled = false;
331
+ setError(null);
332
+ setOpening(true);
333
+ (async () => {
334
+ let presetIssue;
29
335
  try {
30
- response = await fetch(ENDPOINT, {
31
- method: "POST",
32
- headers: { "content-type": "application/json" },
33
- body: JSON.stringify({
34
- method,
35
- params
36
- })
37
- });
38
- } catch (error) {
39
- throw new Error(`wsl-workspace request failed: ${errorMessage(error)}`);
336
+ presetIssue = await checkPreset();
337
+ } catch {
338
+ presetIssue = t("error.loadDistros");
40
339
  }
41
- let envelope;
340
+ let names;
42
341
  try {
43
- envelope = await response.json();
342
+ names = await listDistros$1();
44
343
  } catch {
45
- throw new Error(`wsl-workspace answered non-JSON (${response.status})`);
344
+ if (cancelled) return;
345
+ setOpening(false);
346
+ setError(t("error.loadDistros"));
347
+ return;
46
348
  }
47
- if (!envelope.ok) throw new Error(envelope.error);
48
- return envelope.value;
49
- }
50
- /**
51
- * List the WSL distros installed on the host.
52
- * @returns distro names in registry order.
53
- */
54
- async function listDistros() {
55
- return call("listDistros", {});
56
- }
57
- /**
58
- * List one directory level inside a distro.
59
- * @param distro - distro name.
60
- * @param path - absolute Linux directory to list.
61
- * @returns the level's listing with ancestry.
62
- */
63
- async function listDir(distro, path) {
64
- return call("listDir", {
65
- distro,
66
- path
67
- });
68
- }
69
- /**
70
- * Check whether a Linux path exists and is a directory.
71
- * @param distro - distro name.
72
- * @param path - absolute Linux path.
73
- * @returns existence and directory facts.
74
- */
75
- async function check(distro, path) {
76
- return call("check", {
77
- distro,
78
- path
79
- });
80
- }
81
- /**
82
- * Store (or clear, with an empty string) the username of one WSL workspace.
83
- * @param path - the workspace UNC path.
84
- * @param username - the Linux username; empty string clears the stored value.
85
- */
86
- async function setWorkspaceUser(path, username) {
87
- return call("setUser", {
88
- path,
89
- username
90
- });
91
- }
92
- /**
93
- * Register a `/mnt/<drive>` WSL workspace under its Windows drive path,
94
- * recording the distro (and optional username) for the session env.
95
- * @param linuxPath - the `/mnt/<drive>/…` Linux path.
96
- * @param distro - the WSL distribution the workspace belongs to.
97
- * @param username - optional Linux username.
98
- */
99
- async function registerWindows(linuxPath, distro, username) {
100
- return call("registerWindows", {
101
- linuxPath,
102
- distro,
103
- username
104
- });
105
- }
106
- /**
107
- * List every registered WSL workspace key (canonical UNC and Windows drive
108
- * spellings). The client uses the drive keys to recognize `/mnt` workspaces
109
- * across page reloads.
110
- */
111
- async function listWorkspaces() {
112
- return call("listWorkspaces", {});
113
- }
114
- //#endregion
115
- //#region src/shared/paths.ts
116
- /** The two UNC hosts WSL exposes a distribution's filesystem under. */
117
- const UNC_HOSTS = ["wsl.localhost", "wsl$"];
118
- /**
119
- * Parse a WSL UNC path into its distro and Linux path. Accepts the WSL2
120
- * `\\wsl.localhost\<distro>\<linux>` form, the legacy `\\wsl$\<distro>\<linux>`
121
- * interop form, and forward-slash spellings of either.
122
- * @param raw - candidate absolute path.
123
- * @returns the parsed target, or null when the path is not a WSL UNC.
124
- */
125
- function parseWslUnc(raw) {
126
- const normalized = raw.replace(/\\/g, "/").replace(/\/\/+/g, "//");
127
- if (!normalized.startsWith("//")) return null;
128
- const segments = normalized.slice(2).split("/");
129
- const host = (segments[0] ?? "").toLowerCase();
130
- if (!UNC_HOSTS.includes(host)) return null;
131
- const distro = segments[1] ?? "";
132
- if (distro === "") return null;
133
- return {
134
- distro,
135
- linuxPath: `/${segments.slice(2).filter((segment) => segment.length > 0).join("/")}`
136
- };
137
- }
138
- /**
139
- * Whether a path resolves into a WSL distro through either UNC form.
140
- * @param raw - candidate absolute path.
141
- * @returns whether the path parses as a WSL UNC.
142
- */
143
- function isWslUnc(raw) {
144
- return parseWslUnc(raw) !== null;
145
- }
146
- /**
147
- * Normalize a Linux absolute path for the Host: collapse repeated slashes and
148
- * strip a trailing slash (root becomes `/`).
149
- * @param path - absolute Linux path.
150
- * @returns the normalized path.
151
- */
152
- function normalizeLinuxPath(path) {
153
- const collapsed = path.replace(/\/+/g, "/");
154
- return collapsed === "/" ? "/" : collapsed.replace(/\/$/, "");
155
- }
156
- /**
157
- * Whether a path is an absolute, non-empty Linux path.
158
- * @param path - candidate.
159
- * @returns whether it starts with `/` and contains no NUL.
160
- */
161
- function isAbsoluteLinuxPath(path) {
162
- return path.startsWith("/") && !path.includes("\0");
163
- }
164
- /**
165
- * Join a distro and a Linux absolute path into the WSL2 UNC form used as the
166
- * workspace identity (`\\wsl.localhost\<distro>\<linux>`, backslash segments).
167
- * @param distro - distro name.
168
- * @param linuxPath - absolute Linux path (leading `/`).
169
- * @returns the UNC path.
170
- */
171
- function joinUnc(distro, linuxPath) {
172
- if (!isAbsoluteLinuxPath(linuxPath)) throw new Error(`wsl-workspace: cannot map a non-absolute Linux path "${linuxPath}" to UNC`);
173
- if (distro === "" || distro === "." || distro === ".." || /[\\/]/.test(distro)) throw new Error(`wsl-workspace: invalid distribution name "${distro}"`);
174
- const normalized = linuxPath.replace(/\/+/g, "/").replace(/\/$/, "");
175
- const windowsSegments = (normalized.startsWith("/") ? normalized.slice(1) : normalized).replace(/\//g, "\\");
176
- return `\\\\wsl.localhost\\${distro}${windowsSegments === "" ? "" : `\\${windowsSegments}`}`;
349
+ if (cancelled) return;
350
+ setDistros(names);
351
+ const first = names[0] ?? "";
352
+ setDistro(first);
353
+ setBrowsing(true);
354
+ setOpening(false);
355
+ if (presetIssue !== void 0) setError(presetIssue);
356
+ if (first !== "") refreshBrowse("/", first);
357
+ })();
358
+ return () => {
359
+ cancelled = true;
360
+ };
361
+ }, [open]);
362
+ (0, react.useEffect)(() => {
363
+ if (!open) return;
364
+ const onKey = (event) => {
365
+ if (event.key === "Escape" && !busy) setOpen(false);
366
+ };
367
+ window.addEventListener("keydown", onKey);
368
+ return () => window.removeEventListener("keydown", onKey);
369
+ }, [open, busy]);
370
+ if (!open) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
371
+ type: "button",
372
+ className: wide ? "dww-action dww-action--wide" : "dww-action dww-action--rail",
373
+ title: t("action.title"),
374
+ "aria-label": t("action.title"),
375
+ onClick: () => setOpen(true),
376
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
377
+ className: "dww-letter",
378
+ "aria-hidden": "true",
379
+ children: "W"
380
+ })
381
+ });
382
+ const onDrill = (name) => {
383
+ const next = dirChildPath(listing?.path ?? browsePath, name);
384
+ setPathInput(next);
385
+ refreshBrowse(next, distro);
386
+ };
387
+ const onUp = () => {
388
+ const parent = listing?.parent ?? null;
389
+ if (parent === null) return;
390
+ setPathInput(parent);
391
+ refreshBrowse(parent, distro);
392
+ };
393
+ const onDistroChange = (value) => {
394
+ setDistro(value);
395
+ refreshBrowse(browsePath, value);
396
+ };
397
+ const onCheck = async () => {
398
+ const path = normalizeLinuxPath(pathInput);
399
+ setError(null);
400
+ if (!isAbsoluteLinuxPath(path) || path === "/") {
401
+ setError(t("error.invalidPath"));
402
+ return;
177
403
  }
178
- /**
179
- * Translate a `/mnt/<drive>/…` path back to its Windows drive path.
180
- * @param linuxPath - the candidate Linux path.
181
- * @returns the `X:\…` drive path, or `null` when the path is not a drvfs mount.
182
- */
183
- function mntToWindowsPath(linuxPath) {
184
- const match = /^\/mnt\/([a-zA-Z])(?:\/(.*))?$/.exec(linuxPath);
185
- if (match === null) return null;
186
- const rest = (match[2] ?? "").replace(/\//g, "\\");
187
- return `${(match[1] ?? "").toUpperCase()}:\\${rest}`;
404
+ let facts;
405
+ try {
406
+ facts = await check$1(distro, path);
407
+ } catch {
408
+ setError(t("error.pathNotFound"));
409
+ return;
188
410
  }
189
- /**
190
- * Canonical Windows drive path for store keys and cross-realm identity:
191
- * separators unified to `\`, trailing separator stripped, and the WHOLE path
192
- * lowercased — Windows paths compare case-insensitively, and the workspace
193
- * registry may realpath a different casing than the caller spelled (8.3 or
194
- * on-disk casing), so the store key must collide across casings.
195
- * @param path - candidate Windows drive path.
196
- * @returns the canonical form, or `null` when not drive-shaped.
197
- */
198
- function canonicalWindowsPath(path) {
199
- const match = /^([A-Za-z]):[\\/](.*)$/.exec(path);
200
- if (match === null) return null;
201
- const rest = (match[2] ?? "").replace(/[\\/]+/g, "\\").replace(/\\$/, "").toLowerCase();
202
- return `${(match[1] ?? "").toLowerCase()}:\\${rest}`;
411
+ if (!facts.exists || !facts.isDirectory) {
412
+ setError(t("error.pathNotFound"));
413
+ return;
203
414
  }
204
- /** Linux username shape for `wsl.exe -u`: starts with a letter or underscore, then letters/digits/`_`/`.`/`-` (max 64). */
205
- const WSL_USERNAME_PATTERN = /^[A-Za-z_][A-Za-z0-9_.-]{0,63}$/;
206
- /**
207
- * Whether a value is a safe Linux username for `wsl.exe -u`. The check is
208
- * strict on purpose: a value starting with `-` could be parsed as a wsl.exe
209
- * option instead of a username.
210
- * @param value - candidate username.
211
- * @returns whether it matches the Linux username shape.
212
- */
213
- function isValidWslUsername(value) {
214
- return WSL_USERNAME_PATTERN.test(value);
415
+ refreshBrowse(path, distro);
416
+ };
417
+ const onConfirm = async () => {
418
+ const path = normalizeLinuxPath(pathInput);
419
+ setError(null);
420
+ if (!isAbsoluteLinuxPath(path) || path === "/") {
421
+ setError(t("error.invalidPath"));
422
+ return;
215
423
  }
216
- //#endregion
217
- //#region src/client/AddWslWorkspace.tsx
218
- /**
219
- * Build the Linux child path one level below a parent, for the breadcrumb/
220
- * browse drill.
221
- * @param parent - the currently listed absolute path (`/` for root).
222
- * @param name - the child directory name.
223
- * @returns the child's absolute Linux path.
224
- */
225
- function dirChildPath(parent, name) {
226
- return parent === "/" ? `/${name}` : `${parent}/${name}`;
424
+ const user = username.trim();
425
+ if (user !== "" && !isValidWslUsername(user)) {
426
+ setError(t("error.invalidUsername"));
427
+ return;
227
428
  }
228
- /** A tiny inline terminal glyph for the dialog's directory rows. */
229
- function WslGlyph({ size = 16 }) {
230
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("svg", {
231
- width: size,
232
- height: size,
233
- viewBox: "0 0 24 24",
234
- fill: "none",
235
- "aria-hidden": "true",
236
- children: [
237
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("rect", {
238
- x: "2.5",
239
- y: "4.5",
240
- width: "19",
241
- height: "15",
242
- rx: "2.5",
243
- stroke: "currentColor",
244
- strokeWidth: "1.6"
245
- }),
246
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
247
- d: "M6 9l3.2 2.6L6 14",
248
- stroke: "currentColor",
249
- strokeWidth: "1.6",
250
- strokeLinecap: "round",
251
- strokeLinejoin: "round"
252
- }),
253
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
254
- d: "M12 14h5",
255
- stroke: "currentColor",
256
- strokeWidth: "1.6",
257
- strokeLinecap: "round"
258
- })
259
- ]
260
- });
429
+ setBusy(true);
430
+ try {
431
+ let facts;
432
+ try {
433
+ facts = await check$1(distro, path);
434
+ } catch {
435
+ setError(t("error.pathNotFound"));
436
+ return;
437
+ }
438
+ if (!facts.exists || !facts.isDirectory) {
439
+ setError(t("error.pathNotFound"));
440
+ return;
441
+ }
442
+ const failure = await createWorkspace(path, user, distro);
443
+ if (failure !== void 0) {
444
+ setError(failure);
445
+ return;
446
+ }
447
+ setOpen(false);
448
+ } finally {
449
+ setBusy(false);
261
450
  }
262
- /**
263
- * The "Add WSL workspace…" footer action and its dialog.
264
- * @param props - owner share + injected face.
265
- */
266
- function AddWslWorkspace({ wide, t, checkPreset, listDistros, listDir, check, createWorkspace }) {
267
- const [open, setOpen] = (0, react.useState)(false);
268
- const [opening, setOpening] = (0, react.useState)(false);
269
- const [distros, setDistros] = (0, react.useState)([]);
270
- const [distro, setDistro] = (0, react.useState)("");
271
- const [pathInput, setPathInput] = (0, react.useState)("/home/");
272
- const [username, setUsername] = (0, react.useState)("");
273
- const [listing, setListing] = (0, react.useState)(null);
274
- const [browsePath, setBrowsePath] = (0, react.useState)("/");
275
- const [browsing, setBrowsing] = (0, react.useState)(false);
276
- const [error, setError] = (0, react.useState)(null);
277
- const [busy, setBusy] = (0, react.useState)(false);
278
- const browseSeq = (0, react.useRef)(0);
279
- const refreshBrowse = async (root, targetDistro) => {
280
- const seq = ++browseSeq.current;
281
- setBrowsing(true);
282
- setBrowsePath(root);
283
- try {
284
- const value = await listDir(targetDistro, root);
285
- if (seq === browseSeq.current) setListing(value);
286
- } catch {
287
- if (seq === browseSeq.current) {
288
- setListing(null);
289
- setError((previous) => previous ?? t("error.loadDir"));
290
- }
291
- } finally {
292
- if (seq === browseSeq.current) setBrowsing(false);
293
- }
294
- };
295
- (0, react.useEffect)(() => {
296
- if (!open) return;
297
- let cancelled = false;
298
- setError(null);
299
- setOpening(true);
300
- (async () => {
301
- let presetIssue;
302
- try {
303
- presetIssue = await checkPreset();
304
- } catch {
305
- presetIssue = t("error.loadDistros");
306
- }
307
- let names;
308
- try {
309
- names = await listDistros();
310
- } catch {
311
- if (cancelled) return;
312
- setOpening(false);
313
- setError(t("error.loadDistros"));
314
- return;
315
- }
316
- if (cancelled) return;
317
- setDistros(names);
318
- const first = names[0] ?? "";
319
- setDistro(first);
320
- setBrowsing(true);
321
- setOpening(false);
322
- if (presetIssue !== void 0) setError(presetIssue);
323
- if (first !== "") refreshBrowse("/", first);
324
- })();
325
- return () => {
326
- cancelled = true;
327
- };
328
- }, [open]);
329
- (0, react.useEffect)(() => {
330
- if (!open) return;
331
- const onKey = (event) => {
332
- if (event.key === "Escape" && !busy) setOpen(false);
333
- };
334
- window.addEventListener("keydown", onKey);
335
- return () => window.removeEventListener("keydown", onKey);
336
- }, [open, busy]);
337
- if (!open) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
338
- type: "button",
339
- className: wide ? "dww-action dww-action--wide" : "dww-action dww-action--rail",
340
- title: t("action.title"),
341
- "aria-label": t("action.title"),
342
- onClick: () => setOpen(true),
343
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
344
- className: "dww-letter",
345
- "aria-hidden": "true",
346
- children: "W"
347
- })
348
- });
349
- const onDrill = (name) => {
350
- const next = dirChildPath(listing?.path ?? browsePath, name);
351
- setPathInput(next);
352
- refreshBrowse(next, distro);
353
- };
354
- const onUp = () => {
355
- const parent = listing?.parent ?? null;
356
- if (parent === null) return;
357
- setPathInput(parent);
358
- refreshBrowse(parent, distro);
359
- };
360
- const onDistroChange = (value) => {
361
- setDistro(value);
362
- refreshBrowse(browsePath, value);
363
- };
364
- const onCheck = async () => {
365
- const path = normalizeLinuxPath(pathInput);
366
- setError(null);
367
- if (!isAbsoluteLinuxPath(path) || path === "/") {
368
- setError(t("error.invalidPath"));
369
- return;
370
- }
371
- let facts;
372
- try {
373
- facts = await check(distro, path);
374
- } catch {
375
- setError(t("error.pathNotFound"));
376
- return;
377
- }
378
- if (!facts.exists || !facts.isDirectory) {
379
- setError(t("error.pathNotFound"));
380
- return;
381
- }
382
- refreshBrowse(path, distro);
383
- };
384
- const onConfirm = async () => {
385
- const path = normalizeLinuxPath(pathInput);
386
- setError(null);
387
- if (!isAbsoluteLinuxPath(path) || path === "/") {
388
- setError(t("error.invalidPath"));
389
- return;
390
- }
391
- const user = username.trim();
392
- if (user !== "" && !isValidWslUsername(user)) {
393
- setError(t("error.invalidUsername"));
394
- return;
395
- }
396
- setBusy(true);
397
- try {
398
- let facts;
399
- try {
400
- facts = await check(distro, path);
401
- } catch {
402
- setError(t("error.pathNotFound"));
403
- return;
404
- }
405
- if (!facts.exists || !facts.isDirectory) {
406
- setError(t("error.pathNotFound"));
407
- return;
408
- }
409
- const failure = await createWorkspace(path, user, distro);
410
- if (failure !== void 0) {
411
- setError(failure);
412
- return;
413
- }
414
- setOpen(false);
415
- } finally {
416
- setBusy(false);
417
- }
418
- };
419
- const children = (listing?.entries.filter((entry) => entry.kind === "directory") ?? []).map((entry) => entry.name);
420
- const maskClick = () => {
421
- if (!busy) setOpen(false);
422
- };
423
- const listScroll = () => {};
424
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
425
- className: "dww-overlay",
426
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
427
- className: "dww-overlay-mask",
428
- onClick: maskClick
429
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
430
- className: "dww-card",
431
- role: "dialog",
432
- "aria-modal": "true",
433
- "aria-label": t("dialog.title"),
451
+ };
452
+ const children = (listing?.entries.filter((entry) => entry.kind === "directory") ?? []).map((entry) => entry.name);
453
+ const maskClick = () => {
454
+ if (!busy) setOpen(false);
455
+ };
456
+ const listScroll = () => {};
457
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
458
+ className: "dww-overlay",
459
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
460
+ className: "dww-overlay-mask",
461
+ onClick: maskClick
462
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
463
+ className: "dww-card",
464
+ role: "dialog",
465
+ "aria-modal": "true",
466
+ "aria-label": t("dialog.title"),
467
+ children: [
468
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
469
+ className: "dww-header",
470
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h2", {
471
+ className: "dww-title",
472
+ children: t("dialog.title")
473
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
474
+ type: "button",
475
+ className: "dww-close",
476
+ "aria-label": t("dialog.cancel"),
477
+ onClick: maskClick,
478
+ children: "✕"
479
+ })]
480
+ }),
481
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
482
+ className: "dww-body",
434
483
  children: [
435
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
436
- className: "dww-header",
437
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h2", {
438
- className: "dww-title",
439
- children: t("dialog.title")
440
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
484
+ error !== null ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
485
+ className: "dww-error",
486
+ children: [error, /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
441
487
  type: "button",
442
- className: "dww-close",
443
- "aria-label": t("dialog.cancel"),
444
- onClick: maskClick,
445
- children: "✕"
488
+ className: "dww-retry",
489
+ onClick: () => setError(null),
490
+ children: t("dialog.retry")
491
+ })]
492
+ }) : null,
493
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
494
+ className: "dww-field",
495
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
496
+ className: "dww-field-label",
497
+ htmlFor: "dww-distro",
498
+ children: t("dialog.distro")
499
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("select", {
500
+ id: "dww-distro",
501
+ className: "dww-select",
502
+ value: distro,
503
+ disabled: opening || busy,
504
+ onChange: (event) => onDistroChange(event.target.value),
505
+ children: distros.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
506
+ value: "",
507
+ children: opening ? t("dialog.loading") : ""
508
+ }) : distros.map((name) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
509
+ value: name,
510
+ children: name
511
+ }, name))
446
512
  })]
447
513
  }),
448
514
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
449
- className: "dww-body",
450
- children: [
451
- error !== null ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
452
- className: "dww-error",
453
- children: [error, /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
454
- type: "button",
455
- className: "dww-retry",
456
- onClick: () => setError(null),
457
- children: t("dialog.retry")
458
- })]
459
- }) : null,
460
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
461
- className: "dww-field",
462
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
463
- className: "dww-field-label",
464
- htmlFor: "dww-distro",
465
- children: t("dialog.distro")
466
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("select", {
467
- id: "dww-distro",
468
- className: "dww-select",
469
- value: distro,
470
- disabled: opening || busy,
471
- onChange: (event) => onDistroChange(event.target.value),
472
- children: distros.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
473
- value: "",
474
- children: opening ? t("dialog.loading") : ""
475
- }) : distros.map((name) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
476
- value: name,
477
- children: name
478
- }, name))
479
- })]
480
- }),
481
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
482
- className: "dww-field",
483
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
484
- className: "dww-field-label",
485
- htmlFor: "dww-path",
486
- children: t("dialog.path")
487
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
488
- className: "dww-input-row",
489
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
490
- id: "dww-path",
491
- className: "dww-input",
492
- value: pathInput,
493
- placeholder: t("dialog.pathPlaceholder"),
494
- disabled: opening || busy,
495
- onChange: (event) => setPathInput(event.target.value)
496
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
497
- type: "button",
498
- className: "dww-check-btn",
499
- disabled: opening || busy,
500
- onClick: () => void onCheck(),
501
- children: t("dialog.check")
502
- })]
503
- })]
504
- }),
505
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
506
- className: "dww-field",
507
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
508
- className: "dww-field-label",
509
- htmlFor: "dww-username",
510
- children: t("dialog.username")
511
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
512
- id: "dww-username",
513
- className: "dww-input",
514
- value: username,
515
- placeholder: t("dialog.usernamePlaceholder"),
516
- disabled: opening || busy,
517
- autoComplete: "off",
518
- spellCheck: false,
519
- onChange: (event) => setUsername(event.target.value)
520
- })]
521
- }),
522
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
523
- className: "dww-feedback",
524
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
525
- className: "dww-breadcrumb",
526
- children: browsePath
527
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
528
- className: "dww-dirlist",
529
- onScroll: listScroll,
530
- children: [browsing ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
531
- className: "dww-dir-empty",
532
- children: t("dialog.loading")
533
- }) : listing?.parent !== null && listing !== null ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
534
- type: "button",
535
- className: "dww-dir-row dww-dir-row--up",
536
- onClick: onUp,
537
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(WslGlyph, { size: 14 }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("dialog.upLevel") })]
538
- }) : null, !browsing && children.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
539
- className: "dww-dir-empty",
540
- children: t("dialog.browseEmpty")
541
- }) : children.map((name) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
542
- type: "button",
543
- className: "dww-dir-row",
544
- onClick: () => onDrill(name),
545
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(WslGlyph, { size: 14 }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: name })]
546
- }, name))]
547
- })]
548
- })
549
- ]
515
+ className: "dww-field",
516
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
517
+ className: "dww-field-label",
518
+ htmlFor: "dww-path",
519
+ children: t("dialog.path")
520
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
521
+ className: "dww-input-row",
522
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
523
+ id: "dww-path",
524
+ className: "dww-input",
525
+ value: pathInput,
526
+ placeholder: t("dialog.pathPlaceholder"),
527
+ disabled: opening || busy,
528
+ onChange: (event) => setPathInput(event.target.value)
529
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
530
+ type: "button",
531
+ className: "dww-check-btn",
532
+ disabled: opening || busy,
533
+ onClick: () => void onCheck(),
534
+ children: t("dialog.check")
535
+ })]
536
+ })]
550
537
  }),
551
538
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
552
- className: "dww-actions",
553
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
554
- type: "button",
555
- className: "dww-btn",
556
- disabled: busy,
557
- onClick: maskClick,
558
- children: t("dialog.cancel")
559
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
560
- type: "button",
561
- className: "dww-btn dww-btn--primary",
562
- disabled: busy || opening,
563
- onClick: () => void onConfirm(),
564
- children: busy ? t("dialog.loading") : t("dialog.confirm")
539
+ className: "dww-field",
540
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
541
+ className: "dww-field-label",
542
+ htmlFor: "dww-username",
543
+ children: t("dialog.username")
544
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
545
+ id: "dww-username",
546
+ className: "dww-input",
547
+ value: username,
548
+ placeholder: t("dialog.usernamePlaceholder"),
549
+ disabled: opening || busy,
550
+ autoComplete: "off",
551
+ spellCheck: false,
552
+ onChange: (event) => setUsername(event.target.value)
553
+ })]
554
+ }),
555
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
556
+ className: "dww-feedback",
557
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
558
+ className: "dww-breadcrumb",
559
+ children: browsePath
560
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
561
+ className: "dww-dirlist",
562
+ onScroll: listScroll,
563
+ children: [browsing ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
564
+ className: "dww-dir-empty",
565
+ children: t("dialog.loading")
566
+ }) : listing?.parent !== null && listing !== null ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
567
+ type: "button",
568
+ className: "dww-dir-row dww-dir-row--up",
569
+ onClick: onUp,
570
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(WslGlyph, { size: 14 }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("dialog.upLevel") })]
571
+ }) : null, !browsing && children.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
572
+ className: "dww-dir-empty",
573
+ children: t("dialog.browseEmpty")
574
+ }) : children.map((name) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
575
+ type: "button",
576
+ className: "dww-dir-row",
577
+ onClick: () => onDrill(name),
578
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(WslGlyph, { size: 14 }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: name })]
579
+ }, name))]
565
580
  })]
566
581
  })
567
582
  ]
568
- })]
569
- });
570
- }
571
- //#endregion
572
- //#region src/client/styles.ts
573
- /**
574
- * Third-party stylesheet injection for the WSL workspace UI (the plugin
575
- * builds no CSS bundle, so styles are injected as one idempotent `<style>`).
576
- * Colors derive exclusively from the `--dsw-*` design tokens.
577
- */
578
- const STYLE_TAG_DATA_ATTRIBUTE = "data-plugin=\"dsh-wsl-workspace\"";
579
- const STYLES = `
583
+ }),
584
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
585
+ className: "dww-actions",
586
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
587
+ type: "button",
588
+ className: "dww-btn",
589
+ disabled: busy,
590
+ onClick: maskClick,
591
+ children: t("dialog.cancel")
592
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
593
+ type: "button",
594
+ className: "dww-btn dww-btn--primary",
595
+ disabled: busy || opening,
596
+ onClick: () => void onConfirm(),
597
+ children: busy ? t("dialog.loading") : t("dialog.confirm")
598
+ })]
599
+ })
600
+ ]
601
+ })]
602
+ });
603
+ }
604
+
605
+ //#endregion
606
+ //#region src/client/styles.ts
607
+ /**
608
+ * Third-party stylesheet injection for the WSL workspace UI (the plugin
609
+ * builds no CSS bundle, so styles are injected as one idempotent `<style>`).
610
+ * Colors derive exclusively from the `--dsw-*` design tokens.
611
+ */
612
+ const STYLE_TAG_DATA_ATTRIBUTE = "data-plugin=\"dsh-wsl-workspace\"";
613
+ const STYLES = `
580
614
  /* Sidebar-foot icon action beside Settings (28px round in the wide sidebar,
581
615
  36px round in the rail), matching the shell's icon-button language. */
582
616
  .dww-action {
@@ -837,217 +871,303 @@ window.__ModuleLoader__.load({
837
871
  .dww-btn--primary:hover:not(:disabled) { background: var(--dsw-alias-button-primary-hover); }
838
872
  .dww-btn:disabled { cursor: default; opacity: 0.6; }
839
873
  `;
840
- /**
841
- * Idempotently inject the plugin stylesheet. No-op when a tag with the
842
- * plugin's data attribute already exists.
843
- */
844
- function ensureStyles() {
845
- if (typeof document === "undefined") return;
846
- if (document.querySelector(`style[${STYLE_TAG_DATA_ATTRIBUTE}]`) !== null) return;
847
- const style = document.createElement("style");
848
- style.setAttribute("data-plugin", "dsh-wsl-workspace");
849
- style.textContent = STYLES;
850
- document.head.appendChild(style);
874
+ /**
875
+ * Idempotently inject the plugin stylesheet. No-op when a tag with the
876
+ * plugin's data attribute already exists.
877
+ */
878
+ function ensureStyles() {
879
+ if (typeof document === "undefined") return;
880
+ if (document.querySelector(`style[${STYLE_TAG_DATA_ATTRIBUTE}]`) !== null) return;
881
+ const style = document.createElement("style");
882
+ style.setAttribute("data-plugin", "dsh-wsl-workspace");
883
+ style.textContent = STYLES;
884
+ document.head.appendChild(style);
885
+ }
886
+
887
+ //#endregion
888
+ //#region src/client/locales.ts
889
+ /**
890
+ * Bilingual dictionaries for the `wslWorkspace` locale namespace. Product copy
891
+ * is Chinese; English is the parallel export for the standalone bundle.
892
+ */
893
+ /**
894
+ * The `wslWorkspace` translations (Chinese, the primary product copy).
895
+ */
896
+ const zh = {
897
+ "action.add": "WSL 工作区",
898
+ "action.title": "添加 WSL 工作区…",
899
+ "dialog.title": "添加 WSL 工作区",
900
+ "dialog.distro": "发行版",
901
+ "dialog.path": "路径",
902
+ "dialog.pathPlaceholder": "/home/",
903
+ "dialog.username": "用户名",
904
+ "dialog.usernamePlaceholder": "留空则使用发行版默认用户",
905
+ "dialog.loading": "正在加载…",
906
+ "dialog.browseEmpty": "此目录没有子文件夹",
907
+ "dialog.upLevel": "..(返回上级)",
908
+ "dialog.browse": "浏览",
909
+ "dialog.check": "检查",
910
+ "dialog.confirm": "创建并打开",
911
+ "dialog.cancel": "取消",
912
+ "dialog.retry": "重试",
913
+ "error.loadDistros": "无法获取 WSL 发行版列表,请确认已安装 WSL 且插件宿主端可用",
914
+ "error.rateLimited": "操作过于频繁,请稍后重试",
915
+ "error.loadDir": "无法浏览该目录",
916
+ "error.presetMissing": "未找到健康的 wsl preset,请确认插件宿主端已安装并配置该 preset",
917
+ "error.invalidPath": "请输入以 / 开头的 Linux 绝对路径",
918
+ "error.invalidUsername": "用户名无效:需以字母或下划线开头,仅含字母、数字、_、.、-",
919
+ "error.pathNotFound": "该路径不存在或是文件,请选择一个文件夹",
920
+ "error.createFailed": "创建工作区失败"
921
+ };
922
+ /**
923
+ * The `wslWorkspace` translations (English).
924
+ */
925
+ const en = {
926
+ "action.add": "WSL Workspace",
927
+ "action.title": "Add WSL workspace…",
928
+ "dialog.title": "Add WSL workspace",
929
+ "dialog.distro": "Distro",
930
+ "dialog.path": "Path",
931
+ "dialog.pathPlaceholder": "/home/",
932
+ "dialog.username": "Username",
933
+ "dialog.usernamePlaceholder": "Leave empty to use the distro default user",
934
+ "dialog.loading": "Loading…",
935
+ "dialog.browseEmpty": "No subdirectories here",
936
+ "dialog.upLevel": ".. (up)",
937
+ "dialog.browse": "Browse",
938
+ "dialog.check": "Check",
939
+ "dialog.confirm": "Create & open",
940
+ "dialog.cancel": "Cancel",
941
+ "dialog.retry": "Retry",
942
+ "error.loadDistros": "Could not list WSL distros; confirm WSL is installed and the plugin host side is reachable",
943
+ "error.rateLimited": "Too many attempts; retry in a moment",
944
+ "error.loadDir": "Could not browse this directory",
945
+ "error.presetMissing": "No healthy \"wsl\" preset found; confirm the plugin host side installed and configured it",
946
+ "error.invalidPath": "Enter an absolute Linux path starting with /",
947
+ "error.invalidUsername": "Invalid username: start with a letter or underscore; only letters, digits, _ . -",
948
+ "error.pathNotFound": "The path does not exist or is a file; choose a folder",
949
+ "error.createFailed": "Failed to create the workspace"
950
+ };
951
+
952
+ //#endregion
953
+ //#region src/client/index.ts
954
+ /** Required services (cordis fiber inject). */
955
+ const inject = [
956
+ "slots",
957
+ "locale",
958
+ "sessions",
959
+ "workspaces"
960
+ ];
961
+ /** The legacy standalone WSL preset id (folded into the mode variants). */
962
+ const LEGACY_WSL_PRESET_ID = "wsl";
963
+ /**
964
+ * Mount the sidebar action and the auto-binding effect.
965
+ * @param ctx - the browser plugin context.
966
+ */
967
+ function apply(ctx) {
968
+ const workspaces = ctx.get("workspaces");
969
+ const sessions = ctx.get("sessions");
970
+ const legacyApi = () => ctx.get("connection")?.api;
971
+ const remoteAgentPresets = () => ctx.get("remote.agentPresets");
972
+ const uiWorkspaceService = () => ctx.get("uiWorkspace");
973
+ const hasNoteAgentPreset = typeof sessions.noteAgentPreset === "function";
974
+ /** Unified agent-preset list: new `remote.agentPresets` namespace (v0.1.2-rc.1+) or legacy `connection.api` (v0.1.1-rc.2). */
975
+ const listAgentPresets = async () => {
976
+ const agentPresets = remoteAgentPresets();
977
+ if (agentPresets !== void 0) {
978
+ const r = await agentPresets.list();
979
+ if (!r.ok) return {
980
+ ok: false,
981
+ presets: [],
982
+ error: r.error?.message ?? "list failed"
983
+ };
984
+ return {
985
+ ok: true,
986
+ presets: r.value?.presets ?? []
987
+ };
988
+ }
989
+ const api = legacyApi();
990
+ if (api !== void 0) {
991
+ const r = await api.agentPresets.list({});
992
+ if (!r.result.ok) return {
993
+ ok: false,
994
+ presets: [],
995
+ error: r.result.error?.message ?? "list failed"
996
+ };
997
+ return {
998
+ ok: true,
999
+ presets: r.result.value?.presets ?? []
1000
+ };
851
1001
  }
852
- //#endregion
853
- //#region src/client/locales.ts
854
- /**
855
- * Bilingual dictionaries for the `wslWorkspace` locale namespace. Product copy
856
- * is Chinese; English is the parallel export for the standalone bundle.
857
- */
858
- /**
859
- * The `wslWorkspace` translations (Chinese, the primary product copy).
860
- */
861
- const zh = {
862
- "action.add": "WSL 工作区",
863
- "action.title": "添加 WSL 工作区…",
864
- "dialog.title": "添加 WSL 工作区",
865
- "dialog.distro": "发行版",
866
- "dialog.path": "路径",
867
- "dialog.pathPlaceholder": "/home/",
868
- "dialog.username": "用户名",
869
- "dialog.usernamePlaceholder": "留空则使用发行版默认用户",
870
- "dialog.loading": "正在加载…",
871
- "dialog.browseEmpty": "此目录没有子文件夹",
872
- "dialog.upLevel": "..(返回上级)",
873
- "dialog.browse": "浏览",
874
- "dialog.check": "检查",
875
- "dialog.confirm": "创建并打开",
876
- "dialog.cancel": "取消",
877
- "dialog.retry": "重试",
878
- "error.loadDistros": "无法获取 WSL 发行版列表,请确认已安装 WSL 且插件宿主端可用",
879
- "error.rateLimited": "操作过于频繁,请稍后重试",
880
- "error.loadDir": "无法浏览该目录",
881
- "error.presetMissing": "未找到健康的 wsl preset,请确认插件宿主端已安装并配置该 preset",
882
- "error.invalidPath": "请输入以 / 开头的 Linux 绝对路径",
883
- "error.invalidUsername": "用户名无效:需以字母或下划线开头,仅含字母、数字、_、.、-",
884
- "error.pathNotFound": "该路径不存在或是文件,请选择一个文件夹",
885
- "error.createFailed": "创建工作区失败"
1002
+ return {
1003
+ ok: false,
1004
+ presets: [],
1005
+ error: "no remote api available"
886
1006
  };
887
- /**
888
- * The `wslWorkspace` translations (English).
889
- */
890
- const en = {
891
- "action.add": "WSL Workspace",
892
- "action.title": "Add WSL workspace…",
893
- "dialog.title": "Add WSL workspace",
894
- "dialog.distro": "Distro",
895
- "dialog.path": "Path",
896
- "dialog.pathPlaceholder": "/home/",
897
- "dialog.username": "Username",
898
- "dialog.usernamePlaceholder": "Leave empty to use the distro default user",
899
- "dialog.loading": "Loading…",
900
- "dialog.browseEmpty": "No subdirectories here",
901
- "dialog.upLevel": ".. (up)",
902
- "dialog.browse": "Browse",
903
- "dialog.check": "Check",
904
- "dialog.confirm": "Create & open",
905
- "dialog.cancel": "Cancel",
906
- "dialog.retry": "Retry",
907
- "error.loadDistros": "Could not list WSL distros; confirm WSL is installed and the plugin host side is reachable",
908
- "error.rateLimited": "Too many attempts; retry in a moment",
909
- "error.loadDir": "Could not browse this directory",
910
- "error.presetMissing": "No healthy \"wsl\" preset found; confirm the plugin host side installed and configured it",
911
- "error.invalidPath": "Enter an absolute Linux path starting with /",
912
- "error.invalidUsername": "Invalid username: start with a letter or underscore; only letters, digits, _ . -",
913
- "error.pathNotFound": "The path does not exist or is a file; choose a folder",
914
- "error.createFailed": "Failed to create the workspace"
1007
+ };
1008
+ /** Unified agent-preset select: new `agentPresets.select(id, preset)` or legacy `connection.api.select({...})`. */
1009
+ const selectAgentPreset = async (sessionId, presetId) => {
1010
+ const agentPresets = remoteAgentPresets();
1011
+ if (agentPresets !== void 0) return agentPresets.select(sessionId, presetId);
1012
+ const api = legacyApi();
1013
+ if (api !== void 0) {
1014
+ const r = await api.agentPresets.select({
1015
+ sessionId,
1016
+ agentPreset: presetId
1017
+ });
1018
+ return { ok: r.result.ok };
1019
+ }
1020
+ return { ok: false };
1021
+ };
1022
+ /**
1023
+ * Resolve how this release opens a session for a workspace - v0.1.2-rc.1+
1024
+ * exposes `uiWorkspace.startSession`, v0.1.1-rc.2 keeps it on `workspaces`.
1025
+ *
1026
+ * Resolved BEFORE the workspace is written. A release that offers neither
1027
+ * cannot open a session, and a silent fall-through would leave the workspace
1028
+ * behind with an empty `sessionIds` while the dialog still reports success;
1029
+ * failing here names the missing capability instead.
1030
+ * @returns the starter for the service this release actually exposes.
1031
+ * @throws Error naming both candidates when neither service is available.
1032
+ */
1033
+ const resolveSessionStarter = () => {
1034
+ const ui = uiWorkspaceService();
1035
+ if (ui !== void 0) return (workspaceId) => {
1036
+ ui.startSession(workspaceId);
915
1037
  };
916
- //#endregion
917
- //#region src/client/index.ts
918
- /** Required services (cordis fiber inject). */
919
- const inject = [
920
- "slots",
921
- "locale",
922
- "connection",
923
- "sessions",
924
- "workspaces"
925
- ];
926
- /** The legacy standalone WSL preset id (folded into the mode variants). */
927
- const LEGACY_WSL_PRESET_ID = "wsl";
928
- /**
929
- * Mount the sidebar action and the auto-binding effect.
930
- * @param ctx - the browser plugin context.
931
- */
932
- function apply(ctx) {
933
- const { api } = ctx.get("connection");
934
- const workspaces = ctx.get("workspaces");
935
- const sessions = ctx.get("sessions");
936
- ensureStyles();
937
- ctx.effect(() => ctx.locale.register("wslWorkspace", {
938
- zh,
939
- en
940
- }), "dsh-wsl-workspace: locale dictionaries");
941
- const t = ctx.locale.bind("wslWorkspace");
942
- let wslWindowsPaths = /* @__PURE__ */ new Set();
943
- const injected = () => ({
944
- t,
945
- checkPreset: async () => {
946
- let roster;
947
- try {
948
- roster = (await api.agentPresets.list({})).result;
949
- } catch (error) {
950
- return error instanceof Error ? error.message : String(error);
951
- }
952
- if (!roster.ok) return roster.error.message;
953
- if (roster.value.presets.find((entry) => entry.id.startsWith("wsl-") && entry.broken === void 0) === void 0) return t("error.presetMissing");
954
- },
955
- listDistros: () => listDistros(),
956
- listDir: (distro, path) => listDir(distro, path),
957
- check: (distro, path) => check(distro, path),
958
- createWorkspace: async (linuxPath, username, distro) => {
959
- try {
960
- const winPath = mntToWindowsPath(linuxPath);
961
- if (winPath !== null) {
962
- const view = await workspaces.create({ path: winPath });
963
- await registerWindows(linuxPath, distro, username);
964
- const canonical = canonicalWindowsPath(winPath);
965
- if (canonical !== null) wslWindowsPaths = new Set(wslWindowsPaths).add(canonical);
966
- workspaces.startSession(view.workspaceId);
967
- return;
968
- }
969
- const uncPath = joinUnc(distro, linuxPath);
970
- const view = await workspaces.create({ path: uncPath });
971
- await setWorkspaceUser(uncPath, username);
972
- workspaces.startSession(view.workspaceId);
973
- return;
974
- } catch (error) {
975
- return error instanceof Error ? error.message : String(error);
976
- }
1038
+ const legacyStart = workspaces.startSession;
1039
+ if (typeof legacyStart === "function") return (workspaceId) => {
1040
+ Reflect.apply(legacyStart, workspaces, [workspaceId]);
1041
+ };
1042
+ throw new Error("workspace session API unavailable: this DSH release exposes neither uiWorkspace.startSession nor workspaces.startSession");
1043
+ };
1044
+ /** Read agent preset — v0.1.2-rc.1+ uses projectionValues; v0.1.1-rc.2 uses direct field. */
1045
+ const getAgentPreset = (summary) => {
1046
+ if (summary.projectionValues?.agentPreset !== void 0) {
1047
+ const v = summary.projectionValues.agentPreset;
1048
+ return v === null ? void 0 : v;
1049
+ }
1050
+ return summary.agentPreset;
1051
+ };
1052
+ /** Note preset change — v0.1.1-rc.2 calls noteAgentPreset; v0.1.2-rc.1+ is auto-synced via projection. */
1053
+ const noteAgentPresetCompat = (sessionId, presetId) => {
1054
+ if (hasNoteAgentPreset && sessions.noteAgentPreset) sessions.noteAgentPreset(sessionId, presetId);
1055
+ };
1056
+ ensureStyles();
1057
+ ctx.effect(() => ctx.locale.register("wslWorkspace", {
1058
+ zh,
1059
+ en
1060
+ }), "dsh-wsl-workspace: locale dictionaries");
1061
+ const t = ctx.locale.bind("wslWorkspace");
1062
+ let wslWindowsPaths = /* @__PURE__ */ new Set();
1063
+ const injected = () => ({
1064
+ t,
1065
+ checkPreset: async () => {
1066
+ let roster;
1067
+ try {
1068
+ roster = await listAgentPresets();
1069
+ } catch (error) {
1070
+ return error instanceof Error ? error.message : String(error);
1071
+ }
1072
+ if (!roster.ok) return roster.error;
1073
+ const healthy = roster.presets.find((entry) => entry.id.startsWith("wsl-") && entry.broken === void 0);
1074
+ if (healthy === void 0) return t("error.presetMissing");
1075
+ return void 0;
1076
+ },
1077
+ listDistros: () => listDistros(),
1078
+ listDir: (distro, path) => listDir(distro, path),
1079
+ check: (distro, path) => check(distro, path),
1080
+ createWorkspace: async (linuxPath, username, distro) => {
1081
+ try {
1082
+ const startSession = resolveSessionStarter();
1083
+ const winPath = mntToWindowsPath(linuxPath);
1084
+ if (winPath !== null) {
1085
+ const view$1 = await workspaces.create({ path: winPath });
1086
+ await registerWindows(linuxPath, distro, username);
1087
+ const canonical = canonicalWindowsPath(winPath);
1088
+ if (canonical !== null) wslWindowsPaths = new Set(wslWindowsPaths).add(canonical);
1089
+ await startSession(view$1.workspaceId);
1090
+ return void 0;
977
1091
  }
978
- });
979
- ctx.effect(() => ctx.slots.inject("sidebar.footer.action", () => ctx.slots.register({
980
- name: "sidebar.footer.action",
981
- id: "wsl-workspace",
982
- inject: injected
983
- }, AddWslWorkspace)), "dsh-wsl-workspace: sidebar footer action");
984
- ctx.effect(() => {
985
- const inFlight = /* @__PURE__ */ new Set();
986
- const attempts = /* @__PURE__ */ new Map();
987
- const MAX_ATTEMPTS = 3;
988
- let variants = /* @__PURE__ */ new Set();
989
- let defaultPreset;
990
- const refreshRoster = () => {
991
- api.agentPresets.list({}).then((response) => {
992
- const result = response.result;
993
- if (!result.ok) return;
994
- variants = new Set(result.value.presets.filter((entry) => entry.broken === void 0 && entry.id.startsWith("wsl-")).map((entry) => entry.id));
995
- defaultPreset = result.value.presets.find((entry) => entry.isDefault === true)?.id;
996
- }).catch(() => {});
997
- };
998
- refreshRoster();
999
- const refreshWorkspaces = () => {
1000
- listWorkspaces().then((keys) => {
1001
- const next = /* @__PURE__ */ new Set();
1002
- for (const key of keys) {
1003
- const canonical = canonicalWindowsPath(key);
1004
- if (canonical !== null) next.add(canonical);
1005
- }
1006
- wslWindowsPaths = next;
1007
- }).catch(() => {});
1008
- };
1009
- refreshWorkspaces();
1010
- const maybeBind = () => {
1011
- const state = sessions.list.getSnapshot();
1012
- for (const id of state.ids) {
1013
- const summary = state.byId[id];
1014
- if (summary === void 0 || !summary.blank || summary.cwd === void 0) continue;
1015
- const canonical = canonicalWindowsPath(summary.cwd);
1016
- if (!(isWslUnc(summary.cwd) || canonical !== null && wslWindowsPaths.has(canonical))) continue;
1017
- const current = summary.agentPreset;
1018
- if (current !== void 0 && current.startsWith("wsl-")) continue;
1019
- const base = current === LEGACY_WSL_PRESET_ID ? defaultPreset ?? "standard" : current ?? defaultPreset;
1020
- if (base === void 0 || base === LEGACY_WSL_PRESET_ID || base.startsWith("wsl-")) continue;
1021
- const target = `wsl-${base.toLowerCase()}`;
1022
- if (!variants.has(target)) continue;
1023
- if (inFlight.has(id) || (attempts.get(id) ?? 0) >= MAX_ATTEMPTS) continue;
1024
- inFlight.add(id);
1025
- api.agentPresets.select({
1026
- sessionId: id,
1027
- agentPreset: target
1028
- }).then((response) => {
1029
- if (response.result.ok) sessions.noteAgentPreset(id, target);
1030
- }).catch(() => {
1031
- attempts.set(id, (attempts.get(id) ?? 0) + 1);
1032
- }).finally(() => {
1033
- inFlight.delete(id);
1034
- });
1035
- }
1036
- };
1037
- maybeBind();
1038
- const unsubscribe = sessions.list.subscribe(() => maybeBind());
1039
- const timer = window.setInterval(refreshRoster, 6e4);
1040
- return () => {
1041
- unsubscribe();
1042
- window.clearInterval(timer);
1043
- };
1044
- }, "dsh-wsl-workspace: WSL mode-variant binding");
1092
+ const uncPath = joinUnc(distro, linuxPath);
1093
+ const view = await workspaces.create({ path: uncPath });
1094
+ await setWorkspaceUser(uncPath, username);
1095
+ await startSession(view.workspaceId);
1096
+ return void 0;
1097
+ } catch (error) {
1098
+ return error instanceof Error ? error.message : String(error);
1099
+ }
1045
1100
  }
1046
- //#endregion
1047
- exports.apply = apply;
1048
- exports.inject = inject;
1049
- return module.exports;
1050
- }
1051
- });
1101
+ });
1102
+ ctx.effect(() => ctx.slots.inject("sidebar.footer.action", () => ctx.slots.register({
1103
+ name: "sidebar.footer.action",
1104
+ id: "wsl-workspace",
1105
+ inject: injected
1106
+ }, AddWslWorkspace)), "dsh-wsl-workspace: sidebar footer action");
1107
+ ctx.effect(() => {
1108
+ const inFlight = /* @__PURE__ */ new Set();
1109
+ const attempts = /* @__PURE__ */ new Map();
1110
+ const MAX_ATTEMPTS = 3;
1111
+ let variants = /* @__PURE__ */ new Set();
1112
+ let defaultPreset;
1113
+ const refreshRoster = () => {
1114
+ listAgentPresets().then((result) => {
1115
+ if (!result.ok) return;
1116
+ variants = new Set(result.presets.filter((entry) => entry.broken === void 0 && entry.id.startsWith("wsl-")).map((entry) => entry.id));
1117
+ defaultPreset = result.presets.find((entry) => entry.isDefault === true)?.id;
1118
+ maybeBind();
1119
+ }).catch(() => {});
1120
+ };
1121
+ refreshRoster();
1122
+ const refreshWorkspaces = () => {
1123
+ listWorkspaces().then((keys) => {
1124
+ const next = /* @__PURE__ */ new Set();
1125
+ for (const key of keys) {
1126
+ const canonical = canonicalWindowsPath(key);
1127
+ if (canonical !== null) next.add(canonical);
1128
+ }
1129
+ wslWindowsPaths = next;
1130
+ maybeBind();
1131
+ }).catch(() => {});
1132
+ };
1133
+ refreshWorkspaces();
1134
+ const maybeBind = () => {
1135
+ const state = sessions.list.getSnapshot();
1136
+ for (const id of state.ids) {
1137
+ const summary = state.byId[id];
1138
+ if (summary === void 0 || !summary.blank || summary.cwd === void 0) continue;
1139
+ const canonical = canonicalWindowsPath(summary.cwd);
1140
+ const isWsl = isWslUnc(summary.cwd) || canonical !== null && wslWindowsPaths.has(canonical);
1141
+ if (!isWsl) continue;
1142
+ const current = getAgentPreset(summary);
1143
+ if (current !== void 0 && current.startsWith("wsl-")) continue;
1144
+ const base = current === LEGACY_WSL_PRESET_ID ? defaultPreset ?? "standard" : current ?? defaultPreset;
1145
+ if (base === void 0 || base === LEGACY_WSL_PRESET_ID || base.startsWith("wsl-")) continue;
1146
+ const target = `wsl-${base.toLowerCase()}`;
1147
+ if (!variants.has(target)) continue;
1148
+ if (inFlight.has(id) || (attempts.get(id) ?? 0) >= MAX_ATTEMPTS) continue;
1149
+ inFlight.add(id);
1150
+ selectAgentPreset(id, target).then((result) => {
1151
+ if (result.ok) noteAgentPresetCompat(id, target);
1152
+ }).catch(() => {
1153
+ attempts.set(id, (attempts.get(id) ?? 0) + 1);
1154
+ }).finally(() => {
1155
+ inFlight.delete(id);
1156
+ });
1157
+ }
1158
+ };
1159
+ maybeBind();
1160
+ const unsubscribe = sessions.list.subscribe(() => maybeBind());
1161
+ const timer = window.setInterval(refreshRoster, 6e4);
1162
+ return () => {
1163
+ unsubscribe();
1164
+ window.clearInterval(timer);
1165
+ };
1166
+ }, "dsh-wsl-workspace: WSL mode-variant binding");
1167
+ }
1052
1168
 
1169
+ //#endregion
1170
+ exports.apply = apply;
1171
+ exports.inject = inject;
1172
+ return module.exports; } });
1053
1173
  //# sourceMappingURL=client.js.map