@dsh-electron/dsh-plugin-multi-root-workspace 0.1.6 → 0.1.7
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/CHANGELOG.en.md +44 -1
- package/CHANGELOG.md +44 -1
- package/README.en.md +155 -92
- package/README.md +156 -91
- package/cordis.patch.yml +57 -0
- package/lib/client.js +2678 -276
- package/lib/client.js.map +1 -1
- package/lib/command-CESuwlVt.js +1306 -0
- package/lib/command.js +2 -2
- package/lib/{compat-B6ZqbQBy.js → compat-DF_yL1bB.js} +160 -2
- package/lib/compat.js +1 -1
- package/lib/contract-DZ4zczDT.js +238 -0
- package/lib/{fs-D_Sw3o5z.js → fs-9JIzGaf3.js} +55 -19
- package/lib/fs.js +2 -1
- package/lib/index.js +10 -8
- package/lib/{instructions-DfzN-wVf.js → instructions-D19jRxN0.js} +3 -2
- package/lib/instructions.js +1 -1
- package/lib/lsp.js +7 -2
- package/lib/method-wrapper-CFY3p3j2.js +26 -0
- package/lib/process-fence.js +280 -0
- package/lib/providers.js +87 -0
- package/lib/{registry-DySMbjgW.js → registry-DCxC5Lay.js} +256 -35
- package/lib/registry.js +2 -2
- package/lib/{roots-DasWEBJk.js → roots-B-0l8mNG.js} +117 -20
- package/lib/sandbox-Kup4_ZIb.js +747 -0
- package/lib/sandbox.js +2 -1
- package/lib/scope.js +229 -20
- package/lib/types/client/RootSwitcher.d.ts +63 -0
- package/lib/types/client/WorkspaceFoldersAction.d.ts +39 -1
- package/lib/types/client/files-tree/FilesBody.module.css.d.ts +24 -0
- package/lib/types/client/files-tree/FilesTree.d.ts +47 -0
- package/lib/types/client/files-tree/adapter.d.ts +94 -0
- package/lib/types/client/files-tree/directory-node.d.ts +62 -0
- package/lib/types/client/files-tree/face.d.ts +107 -0
- package/lib/types/client/files-tree/locales.d.ts +10 -0
- package/lib/types/client/files-tree/primitives.d.ts +33 -0
- package/lib/types/client/files-tree/store.d.ts +81 -0
- package/lib/types/client/locales.d.ts +56 -10
- package/lib/types/client/open-in-app/OpenInAppAction.d.ts +27 -0
- package/lib/types/client/open-in-app/OpenTargetButton.d.ts +52 -0
- package/lib/types/client/open-in-app/OpenTargetButton.module.css.d.ts +29 -0
- package/lib/types/client/open-in-app/adapter.d.ts +46 -0
- package/lib/types/client/open-in-app/applications.d.ts +4 -0
- package/lib/types/client/open-in-app/controller.d.ts +53 -0
- package/lib/types/client/open-in-app/locales.d.ts +10 -0
- package/lib/types/client/open-in-app/open-failure-toast.d.ts +11 -0
- package/lib/types/client/open-in-app/open-path.d.ts +13 -0
- package/lib/types/client/panel-client.d.ts +2 -1
- package/lib/types/client/styles.d.ts +1 -1
- package/lib/types/command.d.ts +16 -3
- package/lib/types/compat/client-icons.d.ts +18 -0
- package/lib/types/compat/dsh-version.d.ts +1 -1
- package/lib/types/compat/open-in-app-routes.d.ts +38 -0
- package/lib/types/compat/process-entrypoints.d.ts +31 -0
- package/lib/types/compat/process-result.d.ts +14 -0
- package/lib/types/compat/pty-marker.d.ts +83 -0
- package/lib/types/compat/sandbox-confine.d.ts +2 -0
- package/lib/types/compat/storage-zod.d.ts +65 -0
- package/lib/types/compat/windows-deny-preflight.d.ts +21 -0
- package/lib/types/compat/windows-native.d.ts +68 -0
- package/lib/types/compat/windows-path-guard.d.ts +56 -0
- package/lib/types/compat/workspace-files.d.ts +2 -0
- package/lib/types/contract.d.ts +31 -19
- package/lib/types/dialects.d.ts +99 -3
- package/lib/types/fs.d.ts +32 -10
- package/lib/types/index.d.ts +7 -2
- package/lib/types/process-fence.d.ts +54 -0
- package/lib/types/profile-isolation.d.ts +178 -0
- package/lib/types/providers.d.ts +70 -0
- package/lib/types/registry.d.ts +132 -31
- package/lib/types/roots.d.ts +100 -13
- package/lib/types/sandbox.d.ts +59 -3
- package/lib/types/scope.d.ts +200 -14
- package/lib/types/windows-broker.d.ts +17 -0
- package/lib/types/windows-deny.d.ts +1 -0
- package/lib/types/windows-profile.d.ts +10 -13
- package/lib/types/windows-runner-contract.d.ts +97 -0
- package/lib/windows-runner-contract-CEfmZzu0.js +141 -0
- package/lib/windows-runner.js +1256 -13
- package/lib/workspace-files.js +138 -3
- package/lib/workspace-routing-YPxaQ4V3.js +26 -0
- package/package.json +84 -62
- package/scripts/windows-broker/install.ps1 +89 -0
- package/scripts/windows-broker/rights.cs +58 -0
- package/scripts/windows-broker/service.cs +323 -0
- package/scripts/windows-broker/verify-install.ps1 +80 -0
- package/lib/command-CTdNvwt1.js +0 -791
- package/lib/method-wrapper-Zc-JlDP3.js +0 -51
- package/lib/sandbox-BRXl_FpF.js +0 -132
- package/lib/types/client/RootFileTree.d.ts +0 -12
- package/lib/types/panel-files.d.ts +0 -3
- package/lib/windows-profile-CiFsFYIV.js +0 -216
- package/lib/{registry-lease-win32-dy8TQMD9.js → registry-lease-win32-DTO6QpT9.js} +2 -2
package/lib/command-CTdNvwt1.js
DELETED
|
@@ -1,791 +0,0 @@
|
|
|
1
|
-
import { t as effectiveDirectory } from "./session-directory-BLOstut1.js";
|
|
2
|
-
import { f as resolveRootRef, i as canonicalRoot, l as isCanonicallyUnder, o as entryRootRef, r as availableRoots, t as RootValidationError } from "./roots-DasWEBJk.js";
|
|
3
|
-
import "@deepseek-ai/cordis";
|
|
4
|
-
import { isAbsolute, resolve } from "node:path";
|
|
5
|
-
import { canonicalPath } from "@deepseek-ai/dsh-sandbox";
|
|
6
|
-
import { z } from "zod";
|
|
7
|
-
//#region src/contract.ts
|
|
8
|
-
/**
|
|
9
|
-
* The wire contract between the browser panel and this plugin's host half.
|
|
10
|
-
*
|
|
11
|
-
* It is deliberately transport-agnostic plain data: the host registers one
|
|
12
|
-
* Connection RPC channel (see `command.ts`) and the client bundle inlines this
|
|
13
|
-
* module, so both sides agree on endpoint names and payload shapes without
|
|
14
|
-
* sharing any runtime identity. Error codes are the stable half of the
|
|
15
|
-
* contract; `message` is only a fallback the panel does not have to show.
|
|
16
|
-
*
|
|
17
|
-
* Two properties this module owns:
|
|
18
|
-
*
|
|
19
|
-
* - **One shape per endpoint.** {@link PanelResponseMap} is the single mapping
|
|
20
|
-
* from endpoint to response type, so the panel cannot silently decode one
|
|
21
|
-
* endpoint's answer as another's. `reveal` answers with
|
|
22
|
-
* {@link RevealedView}; root-management endpoints answer with
|
|
23
|
-
* {@link RootsView}, and browsing returns {@link FilesView} or {@link FilePreview}.
|
|
24
|
-
* The host builds those answers in `command.ts` and the
|
|
25
|
-
* panel parses them here — one definition, two consumers.
|
|
26
|
-
* - **Runtime validation at the boundary.** The payload arrives from a browser
|
|
27
|
-
* and the answer arrives from the host; neither is trusted to match the
|
|
28
|
-
* TypeScript types. {@link parsePanelCall} and the `parse*View` functions
|
|
29
|
-
* check the shapes and report structural failures as a message, so a
|
|
30
|
-
* mismatch becomes a visible, localized error instead of a crash or a
|
|
31
|
-
* silently wrong render.
|
|
32
|
-
*
|
|
33
|
-
* Nothing here may import `node:*` or `@deepseek-ai/*`: this module is inlined
|
|
34
|
-
* into the browser bundle.
|
|
35
|
-
*
|
|
36
|
-
* @module @dsh-electron/dsh-plugin-multi-root-workspace/contract
|
|
37
|
-
*/
|
|
38
|
-
/** The absolute logical channel both halves speak over. */
|
|
39
|
-
const PANEL_CHANNEL = "/multi-root-workspace";
|
|
40
|
-
/** Every endpoint name, for exhaustive iteration in tests and callers. */
|
|
41
|
-
const PANEL_ENDPOINTS = [
|
|
42
|
-
"list",
|
|
43
|
-
"add",
|
|
44
|
-
"remove",
|
|
45
|
-
"alias",
|
|
46
|
-
"move",
|
|
47
|
-
"reveal",
|
|
48
|
-
"files",
|
|
49
|
-
"readFile"
|
|
50
|
-
];
|
|
51
|
-
/**
|
|
52
|
-
* The request body on the wire. The endpoint travels as the RPC method, not as
|
|
53
|
-
* a field, so this schema covers the fields only; the host pairs it with the
|
|
54
|
-
* method the channel dispatched (and rejects the pair if the body contradicts
|
|
55
|
-
* it). Unknown keys are rejected rather than ignored: a client sending a field
|
|
56
|
-
* this host does not understand is out of contract, and silently dropping it
|
|
57
|
-
* would hide the drift.
|
|
58
|
-
*/
|
|
59
|
-
const panelRequestSchema = z.object({
|
|
60
|
-
sessionId: z.string().min(1),
|
|
61
|
-
entry: z.object({
|
|
62
|
-
ordinal: z.number().int().positive(),
|
|
63
|
-
id: z.string(),
|
|
64
|
-
path: z.string(),
|
|
65
|
-
addedAt: z.string()
|
|
66
|
-
}).strict().optional(),
|
|
67
|
-
id: z.string().optional(),
|
|
68
|
-
path: z.string().optional(),
|
|
69
|
-
alias: z.string().optional(),
|
|
70
|
-
beforeEntry: z.object({
|
|
71
|
-
ordinal: z.number().int().positive(),
|
|
72
|
-
id: z.string(),
|
|
73
|
-
path: z.string(),
|
|
74
|
-
addedAt: z.string()
|
|
75
|
-
}).strict().optional(),
|
|
76
|
-
beforeId: z.string().optional()
|
|
77
|
-
}).strict();
|
|
78
|
-
/**
|
|
79
|
-
* One root as it travels over the channel. Deliberately NOT strict, in both
|
|
80
|
-
* directions: a newer host that adds a field must not brick an older panel, and
|
|
81
|
-
* the field that matters is the state it reports.
|
|
82
|
-
*/
|
|
83
|
-
const rootViewSchema = z.object({
|
|
84
|
-
ordinal: z.number().int().positive(),
|
|
85
|
-
id: z.string(),
|
|
86
|
-
path: z.string(),
|
|
87
|
-
alias: z.string().optional(),
|
|
88
|
-
addedAt: z.string(),
|
|
89
|
-
state: z.enum([
|
|
90
|
-
"available",
|
|
91
|
-
"missing",
|
|
92
|
-
"redirected",
|
|
93
|
-
"invalid"
|
|
94
|
-
]),
|
|
95
|
-
detail: z.string().optional()
|
|
96
|
-
});
|
|
97
|
-
const rootsViewSchema = z.object({
|
|
98
|
-
primaryRoot: z.string(),
|
|
99
|
-
primaryName: z.string().optional(),
|
|
100
|
-
effectiveCwd: z.string().optional(),
|
|
101
|
-
roots: z.array(rootViewSchema),
|
|
102
|
-
unavailable: z.string().optional()
|
|
103
|
-
});
|
|
104
|
-
const revealedViewSchema = z.object({ revealed: z.string() });
|
|
105
|
-
const filesViewSchema = z.object({
|
|
106
|
-
path: z.string(),
|
|
107
|
-
entries: z.array(z.object({
|
|
108
|
-
name: z.string().min(1).refine((name) => name !== "." && name !== ".." && !/[\\/]/.test(name) && !name.includes(String.fromCharCode(0))),
|
|
109
|
-
type: z.enum([
|
|
110
|
-
"file",
|
|
111
|
-
"directory",
|
|
112
|
-
"other"
|
|
113
|
-
])
|
|
114
|
-
})),
|
|
115
|
-
truncated: z.boolean()
|
|
116
|
-
});
|
|
117
|
-
const filePreviewSchema = z.object({
|
|
118
|
-
text: z.string(),
|
|
119
|
-
eof: z.boolean()
|
|
120
|
-
});
|
|
121
|
-
function parseFilesView(value) {
|
|
122
|
-
const parsed = filesViewSchema.safeParse(value);
|
|
123
|
-
return parsed.success ? {
|
|
124
|
-
ok: true,
|
|
125
|
-
value: parsed.data
|
|
126
|
-
} : {
|
|
127
|
-
ok: false,
|
|
128
|
-
message: firstIssue(parsed.error)
|
|
129
|
-
};
|
|
130
|
-
}
|
|
131
|
-
function parseFilePreview(value) {
|
|
132
|
-
const parsed = filePreviewSchema.safeParse(value);
|
|
133
|
-
return parsed.success ? {
|
|
134
|
-
ok: true,
|
|
135
|
-
value: parsed.data
|
|
136
|
-
} : {
|
|
137
|
-
ok: false,
|
|
138
|
-
message: firstIssue(parsed.error)
|
|
139
|
-
};
|
|
140
|
-
}
|
|
141
|
-
z.object({
|
|
142
|
-
code: z.string(),
|
|
143
|
-
message: z.string(),
|
|
144
|
-
details: z.unknown().optional()
|
|
145
|
-
});
|
|
146
|
-
/**
|
|
147
|
-
* Validate the payload the host received for one endpoint.
|
|
148
|
-
* @param endpoint - the endpoint the channel dispatched (the RPC method).
|
|
149
|
-
* @param payload - the raw channel payload (the request body).
|
|
150
|
-
* @returns the call the host may act on, or the reason it is not one.
|
|
151
|
-
*/
|
|
152
|
-
function parsePanelCall(endpoint, payload) {
|
|
153
|
-
if (!PANEL_ENDPOINTS.includes(endpoint)) return {
|
|
154
|
-
ok: false,
|
|
155
|
-
message: `unknown endpoint "${endpoint}"`
|
|
156
|
-
};
|
|
157
|
-
const parsed = panelRequestSchema.safeParse(payload ?? {});
|
|
158
|
-
if (!parsed.success) return {
|
|
159
|
-
ok: false,
|
|
160
|
-
message: `the request body is invalid: ${firstIssue(parsed.error)}`
|
|
161
|
-
};
|
|
162
|
-
return {
|
|
163
|
-
ok: true,
|
|
164
|
-
value: narrowCall(endpoint, parsed.data)
|
|
165
|
-
};
|
|
166
|
-
}
|
|
167
|
-
/**
|
|
168
|
-
* Validate a `RootsView` when it arrives.
|
|
169
|
-
* @param value - the raw endpoint value.
|
|
170
|
-
* @returns the view, or the reason it is not one.
|
|
171
|
-
*/
|
|
172
|
-
function parseRootsView(value) {
|
|
173
|
-
const parsed = rootsViewSchema.safeParse(value);
|
|
174
|
-
if (!parsed.success) return {
|
|
175
|
-
ok: false,
|
|
176
|
-
message: `the answer is not a roots view: ${firstIssue(parsed.error)}`
|
|
177
|
-
};
|
|
178
|
-
const view = parsed.data;
|
|
179
|
-
return {
|
|
180
|
-
ok: true,
|
|
181
|
-
value: {
|
|
182
|
-
primaryRoot: view.primaryRoot,
|
|
183
|
-
...view.primaryName === void 0 ? {} : { primaryName: view.primaryName },
|
|
184
|
-
...view.effectiveCwd === void 0 ? {} : { effectiveCwd: view.effectiveCwd },
|
|
185
|
-
roots: view.roots.map(narrowRootView),
|
|
186
|
-
...view.unavailable === void 0 ? {} : { unavailable: view.unavailable }
|
|
187
|
-
}
|
|
188
|
-
};
|
|
189
|
-
}
|
|
190
|
-
/**
|
|
191
|
-
* Validate a `RevealedView` when it arrives.
|
|
192
|
-
* @param value - the raw endpoint value.
|
|
193
|
-
* @returns the view, or the reason it is not one.
|
|
194
|
-
*/
|
|
195
|
-
function parseRevealedView(value) {
|
|
196
|
-
const parsed = revealedViewSchema.safeParse(value);
|
|
197
|
-
if (!parsed.success) return {
|
|
198
|
-
ok: false,
|
|
199
|
-
message: `the answer is not a reveal result: ${firstIssue(parsed.error)}`
|
|
200
|
-
};
|
|
201
|
-
return {
|
|
202
|
-
ok: true,
|
|
203
|
-
value: parsed.data
|
|
204
|
-
};
|
|
205
|
-
}
|
|
206
|
-
/**
|
|
207
|
-
* Rebuild a parsed call as the declared type, keeping absent optional fields
|
|
208
|
-
* ABSENT (`exactOptionalPropertyTypes` distinguishes "absent" from "undefined",
|
|
209
|
-
* and the host's own code reads the two the same way the panel sent them).
|
|
210
|
-
*/
|
|
211
|
-
function narrowCall(endpoint, value) {
|
|
212
|
-
return {
|
|
213
|
-
endpoint,
|
|
214
|
-
sessionId: value.sessionId,
|
|
215
|
-
...value.entry === void 0 ? {} : { entry: value.entry },
|
|
216
|
-
...value.id === void 0 ? {} : { id: value.id },
|
|
217
|
-
...value.path === void 0 ? {} : { path: value.path },
|
|
218
|
-
...value.alias === void 0 ? {} : { alias: value.alias },
|
|
219
|
-
...value.beforeEntry === void 0 ? {} : { beforeEntry: value.beforeEntry },
|
|
220
|
-
...value.beforeId === void 0 ? {} : { beforeId: value.beforeId }
|
|
221
|
-
};
|
|
222
|
-
}
|
|
223
|
-
/** Rebuild one parsed root as the declared `RootView`. */
|
|
224
|
-
function narrowRootView(value) {
|
|
225
|
-
return {
|
|
226
|
-
ordinal: value.ordinal,
|
|
227
|
-
id: value.id,
|
|
228
|
-
path: value.path,
|
|
229
|
-
addedAt: value.addedAt,
|
|
230
|
-
state: value.state,
|
|
231
|
-
...value.alias === void 0 ? {} : { alias: value.alias },
|
|
232
|
-
...value.detail === void 0 ? {} : { detail: value.detail }
|
|
233
|
-
};
|
|
234
|
-
}
|
|
235
|
-
/** Render the first zod issue as one short sentence. */
|
|
236
|
-
function firstIssue(error) {
|
|
237
|
-
const issue = error.issues[0];
|
|
238
|
-
if (issue === void 0) return "invalid";
|
|
239
|
-
return `${issue.path.length === 0 ? "" : `${issue.path.join(".")}: `}${issue.message}`;
|
|
240
|
-
}
|
|
241
|
-
//#endregion
|
|
242
|
-
//#region src/panel-files.ts
|
|
243
|
-
/** Additional-root file browsing through the plugin's existing Connection channel. */
|
|
244
|
-
async function browseRoot(ctx, primaryRoot, request, signal) {
|
|
245
|
-
const roots = await ctx.multiRootRegistry.refresh(primaryRoot);
|
|
246
|
-
const ref = request.entry === void 0 ? request.id === void 0 ? void 0 : {
|
|
247
|
-
kind: "id",
|
|
248
|
-
id: request.id
|
|
249
|
-
} : {
|
|
250
|
-
kind: "entry",
|
|
251
|
-
...request.entry
|
|
252
|
-
};
|
|
253
|
-
if (ref === void 0) throw new RootValidationError("invalid-ref", "an exact root entry is required");
|
|
254
|
-
const root = resolveRootRef(roots, ref);
|
|
255
|
-
if (root.state !== "available") throw new RootValidationError("missing", "the additional root is not available");
|
|
256
|
-
const path = request.path ?? ".";
|
|
257
|
-
if (isAbsolute(path) || /^[a-z][a-z\d+.-]*:/iu.test(path) || path.includes("\0")) throw new RootValidationError("invalid-ref", "a root-relative path is required");
|
|
258
|
-
const absolute = canonicalPath(resolve(root.path, path));
|
|
259
|
-
if (!isCanonicallyUnder(absolute, root.path)) throw new RootValidationError("invalid-ref", "the path is outside the selected root");
|
|
260
|
-
const files = ctx.root.get("workspaceFiles");
|
|
261
|
-
if (files === void 0) throw new Error("workspace-files is not composed");
|
|
262
|
-
const scope = {
|
|
263
|
-
sessionId: request.sessionId,
|
|
264
|
-
workspaceRoot: root.path
|
|
265
|
-
};
|
|
266
|
-
let result;
|
|
267
|
-
if (request.endpoint === "files") {
|
|
268
|
-
const listing = await files.list(scope, absolute, signal);
|
|
269
|
-
result = {
|
|
270
|
-
path,
|
|
271
|
-
entries: listing.entries.map((entry) => ({
|
|
272
|
-
name: entry.name,
|
|
273
|
-
type: entry.type
|
|
274
|
-
})),
|
|
275
|
-
truncated: listing.truncated
|
|
276
|
-
};
|
|
277
|
-
} else {
|
|
278
|
-
const preview = await files.read(scope, absolute, {
|
|
279
|
-
offset: 1,
|
|
280
|
-
limit: 200
|
|
281
|
-
}, signal);
|
|
282
|
-
result = {
|
|
283
|
-
text: preview.text,
|
|
284
|
-
eof: preview.eof
|
|
285
|
-
};
|
|
286
|
-
}
|
|
287
|
-
const latest = resolveRootRef(await ctx.multiRootRegistry.refresh(primaryRoot), ref);
|
|
288
|
-
if (latest.state !== "available" || latest.path !== root.path || canonicalPath(resolve(root.path, path)) !== absolute) throw new RootValidationError("missing", "the additional root changed while reading");
|
|
289
|
-
return result;
|
|
290
|
-
}
|
|
291
|
-
//#endregion
|
|
292
|
-
//#region src/command.ts
|
|
293
|
-
/** Services this row needs before it may activate. */
|
|
294
|
-
const inject = [
|
|
295
|
-
"commands",
|
|
296
|
-
"sandboxPolicy",
|
|
297
|
-
"multiRootRegistry"
|
|
298
|
-
];
|
|
299
|
-
/** The command name, without the leading slash. */
|
|
300
|
-
const COMMAND_NAME = "workspace-folders";
|
|
301
|
-
/**
|
|
302
|
-
* Parse the text that follows `/workspace-folders`.
|
|
303
|
-
* @param rawInput - the verbatim `rawInput` of the command invocation.
|
|
304
|
-
* @returns the subcommand and its remaining text.
|
|
305
|
-
* @throws {RootValidationError} `invalid-ref` for an unknown subcommand.
|
|
306
|
-
*/
|
|
307
|
-
function parseFoldersCommand(rawInput) {
|
|
308
|
-
const trimmed = rawInput.trim();
|
|
309
|
-
if (trimmed === "") return {
|
|
310
|
-
verb: "list",
|
|
311
|
-
rest: ""
|
|
312
|
-
};
|
|
313
|
-
const match = /^(\S+)(?:\s+([\s\S]*))?$/.exec(trimmed);
|
|
314
|
-
const verb = match?.[1] ?? "";
|
|
315
|
-
const rest = (match?.[2] ?? "").trim();
|
|
316
|
-
switch (verb) {
|
|
317
|
-
case "list":
|
|
318
|
-
case "add":
|
|
319
|
-
case "remove":
|
|
320
|
-
case "alias":
|
|
321
|
-
case "reveal":
|
|
322
|
-
case "help": return {
|
|
323
|
-
verb,
|
|
324
|
-
rest
|
|
325
|
-
};
|
|
326
|
-
default: throw new RootValidationError("invalid-ref", `unknown subcommand "${verb}"; try /${COMMAND_NAME} help`);
|
|
327
|
-
}
|
|
328
|
-
}
|
|
329
|
-
/** Strip one layer of matching quotes from an operator-supplied path. */
|
|
330
|
-
function unquote(text) {
|
|
331
|
-
if (text.length >= 2) {
|
|
332
|
-
const first = text[0];
|
|
333
|
-
const last = text[text.length - 1];
|
|
334
|
-
if (first === "\"" && last === "\"" || first === "'" && last === "'") return text.slice(1, -1);
|
|
335
|
-
}
|
|
336
|
-
return text;
|
|
337
|
-
}
|
|
338
|
-
/**
|
|
339
|
-
* Render the roots report the command returns. Additional roots are numbered
|
|
340
|
-
* from 1, which is the numbering every other subcommand accepts; the workspace
|
|
341
|
-
* root is never numbered because it cannot be removed.
|
|
342
|
-
* @param primaryRoot - the canonical workspace root.
|
|
343
|
-
* @param statuses - the registry's status list.
|
|
344
|
-
* @param unavailable - the store failure, when the registry could not read it.
|
|
345
|
-
* @returns the report text.
|
|
346
|
-
*/
|
|
347
|
-
function renderRootsReport(primaryRoot, statuses, unavailable) {
|
|
348
|
-
const lines = [`Workspace root (primary; access follows the current sandbox mode): ${primaryRoot}`];
|
|
349
|
-
if (unavailable !== void 0) {
|
|
350
|
-
lines.push(`Root registry unavailable: ${unavailable}`);
|
|
351
|
-
return lines.join("\n");
|
|
352
|
-
}
|
|
353
|
-
if (statuses.length === 0) {
|
|
354
|
-
lines.push(`No additional roots. Add one with /${COMMAND_NAME} add <absolute path>.`);
|
|
355
|
-
return lines.join("\n");
|
|
356
|
-
}
|
|
357
|
-
for (const [index, status] of statuses.entries()) {
|
|
358
|
-
const alias = status.alias === void 0 ? "" : ` [${status.alias}]`;
|
|
359
|
-
const state = status.state === "available" ? "" : ` (${status.state}: ${status.detail ?? "unavailable"})`;
|
|
360
|
-
lines.push(` ${index + 1} ${status.path}${alias}${state}`);
|
|
361
|
-
}
|
|
362
|
-
const withheld = statuses.length - availableRoots(statuses).length;
|
|
363
|
-
if (withheld > 0) lines.push(`${withheld} root(s) are registered but not writable right now; restore the directory and run /${COMMAND_NAME} list, or remove the entry.`);
|
|
364
|
-
lines.push(`Writable additional roots: ${availableRoots(statuses).length} of ${statuses.length}.`);
|
|
365
|
-
return lines.join("\n");
|
|
366
|
-
}
|
|
367
|
-
/**
|
|
368
|
-
* Register the command and, where a host Connection exists, the panel channel.
|
|
369
|
-
* @param ctx - the host context carrying `commands`, `sandboxPolicy`, and the registry.
|
|
370
|
-
*/
|
|
371
|
-
function apply(ctx) {
|
|
372
|
-
ctx.commands.register({
|
|
373
|
-
name: COMMAND_NAME,
|
|
374
|
-
description: "List, add, remove, alias, or reveal the additional workspace roots of this session",
|
|
375
|
-
input: { hint: "[list|add [path]|remove <n|path>|alias <n|path> [name]|reveal <n|path>|help]" },
|
|
376
|
-
handler: async (invocation) => await runCommand(ctx, invocation)
|
|
377
|
-
});
|
|
378
|
-
ctx.inject(["connection", "webServer"], (connectionCtx) => {
|
|
379
|
-
const connection = connectionCtx.root.get("connection");
|
|
380
|
-
if (connection === void 0) {
|
|
381
|
-
ctx.logger.warn("multi-root: connection resolved without the service; the panel channel is not mounted");
|
|
382
|
-
return;
|
|
383
|
-
}
|
|
384
|
-
const dispose = connection.rpc.handle(PANEL_CHANNEL, async (endpoint, payload, signal) => await dispatchPanelRequest(ctx, endpoint, payload, signal));
|
|
385
|
-
return () => {
|
|
386
|
-
dispose();
|
|
387
|
-
};
|
|
388
|
-
});
|
|
389
|
-
}
|
|
390
|
-
/** Execute one `/workspace-folders` invocation. */
|
|
391
|
-
async function runCommand(ctx, invocation) {
|
|
392
|
-
let parsed;
|
|
393
|
-
try {
|
|
394
|
-
parsed = parseFoldersCommand(invocation.rawInput);
|
|
395
|
-
} catch (error) {
|
|
396
|
-
return {
|
|
397
|
-
kind: "error",
|
|
398
|
-
text: failureText(error)
|
|
399
|
-
};
|
|
400
|
-
}
|
|
401
|
-
const primaryRoot = ctx.sandboxPolicy.resolve({ session: invocation.agent.session }).workspaceRoot;
|
|
402
|
-
const registry = ctx.multiRootRegistry;
|
|
403
|
-
try {
|
|
404
|
-
switch (parsed.verb) {
|
|
405
|
-
case "help": return {
|
|
406
|
-
kind: "success",
|
|
407
|
-
text: helpText()
|
|
408
|
-
};
|
|
409
|
-
case "list": return {
|
|
410
|
-
kind: "success",
|
|
411
|
-
text: await report(registry, primaryRoot)
|
|
412
|
-
};
|
|
413
|
-
case "add": {
|
|
414
|
-
const typed = unquote(parsed.rest);
|
|
415
|
-
const path = typed === "" ? await pickRootPath(ctx, invocation.signal) : typed;
|
|
416
|
-
if (path === void 0) return {
|
|
417
|
-
kind: "error",
|
|
418
|
-
text: `/${COMMAND_NAME}: no directory was selected; pass an absolute path or use the Workspace Folders panel`
|
|
419
|
-
};
|
|
420
|
-
await registry.add(primaryRoot, { path });
|
|
421
|
-
return {
|
|
422
|
-
kind: "success",
|
|
423
|
-
text: await report(registry, primaryRoot)
|
|
424
|
-
};
|
|
425
|
-
}
|
|
426
|
-
case "remove": {
|
|
427
|
-
const { ref } = await targetOf(registry, primaryRoot, parsed.rest);
|
|
428
|
-
await registry.removeAt(primaryRoot, ref);
|
|
429
|
-
return {
|
|
430
|
-
kind: "success",
|
|
431
|
-
text: await report(registry, primaryRoot)
|
|
432
|
-
};
|
|
433
|
-
}
|
|
434
|
-
case "alias": {
|
|
435
|
-
const { text: reference, rest: alias } = splitReference(parsed.rest);
|
|
436
|
-
const { ref } = await targetOf(registry, primaryRoot, reference);
|
|
437
|
-
await registry.setAlias(primaryRoot, ref, alias === "" ? void 0 : unquote(alias));
|
|
438
|
-
return {
|
|
439
|
-
kind: "success",
|
|
440
|
-
text: await report(registry, primaryRoot)
|
|
441
|
-
};
|
|
442
|
-
}
|
|
443
|
-
case "reveal": {
|
|
444
|
-
const { status: target } = await targetOf(registry, primaryRoot, parsed.rest);
|
|
445
|
-
await revealRoot(ctx, target.path);
|
|
446
|
-
return {
|
|
447
|
-
kind: "success",
|
|
448
|
-
text: `revealed ${target.path}`
|
|
449
|
-
};
|
|
450
|
-
}
|
|
451
|
-
}
|
|
452
|
-
} catch (error) {
|
|
453
|
-
return {
|
|
454
|
-
kind: "error",
|
|
455
|
-
text: failureText(error)
|
|
456
|
-
};
|
|
457
|
-
}
|
|
458
|
-
}
|
|
459
|
-
/**
|
|
460
|
-
* The current report for one primary root, after re-checking every registered
|
|
461
|
-
* directory. The refresh is what makes the report describe the present rather
|
|
462
|
-
* than the moment the process started.
|
|
463
|
-
*/
|
|
464
|
-
async function report(registry, primaryRoot) {
|
|
465
|
-
const statuses = await registry.refresh(primaryRoot);
|
|
466
|
-
return renderRootsReport(canonicalRoot(primaryRoot), statuses, registry.unavailable);
|
|
467
|
-
}
|
|
468
|
-
/** Render a failure the way the composer displays it. */
|
|
469
|
-
function failureText(error) {
|
|
470
|
-
if (error instanceof RootValidationError) return `/${COMMAND_NAME}: ${error.code}: ${error.message}`;
|
|
471
|
-
return `/${COMMAND_NAME}: ${error instanceof Error ? error.message : String(error)}`;
|
|
472
|
-
}
|
|
473
|
-
/** The command's own help text. */
|
|
474
|
-
function helpText() {
|
|
475
|
-
return [
|
|
476
|
-
`/${COMMAND_NAME} — additional workspace roots of this session`,
|
|
477
|
-
` /${COMMAND_NAME} list`,
|
|
478
|
-
` /${COMMAND_NAME} add [absolute path] (no path opens the directory picker when one is composed)`,
|
|
479
|
-
` /${COMMAND_NAME} remove <n|path>`,
|
|
480
|
-
` /${COMMAND_NAME} alias <n|path> [name] (no name clears the alias)`,
|
|
481
|
-
` /${COMMAND_NAME} reveal <n|path>`,
|
|
482
|
-
"Roots are canonicalized before they are stored: `~` expands, and a duplicate, a nested directory,",
|
|
483
|
-
"or this session's own workspace root is rejected."
|
|
484
|
-
].join("\n");
|
|
485
|
-
}
|
|
486
|
-
/**
|
|
487
|
-
* Split `alias`'s argument into a reference and the alias text.
|
|
488
|
-
*
|
|
489
|
-
* The reference is the first whitespace-delimited token, except when it opens
|
|
490
|
-
* with a quote: a quoted path may contain spaces, so the whole quoted span is
|
|
491
|
-
* the reference and the remainder is the alias.
|
|
492
|
-
*/
|
|
493
|
-
function splitReference(text) {
|
|
494
|
-
const trimmed = text.trim();
|
|
495
|
-
if (trimmed === "") throw new RootValidationError("invalid-ref", "a root reference is required");
|
|
496
|
-
if (trimmed.startsWith("\"") || trimmed.startsWith("'")) {
|
|
497
|
-
const quote = trimmed[0] ?? "";
|
|
498
|
-
const end = trimmed.indexOf(quote, 1);
|
|
499
|
-
if (end > 0) return {
|
|
500
|
-
text: trimmed.slice(0, end + 1),
|
|
501
|
-
rest: trimmed.slice(end + 1).trim()
|
|
502
|
-
};
|
|
503
|
-
}
|
|
504
|
-
const match = /^(\S+)(?:\s+([\s\S]*))?$/.exec(trimmed);
|
|
505
|
-
return {
|
|
506
|
-
text: match?.[1] ?? "",
|
|
507
|
-
rest: (match?.[2] ?? "").trim()
|
|
508
|
-
};
|
|
509
|
-
}
|
|
510
|
-
/**
|
|
511
|
-
* Resolve an operator reference — a 1-based number, a path, or an id — to the
|
|
512
|
-
* registered root it names.
|
|
513
|
-
* @param registry - the registry to read.
|
|
514
|
-
* @param primaryRoot - the workspace root the registrations belong to.
|
|
515
|
-
* @param text - the operator's text.
|
|
516
|
-
* @returns the matched status, after re-checking the registered directories.
|
|
517
|
-
* @throws {RootValidationError} `invalid-ref` for empty text, `not-found` when nothing matches.
|
|
518
|
-
*/
|
|
519
|
-
async function targetOf(registry, primaryRoot, text) {
|
|
520
|
-
const trimmed = text.trim();
|
|
521
|
-
if (trimmed === "") throw new RootValidationError("invalid-ref", "a root reference is required");
|
|
522
|
-
const statuses = await registry.refresh(primaryRoot);
|
|
523
|
-
let status;
|
|
524
|
-
if (/^\d+$/.test(trimmed)) {
|
|
525
|
-
const ordinal = Number(trimmed);
|
|
526
|
-
status = resolveRootRef(statuses, {
|
|
527
|
-
kind: "ordinal",
|
|
528
|
-
ordinal
|
|
529
|
-
});
|
|
530
|
-
return {
|
|
531
|
-
status,
|
|
532
|
-
ref: entryRootRef(status, ordinal)
|
|
533
|
-
};
|
|
534
|
-
}
|
|
535
|
-
const asPath = unquote(trimmed);
|
|
536
|
-
if (asPath === "~" || asPath.startsWith("~") || asPath.startsWith("/") || /^[A-Za-z]:[\\/]/.test(asPath)) status = resolveRootRef(statuses, {
|
|
537
|
-
kind: "path",
|
|
538
|
-
path: asPath
|
|
539
|
-
});
|
|
540
|
-
else status = resolveRootRef(statuses, {
|
|
541
|
-
kind: "id",
|
|
542
|
-
id: trimmed
|
|
543
|
-
});
|
|
544
|
-
const ordinal = statuses.indexOf(status) + 1;
|
|
545
|
-
return {
|
|
546
|
-
status,
|
|
547
|
-
ref: entryRootRef(status, ordinal)
|
|
548
|
-
};
|
|
549
|
-
}
|
|
550
|
-
/**
|
|
551
|
-
* Derive the primary root a panel request may act on.
|
|
552
|
-
*
|
|
553
|
-
* The browser names a live session; the host looks that session up and uses
|
|
554
|
-
* its immutable cwd. There is no client-supplied path and no deployment-default
|
|
555
|
-
* fallback: a panel call without a real host session has no workspace to
|
|
556
|
-
* manage. Property access would demand an injected `sessions` dependency this
|
|
557
|
-
* row does not name, so the lookup stays an explicit sibling `get` (the same
|
|
558
|
-
* pattern as `directoryPicker` / `subprocess`).
|
|
559
|
-
*
|
|
560
|
-
* @param ctx - the host context that may carry `sessions`.
|
|
561
|
-
* @param sessionId - the id the panel sent; already required by the wire schema.
|
|
562
|
-
* @returns the canonical cwd of that session.
|
|
563
|
-
* @throws {RootValidationError} `session-not-found` when the service is absent,
|
|
564
|
-
* the id does not name a live session, or that session has no cwd.
|
|
565
|
-
*/
|
|
566
|
-
function resolvePanelPrimaryRoot(ctx, sessionId) {
|
|
567
|
-
const session = ctx.get("sessions")?.get(sessionId);
|
|
568
|
-
const cwd = session?.header.cwd;
|
|
569
|
-
if (session === void 0 || cwd === void 0 || cwd === "") throw new RootValidationError("session-not-found", `session "${sessionId}" is not an active host session`, { reference: sessionId });
|
|
570
|
-
return canonicalPath(cwd);
|
|
571
|
-
}
|
|
572
|
-
/** Project one status into the panel's view. */
|
|
573
|
-
function toRootView(status, index) {
|
|
574
|
-
return {
|
|
575
|
-
ordinal: index + 1,
|
|
576
|
-
id: status.id,
|
|
577
|
-
path: status.path,
|
|
578
|
-
...status.alias === void 0 ? {} : { alias: status.alias },
|
|
579
|
-
addedAt: status.addedAt,
|
|
580
|
-
state: status.state,
|
|
581
|
-
...status.detail === void 0 ? {} : { detail: status.detail }
|
|
582
|
-
};
|
|
583
|
-
}
|
|
584
|
-
/**
|
|
585
|
-
* Resolve the display name of the primary root from the host's workspace
|
|
586
|
-
* registry — the title the operator set in the workspace browser, falling back
|
|
587
|
-
* to nothing (the panel then shows the path's basename).
|
|
588
|
-
*
|
|
589
|
-
* The registry is an optional sibling service, read through the explicit
|
|
590
|
-
* lookup like `sessions`/`subprocess`: property access would demand an
|
|
591
|
-
* injected dependency this row does not name. Membership wins over cwd, which
|
|
592
|
-
* mirrors the host's own session→workspace resolution.
|
|
593
|
-
*/
|
|
594
|
-
async function primaryNameOf(ctx, primaryRoot, request) {
|
|
595
|
-
const registry = ctx.get("workspaceRegistry");
|
|
596
|
-
if (registry === void 0) return void 0;
|
|
597
|
-
try {
|
|
598
|
-
const sessionId = request.sessionId;
|
|
599
|
-
if (sessionId !== void 0 && sessionId !== "") {
|
|
600
|
-
const member = registry.list().find((workspace) => workspace.sessionIds.includes(sessionId));
|
|
601
|
-
if (member !== void 0) return member.title;
|
|
602
|
-
}
|
|
603
|
-
return (await registry.resolveByPath(primaryRoot))?.title;
|
|
604
|
-
} catch (error) {
|
|
605
|
-
ctx.logger.warn("multi-root: workspace title lookup failed; the panel falls back to the path basename", error);
|
|
606
|
-
return;
|
|
607
|
-
}
|
|
608
|
-
}
|
|
609
|
-
/**
|
|
610
|
-
* Build the panel's view of one primary root, after re-checking every
|
|
611
|
-
* registered directory — the panel's read path is the same revalidation the
|
|
612
|
-
* command uses, so "the list the operator sees" is always the list the scope
|
|
613
|
-
* currently enforces.
|
|
614
|
-
*/
|
|
615
|
-
async function rootsViewOf(ctx, registry, primaryRoot, request) {
|
|
616
|
-
const statuses = await registry.refresh(primaryRoot);
|
|
617
|
-
const unavailable = registry.unavailable;
|
|
618
|
-
const primaryName = await primaryNameOf(ctx, primaryRoot, request);
|
|
619
|
-
const session = ctx.get("sessions")?.get(request.sessionId);
|
|
620
|
-
const effectiveCwd = session === void 0 ? void 0 : effectiveDirectory(ctx, session);
|
|
621
|
-
return {
|
|
622
|
-
primaryRoot: canonicalRoot(primaryRoot),
|
|
623
|
-
...primaryName === void 0 ? {} : { primaryName },
|
|
624
|
-
...effectiveCwd === void 0 ? {} : { effectiveCwd },
|
|
625
|
-
roots: statuses.map(toRootView),
|
|
626
|
-
...unavailable === void 0 ? {} : { unavailable }
|
|
627
|
-
};
|
|
628
|
-
}
|
|
629
|
-
/**
|
|
630
|
-
* Serve one panel endpoint.
|
|
631
|
-
*
|
|
632
|
-
* The payload is validated before anything acts on it: it arrived from a
|
|
633
|
-
* browser, and the host is the side that must not be surprised by a shape it
|
|
634
|
-
* did not expect (see `contract.ts`, which owns the schema both halves share).
|
|
635
|
-
* A malformed request is reported as `panel/bad-request` rather than throwing
|
|
636
|
-
* through the channel.
|
|
637
|
-
*/
|
|
638
|
-
async function dispatchPanelRequest(ctx, endpoint, payload, signal) {
|
|
639
|
-
try {
|
|
640
|
-
const parsed = parsePanelCall(endpoint, payload);
|
|
641
|
-
if (!parsed.ok) return {
|
|
642
|
-
ok: false,
|
|
643
|
-
error: {
|
|
644
|
-
code: "panel/bad-request",
|
|
645
|
-
message: parsed.message,
|
|
646
|
-
details: {}
|
|
647
|
-
}
|
|
648
|
-
};
|
|
649
|
-
const request = parsed.value;
|
|
650
|
-
const registry = ctx.multiRootRegistry;
|
|
651
|
-
const primaryRoot = resolvePanelPrimaryRoot(ctx, request.sessionId);
|
|
652
|
-
switch (request.endpoint) {
|
|
653
|
-
case "files":
|
|
654
|
-
case "readFile": return {
|
|
655
|
-
ok: true,
|
|
656
|
-
value: await browseRoot(ctx, primaryRoot, request, signal)
|
|
657
|
-
};
|
|
658
|
-
case "list": return {
|
|
659
|
-
ok: true,
|
|
660
|
-
value: await rootsViewOf(ctx, registry, primaryRoot, request)
|
|
661
|
-
};
|
|
662
|
-
case "add":
|
|
663
|
-
if (request.path === void 0 || request.path === "") throw new RootValidationError("missing", "a directory path is required");
|
|
664
|
-
await registry.add(primaryRoot, {
|
|
665
|
-
path: request.path,
|
|
666
|
-
...request.alias === void 0 ? {} : { alias: request.alias }
|
|
667
|
-
});
|
|
668
|
-
return {
|
|
669
|
-
ok: true,
|
|
670
|
-
value: await rootsViewOf(ctx, registry, primaryRoot, request)
|
|
671
|
-
};
|
|
672
|
-
case "remove":
|
|
673
|
-
await registry.removeAt(primaryRoot, requireEntry(request));
|
|
674
|
-
return {
|
|
675
|
-
ok: true,
|
|
676
|
-
value: await rootsViewOf(ctx, registry, primaryRoot, request)
|
|
677
|
-
};
|
|
678
|
-
case "alias":
|
|
679
|
-
await registry.setAlias(primaryRoot, requireEntry(request), request.alias);
|
|
680
|
-
return {
|
|
681
|
-
ok: true,
|
|
682
|
-
value: await rootsViewOf(ctx, registry, primaryRoot, request)
|
|
683
|
-
};
|
|
684
|
-
case "move":
|
|
685
|
-
await registry.move(primaryRoot, requireEntry(request), request.beforeEntry !== void 0 ? entryRefOf(request.beforeEntry) : request.beforeId === void 0 ? void 0 : {
|
|
686
|
-
kind: "id",
|
|
687
|
-
id: request.beforeId
|
|
688
|
-
});
|
|
689
|
-
return {
|
|
690
|
-
ok: true,
|
|
691
|
-
value: await rootsViewOf(ctx, registry, primaryRoot, request)
|
|
692
|
-
};
|
|
693
|
-
case "reveal": {
|
|
694
|
-
const ref = requireEntry(request);
|
|
695
|
-
const target = resolveRootRef(await registry.refresh(primaryRoot), ref);
|
|
696
|
-
await revealRoot(ctx, target.path);
|
|
697
|
-
return {
|
|
698
|
-
ok: true,
|
|
699
|
-
value: { revealed: target.path }
|
|
700
|
-
};
|
|
701
|
-
}
|
|
702
|
-
}
|
|
703
|
-
} catch (error) {
|
|
704
|
-
if (error instanceof RootValidationError) return {
|
|
705
|
-
ok: false,
|
|
706
|
-
error: {
|
|
707
|
-
code: error.code,
|
|
708
|
-
message: error.message,
|
|
709
|
-
details: {}
|
|
710
|
-
}
|
|
711
|
-
};
|
|
712
|
-
return {
|
|
713
|
-
ok: false,
|
|
714
|
-
error: {
|
|
715
|
-
code: "panel/internal",
|
|
716
|
-
message: error instanceof Error ? error.message : String(error),
|
|
717
|
-
details: {}
|
|
718
|
-
}
|
|
719
|
-
};
|
|
720
|
-
}
|
|
721
|
-
}
|
|
722
|
-
/** Convert a validated wire snapshot to the registry's exact-entry reference. */
|
|
723
|
-
function entryRefOf(entry) {
|
|
724
|
-
return {
|
|
725
|
-
kind: "entry",
|
|
726
|
-
...entry
|
|
727
|
-
};
|
|
728
|
-
}
|
|
729
|
-
/** Require the exact list row a mutating panel request must carry. */
|
|
730
|
-
function requireEntry(request) {
|
|
731
|
-
if (request.entry !== void 0) return entryRefOf(request.entry);
|
|
732
|
-
if (request.id !== void 0 && request.id !== "") return {
|
|
733
|
-
kind: "id",
|
|
734
|
-
id: request.id
|
|
735
|
-
};
|
|
736
|
-
throw new RootValidationError("invalid-ref", "an exact root entry is required");
|
|
737
|
-
}
|
|
738
|
-
/**
|
|
739
|
-
* Open the composed directory picker when one exists, and answer `undefined`
|
|
740
|
-
* otherwise (the command then reports the missing affordance).
|
|
741
|
-
*
|
|
742
|
-
* Only the `native` capability can answer a host-side command: the `browse`
|
|
743
|
-
* capability is an in-app dialog that only the browser panel can render.
|
|
744
|
-
*/
|
|
745
|
-
async function pickRootPath(ctx, signal) {
|
|
746
|
-
const picker = ctx.get("directoryPicker");
|
|
747
|
-
if (picker === void 0) return void 0;
|
|
748
|
-
const capability = picker.capability();
|
|
749
|
-
if (capability.kind !== "native") return void 0;
|
|
750
|
-
return await capability.pick(signal) ?? void 0;
|
|
751
|
-
}
|
|
752
|
-
/** The per-platform argv that reveals one directory in the OS file manager. */
|
|
753
|
-
function revealArgv(platform, path) {
|
|
754
|
-
switch (platform) {
|
|
755
|
-
case "darwin": return [
|
|
756
|
-
"open",
|
|
757
|
-
"-R",
|
|
758
|
-
path
|
|
759
|
-
];
|
|
760
|
-
case "win32": return ["explorer", `/select,${path}`];
|
|
761
|
-
default: return ["xdg-open", path];
|
|
762
|
-
}
|
|
763
|
-
}
|
|
764
|
-
/**
|
|
765
|
-
* Reveal one directory in the OS file manager through `ctx.subprocess`.
|
|
766
|
-
*
|
|
767
|
-
* This runs no confined command and grants the agent nothing: it is an
|
|
768
|
-
* operator action taken from the panel or the command line, and the process is
|
|
769
|
-
* the platform's own file manager. Failures are reported to the caller.
|
|
770
|
-
*/
|
|
771
|
-
async function revealRoot(ctx, path) {
|
|
772
|
-
const subprocess = ctx.get("subprocess");
|
|
773
|
-
if (subprocess === void 0) throw new RootValidationError("reveal-unavailable", "revealing a root needs a process runtime, which this composition does not mount");
|
|
774
|
-
const [program = "", ...args] = revealArgv(process.platform, path);
|
|
775
|
-
const executable = await subprocess.resolveExecutable(program);
|
|
776
|
-
const handle = subprocess.spawn({
|
|
777
|
-
argv: [executable, ...args],
|
|
778
|
-
cwd: path,
|
|
779
|
-
stdio: {
|
|
780
|
-
stdin: "ignore",
|
|
781
|
-
stdout: { maxBytes: 4096 },
|
|
782
|
-
stderr: { maxBytes: 4096 }
|
|
783
|
-
},
|
|
784
|
-
graceMs: 3e3
|
|
785
|
-
});
|
|
786
|
-
const outcome = await handle.done;
|
|
787
|
-
await handle.waitForExit();
|
|
788
|
-
if (outcome.exitCode !== 0) throw new RootValidationError("reveal-unavailable", `the file manager exited with ${String(outcome.exitCode)}`);
|
|
789
|
-
}
|
|
790
|
-
//#endregion
|
|
791
|
-
export { renderRootsReport as a, PANEL_CHANNEL as c, parseFilesView as d, parsePanelCall as f, parseFoldersCommand as i, PANEL_ENDPOINTS as l, parseRootsView as m, apply as n, resolvePanelPrimaryRoot as o, parseRevealedView as p, inject as r, revealArgv as s, COMMAND_NAME as t, parseFilePreview as u };
|