@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.
Files changed (93) hide show
  1. package/CHANGELOG.en.md +44 -1
  2. package/CHANGELOG.md +44 -1
  3. package/README.en.md +155 -92
  4. package/README.md +156 -91
  5. package/cordis.patch.yml +57 -0
  6. package/lib/client.js +2678 -276
  7. package/lib/client.js.map +1 -1
  8. package/lib/command-CESuwlVt.js +1306 -0
  9. package/lib/command.js +2 -2
  10. package/lib/{compat-B6ZqbQBy.js → compat-DF_yL1bB.js} +160 -2
  11. package/lib/compat.js +1 -1
  12. package/lib/contract-DZ4zczDT.js +238 -0
  13. package/lib/{fs-D_Sw3o5z.js → fs-9JIzGaf3.js} +55 -19
  14. package/lib/fs.js +2 -1
  15. package/lib/index.js +10 -8
  16. package/lib/{instructions-DfzN-wVf.js → instructions-D19jRxN0.js} +3 -2
  17. package/lib/instructions.js +1 -1
  18. package/lib/lsp.js +7 -2
  19. package/lib/method-wrapper-CFY3p3j2.js +26 -0
  20. package/lib/process-fence.js +280 -0
  21. package/lib/providers.js +87 -0
  22. package/lib/{registry-DySMbjgW.js → registry-DCxC5Lay.js} +256 -35
  23. package/lib/registry.js +2 -2
  24. package/lib/{roots-DasWEBJk.js → roots-B-0l8mNG.js} +117 -20
  25. package/lib/sandbox-Kup4_ZIb.js +747 -0
  26. package/lib/sandbox.js +2 -1
  27. package/lib/scope.js +229 -20
  28. package/lib/types/client/RootSwitcher.d.ts +63 -0
  29. package/lib/types/client/WorkspaceFoldersAction.d.ts +39 -1
  30. package/lib/types/client/files-tree/FilesBody.module.css.d.ts +24 -0
  31. package/lib/types/client/files-tree/FilesTree.d.ts +47 -0
  32. package/lib/types/client/files-tree/adapter.d.ts +94 -0
  33. package/lib/types/client/files-tree/directory-node.d.ts +62 -0
  34. package/lib/types/client/files-tree/face.d.ts +107 -0
  35. package/lib/types/client/files-tree/locales.d.ts +10 -0
  36. package/lib/types/client/files-tree/primitives.d.ts +33 -0
  37. package/lib/types/client/files-tree/store.d.ts +81 -0
  38. package/lib/types/client/locales.d.ts +56 -10
  39. package/lib/types/client/open-in-app/OpenInAppAction.d.ts +27 -0
  40. package/lib/types/client/open-in-app/OpenTargetButton.d.ts +52 -0
  41. package/lib/types/client/open-in-app/OpenTargetButton.module.css.d.ts +29 -0
  42. package/lib/types/client/open-in-app/adapter.d.ts +46 -0
  43. package/lib/types/client/open-in-app/applications.d.ts +4 -0
  44. package/lib/types/client/open-in-app/controller.d.ts +53 -0
  45. package/lib/types/client/open-in-app/locales.d.ts +10 -0
  46. package/lib/types/client/open-in-app/open-failure-toast.d.ts +11 -0
  47. package/lib/types/client/open-in-app/open-path.d.ts +13 -0
  48. package/lib/types/client/panel-client.d.ts +2 -1
  49. package/lib/types/client/styles.d.ts +1 -1
  50. package/lib/types/command.d.ts +16 -3
  51. package/lib/types/compat/client-icons.d.ts +18 -0
  52. package/lib/types/compat/dsh-version.d.ts +1 -1
  53. package/lib/types/compat/open-in-app-routes.d.ts +38 -0
  54. package/lib/types/compat/process-entrypoints.d.ts +31 -0
  55. package/lib/types/compat/process-result.d.ts +14 -0
  56. package/lib/types/compat/pty-marker.d.ts +83 -0
  57. package/lib/types/compat/sandbox-confine.d.ts +2 -0
  58. package/lib/types/compat/storage-zod.d.ts +65 -0
  59. package/lib/types/compat/windows-deny-preflight.d.ts +21 -0
  60. package/lib/types/compat/windows-native.d.ts +68 -0
  61. package/lib/types/compat/windows-path-guard.d.ts +56 -0
  62. package/lib/types/compat/workspace-files.d.ts +2 -0
  63. package/lib/types/contract.d.ts +31 -19
  64. package/lib/types/dialects.d.ts +99 -3
  65. package/lib/types/fs.d.ts +32 -10
  66. package/lib/types/index.d.ts +7 -2
  67. package/lib/types/process-fence.d.ts +54 -0
  68. package/lib/types/profile-isolation.d.ts +178 -0
  69. package/lib/types/providers.d.ts +70 -0
  70. package/lib/types/registry.d.ts +132 -31
  71. package/lib/types/roots.d.ts +100 -13
  72. package/lib/types/sandbox.d.ts +59 -3
  73. package/lib/types/scope.d.ts +200 -14
  74. package/lib/types/windows-broker.d.ts +17 -0
  75. package/lib/types/windows-deny.d.ts +1 -0
  76. package/lib/types/windows-profile.d.ts +10 -13
  77. package/lib/types/windows-runner-contract.d.ts +97 -0
  78. package/lib/windows-runner-contract-CEfmZzu0.js +141 -0
  79. package/lib/windows-runner.js +1256 -13
  80. package/lib/workspace-files.js +138 -3
  81. package/lib/workspace-routing-YPxaQ4V3.js +26 -0
  82. package/package.json +84 -62
  83. package/scripts/windows-broker/install.ps1 +89 -0
  84. package/scripts/windows-broker/rights.cs +58 -0
  85. package/scripts/windows-broker/service.cs +323 -0
  86. package/scripts/windows-broker/verify-install.ps1 +80 -0
  87. package/lib/command-CTdNvwt1.js +0 -791
  88. package/lib/method-wrapper-Zc-JlDP3.js +0 -51
  89. package/lib/sandbox-BRXl_FpF.js +0 -132
  90. package/lib/types/client/RootFileTree.d.ts +0 -12
  91. package/lib/types/panel-files.d.ts +0 -3
  92. package/lib/windows-profile-CiFsFYIV.js +0 -216
  93. package/lib/{registry-lease-win32-dy8TQMD9.js → registry-lease-win32-DTO6QpT9.js} +2 -2
package/lib/sandbox.js CHANGED
@@ -1,2 +1,3 @@
1
- import { t as MultiRootSandboxProvider } from "./sandbox-BRXl_FpF.js";
1
+ import { t as MultiRootSandboxProvider } from "./sandbox-Kup4_ZIb.js";
2
+ import "./providers.js";
2
3
  export { MultiRootSandboxProvider, MultiRootSandboxProvider as default };
package/lib/scope.js CHANGED
@@ -1,6 +1,10 @@
1
+ import { t as aroundTool } from "./tool-context-CpgIFcCB.js";
2
+ import { r as effectiveRootAccess } from "./contract-DZ4zczDT.js";
1
3
  import { Service } from "@deepseek-ai/cordis";
2
- import { canonicalPath } from "@deepseek-ai/dsh-sandbox";
3
4
  import { statSync } from "node:fs";
5
+ import { isAbsolute } from "node:path";
6
+ import { AsyncLocalStorage } from "node:async_hooks";
7
+ import { canonicalPath } from "@deepseek-ai/dsh-sandbox";
4
8
  //#region src/scope.ts
5
9
  /**
6
10
  * The multi-root workspace scope: the plugin's single home for the question
@@ -38,9 +42,14 @@ import { statSync } from "node:fs";
38
42
  * @returns the sanitized additional roots.
39
43
  */
40
44
  function sanitizeAdditionalRoots(primaryRoot, roots) {
45
+ return sanitizeMembers(primaryRoot, roots).map((root) => root.path);
46
+ }
47
+ /** The member-level form of {@link sanitizeAdditionalRoots}: same rules, with access kept. */
48
+ function sanitizeMembers(primaryRoot, roots) {
41
49
  const seen = /* @__PURE__ */ new Set([primaryRoot]);
42
50
  const result = [];
43
51
  for (const root of roots) {
52
+ if (root.withheld) continue;
44
53
  const recorded = root.recordedPath;
45
54
  if (typeof recorded !== "string" || recorded === "") continue;
46
55
  const canonical = canonicalPath(root.path);
@@ -52,7 +61,12 @@ function sanitizeAdditionalRoots(primaryRoot, roots) {
52
61
  }
53
62
  if (seen.has(canonical)) continue;
54
63
  seen.add(canonical);
55
- result.push(canonical);
64
+ result.push({
65
+ path: canonical,
66
+ access: root.access,
67
+ id: root.id,
68
+ ...root.alias === void 0 ? {} : { alias: root.alias }
69
+ });
56
70
  }
57
71
  return result;
58
72
  }
@@ -62,18 +76,75 @@ function sanitizeAdditionalRoots(primaryRoot, roots) {
62
76
  */
63
77
  const MULTI_ROOT_CONTEXT_NAME = "multi-root:scope";
64
78
  /**
65
- * Render the workspace topology the model is told about. Only roots are listed
66
- * (never files), and the sentence states the two facts a denied write would
67
- * otherwise have to teach: that the additional roots are part of the SAME
68
- * workspace, and that the session cwd is unchanged (requirement §5, §8).
69
- * @param primaryRoot - the canonical session workspace root.
70
- * @param additionalRoots - canonical additional roots, in scope order.
71
- * @returns the stable topology sentence.
79
+ * The deterministic sentence the model hears while the registry authority is
80
+ * unavailable under either writable mode — enforcement is then the
81
+ * primary-root-only one, which the prompt must not leave implicit.
82
+ */
83
+ const REGISTRY_UNAVAILABLE_NOTE = "The DSH multi-root registry is not available in this process, so writes are temporarily limited to the primary root; shared platform temp directories are not writable. Explicit deny boundaries still take precedence.";
84
+ /**
85
+ * The B9 effective-access rule (ADR-0017's nine-cell matrix): whether a root
86
+ * with this registration policy is writable under this session mode.
87
+ * `read-write` is writable everywhere; `inherit` follows the mode;
88
+ * `read-only` never is.
72
89
  */
73
- function renderWorkspaceRootsContext(primaryRoot, additionalRoots) {
74
- return `Current DSH workspace roots: ${JSON.stringify(additionalRoots)} are additional roots of this session's workspace. Under workspace-write they may be modified like the session workspace; the session origin remains the primary root (${JSON.stringify(primaryRoot)}); changing the working directory does not change these grants.`;
90
+ function effectivelyWritable(mode, access) {
91
+ return effectiveRootAccess(access, mode) !== "read-only";
75
92
  }
76
93
  /**
94
+ * Render the workspace topology the model is told about. Pure: the text
95
+ * depends only on its inputs, so two consecutive assemblies of the same
96
+ * session state produce the same contribution byte for byte. Only granted
97
+ * members are ever listed — withheld, missing, or invalid roots never reach
98
+ * `roots` in the first place — and no host internals (ids, SIDs, store or
99
+ * lease paths) are ever named.
100
+ * @param options.platform - the host platform (`process.platform` of the contribution).
101
+ * @param options.mode - the session's sandbox mode.
102
+ * @param options.primaryRoot - the canonical session workspace root.
103
+ * @param options.roots - the granted additional members, in registry order.
104
+ * @param options.cappedTools - sorted tool names that can never write an additional root.
105
+ * @param options.toolAccessInvalid - invalid host config caps every additional-root write.
106
+ * @param options.authority - the registry-authority fact; `unavailable` under
107
+ * `danger-full-access` and `workspace-write` limit writes to the primary root, which the
108
+ * model must hear even when there is no topology to describe.
109
+ * @param options.protectedHome - the canonical DSH home when the scope appended
110
+ * it to the deny set (`danger-full-access` with restrictions); stated by path
111
+ * so the model can place the refusal.
112
+ * @returns the stable topology text; empty when there are no additional roots
113
+ * and no downgrade to report.
114
+ */
115
+ function renderWorkspaceRootsContext(options) {
116
+ const { platform, mode, primaryRoot, roots, cappedTools } = options;
117
+ const lead = [options.authority === "unavailable" && mode !== "read-only" ? REGISTRY_UNAVAILABLE_NOTE : "", options.protectedHome !== void 0 && mode === "danger-full-access" ? `Writes under the DSH home directory at ${JSON.stringify(options.protectedHome)} are refused.` : ""].filter((note) => note !== "").join(" ");
118
+ if (options.authority === "unavailable") return lead;
119
+ if (roots.length === 0) return lead;
120
+ const prefix = lead === "" ? "" : `${lead} `;
121
+ const members = JSON.stringify(roots.map((root) => root.path));
122
+ const readOnly = roots.filter((root) => root.access === "read-only").map((root) => root.path);
123
+ const capNote = cappedTools.length === 0 ? "" : ` The tools ${JSON.stringify(cappedTools)} cannot write additional roots even where they are writable.`;
124
+ if (mode === "read-only") {
125
+ const writable = options.toolAccessInvalid ? [] : roots.filter((root) => root.access === "read-write").map((root) => root.path);
126
+ if (writable.length === 0) return prefix + `Current DSH workspace roots: ${members} are additional roots of this session's workspace and are readable; nothing is writable in read-only mode.` + capNote;
127
+ const readable = roots.filter((root) => root.access !== "read-write").map((root) => root.path);
128
+ let text = `Current DSH workspace roots: ${JSON.stringify(writable)} are additional roots that stay writable even in read-only mode`;
129
+ if (readable.length > 0) text += `, and ${JSON.stringify(readable)} are readable additional roots`;
130
+ return prefix + text + " of this session's workspace; everything else is read-only." + capNote;
131
+ }
132
+ if (mode === "danger-full-access") {
133
+ const refused = options.toolAccessInvalid ? roots.map((root) => root.path) : readOnly;
134
+ return prefix + `Current DSH workspace roots: ${members} are additional roots of this session's workspace; danger-full-access mode has no sandbox isolation` + (refused.length > 0 ? `, except writes under the additional roots ${JSON.stringify(refused)} are still refused${options.toolAccessInvalid === true ? " because toolAccess is invalid" : ""}. Grant changes apply to later operations and newly started processes, not to processes already running.` : ".") + capNote;
135
+ }
136
+ const writable = options.toolAccessInvalid ? [] : roots.filter((root) => root.access !== "read-only").map((root) => root.path);
137
+ const groups = [];
138
+ if (options.toolAccessInvalid) groups.push(`${members} are readable additional roots (all additional-root writes are refused because toolAccess is invalid)`);
139
+ if (writable.length > 0) groups.push(`${JSON.stringify(writable)} are writable additional roots`);
140
+ if (!options.toolAccessInvalid && readOnly.length > 0) groups.push(`${JSON.stringify(readOnly)} are read-only additional roots (writes are refused)`);
141
+ let text = `Current DSH workspace roots: ${groups.join(" and ")} of this session's workspace. The session origin remains the primary root (${JSON.stringify(primaryRoot)}); changing the working directory does not change these grants. Grant changes apply to later operations and newly started processes, not to processes already running.`;
142
+ if (platform === "win32" && !options.toolAccessInvalid) text += " Writable additional roots are enforced for shell processes by the upstream Windows ACL restricted-token sandbox, whose enforcement is reported as partial; reads are not confined by it.";
143
+ return prefix + text + capNote;
144
+ }
145
+ /** The tool-dispatch cap carried across async boundaries by the waterfall. */
146
+ const toolCapStore = new AsyncLocalStorage();
147
+ /**
77
148
  * The `ctx.multiRootScope` service. Providers ask it — never the filesystem —
78
149
  * what the current scope is, which is what keeps the fs fence and every kernel
79
150
  * dialect on one permission world (requirement §13).
@@ -82,6 +153,26 @@ var MultiRootScopeService = class extends Service {
82
153
  /** Additional roots keyed by canonical primary root; empty until roots are registered. */
83
154
  rootsByPrimary = /* @__PURE__ */ new Map();
84
155
  listeners = /* @__PURE__ */ new Set();
156
+ /**
157
+ * The registry-authority fact the registry row publishes. Defaults to
158
+ * `available`: a composition without a registry row can never have
159
+ * registered roots, so nothing is unknown there. The registry sets it false
160
+ * the moment it cannot prove the store is its own — before the lease is
161
+ * acquired, while contended or storage-failed, and at teardown — and true
162
+ * only after the accepted records have been published.
163
+ */
164
+ registryAuthority = "available";
165
+ /** Validated `toolAccess` row config; empty when no tool is capped. */
166
+ caps;
167
+ /** A malformed `toolAccess` refuses every additional-root write (fail closed). */
168
+ capsInvalid;
169
+ /**
170
+ * The configured DSH home boundary; `undefined` when the row carries no
171
+ * `dshHome` (bare test compositions) or one that is not an absolute string —
172
+ * an invalid value is logged at construction and ignored rather than
173
+ * treated as evidence.
174
+ */
175
+ dshHome;
85
176
  /** Observe registry publications; consumers still resolve authority from scopeOf(). */
86
177
  subscribe(listener) {
87
178
  this.listeners.add(listener);
@@ -89,8 +180,39 @@ var MultiRootScopeService = class extends Service {
89
180
  this.listeners.delete(listener);
90
181
  };
91
182
  }
92
- constructor(ctx) {
183
+ /**
184
+ * The sorted names of the tools `toolAccess` caps — read-only for the
185
+ * prompt contribution. An invalid config is not enumerated (it caps every
186
+ * call but is not a listable tool set).
187
+ */
188
+ toolCaps() {
189
+ return Object.keys(this.caps).sort();
190
+ }
191
+ /** Whether malformed toolAccess caps every call, including unknown tools. */
192
+ get toolAccessInvalid() {
193
+ return this.capsInvalid;
194
+ }
195
+ constructor(ctx, config = {}) {
93
196
  super(ctx, "multiRootScope");
197
+ const toolAccess = config.toolAccess;
198
+ if (toolAccess === void 0) {
199
+ this.caps = {};
200
+ this.capsInvalid = false;
201
+ } else if (typeof toolAccess !== "object" || toolAccess === null || Array.isArray(toolAccess) || Object.values(toolAccess).some((value) => value !== "read-only")) {
202
+ this.caps = {};
203
+ this.capsInvalid = true;
204
+ ctx.logger.error(`multi-root-workspace: invalid toolAccess config (expected an object mapping tool names to "read-only", got ${JSON.stringify(toolAccess)}); no additional root is writable`);
205
+ } else {
206
+ this.caps = { ...toolAccess };
207
+ this.capsInvalid = false;
208
+ }
209
+ const dshHome = config.dshHome;
210
+ if (dshHome !== void 0 && (typeof dshHome !== "string" || dshHome.trim() === "" || !isAbsolute(dshHome))) ctx.logger.error(`multi-root-workspace: invalid dshHome config (expected an absolute path, got ${JSON.stringify(dshHome)}); the DSH-home deny boundary is disabled`);
211
+ else this.dshHome = dshHome;
212
+ aroundTool(ctx, (exec, next) => {
213
+ const parent = toolCapStore.getStore();
214
+ return toolCapStore.run({ capped: parent?.capped === true || this.caps[exec.name] === "read-only" }, next);
215
+ });
94
216
  ctx.inject(["systemPrompt", "sandboxPolicy"], (scope) => {
95
217
  scope.systemPrompt.context({
96
218
  name: MULTI_ROOT_CONTEXT_NAME,
@@ -99,10 +221,18 @@ var MultiRootScopeService = class extends Service {
99
221
  const session = assembly.agent?.session;
100
222
  if (session === void 0) return "";
101
223
  const policy = scope.sandboxPolicy.resolve({ session });
102
- if (policy.mode !== "workspace-write") return "";
103
224
  const resolved = this.resolve(policy);
104
- if (resolved.additionalRoots.length === 0) return "";
105
- return renderWorkspaceRootsContext(resolved.primaryRoot, resolved.additionalRoots);
225
+ if (resolved.roots.length === 0 && resolved.authority !== "unavailable" && resolved.protectedHome === void 0) return "";
226
+ return renderWorkspaceRootsContext({
227
+ platform: process.platform,
228
+ mode: policy.mode,
229
+ primaryRoot: resolved.primaryRoot,
230
+ roots: resolved.roots,
231
+ cappedTools: this.toolCaps(),
232
+ toolAccessInvalid: this.toolAccessInvalid,
233
+ authority: resolved.authority,
234
+ ...resolved.protectedHome === void 0 ? {} : { protectedHome: resolved.protectedHome }
235
+ });
106
236
  }
107
237
  });
108
238
  });
@@ -114,20 +244,86 @@ var MultiRootScopeService = class extends Service {
114
244
  */
115
245
  resolve(policy) {
116
246
  const primaryRoot = canonicalPath(policy.workspaceRoot);
247
+ const roots = this.membersOf(primaryRoot);
248
+ const capped = this.capped();
249
+ const writable = capped || this.registryAuthority === "unavailable" ? [] : roots.filter((root) => effectivelyWritable(policy.mode, root.access)).map((root) => root.path);
250
+ const denied = (this.rootsByPrimary.get(primaryRoot) ?? []).filter((root) => typeof root.recordedPath === "string" && root.recordedPath !== "" && root.recordedPath !== primaryRoot && (capped || !effectivelyWritable(policy.mode, root.access))).map((root) => root.recordedPath);
251
+ const deniedRoots = [...new Set(denied)];
252
+ let protectedHome;
253
+ if (this.dshHome !== void 0 && policy.mode === "danger-full-access" && (deniedRoots.length > 0 || this.registryAuthority === "unavailable")) {
254
+ const home = canonicalPath(this.dshHome);
255
+ if (!deniedRoots.includes(home)) {
256
+ deniedRoots.push(home);
257
+ protectedHome = home;
258
+ }
259
+ }
117
260
  return {
118
261
  primaryRoot,
119
- additionalRoots: this.scopeOf(primaryRoot)
262
+ additionalRoots: roots.map((root) => root.path),
263
+ roots,
264
+ writableRoots: writable,
265
+ deniedRoots,
266
+ authority: this.registryAuthority,
267
+ ...protectedHome === void 0 ? {} : { protectedHome }
120
268
  };
121
269
  }
122
270
  /**
123
- * The additional roots registered for one canonical primary root.
271
+ * Whether THIS call's additional-root writes are capped. Inside a tool
272
+ * dispatch the AsyncLocalStorage store answers; outside any dispatch (user
273
+ * PTY, panel, an unknown async boundary) the answer is fail-closed: capped
274
+ * whenever `toolAccess` names any tool, because an operation whose tool
275
+ * identity is unknown gets the most restrictive configured cap. Membership
276
+ * is never affected — only writability.
277
+ */
278
+ capped() {
279
+ if (this.capsInvalid) return true;
280
+ const store = toolCapStore.getStore();
281
+ if (store !== void 0) return store.capped;
282
+ return Object.keys(this.caps).length > 0;
283
+ }
284
+ /**
285
+ * The additional roots registered for one canonical primary root — every
286
+ * granted member, ANY access level (a read-only root is still a member of
287
+ * the workspace for instructions, LSP, routing, and reads).
124
288
  * @param primaryRoot - canonical primary root (or any spelling thereof).
125
289
  * @returns the canonical additional roots; empty when none are registered.
126
290
  */
127
291
  scopeOf(primaryRoot) {
128
- const registered = this.rootsByPrimary.get(canonicalPath(primaryRoot));
292
+ return this.membersOf(primaryRoot).map((root) => root.path);
293
+ }
294
+ /**
295
+ * The additional roots a WRITE may reach for one primary root under
296
+ * `workspace-write` — the members whose effective access is writable there
297
+ * (`inherit` and fixed `read-write`). Tool caps are per-call and do not
298
+ * apply to this diagnostic projection.
299
+ * @param primaryRoot - canonical primary root (or any spelling thereof).
300
+ * @returns the canonical writable roots; empty when none qualify.
301
+ */
302
+ writableOf(primaryRoot) {
303
+ return this.membersOf(primaryRoot).filter((root) => root.access !== "read-only").map((root) => root.path);
304
+ }
305
+ /**
306
+ * Whether ANY registered primary currently publishes an explicit `read-only`
307
+ * member. The PTY seam resolves its caller through the agent-PTY
308
+ * environment markers; when that lookup fails this flag is the difference
309
+ * between pass-through (nothing to deny anywhere on the store) and fail
310
+ * closed. An unavailable registry authority answers `true` by itself: a
311
+ * terminal whose session cannot be resolved must not inherit a deny set
312
+ * this process cannot see.
313
+ * @returns whether at least one primary holds a restricted root, or the
314
+ * registry cannot prove none does.
315
+ */
316
+ hasActiveDenyRoots() {
317
+ if (this.registryAuthority === "unavailable") return true;
318
+ for (const members of this.rootsByPrimary.values()) if (members.some((root) => root.access === "read-only") || members.length > 0 && this.capped()) return true;
319
+ return false;
320
+ }
321
+ /** The sanitized members of one primary root, in registry order. */
322
+ membersOf(primaryRoot) {
323
+ const key = canonicalPath(primaryRoot);
324
+ const registered = this.rootsByPrimary.get(key);
129
325
  if (registered === void 0) return [];
130
- return sanitizeAdditionalRoots(canonicalPath(primaryRoot), registered);
326
+ return sanitizeMembers(key, registered);
131
327
  }
132
328
  /**
133
329
  * Replace the roots registered for one primary root.
@@ -144,6 +340,19 @@ var MultiRootScopeService = class extends Service {
144
340
  else this.rootsByPrimary.set(key, [...roots]);
145
341
  for (const listener of this.listeners) listener();
146
342
  }
343
+ /**
344
+ * Publish the registry-authority fact. This is the REGISTRY's write port —
345
+ * the counterpart of {@link setAdditionalRoots}: the registry marks the
346
+ * scope `unavailable` the moment it stops being the store's authority
347
+ * (before acquisition, on contention or storage failure, at teardown) and
348
+ * `available` only once its stored records have been published. Nothing
349
+ * else may call it, and it never touches the root table.
350
+ * @param available - whether this process holds the registry authority.
351
+ */
352
+ setRegistryAuthority(available) {
353
+ this.registryAuthority = available ? "available" : "unavailable";
354
+ for (const listener of this.listeners) listener();
355
+ }
147
356
  };
148
357
  //#endregion
149
- export { MULTI_ROOT_CONTEXT_NAME, MultiRootScopeService, MultiRootScopeService as default, renderWorkspaceRootsContext, sanitizeAdditionalRoots };
358
+ export { MULTI_ROOT_CONTEXT_NAME, MultiRootScopeService, MultiRootScopeService as default, REGISTRY_UNAVAILABLE_NOTE, renderWorkspaceRootsContext, sanitizeAdditionalRoots };
@@ -0,0 +1,63 @@
1
+ /**
2
+ * The Files-tab root switcher — the entry that took over the header position
3
+ * the P1 gear launcher first claimed. It poses as the pane's path label: a
4
+ * trigger that opens a menu of this session's roots (primary first, the
5
+ * registered additional roots next, and the Workspace Folders manager last),
6
+ * followed by a `PathLabel` of the selected root's full path — the same
7
+ * component upstream draws in that spot, so both primary and additional
8
+ * selections read exactly like the native header. While an additional root
9
+ * is selected it also lends the pane's body area to this plugin's verbatim
10
+ * fork of the native Files tree (`files-tree/`), so an additional root reads
11
+ * and behaves exactly like the primary one.
12
+ *
13
+ * Two upstream DOM hooks make the takeover work, and both degrade silently:
14
+ *
15
+ * - The stylesheet rules: while `.mrfw-rootSwitcher` exists inside a
16
+ * `[data-files-root]` pane the upstream `data-files-path` is hidden and the
17
+ * switcher takes its flex position; while the switcher carries
18
+ * `data-root-kind="additional"` the native `data-files-body`,
19
+ * `data-files-reload`, and `data-open-target="directory"` split button
20
+ * hide too (the reload refreshes the very tree that is no longer
21
+ * visible, and the native open-in-app button only ever receives the
22
+ * primary root through its slot props — the header gets a
23
+ * `.mrfw-switcherReload` replacement and the plugin's verbatim fork of
24
+ * the open-in-app control, pointed at the selected root). Upstream
25
+ * changing that DOM shape simply stops the selectors from matching — the
26
+ * native header keeps working and the switcher is just another actions
27
+ * occupant.
28
+ * - `closest('[data-files-root]')` from the switcher's own element locates
29
+ * the pane the forked Files tree is portaled into, as a sibling of
30
+ * `data-files-body`. No match → no portal: the component keeps working as
31
+ * a menu and the native tree keeps its area. The tree's tab id comes from
32
+ * the same element's `closest('[data-dockkit-content]')` — the dockkit
33
+ * tabHost section — with a generated fallback where the hook is absent.
34
+ *
35
+ * The selected root is component state on purpose: every tab render
36
+ * occurrence gets its own, and reopening the pane resets to the primary
37
+ * root. There is no cross-tab or persisted selection.
38
+ *
39
+ * @module @dsh-electron/dsh-plugin-multi-root-workspace/client/RootSwitcher
40
+ */
41
+ import { type ReactNode } from 'react';
42
+ import { type FilesTreeSurface } from './files-tree/adapter.tsx';
43
+ import { type OpenInAppAdapter } from './open-in-app/adapter.tsx';
44
+ import { type WorkspaceFoldersActionProps } from './WorkspaceFoldersAction.tsx';
45
+ /** What the registered Files-actions occupant needs beyond the slot owner. */
46
+ export interface RootSwitcherProps extends Omit<WorkspaceFoldersActionProps, 'compact' | 'wide'> {
47
+ /** This pane's workspace root, carried by the slot owner's `absolutePath`. */
48
+ readonly primaryPath: string;
49
+ /**
50
+ * The forked open-in-app control's shared props, or `undefined` where the
51
+ * fork cannot render (an undefined adapter is upstream's own "no button"
52
+ * state, so the switcher just draws nothing there too).
53
+ */
54
+ readonly openInApp?: OpenInAppAdapter | undefined;
55
+ /**
56
+ * The forked Files tree's shared props, or `undefined` where the fork
57
+ * cannot render — the additional-root rows then stay out of the menu at
58
+ * all, which is the same absence upstream's own tree produces.
59
+ */
60
+ readonly filesTree?: FilesTreeSurface | undefined;
61
+ }
62
+ /** The Files-header switcher: root menu, dialog entry, and the portal tree. */
63
+ export declare function RootSwitcher(props: RootSwitcherProps): ReactNode;
@@ -16,11 +16,16 @@
16
16
  * native look — sidebar trigger, modal card, capsule buttons — in both themes
17
17
  * without naming a single color here (see ADR-0006).
18
18
  *
19
+ * The dialog itself is also exported: the Files-tab root switcher opens the
20
+ * same manager through its trailing menu item instead of carrying a second
21
+ * dialog.
22
+ *
19
23
  * @module @dsh-electron/dsh-plugin-multi-root-workspace/client/WorkspaceFoldersAction
20
24
  */
21
25
  import { type ReactNode } from 'react';
22
26
  import { type PanelClient } from './panel-client.ts';
23
27
  import type { Key } from './locales.ts';
28
+ import type { RootAccess, RootEntryView, RootState, RootView } from '../contract.ts';
24
29
  /** Translate one key of this plugin's namespace. */
25
30
  export type Translate = (key: Key, params?: Record<string, unknown>) => string;
26
31
  /** What the registered action receives from the plugin body. */
@@ -39,8 +44,41 @@ export interface WorkspaceFoldersActionProps {
39
44
  readonly subscribeSession?: (listener: () => void) => () => void;
40
45
  /** Whether the sidebar renders wide content (`false` in the icon rail). */
41
46
  readonly wide?: boolean;
42
- /** Compact launcher in the upstream Files tab's public actions slot. */
47
+ /** Icon-only launcher in the upstream Files tab's public actions slot. */
43
48
  readonly compact?: boolean;
44
49
  }
50
+ /** The locale key for one reported root state. */
51
+ export declare function stateKey(state: RootState): Key;
52
+ /** The locale key for one registration's access level. */
53
+ export declare function accessKey(access: RootAccess): Key;
54
+ /**
55
+ * Whether moving FROM `from` TO `to` could widen the agent's write authority
56
+ * under some session mode. The three levels are not a total order — `inherit`
57
+ * is weaker than `read-write` under a read-only session but never weaker
58
+ * under the others — so the check is spelled out per pair rather than
59
+ * computed from a rank. Only a widening change asks for confirmation.
60
+ */
61
+ export declare function widensAccess(from: RootAccess, to: RootAccess): boolean;
62
+ /** The final segment of an absolute path, on either platform separator. */
63
+ export declare function basename(path: string): string;
64
+ /**
65
+ * The primary text of one row: the stored alias when set, otherwise the
66
+ * directory's own name — the display the registration was added with.
67
+ */
68
+ export declare function displayNameOf(root: RootView): string;
45
69
  /** The sidebar footer action plus the dialog it opens. */
46
70
  export declare function WorkspaceFoldersAction(props: WorkspaceFoldersActionProps): ReactNode;
71
+ /** The exact row reference the browsing endpoints and mutations carry. */
72
+ export declare function entryOf(root: RootView): RootEntryView;
73
+ /**
74
+ * The dialog: reads the view once per open, then after every mutation.
75
+ *
76
+ * Two shapes travel back over the channel, and they are handled separately on
77
+ * purpose: every mutating endpoint answers with the whole refreshed view, while
78
+ * `reveal` answers with the path it revealed and leaves the list untouched
79
+ * (see `contract.ts`). Treating a reveal answer as a roots view is exactly the
80
+ * contract mismatch this split removes.
81
+ */
82
+ export declare function WorkspaceFoldersDialog(props: WorkspaceFoldersActionProps & {
83
+ onClose: () => void;
84
+ }): ReactNode;
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Shim for the CSS-module virtual module `./FilesBody.module.css`.
3
+ *
4
+ * This toolchain resolves real files, not CSS-module virtual modules — so
5
+ * the forked body imports this file instead (the one-line specifier delta
6
+ * recorded in FORK.md). It exports the same `css.<name>` map, pointed at the
7
+ * plugin-prefixed `mrfw-ft*` classes whose rules live in `styles.ts`; the
8
+ * upstream `FilesBody.module.css` sits beside it verbatim as the diffable
9
+ * source. Only the names the fork's JSX actually reads are mapped — the
10
+ * header-only entries (`root`, `header`, `path`, `tool`, `status`,
11
+ * `statusLine`, `titleIcon`) belong to the shell this fork drops.
12
+ */
13
+ declare const css: {
14
+ readonly body: "mrfw-ftBody";
15
+ readonly level: "mrfw-ftLevel";
16
+ readonly item: "mrfw-ftItem";
17
+ readonly row: "mrfw-ftRow";
18
+ readonly icon: "mrfw-ftIcon";
19
+ readonly fileIcon: "mrfw-ftFileIcon";
20
+ readonly name: "mrfw-ftName";
21
+ readonly other: "mrfw-ftOther";
22
+ readonly note: "mrfw-ftNote";
23
+ };
24
+ export default css;
@@ -0,0 +1,47 @@
1
+ import type { ReactNode } from 'react';
2
+ import type { RemoteFailure } from '@deepseek-ai/dsh-api-remotes/client';
3
+ import type { PropsLocale, PropsStore, TranslateNS } from '@deepseek-ai/dsh-client-ui-slots';
4
+ import type { TabId } from '@deepseek-ai/dsh-client-ui-dockkit';
5
+ import type { SessionId } from '@deepseek-ai/dsh-session/types';
6
+ import type { WorkspaceDirectoryEntry } from '@deepseek-ai/dsh-api-workspace-files/types';
7
+ import type { FilesInjected } from './face.ts';
8
+ import type { createFilesStore } from './store.ts';
9
+ /**
10
+ * The fork's props: the store/locale shares verbatim from upstream's
11
+ * composed props, `start`/`toggle` from the injected face, and the four
12
+ * seams upstream binds inside `FilesBody` — tab id, lifetime, root, and the
13
+ * resource-opening callback — supplied by the adapter instead.
14
+ */
15
+ export type FilesTreeProps = PropsStore<ReturnType<typeof createFilesStore>> & Pick<FilesInjected, 'start' | 'toggle'> & PropsLocale<'sidebarFiles'> & {
16
+ /** The store bucket this tree reads and writes — the pane's own dockkit tab id. */
17
+ readonly tabId: TabId;
18
+ /** Session the listing and the resource addresses belong to. */
19
+ readonly sessionId: SessionId;
20
+ /** Absolute path of the root this tree lists (the selected additional root). */
21
+ readonly root: string;
22
+ /** Takeover lifetime: aborting forgets the bucket and closes the watch tree. */
23
+ readonly signal: AbortSignal;
24
+ /** Open one `dsh-resource://file` address (the adapter's sidebarRight route). */
25
+ readonly onOpen: (address: string) => void;
26
+ };
27
+ /**
28
+ * Order one level's entries for display: directories first, then everything
29
+ * else, each group by name. The endpoint's order is a listing fact; this is the
30
+ * reader's.
31
+ * @param entries - the listing as the endpoint returned it.
32
+ * @returns a new array, directories first, then by name within each group.
33
+ */
34
+ export declare function orderEntries(entries: readonly WorkspaceDirectoryEntry[]): WorkspaceDirectoryEntry[];
35
+ /**
36
+ * Say why a directory could not be listed, in terms of the directory.
37
+ * @param t - namespace-bound translate.
38
+ * @param failure - the settled Remote failure.
39
+ * @returns the line to show under the directory.
40
+ */
41
+ export declare function failureLine(t: TranslateNS<'sidebarFiles'>, failure: RemoteFailure): string;
42
+ /**
43
+ * The additional-root tree's body: the selected root and whatever the reader
44
+ * has opened under it — the upstream body's scroll, seed, and render logic
45
+ * verbatim, with the root arriving as a prop instead of `useSessions(cwd)`.
46
+ */
47
+ export declare function FilesTree({ tabId, sessionId, root, signal, useStore, actions, start, toggle, onOpen, t, }: FilesTreeProps): ReactNode;
@@ -0,0 +1,94 @@
1
+ /**
2
+ * The plugin-owned wiring around the verbatim files-tree fork.
3
+ *
4
+ * Upstream wires its store and face inside `apply` over `ctx.remote`, and the
5
+ * slot framework resolves the session and hands the face its bound actions.
6
+ * This plugin registers onto `sidebar.right.tab.files.actions` — a
7
+ * session-scoped list slot — so the same `store`/`inject` declaration gives
8
+ * the fork the same props; what differs is only *which* remote and *which*
9
+ * open-route the fork sees:
10
+ *
11
+ * - `remote` is an optional service here (upstream injects it unconditionally),
12
+ * so {@link lazyFilesRemote} resolves `remote` / `remote.workspaceFiles` per
13
+ * call through `ctx.get` — the inject-free lookup, since a declaration would
14
+ * gate this whole row on an optional service — and folds absence into the
15
+ * same `gateway/internal` failure a carrier error would produce;
16
+ * - opening a file goes through `sidebarRight.openResourceIn`, bound with the
17
+ * session inside the `inject` factory so the component never resolves one;
18
+ * - the primitives the forked rows spell are runtime-probed (pixel-era
19
+ * manifests lack the `*Regular` names), and the gate's `false` means the
20
+ * caller simply never mounts the tree — upstream's own absence shape.
21
+ *
22
+ * Everything here is ours; nothing in this file needs upstream sync.
23
+ *
24
+ * @module @dsh-electron/dsh-plugin-multi-root-workspace/client/files-tree/adapter
25
+ */
26
+ import type { Context as ClientContext } from '@deepseek-ai/cordis';
27
+ import type { ClientRemote } from '@deepseek-ai/dsh-api-remotes/client';
28
+ import type { BoundActions } from '@deepseek-ai/dsh-client-store';
29
+ import type { SessionId } from '@deepseek-ai/dsh-session/types';
30
+ import { type FilesInjected } from './face.ts';
31
+ import { createFilesStore } from './store.ts';
32
+ import { filesTreeSurfaceSupported } from './primitives.ts';
33
+ import type { FilesTreeProps } from './FilesTree.tsx';
34
+ import { NS } from './locales.ts';
35
+ /** The inject face the registration composes onto the occupant's props. */
36
+ export interface FilesTreeInjected {
37
+ /** The verbatim files face (list/watch bookkeeping included). */
38
+ readonly face: FilesInjected;
39
+ /** Open one `dsh-resource://file` address in the host viewer. */
40
+ readonly openFile: (address: string) => void;
41
+ }
42
+ /** Everything `FilesTree` needs that the slot framework does not synthesize. */
43
+ export interface FilesTreeSurface {
44
+ readonly sessionId: SessionId;
45
+ readonly useStore: FilesTreeProps['useStore'];
46
+ readonly actions: FilesTreeProps['actions'];
47
+ readonly face: FilesInjected;
48
+ readonly openFile: (address: string) => void;
49
+ readonly t: FilesTreeProps['t'];
50
+ }
51
+ /**
52
+ * A `ClientRemote` stand-in that resolves the real service per call.
53
+ *
54
+ * The verbatim `createList`/`createWatch` hold one remote face for the
55
+ * session's life; resolving lazily keeps the fork correct on compositions
56
+ * that acquire `remote` after this plugin activates, and a composition that
57
+ * never acquires it still answers every call — with the same failure kind a
58
+ * dead carrier returns — instead of crashing the slot. Every method upholds
59
+ * the Remote contract the face relies on: `list` resolves a `RemoteResult`
60
+ * and never rejects; `changes`/`$stream` surface synchronous lookup failures
61
+ * through the stream's own failure path.
62
+ */
63
+ export declare function lazyFilesRemote(ctx: ClientContext): ClientRemote;
64
+ /**
65
+ * The registration's `store`/`inject` declarations, built once in `apply`.
66
+ *
67
+ * The inject face nests the verbatim files face under `face` and adds the
68
+ * session-bound `openFile` — keeping `FilesInjected` untouched means the
69
+ * fork's call sites and the sync diff stay upstream-shaped.
70
+ * @param ctx - the client root context (the remote/sidebarRight lookups).
71
+ * @returns the store handle and the inject factory for `register`.
72
+ */
73
+ export declare function createFilesTreeSeats(ctx: ClientContext): {
74
+ readonly store: ReturnType<typeof createFilesStore>;
75
+ readonly inject: (sessionId: SessionId, actions: BoundActions<ReturnType<typeof createFilesStore>>) => FilesTreeInjected;
76
+ };
77
+ /**
78
+ * Assemble the props `RootSwitcher` portals into `FilesTree`, or `undefined`
79
+ * when this composition cannot render the fork — the caller then keeps the
80
+ * additional-root rows out of the menu entirely, which is the same absence
81
+ * upstream produces when its own tree cannot draw.
82
+ * @param props - the occupant's composed props (store, inject, session shares).
83
+ * @param t - the `sidebarFiles`-bound translate.
84
+ * @returns the stable surface, or `undefined` when the fork cannot mount.
85
+ */
86
+ export declare function createFilesTreeSurface(props: {
87
+ readonly sessionId: SessionId;
88
+ readonly useStore: FilesTreeSurface['useStore'];
89
+ readonly actions: FilesTreeSurface['actions'];
90
+ readonly face: FilesInjected | undefined;
91
+ readonly openFile: ((address: string) => void) | undefined;
92
+ }, t: FilesTreeProps['t']): FilesTreeSurface | undefined;
93
+ export { filesTreeSurfaceSupported, NS as FILES_TREE_NS };
94
+ export { FilesTree } from './FilesTree.tsx';