@reed-io/dsh-workspace-move 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/client.js ADDED
@@ -0,0 +1,534 @@
1
+ /* @reed-io/dsh-workspace-move — generated by scripts/build-client.mjs from src/client.js; do not edit. */
2
+ window.__ModuleLoader__.load({
3
+ id: "@reed-io/dsh-workspace-move",
4
+ factory: function (require) {
5
+ var module = { exports: {} };
6
+ var exports = module.exports;
7
+ var __defProp = Object.defineProperty;
8
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
9
+ var __getOwnPropNames = Object.getOwnPropertyNames;
10
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
11
+ var __export = (target, all) => {
12
+ for (var name in all)
13
+ __defProp(target, name, { get: all[name], enumerable: true });
14
+ };
15
+ var __copyProps = (to, from, except, desc) => {
16
+ if (from && typeof from === "object" || typeof from === "function") {
17
+ for (let key of __getOwnPropNames(from))
18
+ if (!__hasOwnProp.call(to, key) && key !== except)
19
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
20
+ }
21
+ return to;
22
+ };
23
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
24
+
25
+ // src/client.js
26
+ var client_exports = {};
27
+ __export(client_exports, {
28
+ apply: () => apply,
29
+ inject: () => inject
30
+ });
31
+ module.exports = __toCommonJS(client_exports);
32
+ var import_react = require("react");
33
+ var translations = {
34
+ "zh": {
35
+ "meta.title": "\u79FB\u52A8\u5DE5\u4F5C\u533A",
36
+ "meta.description": "\u628A\u5DE5\u4F5C\u533A\u76EE\u5F55\u3001\u5176\u5B50\u76EE\u5F55\u3001\u5176\u4E0B\u7684\u5B50\u5DE5\u4F5C\u533A\u548C\u5176\u4E2D\u7684\u4F1A\u8BDD\u6574\u4F53\u642C\u5230\u65B0\u4F4D\u7F6E\uFF0C\u652F\u6301\u91CD\u547D\u540D\u3001\u5931\u8D25\u56DE\u6EDA\u4E0E\u64A4\u9500\u3002",
37
+ "menu.move": "\u79FB\u52A8\u5DE5\u4F5C\u533A\u2026",
38
+ "menu.undo": "\u64A4\u9500\u4E0A\u6B21\u79FB\u52A8",
39
+ "dialog.title": "\u79FB\u52A8\u5DE5\u4F5C\u533A",
40
+ "dialog.source": "\u5F53\u524D\u4F4D\u7F6E",
41
+ "dialog.target": "\u76EE\u6807\u4E0A\u7EA7\u6587\u4EF6\u5939",
42
+ "dialog.folderName": "\u6587\u4EF6\u5939\u540D\u79F0",
43
+ "dialog.folderHint": "\u5DE5\u4F5C\u533A\u4F1A\u4EE5\u6B64\u540D\u79F0\u653E\u8FDB\u6240\u9009\u6587\u4EF6\u5939\uFF1B\u6539\u540D\u5373\u91CD\u547D\u540D\u5DE5\u4F5C\u533A\u3002",
44
+ "dialog.destination": "\u79FB\u52A8\u540E\u4F4D\u7F6E",
45
+ "dialog.up": "\u4E0A\u4E00\u5C42",
46
+ "dialog.home": "\u4E3B\u76EE\u5F55",
47
+ "dialog.newFolder": "\u65B0\u5EFA\u6587\u4EF6\u5939",
48
+ "dialog.newFolderPrompt": "\u65B0\u6587\u4EF6\u5939\u540D\u79F0",
49
+ "dialog.pickHere": "\u9009\u62E9\u6B64\u6587\u4EF6\u5939",
50
+ "dialog.confirm": "\u79FB\u52A8",
51
+ "dialog.cancel": "\u53D6\u6D88",
52
+ "dialog.moving": "\u6B63\u5728\u79FB\u52A8\u2026",
53
+ "dialog.loading": "\u52A0\u8F7D\u4E2D\u2026",
54
+ "dialog.emptyFolder": "\u8FD9\u91CC\u6CA1\u6709\u5B50\u6587\u4EF6\u5939",
55
+ "dialog.invalidName": "\u8BF7\u586B\u5199\u4E0D\u542B\u659C\u6760\u7684\u6587\u4EF6\u5939\u540D\u79F0\u3002",
56
+ "dialog.targetExists": "\u76EE\u6807\u6587\u4EF6\u5939\u91CC\u5DF2\u6709\u540C\u540D\u9879\u4E14\u975E\u7A7A\uFF0C\u8BF7\u6362\u4E2A\u540D\u5B57\u3002",
57
+ "dialog.sameLocation": "\u5DE5\u4F5C\u533A\u5DF2\u7ECF\u5728\u8FD9\u4E2A\u4F4D\u7F6E\u4E86\uFF0C\u6362\u4E2A\u76EE\u6807\u6216\u6539\u4E2A\u540D\u5B57\u3002",
58
+ "dialog.insideSelf": "\u4E0D\u80FD\u628A\u5DE5\u4F5C\u533A\u642C\u5230\u5B83\u81EA\u5DF1\u7684\u6587\u4EF6\u5939\u91CC\u9762\u3002",
59
+ "dialog.ancestor": "\u76EE\u6807\u5728\u5DE5\u4F5C\u533A\u5F53\u524D\u8DEF\u5F84\u7684\u4E0A\u5C42\uFF0C\u8BF7\u6362\u4E00\u4E2A\u4F4D\u7F6E\u3002",
60
+ "dialog.missingFolder": "\u8FD9\u4E2A\u6587\u4EF6\u5939\u4E0D\u5B58\u5728\uFF0C\u8BF7\u5237\u65B0\u540E\u91CD\u8BD5\u3002",
61
+ "dialog.unknownWorkspace": "\u8FD9\u4E2A\u5DE5\u4F5C\u533A\u5DF2\u7ECF\u4E0D\u5728\u5217\u8868\u91CC\u4E86\uFF0C\u8BF7\u91CD\u65B0\u6253\u5F00\u83DC\u5355\u3002",
62
+ "toast.moved": "\u5DE5\u4F5C\u533A\u5DF2\u79FB\u52A8\u3002",
63
+ "toast.undone": "\u5DF2\u64A4\u9500\u79FB\u52A8\u3002",
64
+ "toast.failed": "\u64CD\u4F5C\u5931\u8D25\u3002",
65
+ "running.title": "\u8BF7\u5148\u505C\u6B62\u8FD9\u4E9B\u4F1A\u8BDD",
66
+ "running.hint": "\u6709\u6B63\u5728\u8FD0\u884C\u7684\u4F1A\u8BDD\u4E0D\u80FD\u79FB\u52A8\uFF0C\u505C\u6B62\u540E\u518D\u8BD5\u3002",
67
+ "undo.title": "\u64A4\u9500\u4E0A\u6B21\u79FB\u52A8",
68
+ "undo.body": "\u5C06\u628A\u5DE5\u4F5C\u533A\u53CA\u5176\u4F1A\u8BDD\u6062\u590D\u5230\u539F\u6765\u7684\u4F4D\u7F6E\u3002",
69
+ "undo.confirm": "\u64A4\u9500",
70
+ "undo.none": "\u6CA1\u6709\u53EF\u64A4\u9500\u7684\u79FB\u52A8\u3002"
71
+ },
72
+ "en": {
73
+ "meta.title": "Workspace Move",
74
+ "meta.description": "Move a workspace directory \u2014 its subfolders, nested workspaces and sessions \u2014 to a new location, with rename, rollback and undo.",
75
+ "menu.move": "Move workspace\u2026",
76
+ "menu.undo": "Undo last move",
77
+ "dialog.title": "Move workspace",
78
+ "dialog.source": "Current location",
79
+ "dialog.target": "Destination parent folder",
80
+ "dialog.folderName": "Folder name",
81
+ "dialog.folderHint": "The workspace lands in the chosen folder under this name. Changing the name renames the workspace.",
82
+ "dialog.destination": "New location",
83
+ "dialog.up": "Up",
84
+ "dialog.home": "Home",
85
+ "dialog.newFolder": "New folder",
86
+ "dialog.newFolderPrompt": "New folder name",
87
+ "dialog.pickHere": "Use this folder",
88
+ "dialog.confirm": "Move",
89
+ "dialog.cancel": "Cancel",
90
+ "dialog.moving": "Moving\u2026",
91
+ "dialog.loading": "Loading\u2026",
92
+ "dialog.emptyFolder": "No subfolders here",
93
+ "dialog.invalidName": "Enter a folder name without slashes.",
94
+ "dialog.targetExists": "That name already exists there and is not empty. Pick another name.",
95
+ "dialog.sameLocation": "The workspace is already there. Pick another place or change the name.",
96
+ "dialog.insideSelf": "A workspace cannot be moved inside its own folder.",
97
+ "dialog.ancestor": "That place is above the workspace's current path. Pick another one.",
98
+ "dialog.missingFolder": "That folder does not exist. Refresh and try again.",
99
+ "dialog.unknownWorkspace": "That workspace is no longer listed. Reopen the menu.",
100
+ "toast.moved": "Workspace moved.",
101
+ "toast.undone": "Move undone.",
102
+ "toast.failed": "The action failed.",
103
+ "running.title": "Stop these sessions first",
104
+ "running.hint": "Sessions with running work cannot be moved. Stop them and try again.",
105
+ "undo.title": "Undo last move",
106
+ "undo.body": "Restores the workspace and its sessions to the previous location.",
107
+ "undo.confirm": "Undo",
108
+ "undo.none": "Nothing to undo."
109
+ }
110
+ };
111
+ var API_PREFIX = "/dsh-workspace-move/api";
112
+ var API_HEADER = "x-dsh-workspace-move";
113
+ async function callHost(action, body = {}) {
114
+ const response = await fetch(`${API_PREFIX}/${action}`, {
115
+ method: "POST",
116
+ headers: { "content-type": "application/json", [API_HEADER]: "1" },
117
+ body: JSON.stringify(body),
118
+ credentials: "same-origin"
119
+ });
120
+ if (!response.ok) {
121
+ const error = new Error(`request failed with status ${response.status}`);
122
+ error.code = `http-${response.status}`;
123
+ throw error;
124
+ }
125
+ const envelope = await response.json();
126
+ if (envelope.ok !== true) {
127
+ const error = new Error(envelope.message ?? envelope.code ?? "request failed");
128
+ error.code = envelope.code;
129
+ error.details = envelope.details ?? {};
130
+ throw error;
131
+ }
132
+ return envelope.result;
133
+ }
134
+ function describeFailure(t, error) {
135
+ switch (error?.code) {
136
+ case "sessions-running": {
137
+ const running = error.details?.active ?? [];
138
+ const names = running.map((item) => `${item.sessionId} (${item.kinds?.join(", ") ?? "busy"})`).join("\n");
139
+ return `${t("running.title")}
140
+ ${names}
141
+ ${t("running.hint")}`;
142
+ }
143
+ case "target-exists":
144
+ return t("dialog.targetExists");
145
+ case "target-same":
146
+ return t("dialog.sameLocation");
147
+ case "target-inside":
148
+ return t("dialog.insideSelf");
149
+ case "target-ancestor":
150
+ return t("dialog.ancestor");
151
+ case "target-missing":
152
+ return t("dialog.missingFolder");
153
+ case "workspace-unknown":
154
+ return t("dialog.unknownWorkspace");
155
+ case "name-invalid":
156
+ case "name-empty":
157
+ case "name-padded":
158
+ case "name-relative":
159
+ case "name-trailing":
160
+ return t("dialog.invalidName");
161
+ case "nothing-to-undo":
162
+ return t("undo.none");
163
+ default:
164
+ return `${t("toast.failed")} ${error?.message ?? ""}`.trim();
165
+ }
166
+ }
167
+ var CSS = `
168
+ .dshwm-backdrop{position:fixed;inset:0;z-index:60;display:flex;align-items:center;justify-content:center;background:var(--dsw-alias-bg-mask-1,rgba(0,0,0,.42));backdrop-filter:var(--dsw-mask-blur,blur(2px))}
169
+ .dshwm-panel{display:flex;flex-direction:column;gap:14px;width:min(760px,calc(100vw - 48px));max-height:calc(100vh - 96px);padding:20px 22px;border:.5px solid var(--dsw-alias-border-l2);border-radius:var(--dsw-radius-xl,14px);background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-primary);box-shadow:var(--dsw-elevation-panel);font-family:var(--dsw-font-family);overflow:hidden}
170
+ .dshwm-title{margin:0;font-size:16px;font-weight:600;line-height:24px}
171
+ .dshwm-field{display:flex;flex-direction:column;gap:6px;min-width:0}
172
+ .dshwm-label{font-size:12px;line-height:18px;color:var(--dsw-alias-label-secondary)}
173
+ .dshwm-value{padding:7px 10px;border:.5px solid var(--dsw-alias-border-l3);border-radius:var(--dsw-radius-md,8px);background:var(--dsw-alias-bg-module-platform);font-size:12.5px;word-break:break-all}
174
+ .dshwm-browser{display:flex;flex-direction:column;gap:8px;padding:10px;border:.5px solid var(--dsw-alias-border-l3);border-radius:var(--dsw-radius-lg,10px)}
175
+ .dshwm-crumbs{display:flex;flex-wrap:wrap;align-items:center;gap:2px;min-height:22px;font-size:12px}
176
+ .dshwm-crumb{padding:2px 6px;border:0;border-radius:var(--dsw-radius-sm,6px);background:0 0;color:var(--dsw-alias-label-tertiary);font:inherit;font-size:12px;cursor:pointer}
177
+ .dshwm-crumb:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}
178
+ .dshwm-crumb[data-active=true]{color:var(--dsw-alias-label-primary);font-weight:600}
179
+ .dshwm-list{display:grid;grid-template-columns:repeat(auto-fill,minmax(190px,1fr));gap:6px;max-height:220px;overflow-y:auto;padding:2px}
180
+ .dshwm-entry{display:flex;align-items:center;gap:6px;padding:6px 8px;border:0;border-radius:var(--dsw-radius-sm,6px);background:var(--dsw-alias-bg-module-platform);color:var(--dsw-alias-label-primary);font:inherit;font-size:12.5px;text-align:left;overflow:hidden;white-space:nowrap;cursor:pointer}
181
+ .dshwm-entry:hover{background:var(--dsw-alias-interactive-bg-hover)}
182
+ .dshwm-entry>span:last-child{overflow:hidden;text-overflow:ellipsis}
183
+ .dshwm-input{padding:7px 10px;border:.5px solid var(--dsw-alias-border-l4);border-radius:var(--dsw-radius-md,8px);background:var(--dsw-specific-input-major,#fff);color:var(--dsw-alias-label-primary);font:inherit;font-size:12.5px}
184
+ .dshwm-input:focus-visible{outline:var(--dsw-focus-ring-width,2px) solid var(--dsw-focus-ring-color,var(--dsw-alias-state-business-primary));outline-offset:1px}
185
+ .dshwm-hint{margin:0;font-size:11.5px;line-height:17px;color:var(--dsw-alias-label-tertiary)}
186
+ .dshwm-footer{display:flex;justify-content:flex-end;gap:8px;margin-top:2px}
187
+ .dshwm-button{padding:7px 14px;border:.5px solid var(--dsw-alias-border-l4);border-radius:var(--dsw-radius-md,8px);background:0 0;color:var(--dsw-alias-label-primary);font:inherit;font-size:12.5px;cursor:pointer}
188
+ .dshwm-button:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover)}
189
+ .dshwm-button:disabled{opacity:.45;cursor:default}
190
+ .dshwm-button:focus-visible{outline:var(--dsw-focus-ring-width,2px) solid var(--dsw-focus-ring-color,var(--dsw-alias-state-business-primary));outline-offset:1px}
191
+ .dshwm-primary{border-color:transparent;background:var(--dsw-alias-button-primary-fill);color:var(--dsw-alias-label-primary-foreground);font-weight:600}
192
+ .dshwm-primary:hover:not(:disabled){background:var(--dsw-alias-button-primary-hover)}
193
+ .dshwm-error{margin:0;font-size:12px;line-height:18px;color:var(--dsw-alias-state-error-primary);white-space:pre-wrap}
194
+ `;
195
+ var CSS_ID = "@reed-io/dsh-workspace-move/dialog.css";
196
+ function installStyles() {
197
+ if (typeof document === "undefined") return;
198
+ if (document.querySelector(`style[data-plugin-css=${JSON.stringify(CSS_ID)}]`) !== null) return;
199
+ const tag = document.createElement("style");
200
+ tag.dataset.plugin = "@reed-io/dsh-workspace-move";
201
+ tag.dataset.pluginCss = CSS_ID;
202
+ tag.textContent = CSS;
203
+ document.head.appendChild(tag);
204
+ }
205
+ function FolderRow({ entry, onOpen }) {
206
+ return (0, import_react.createElement)(
207
+ "button",
208
+ {
209
+ type: "button",
210
+ title: entry.path,
211
+ className: "dshwm-entry",
212
+ onClick: () => onOpen(entry.path)
213
+ },
214
+ (0, import_react.createElement)("span", { "aria-hidden": "true" }, entry.isSymbolicLink ? "\u{1F517}" : "\u{1F4C1}"),
215
+ (0, import_react.createElement)("span", null, entry.name)
216
+ );
217
+ }
218
+ function MoveDialog({ ctx, t, request, onClose, onDone }) {
219
+ const [listing, setListing] = (0, import_react.useState)(void 0);
220
+ const [target, setTarget] = (0, import_react.useState)("");
221
+ const [current, setCurrent] = (0, import_react.useState)("");
222
+ const [folderName, setFolderName] = (0, import_react.useState)(request.currentName ?? "");
223
+ const [plan, setPlan] = (0, import_react.useState)(void 0);
224
+ const [error, setError] = (0, import_react.useState)(void 0);
225
+ const [busy, setBusy] = (0, import_react.useState)(false);
226
+ const alive = (0, import_react.useRef)(true);
227
+ (0, import_react.useEffect)(() => {
228
+ alive.current = true;
229
+ return () => {
230
+ alive.current = false;
231
+ };
232
+ }, []);
233
+ const open = (0, import_react.useCallback)(
234
+ async (path) => {
235
+ setError(void 0);
236
+ try {
237
+ const next = await callHost("browse", { path });
238
+ if (!alive.current) return;
239
+ setListing(next);
240
+ setCurrent(next.path);
241
+ setTarget(next.path);
242
+ } catch (failure) {
243
+ if (alive.current) setError(describeFailure(t, failure));
244
+ }
245
+ },
246
+ [t]
247
+ );
248
+ (0, import_react.useEffect)(() => {
249
+ void open(request.startAt);
250
+ }, [open, request.startAt]);
251
+ (0, import_react.useEffect)(() => {
252
+ if (target === "") return;
253
+ let cancelled = false;
254
+ callHost("plan", { workspaceId: request.workspaceId, targetParent: target, folderName }).then((next) => {
255
+ if (!cancelled) setPlan(next);
256
+ }).catch((failure) => {
257
+ if (!cancelled) setPlan({ failure: describeFailure(t, failure), failureCode: failure.code });
258
+ });
259
+ return () => {
260
+ cancelled = true;
261
+ };
262
+ }, [target, folderName, request.workspaceId, t]);
263
+ const createFolder = (0, import_react.useCallback)(async () => {
264
+ const name = window.prompt(t("dialog.newFolderPrompt"), "");
265
+ if (name === null) return;
266
+ try {
267
+ const made = await callHost("createFolder", { parent: current, name });
268
+ await open(made.path);
269
+ } catch (failure) {
270
+ setError(describeFailure(t, failure));
271
+ }
272
+ }, [current, open, t]);
273
+ const confirm = (0, import_react.useCallback)(async () => {
274
+ setBusy(true);
275
+ setError(void 0);
276
+ try {
277
+ const result = await callHost("move", { workspaceId: request.workspaceId, targetParent: target, folderName });
278
+ onDone(result);
279
+ } catch (failure) {
280
+ setError(describeFailure(t, failure));
281
+ setBusy(false);
282
+ }
283
+ }, [folderName, onDone, request.workspaceId, t, target]);
284
+ const crumbs = listing?.crumbs ?? [];
285
+ const entries = listing?.entries ?? [];
286
+ const blocked = plan?.failure !== void 0;
287
+ return (0, import_react.createElement)(
288
+ "div",
289
+ {
290
+ className: "dshwm-backdrop",
291
+ role: "presentation",
292
+ onMouseDown: (event) => {
293
+ if (event.target === event.currentTarget && !busy) onClose();
294
+ }
295
+ },
296
+ (0, import_react.createElement)(
297
+ "div",
298
+ { role: "dialog", "aria-modal": "true", "aria-label": t("dialog.title"), className: "dshwm-panel" },
299
+ (0, import_react.createElement)("h2", { className: "dshwm-title" }, t("dialog.title")),
300
+ (0, import_react.createElement)(
301
+ "div",
302
+ { className: "dshwm-field" },
303
+ (0, import_react.createElement)("span", { className: "dshwm-label" }, t("dialog.source")),
304
+ (0, import_react.createElement)("div", { className: "dshwm-value" }, request.source)
305
+ ),
306
+ (0, import_react.createElement)(
307
+ "div",
308
+ { className: "dshwm-field" },
309
+ (0, import_react.createElement)("span", { className: "dshwm-label" }, t("dialog.target")),
310
+ (0, import_react.createElement)(
311
+ "div",
312
+ { className: "dshwm-browser" },
313
+ (0, import_react.createElement)(
314
+ "div",
315
+ { className: "dshwm-crumbs" },
316
+ crumbs.map(
317
+ (crumb, index) => (0, import_react.createElement)(
318
+ "button",
319
+ {
320
+ key: crumb.path,
321
+ type: "button",
322
+ className: "dshwm-crumb",
323
+ "data-active": index === crumbs.length - 1 ? "true" : "false",
324
+ onClick: () => void open(crumb.path)
325
+ },
326
+ crumb.name
327
+ )
328
+ )
329
+ ),
330
+ (0, import_react.createElement)(
331
+ "div",
332
+ { className: "dshwm-list" },
333
+ listing === void 0 ? (0, import_react.createElement)("span", { className: "dshwm-hint" }, t("dialog.loading")) : entries.length === 0 ? (0, import_react.createElement)("span", { className: "dshwm-hint" }, t("dialog.emptyFolder")) : entries.map((entry) => (0, import_react.createElement)(FolderRow, { key: entry.path, entry, onOpen: (path) => void open(path) }))
334
+ ),
335
+ (0, import_react.createElement)(
336
+ "div",
337
+ { className: "dshwm-footer" },
338
+ (0, import_react.createElement)("button", { type: "button", className: "dshwm-button", onClick: () => void createFolder() }, t("dialog.newFolder")),
339
+ (0, import_react.createElement)("button", { type: "button", className: "dshwm-button", onClick: () => void open(current) }, t("dialog.pickHere"))
340
+ )
341
+ )
342
+ ),
343
+ (0, import_react.createElement)(
344
+ "div",
345
+ { className: "dshwm-field" },
346
+ (0, import_react.createElement)("span", { className: "dshwm-label" }, t("dialog.folderName")),
347
+ (0, import_react.createElement)("input", {
348
+ className: "dshwm-input",
349
+ value: folderName,
350
+ spellCheck: false,
351
+ onChange: (event) => setFolderName(event.target.value)
352
+ }),
353
+ (0, import_react.createElement)("p", { className: "dshwm-hint" }, t("dialog.folderHint"))
354
+ ),
355
+ (0, import_react.createElement)(
356
+ "div",
357
+ { className: "dshwm-field" },
358
+ (0, import_react.createElement)("span", { className: "dshwm-label" }, t("dialog.destination")),
359
+ blocked ? (0, import_react.createElement)("p", { className: "dshwm-hint" }, plan.failure) : (0, import_react.createElement)("div", { className: "dshwm-value" }, plan?.destination ?? t("dialog.loading"))
360
+ ),
361
+ error === void 0 ? null : (0, import_react.createElement)("p", { className: "dshwm-error" }, error),
362
+ (0, import_react.createElement)(
363
+ "div",
364
+ { className: "dshwm-footer" },
365
+ (0, import_react.createElement)("button", { type: "button", className: "dshwm-button", disabled: busy, onClick: () => onClose() }, t("dialog.cancel")),
366
+ (0, import_react.createElement)(
367
+ "button",
368
+ {
369
+ type: "button",
370
+ className: "dshwm-button dshwm-primary",
371
+ disabled: busy || blocked || target === "",
372
+ onClick: () => void confirm()
373
+ },
374
+ busy ? t("dialog.moving") : t("dialog.confirm")
375
+ )
376
+ )
377
+ )
378
+ );
379
+ }
380
+ function MoveIcon({ size = 16 }) {
381
+ return (0, import_react.createElement)(
382
+ "svg",
383
+ { width: size, height: size, viewBox: "0 0 16 16", fill: "none", "aria-hidden": "true" },
384
+ (0, import_react.createElement)("path", {
385
+ d: "M2 4.25c0-.69.56-1.25 1.25-1.25h2.6c.35 0 .68.15.9.41l.62.74h4.38c.69 0 1.25.56 1.25 1.25v.6H2v-1.75Z",
386
+ fill: "currentColor",
387
+ opacity: 0.55
388
+ }),
389
+ (0, import_react.createElement)("rect", { x: 2, y: 5.25, width: 8.2, height: 6.5, rx: 1.25, fill: "currentColor", opacity: 0.9 }),
390
+ (0, import_react.createElement)("path", {
391
+ d: "M9.6 8.5h3.6m0 0-1.7-1.7m1.7 1.7-1.7 1.7",
392
+ stroke: "currentColor",
393
+ strokeWidth: 1.35,
394
+ strokeLinecap: "round",
395
+ strokeLinejoin: "round"
396
+ })
397
+ );
398
+ }
399
+ function UndoIcon({ size = 16 }) {
400
+ return (0, import_react.createElement)(
401
+ "svg",
402
+ { width: size, height: size, viewBox: "0 0 16 16", fill: "none", "aria-hidden": "true" },
403
+ (0, import_react.createElement)("path", {
404
+ d: "M4.2 6.1V3.3L1.7 5.8l2.5 2.5V6.1Z",
405
+ fill: "currentColor",
406
+ opacity: 0.85
407
+ }),
408
+ (0, import_react.createElement)("path", {
409
+ d: "M2.4 6.6a5.4 5.4 0 1 1 .9 5.3",
410
+ stroke: "currentColor",
411
+ strokeWidth: 1.4,
412
+ strokeLinecap: "round",
413
+ fill: "none"
414
+ })
415
+ );
416
+ }
417
+ var inject = ["slots", "locale", "contextMenus"];
418
+ function apply(ctx) {
419
+ try {
420
+ mount(ctx);
421
+ } catch (error) {
422
+ console.error("workspace-move: menu contributions unavailable", error);
423
+ }
424
+ }
425
+ function mount(ctx) {
426
+ installStyles();
427
+ const namespace = "workspaceMove";
428
+ ctx.effect(() => ctx.locale.register(namespace, translations), "workspace-move: dictionaries");
429
+ const t = ctx.locale.bind(namespace);
430
+ let undo = { available: false };
431
+ const refreshUndo = () => callHost("undoInfo").then((info) => {
432
+ undo = info ?? { available: false };
433
+ return undo;
434
+ }).catch(() => undo);
435
+ const openDialog = (request) => {
436
+ const slot = `workspace-move-dialog-${request.workspaceId}`;
437
+ let dispose;
438
+ const close = () => {
439
+ if (dispose === void 0) return;
440
+ dispose();
441
+ dispose = void 0;
442
+ };
443
+ dispose = ctx.slots.register(
444
+ { name: "shell.overlay", id: slot, order: 40, inject: () => ({}) },
445
+ () => (0, import_react.createElement)(MoveDialog, {
446
+ ctx,
447
+ t,
448
+ request,
449
+ onClose: close,
450
+ onDone: async (result) => {
451
+ close();
452
+ await refreshUndo();
453
+ ctx.contextMenus.notify(
454
+ `${t("toast.moved")} ${result.destination}`,
455
+ true,
456
+ result.undoable ? {
457
+ label: t("menu.undo"),
458
+ run: async () => {
459
+ await callHost("undo");
460
+ undo = { available: false };
461
+ ctx.contextMenus.notify(t("toast.undone"), true);
462
+ },
463
+ failureLabel: t("toast.failed")
464
+ } : void 0
465
+ );
466
+ }
467
+ })
468
+ );
469
+ };
470
+ ctx.effect(
471
+ () => ctx.contextMenus.register({
472
+ id: "workspace-move.move",
473
+ target: "workspace",
474
+ order: 200,
475
+ resolve: (value) => {
476
+ const workspaceId = typeof value?.workspaceId === "string" ? value.workspaceId : "";
477
+ const path = typeof value?.path === "string" ? value.path : "";
478
+ if (workspaceId === "" || path === "") return [];
479
+ const trimmed = path.replace(/[\\/]+$/u, "");
480
+ const name = trimmed.split(/[\\/]/u).pop() ?? "";
481
+ const cut = trimmed.length - name.length;
482
+ const startAt = cut > 1 ? trimmed.slice(0, cut - 1) : trimmed;
483
+ return [
484
+ {
485
+ id: "workspace-move.move",
486
+ label: t("menu.move"),
487
+ icon: { key: "workspaceMove.move" },
488
+ failureLabel: t("toast.failed"),
489
+ run: () => {
490
+ openDialog({ workspaceId, source: path, currentName: name, startAt });
491
+ }
492
+ }
493
+ ];
494
+ }
495
+ }),
496
+ "workspace-move: move entry"
497
+ );
498
+ ctx.effect(
499
+ () => ctx.contextMenus.register({
500
+ id: "workspace-move.undo",
501
+ target: "workspace",
502
+ order: 210,
503
+ resolve: () => undo.available !== true ? [] : [
504
+ {
505
+ id: "workspace-move.undo",
506
+ label: t("menu.undo"),
507
+ icon: { key: "workspaceMove.undo" },
508
+ separatorBefore: true,
509
+ failureLabel: t("toast.failed"),
510
+ run: async () => {
511
+ try {
512
+ await callHost("undo");
513
+ undo = { available: false };
514
+ ctx.contextMenus.notify(t("toast.undone"), true);
515
+ } catch (failure) {
516
+ await refreshUndo();
517
+ ctx.contextMenus.notify(describeFailure(t, failure));
518
+ }
519
+ }
520
+ }
521
+ ]
522
+ }),
523
+ "workspace-move: undo entry"
524
+ );
525
+ ctx.slots.inject("contextMenu.icon", () => [
526
+ ctx.slots.register({ name: "contextMenu.icon", key: "workspaceMove.move" }, MoveIcon),
527
+ ctx.slots.register({ name: "contextMenu.icon", key: "workspaceMove.undo" }, UndoIcon)
528
+ ]);
529
+ void refreshUndo();
530
+ }
531
+
532
+ return module.exports;
533
+ }
534
+ });
@@ -0,0 +1,98 @@
1
+ /**
2
+ * Folder browsing for the destination picker. The Host answers from the same
3
+ * filesystem the move will use, so a folder only appears when it is genuinely a
4
+ * place a workspace can live.
5
+ * @module @reed-io/dsh-workspace-move
6
+ */
7
+ import { readdir } from "node:fs/promises";
8
+ import { homedir } from "node:os";
9
+ import { basename, dirname, join, resolve } from "node:path";
10
+ import { canonicalPath, isMissing } from "./fsx.js";
11
+
12
+ const MAX_ENTRIES = 2000;
13
+
14
+ /**
15
+ * Describe the chain of folders leading to one directory.
16
+ * @param path - absolute directory.
17
+ * @returns breadcrumbs from the filesystem root to the directory.
18
+ */
19
+ function crumbsOf(path) {
20
+ const chain = [];
21
+ let cursor = path;
22
+ for (;;) {
23
+ const parent = dirname(cursor);
24
+ if (parent === cursor) {
25
+ chain.unshift({ name: cursor, path: cursor, hidden: false });
26
+ break;
27
+ }
28
+ const name = basename(cursor);
29
+ chain.unshift({ name, path: cursor, hidden: name.startsWith(".") });
30
+ cursor = parent;
31
+ }
32
+ return chain;
33
+ }
34
+
35
+ /**
36
+ * List the subfolders of one directory for the destination picker.
37
+ * @param requested - directory to list; empty selects the home directory.
38
+ * @returns the canonical directory, its breadcrumbs, and its subfolders.
39
+ */
40
+ export async function browse(requested) {
41
+ const target = typeof requested === "string" && requested.trim() !== "" ? requested : homedir();
42
+ const path = await canonicalPath(target);
43
+ let entries;
44
+ try {
45
+ entries = await readdir(path, { withFileTypes: true });
46
+ } catch (error) {
47
+ if (isMissing(error)) {
48
+ const failure = new Error(`folder '${path}' does not exist`);
49
+ failure.code = "directory-missing";
50
+ throw failure;
51
+ }
52
+ const failure = new Error(`folder '${path}' cannot be read`);
53
+ failure.code = "directory-unreadable";
54
+ throw failure;
55
+ }
56
+ const folders = entries
57
+ .filter((entry) => entry.isDirectory() || entry.isSymbolicLink())
58
+ .map((entry) => ({
59
+ name: entry.name,
60
+ path: join(path, entry.name),
61
+ hidden: entry.name.startsWith("."),
62
+ isSymbolicLink: entry.isSymbolicLink()
63
+ }))
64
+ .sort((left, right) => left.name.localeCompare(right.name, undefined, { numeric: true, sensitivity: "base" }));
65
+ return {
66
+ path,
67
+ home: await canonicalPath(homedir()),
68
+ crumbs: crumbsOf(path),
69
+ entries: folders.slice(0, MAX_ENTRIES),
70
+ truncated: folders.length > MAX_ENTRIES
71
+ };
72
+ }
73
+
74
+ /**
75
+ * Create one folder inside a directory for the destination picker.
76
+ * @param parent - existing directory.
77
+ * @param name - new folder name.
78
+ * @returns the canonical new folder path.
79
+ */
80
+ export async function createFolder(parent, name) {
81
+ const trimmed = typeof name === "string" ? name.trim() : "";
82
+ if (trimmed === "" || trimmed === "." || trimmed === ".." || /[\\/]/u.test(trimmed)) {
83
+ const failure = new Error("folder name must be one path segment");
84
+ failure.code = "directory-invalid-name";
85
+ throw failure;
86
+ }
87
+ const base = await canonicalPath(parent);
88
+ const target = resolve(base, trimmed);
89
+ const { mkdir } = await import("node:fs/promises");
90
+ try {
91
+ await mkdir(target);
92
+ } catch (error) {
93
+ const failure = new Error(`folder '${target}' could not be created`);
94
+ failure.code = error?.code === "EEXIST" ? "directory-exists" : "directory-create-failed";
95
+ throw failure;
96
+ }
97
+ return canonicalPath(target);
98
+ }