@dsh-electron/dsh-plugin-multi-root-workspace 0.1.5 → 0.1.6

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 (47) hide show
  1. package/CHANGELOG.en.md +13 -1
  2. package/CHANGELOG.md +13 -1
  3. package/README.en.md +12 -10
  4. package/README.md +11 -11
  5. package/cordis.patch.yml +4 -0
  6. package/lib/client.js +320 -18
  7. package/lib/client.js.map +1 -1
  8. package/lib/{command-DyOWujPo.js → command-CTdNvwt1.js} +107 -6
  9. package/lib/command.js +1 -1
  10. package/lib/{compat-82Y27Iom.js → compat-B6ZqbQBy.js} +21 -3
  11. package/lib/compat.js +1 -1
  12. package/lib/index.js +5 -5
  13. package/lib/{instructions-Cn1Jj5Xi.js → instructions-DfzN-wVf.js} +106 -26
  14. package/lib/instructions.js +1 -1
  15. package/lib/lsp.js +38 -0
  16. package/lib/method-wrapper-Zc-JlDP3.js +51 -0
  17. package/lib/sandbox-BRXl_FpF.js +132 -0
  18. package/lib/sandbox.js +1 -1
  19. package/lib/scope.js +12 -6
  20. package/lib/session-directory-BLOstut1.js +9 -0
  21. package/lib/tool-context-CpgIFcCB.js +6 -0
  22. package/lib/types/client/RootFileTree.d.ts +12 -0
  23. package/lib/types/client/WorkspaceFoldersAction.d.ts +3 -0
  24. package/lib/types/client/index.d.ts +13 -2
  25. package/lib/types/client/locales.d.ts +14 -0
  26. package/lib/types/client/styles.d.ts +1 -1
  27. package/lib/types/compat/client-session.d.ts +2 -0
  28. package/lib/types/compat/dsh-version.d.ts +5 -3
  29. package/lib/types/compat/instruction-overlap.d.ts +7 -0
  30. package/lib/types/compat/session-directory.d.ts +4 -0
  31. package/lib/types/compat/tool-context.d.ts +9 -0
  32. package/lib/types/compat/workspace-files.d.ts +5 -0
  33. package/lib/types/contract.d.ts +24 -5
  34. package/lib/types/index.d.ts +2 -2
  35. package/lib/types/lsp.d.ts +4 -0
  36. package/lib/types/method-wrapper.d.ts +2 -0
  37. package/lib/types/panel-files.d.ts +3 -0
  38. package/lib/types/sandbox.d.ts +2 -11
  39. package/lib/types/scope.d.ts +3 -0
  40. package/lib/types/windows-profile.d.ts +13 -0
  41. package/lib/types/windows-runner.d.ts +1 -0
  42. package/lib/types/workspace-files.d.ts +4 -0
  43. package/lib/types/workspace-routing.d.ts +11 -0
  44. package/lib/{sandbox-D8Fw95FM.js → windows-profile-CiFsFYIV.js} +64 -138
  45. package/lib/windows-runner.js +68 -0
  46. package/lib/workspace-files.js +68 -0
  47. package/package.json +90 -47
@@ -1,5 +1,7 @@
1
- import { f as resolveRootRef, i as canonicalRoot, o as entryRootRef, r as availableRoots, t as RootValidationError } from "./roots-DasWEBJk.js";
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";
2
3
  import "@deepseek-ai/cordis";
4
+ import { isAbsolute, resolve } from "node:path";
3
5
  import { canonicalPath } from "@deepseek-ai/dsh-sandbox";
4
6
  import { z } from "zod";
5
7
  //#region src/contract.ts
@@ -17,8 +19,9 @@ import { z } from "zod";
17
19
  * - **One shape per endpoint.** {@link PanelResponseMap} is the single mapping
18
20
  * from endpoint to response type, so the panel cannot silently decode one
19
21
  * endpoint's answer as another's. `reveal` answers with
20
- * {@link RevealedView}; every other endpoint answers with a whole
21
- * {@link RootsView}. The host builds those answers in `command.ts` and the
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
22
25
  * panel parses them here — one definition, two consumers.
23
26
  * - **Runtime validation at the boundary.** The payload arrives from a browser
24
27
  * and the answer arrives from the host; neither is trusted to match the
@@ -41,7 +44,9 @@ const PANEL_ENDPOINTS = [
41
44
  "remove",
42
45
  "alias",
43
46
  "move",
44
- "reveal"
47
+ "reveal",
48
+ "files",
49
+ "readFile"
45
50
  ];
46
51
  /**
47
52
  * The request body on the wire. The endpoint travels as the RPC method, not as
@@ -92,10 +97,47 @@ const rootViewSchema = z.object({
92
97
  const rootsViewSchema = z.object({
93
98
  primaryRoot: z.string(),
94
99
  primaryName: z.string().optional(),
100
+ effectiveCwd: z.string().optional(),
95
101
  roots: z.array(rootViewSchema),
96
102
  unavailable: z.string().optional()
97
103
  });
98
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
+ }
99
141
  z.object({
100
142
  code: z.string(),
101
143
  message: z.string(),
@@ -139,6 +181,7 @@ function parseRootsView(value) {
139
181
  value: {
140
182
  primaryRoot: view.primaryRoot,
141
183
  ...view.primaryName === void 0 ? {} : { primaryName: view.primaryName },
184
+ ...view.effectiveCwd === void 0 ? {} : { effectiveCwd: view.effectiveCwd },
142
185
  roots: view.roots.map(narrowRootView),
143
186
  ...view.unavailable === void 0 ? {} : { unavailable: view.unavailable }
144
187
  }
@@ -196,6 +239,56 @@ function firstIssue(error) {
196
239
  return `${issue.path.length === 0 ? "" : `${issue.path.join(".")}: `}${issue.message}`;
197
240
  }
198
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
199
292
  //#region src/command.ts
200
293
  /** Services this row needs before it may activate. */
201
294
  const inject = [
@@ -523,9 +616,12 @@ async function rootsViewOf(ctx, registry, primaryRoot, request) {
523
616
  const statuses = await registry.refresh(primaryRoot);
524
617
  const unavailable = registry.unavailable;
525
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);
526
621
  return {
527
622
  primaryRoot: canonicalRoot(primaryRoot),
528
623
  ...primaryName === void 0 ? {} : { primaryName },
624
+ ...effectiveCwd === void 0 ? {} : { effectiveCwd },
529
625
  roots: statuses.map(toRootView),
530
626
  ...unavailable === void 0 ? {} : { unavailable }
531
627
  };
@@ -539,7 +635,7 @@ async function rootsViewOf(ctx, registry, primaryRoot, request) {
539
635
  * A malformed request is reported as `panel/bad-request` rather than throwing
540
636
  * through the channel.
541
637
  */
542
- async function dispatchPanelRequest(ctx, endpoint, payload, _signal) {
638
+ async function dispatchPanelRequest(ctx, endpoint, payload, signal) {
543
639
  try {
544
640
  const parsed = parsePanelCall(endpoint, payload);
545
641
  if (!parsed.ok) return {
@@ -554,6 +650,11 @@ async function dispatchPanelRequest(ctx, endpoint, payload, _signal) {
554
650
  const registry = ctx.multiRootRegistry;
555
651
  const primaryRoot = resolvePanelPrimaryRoot(ctx, request.sessionId);
556
652
  switch (request.endpoint) {
653
+ case "files":
654
+ case "readFile": return {
655
+ ok: true,
656
+ value: await browseRoot(ctx, primaryRoot, request, signal)
657
+ };
557
658
  case "list": return {
558
659
  ok: true,
559
660
  value: await rootsViewOf(ctx, registry, primaryRoot, request)
@@ -687,4 +788,4 @@ async function revealRoot(ctx, path) {
687
788
  if (outcome.exitCode !== 0) throw new RootValidationError("reveal-unavailable", `the file manager exited with ${String(outcome.exitCode)}`);
688
789
  }
689
790
  //#endregion
690
- export { renderRootsReport as a, PANEL_CHANNEL as c, parseRevealedView as d, parseRootsView as f, parseFoldersCommand as i, PANEL_ENDPOINTS as l, apply as n, resolvePanelPrimaryRoot as o, inject as r, revealArgv as s, COMMAND_NAME as t, parsePanelCall as u };
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 };
package/lib/command.js CHANGED
@@ -1,2 +1,2 @@
1
- import { a as renderRootsReport, i as parseFoldersCommand, n as apply, o as resolvePanelPrimaryRoot, r as inject, s as revealArgv, t as COMMAND_NAME } from "./command-DyOWujPo.js";
1
+ import { a as renderRootsReport, i as parseFoldersCommand, n as apply, o as resolvePanelPrimaryRoot, r as inject, s as revealArgv, t as COMMAND_NAME } from "./command-CTdNvwt1.js";
2
2
  export { COMMAND_NAME, apply, inject, parseFoldersCommand, renderRootsReport, resolvePanelPrimaryRoot, revealArgv };
@@ -40,7 +40,9 @@ const SUPPORTED_DSH_RELEASES = [
40
40
  "0.1.7-rc.1",
41
41
  "0.1.7-rc.2",
42
42
  "0.2.0-rc.1",
43
- "0.2.0-rc.2"
43
+ "0.2.0-rc.2",
44
+ "0.2.1-alpha.1",
45
+ "0.2.1-alpha.2"
44
46
  ];
45
47
  /**
46
48
  * Packages whose absence means this plugin cannot function at all: every one of
@@ -61,6 +63,13 @@ const REQUIRED_CORE_PACKAGES = [
61
63
  * that is the mixed-installation failure this contract exists to catch.
62
64
  */
63
65
  const OPTIONAL_CORE_PACKAGES = [
66
+ "@deepseek-ai/dsh-tools",
67
+ "@deepseek-ai/dsh-working-directory",
68
+ "@deepseek-ai/dsh-lsp",
69
+ "@deepseek-ai/dsh-lsp-stdio",
70
+ "@deepseek-ai/dsh-api-workspace-files",
71
+ "@deepseek-ai/dsh-typert-protocol",
72
+ "@deepseek-ai/dsh-sandbox-windows-acl",
64
73
  "@deepseek-ai/dsh-agent",
65
74
  "@deepseek-ai/dsh-agent-instructions",
66
75
  "@deepseek-ai/dsh-client-connection",
@@ -72,6 +81,8 @@ const OPTIONAL_CORE_PACKAGES = [
72
81
  ];
73
82
  /** Every package the contract inspects, required first. */
74
83
  const CORE_PACKAGES = [...REQUIRED_CORE_PACKAGES, ...OPTIONAL_CORE_PACKAGES];
84
+ /** Services looked up structurally; older runtimes never install these modules. */
85
+ const HOST_RESOLVED_PACKAGES = ["@deepseek-ai/dsh-working-directory"];
75
86
  /** The environment variable that relaxes the gate. */
76
87
  const COMPAT_ENFORCEMENT_ENV = "DSH_MULTI_ROOT_COMPAT";
77
88
  /**
@@ -88,11 +99,18 @@ const COMPAT_ENFORCEMENT_ENV = "DSH_MULTI_ROOT_COMPAT";
88
99
  * manifest cannot be resolved.
89
100
  */
90
101
  function readInstalledVersion(packageName) {
102
+ const require = createRequire(import.meta.url);
91
103
  try {
92
- const manifest = createRequire(import.meta.url)(`${packageName}/package.json`);
104
+ const manifest = require(`${packageName}/package.json`);
93
105
  return typeof manifest.version === "string" ? manifest.version : void 0;
94
106
  } catch {
95
- return;
107
+ if (!HOST_RESOLVED_PACKAGES.includes(packageName)) return void 0;
108
+ try {
109
+ const manifest = createRequire(require.resolve("@deepseek-ai/dsh/package.json"))(`${packageName}/package.json`);
110
+ return typeof manifest.version === "string" ? manifest.version : void 0;
111
+ } catch {
112
+ return;
113
+ }
96
114
  }
97
115
  }
98
116
  /**
package/lib/compat.js CHANGED
@@ -1,2 +1,2 @@
1
- import { a as inject, i as assertSupportedInstallation, n as MultiRootCompatService, r as apply, t as DshCompatUnsupportedError } from "./compat-82Y27Iom.js";
1
+ import { a as inject, i as assertSupportedInstallation, n as MultiRootCompatService, r as apply, t as DshCompatUnsupportedError } from "./compat-B6ZqbQBy.js";
2
2
  export { DshCompatUnsupportedError, MultiRootCompatService, apply, assertSupportedInstallation, inject };
package/lib/index.js CHANGED
@@ -1,11 +1,11 @@
1
- import { c as REQUIRED_CORE_PACKAGES, d as compatEnforcement, f as readInstalledVersion, i as assertSupportedInstallation, l as SUPPORTED_DSH_RELEASES, n as MultiRootCompatService, o as CORE_PACKAGES, s as OPTIONAL_CORE_PACKAGES, t as DshCompatUnsupportedError, u as classifyInstallation } from "./compat-82Y27Iom.js";
2
- import { a as composeInstructionMessage, c as createInstructionMessage, l as instructionsApi, r as PLUGIN_SOURCE, t as DEFAULT_MAX_BYTES, u as renderInstructions } from "./instructions-Cn1Jj5Xi.js";
3
- import { n as widenConfined, t as MultiRootSandboxProvider } from "./sandbox-D8Fw95FM.js";
1
+ import { c as REQUIRED_CORE_PACKAGES, d as compatEnforcement, f as readInstalledVersion, i as assertSupportedInstallation, l as SUPPORTED_DSH_RELEASES, n as MultiRootCompatService, o as CORE_PACKAGES, s as OPTIONAL_CORE_PACKAGES, t as DshCompatUnsupportedError, u as classifyInstallation } from "./compat-B6ZqbQBy.js";
2
+ import { a as composeInstructionMessage, c as createInstructionMessage, l as instructionsApi, r as PLUGIN_SOURCE, t as DEFAULT_MAX_BYTES, u as renderInstructions } from "./instructions-DfzN-wVf.js";
3
+ import { n as widenConfined, t as MultiRootSandboxProvider } from "./sandbox-BRXl_FpF.js";
4
4
  import { a as classifyStoredRoots, c as indexedStatuses, f as resolveRootRef, i as canonicalRoot, l as isCanonicallyUnder, n as additionalRootId, p as validateRootCandidate, r as availableRoots, s as expandRootInput, t as RootValidationError, u as removeStatusAt } from "./roots-DasWEBJk.js";
5
5
  import { t as MultiRootFileSystem } from "./fs-D_Sw3o5z.js";
6
6
  import { MultiRootScopeService, renderWorkspaceRootsContext, sanitizeAdditionalRoots } from "./scope.js";
7
7
  import { c as RegistryLeaseContendedError, i as REGISTRY_CONTENDED_MESSAGE, n as MAX_ALIAS_LENGTH, o as multiRootDomainSpec, r as MultiRootRegistry, s as RegistryAuthorityLease, t as DOMAIN_NAME } from "./registry-DySMbjgW.js";
8
- import { a as renderRootsReport, c as PANEL_CHANNEL, d as parseRevealedView, f as parseRootsView, i as parseFoldersCommand, l as PANEL_ENDPOINTS, o as resolvePanelPrimaryRoot, s as revealArgv, t as COMMAND_NAME, u as parsePanelCall } from "./command-DyOWujPo.js";
8
+ import { a as renderRootsReport, c as PANEL_CHANNEL, d as parseFilesView, f as parsePanelCall, i as parseFoldersCommand, l as PANEL_ENDPOINTS, m as parseRootsView, o as resolvePanelPrimaryRoot, p as parseRevealedView, s as revealArgv, t as COMMAND_NAME, u as parseFilePreview } from "./command-CTdNvwt1.js";
9
9
  //#region src/index.ts
10
10
  /**
11
11
  * Carrier plugin face for the bare-package-name loader row (see the module
@@ -16,4 +16,4 @@ import { a as renderRootsReport, c as PANEL_CHANNEL, d as parseRevealedView, f a
16
16
  const inject = [];
17
17
  function apply() {}
18
18
  //#endregion
19
- export { COMMAND_NAME, CORE_PACKAGES, DEFAULT_MAX_BYTES, DOMAIN_NAME, DshCompatUnsupportedError, MAX_ALIAS_LENGTH, MultiRootCompatService, MultiRootFileSystem, MultiRootRegistry, MultiRootSandboxProvider, MultiRootScopeService, OPTIONAL_CORE_PACKAGES, PANEL_CHANNEL, PANEL_ENDPOINTS, PLUGIN_SOURCE, REGISTRY_CONTENDED_MESSAGE, REQUIRED_CORE_PACKAGES, RegistryAuthorityLease, RegistryLeaseContendedError, RootValidationError, SUPPORTED_DSH_RELEASES, additionalRootId, apply, assertSupportedInstallation, availableRoots, canonicalRoot, classifyInstallation, classifyStoredRoots, compatEnforcement, composeInstructionMessage, createInstructionMessage, expandRootInput, indexedStatuses, inject, instructionsApi, isCanonicallyUnder, multiRootDomainSpec, parseFoldersCommand, parsePanelCall, parseRevealedView, parseRootsView, readInstalledVersion, removeStatusAt, renderInstructions, renderRootsReport, renderWorkspaceRootsContext, resolvePanelPrimaryRoot, resolveRootRef, revealArgv, sanitizeAdditionalRoots, validateRootCandidate, widenConfined };
19
+ export { COMMAND_NAME, CORE_PACKAGES, DEFAULT_MAX_BYTES, DOMAIN_NAME, DshCompatUnsupportedError, MAX_ALIAS_LENGTH, MultiRootCompatService, MultiRootFileSystem, MultiRootRegistry, MultiRootSandboxProvider, MultiRootScopeService, OPTIONAL_CORE_PACKAGES, PANEL_CHANNEL, PANEL_ENDPOINTS, PLUGIN_SOURCE, REGISTRY_CONTENDED_MESSAGE, REQUIRED_CORE_PACKAGES, RegistryAuthorityLease, RegistryLeaseContendedError, RootValidationError, SUPPORTED_DSH_RELEASES, additionalRootId, apply, assertSupportedInstallation, availableRoots, canonicalRoot, classifyInstallation, classifyStoredRoots, compatEnforcement, composeInstructionMessage, createInstructionMessage, expandRootInput, indexedStatuses, inject, instructionsApi, isCanonicallyUnder, multiRootDomainSpec, parseFilePreview, parseFilesView, parseFoldersCommand, parsePanelCall, parseRevealedView, parseRootsView, readInstalledVersion, removeStatusAt, renderInstructions, renderRootsReport, renderWorkspaceRootsContext, resolvePanelPrimaryRoot, resolveRootRef, revealArgv, sanitizeAdditionalRoots, validateRootCandidate, widenConfined };
@@ -1,7 +1,10 @@
1
+ import { t as effectiveDirectory } from "./session-directory-BLOstut1.js";
2
+ import { t as aroundTool } from "./tool-context-CpgIFcCB.js";
1
3
  import { l as isCanonicallyUnder } from "./roots-DasWEBJk.js";
2
4
  import { createRequire } from "node:module";
3
5
  import "@deepseek-ai/cordis";
4
6
  import { createHash } from "node:crypto";
7
+ import { AsyncLocalStorage } from "node:async_hooks";
5
8
  import { basename, dirname, isAbsolute, relative, resolve, sep } from "node:path";
6
9
  import { canonicalPath } from "@deepseek-ai/dsh-sandbox";
7
10
  //#region src/compat/agent-instructions.ts
@@ -221,6 +224,51 @@ function toolResultFailed(message) {
221
224
  return block.isError === true;
222
225
  }
223
226
  //#endregion
227
+ //#region src/compat/instruction-overlap.ts
228
+ /** Recognize files already represented by the native instruction baseline in this step. */
229
+ function nativeInstructionCopies(messages, cwd) {
230
+ const represented = /* @__PURE__ */ new Map();
231
+ const removed = /* @__PURE__ */ new Set();
232
+ let replacesBaseline = false;
233
+ for (const message of messages) {
234
+ const source = message.source;
235
+ if (source.kind !== "agent-instructions" || source.changes === void 0) continue;
236
+ if (source.baseline === true) {
237
+ replacesBaseline = true;
238
+ represented.clear();
239
+ }
240
+ for (const change of source.changes) {
241
+ if (change.action !== "remove") continue;
242
+ removed.add(change.path);
243
+ for (const [path, copy] of represented) if (copy.displayPath === change.path) represented.delete(path);
244
+ }
245
+ if (cwd === void 0 || source.baselineIdentity === void 0) continue;
246
+ let identity;
247
+ try {
248
+ identity = JSON.parse(source.baselineIdentity);
249
+ } catch {
250
+ continue;
251
+ }
252
+ if (typeof identity.projectRoot !== "string") continue;
253
+ const project = resolve(cwd, identity.projectRoot);
254
+ for (const change of source.changes) {
255
+ if (change.action === "remove" || typeof change.digest !== "string") continue;
256
+ if (/^(?:~|\$|user-global[\\/]|agents-global[\\/])/.test(change.path)) continue;
257
+ represented.set(resolve(project, change.path), {
258
+ digest: change.digest,
259
+ displayPath: change.path
260
+ });
261
+ }
262
+ }
263
+ return {
264
+ pathOf: (file) => {
265
+ const copy = represented.get(file.absolutePath);
266
+ return copy?.digest === createHash("sha1").update(file.content).digest("hex") ? copy.displayPath : void 0;
267
+ },
268
+ withdraws: (displayPath) => replacesBaseline || removed.has(displayPath)
269
+ };
270
+ }
271
+ //#endregion
224
272
  //#region src/instructions.ts
225
273
  /**
226
274
  * `multi-root-instructions`: the additional roots' own instruction files, made
@@ -342,16 +390,13 @@ function touchedPathOfToolCall(name, argumentsJson) {
342
390
  * The directory a touched path's instructions would live in.
343
391
  *
344
392
  * Tool arguments are the model's raw input, so a relative `file_path` is
345
- * resolved against the session cwd — the base the tool layer itself uses —
346
- * rather than against this process's cwd. Mac's `realpath` spelling is applied
347
- * here so the result compares against the canonical roots.
348
- * @param path - a touched path, absolute or relative to the session cwd.
349
- * @param session - the session the touch belongs to.
393
+ * captured as an absolute path at the operation boundary. Mac's `realpath`
394
+ * spelling is applied here so it compares against the canonical roots.
395
+ * @param path - the absolute touched file path.
350
396
  * @returns the canonical parent directory, or `undefined` when it cannot be named.
351
397
  */
352
- function parentDirectoryOf(path, session) {
353
- const cwd = session.header.cwd;
354
- const absolute = isAbsolute(path) ? path : cwd === void 0 ? void 0 : resolve(cwd, path);
398
+ function parentDirectoryOf(path) {
399
+ const absolute = isAbsolute(path) ? path : void 0;
355
400
  if (absolute === void 0) return void 0;
356
401
  try {
357
402
  return canonicalPath(dirname(absolute));
@@ -407,6 +452,8 @@ function apply(ctx, config = {}) {
407
452
  if (maxBytes <= 0 || !Number.isFinite(maxBytes)) return;
408
453
  const states = /* @__PURE__ */ new WeakMap();
409
454
  const callsBySession = /* @__PURE__ */ new WeakMap();
455
+ const executions = new AsyncLocalStorage();
456
+ aroundTool(ctx, (exec, next) => executions.run(exec, next));
410
457
  const stateOf = (session) => {
411
458
  const existing = states.get(session);
412
459
  if (existing !== void 0) return existing;
@@ -427,7 +474,8 @@ function apply(ctx, config = {}) {
427
474
  }
428
475
  pendingCalls.set(event.data.callId, {
429
476
  name: event.data.name,
430
- arguments: event.data.arguments
477
+ arguments: event.data.arguments,
478
+ cwd: effectiveDirectory(ctx, session)
431
479
  });
432
480
  return;
433
481
  }
@@ -437,17 +485,24 @@ function apply(ctx, config = {}) {
437
485
  pendingCalls.delete(callId);
438
486
  if (call === void 0) return;
439
487
  if (event.data.error !== void 0 || toolResultFailed(event.data.message)) return;
440
- const path = touchedPathOfToolCall(call.name, call.arguments);
488
+ const path = call.observedPath ?? touchedPathOfToolCall(call.name, call.arguments);
441
489
  if (path === void 0) return;
442
490
  const state = stateOf(session);
443
491
  if (state.touched.size >= MAX_TOUCHED_PATHS) return;
444
- state.touched.add(path);
492
+ if (isAbsolute(path)) state.touched.add(path);
493
+ else if (call.cwd !== void 0) state.touched.add(resolve(call.cwd, path));
445
494
  });
446
495
  ctx.inject(["fs"], (scope) => {
496
+ scope.on("fs/observed", (target, _observation, actor) => {
497
+ const exec = executions.getStore();
498
+ if (exec?.agent === void 0 || FILE_TOUCH_TOOL_NAMES[exec.name] !== true || actor !== exec) return;
499
+ const call = callsBySession.get(exec.agent.session)?.get(exec.callId);
500
+ if (call !== void 0) call.observedPath = scope.fs.processPath(target);
501
+ });
447
502
  scope.on("agent/pre-step", async ({ agent, messages, signal }, next) => {
448
503
  const decision = await next();
449
504
  if (decision.kind !== "enter") return decision;
450
- const seeded = await pending(scope, states, agent, config, maxBytes, signal);
505
+ const seeded = await pending(scope, states, agent, config, maxBytes, signal, decision.messages);
451
506
  if (seeded === void 0) return decision;
452
507
  signal.throwIfAborted();
453
508
  const lastClaimed = decision.messages.findLastIndex((message) => messages.includes(message));
@@ -455,7 +510,7 @@ function apply(ctx, config = {}) {
455
510
  ...decision,
456
511
  messages: decision.messages.toSpliced(lastClaimed + 1, 0, seeded)
457
512
  };
458
- });
513
+ }, { prepend: true });
459
514
  });
460
515
  }
461
516
  /**
@@ -471,7 +526,7 @@ function apply(ctx, config = {}) {
471
526
  * @param signal - the turn's cancellation signal.
472
527
  * @returns the message to inject, or `undefined`.
473
528
  */
474
- async function pending(ctx, states, agent, config, maxBytes, signal) {
529
+ async function pending(ctx, states, agent, config, maxBytes, signal, messages) {
475
530
  const session = agent.session;
476
531
  const state = states.get(session) ?? {
477
532
  scopes: /* @__PURE__ */ new Map(),
@@ -481,9 +536,12 @@ async function pending(ctx, states, agent, config, maxBytes, signal) {
481
536
  const evaluatedTouches = new Set(state.touched);
482
537
  let retryTouches = false;
483
538
  const scope = ctx.multiRootScope.resolve(ctx.sandboxPolicy.resolve({ session }));
484
- const granted = new Set(scope.additionalRoots);
485
- const revoked = [...new Set([...state.scopes.values()].map((entry) => entry.root))].filter((root) => !granted.has(root));
486
539
  const rendered = [];
540
+ const nativeCopy = nativeInstructionCopies(messages, effectiveDirectory(ctx, session));
541
+ for (const [key, entry] of state.scopes) if (entry.nativePath !== void 0 && nativeCopy.withdraws(entry.nativePath)) state.scopes.set(key, {
542
+ ...entry,
543
+ digest: void 0
544
+ });
487
545
  const gone = [];
488
546
  const updates = [];
489
547
  if (scope.additionalRoots.length > 0) {
@@ -492,14 +550,29 @@ async function pending(ctx, states, agent, config, maxBytes, signal) {
492
550
  let remaining = maxBytes;
493
551
  for (const root of scope.additionalRoots) {
494
552
  signal.throwIfAborted();
495
- const plan = await planRoot(ctx, api, root, config, state, session, signal);
553
+ const plan = await planRoot(ctx, api, root, config, state, signal);
496
554
  gone.push(...plan.gone);
497
555
  if (plan.deliveries.length === 0) continue;
498
556
  if (remaining <= 0) {
499
557
  retryTouches = true;
500
558
  continue;
501
559
  }
502
- const { text, omitted, truncated } = api.render(plan.deliveries.map((entry) => entry.file), { maxBytes: remaining });
560
+ const deliveries = plan.deliveries.filter((entry) => {
561
+ const nativePath = nativeCopy.pathOf(entry.file);
562
+ if (nativePath === void 0) return true;
563
+ updates.push({
564
+ key: entry.key,
565
+ scope: {
566
+ root,
567
+ path: entry.file.absolutePath,
568
+ digest: digestOf(entry.file.content),
569
+ nativePath
570
+ }
571
+ });
572
+ return false;
573
+ });
574
+ if (deliveries.length === 0) continue;
575
+ const { text, omitted, truncated } = api.render(deliveries.map((entry) => entry.file), { maxBytes: remaining });
503
576
  const omittedPaths = new Set(omitted.map((file) => file.absolutePath));
504
577
  const truncatedPaths = new Set(truncated.map((file) => file.displayPath));
505
578
  if (omitted.length > 0 || truncated.length > 0 || text === "") retryTouches = true;
@@ -509,7 +582,7 @@ async function pending(ctx, states, agent, config, maxBytes, signal) {
509
582
  root,
510
583
  text
511
584
  });
512
- for (const entry of plan.deliveries) {
585
+ for (const entry of deliveries) {
513
586
  if (omittedPaths.has(entry.file.absolutePath)) continue;
514
587
  updates.push({
515
588
  key: entry.key,
@@ -523,17 +596,20 @@ async function pending(ctx, states, agent, config, maxBytes, signal) {
523
596
  }
524
597
  }
525
598
  }
526
- const text = composeInstructionMessage(rendered, revoked, gone.map((entry) => entry.scope));
599
+ const current = new Set(ctx.multiRootScope.resolve(ctx.sandboxPolicy.resolve({ session })).additionalRoots);
600
+ const revoked = [...new Set([...state.scopes.values()].map((entry) => entry.root))].filter((root) => !current.has(root));
601
+ const liveUpdates = updates.filter((entry) => current.has(entry.scope.root));
602
+ const text = composeInstructionMessage(rendered.filter((entry) => current.has(entry.root)), revoked, gone.filter((entry) => current.has(entry.scope.root)).map((entry) => entry.scope));
527
603
  const message = text === void 0 ? void 0 : await createInstructionMessage({
528
604
  text,
529
605
  plugin: PLUGIN_SOURCE
530
606
  });
531
607
  signal.throwIfAborted();
532
608
  if (!retryTouches) for (const touch of evaluatedTouches) state.touched.delete(touch);
533
- if (text !== void 0) {
609
+ if (text !== void 0 || liveUpdates.length > 0) {
534
610
  for (const root of revoked) for (const [key, entry] of state.scopes) if (entry.root === root) state.scopes.delete(key);
535
611
  for (const entry of gone) state.scopes.delete(entry.key);
536
- for (const entry of updates) state.scopes.set(entry.key, entry.scope);
612
+ for (const entry of liveUpdates) state.scopes.set(entry.key, entry.scope);
537
613
  }
538
614
  return message;
539
615
  }
@@ -554,11 +630,10 @@ async function pending(ctx, states, agent, config, maxBytes, signal) {
554
630
  * @param root - the canonical additional root.
555
631
  * @param config - the row's configuration.
556
632
  * @param state - the session's delivery state.
557
- * @param session - the session, for the cwd a relative touch resolves against.
558
633
  * @param signal - the turn's cancellation signal.
559
634
  * @returns the files to send and the deliveries to withdraw.
560
635
  */
561
- async function planRoot(ctx, api, root, config, state, session, signal) {
636
+ async function planRoot(ctx, api, root, config, state, signal) {
562
637
  const delivered = /* @__PURE__ */ new Map();
563
638
  for (const [key, entry] of state.scopes) if (entry.root === root) delivered.set(key, entry);
564
639
  const dirs = /* @__PURE__ */ new Set([root]);
@@ -567,7 +642,7 @@ async function planRoot(ctx, api, root, config, state, session, signal) {
567
642
  if (dir !== root) dirs.add(dir);
568
643
  }
569
644
  for (const touched of state.touched) {
570
- const parent = parentDirectoryOf(touched, session);
645
+ const parent = parentDirectoryOf(touched);
571
646
  if (parent === void 0 || !isCanonicallyUnder(parent, root)) continue;
572
647
  let dir = parent;
573
648
  for (;;) {
@@ -678,16 +753,21 @@ async function loadDirectory(fs, api, root, config, signal, cwd) {
678
753
  }
679
754
  const files = [];
680
755
  for (const candidate of candidates) {
756
+ if (isAbsolute(candidate.displayPath) || resolve(root, candidate.displayPath) !== resolve(candidate.absolutePath)) continue;
681
757
  if (!isCanonicallyUnder(canonicalPath(candidate.absolutePath), root)) continue;
682
758
  try {
683
759
  const target = await fs.resolve(candidate.absolutePath);
760
+ const path = fs.processPath(target);
761
+ if (!isCanonicallyUnder(path, root)) continue;
684
762
  const info = await fs.stat(target, signal);
685
763
  if (info === void 0 || info.type !== "file") continue;
686
764
  if (info.size !== void 0 && info.size > maxSourceBytes) continue;
765
+ const content = await fs.readText(target, signal);
766
+ if (canonicalPath(candidate.absolutePath) !== path || !isCanonicallyUnder(path, root)) continue;
687
767
  files.push({
688
768
  ...candidate,
689
769
  displayPath: candidate.absolutePath,
690
- content: await fs.readText(target, signal)
770
+ content
691
771
  });
692
772
  } catch {
693
773
  continue;
@@ -1,2 +1,2 @@
1
- import { a as composeInstructionMessage, i as apply, n as DEFAULT_MAX_SOURCE_BYTES, o as inject, r as PLUGIN_SOURCE, s as touchedPathOfToolCall, t as DEFAULT_MAX_BYTES } from "./instructions-Cn1Jj5Xi.js";
1
+ import { a as composeInstructionMessage, i as apply, n as DEFAULT_MAX_SOURCE_BYTES, o as inject, r as PLUGIN_SOURCE, s as touchedPathOfToolCall, t as DEFAULT_MAX_BYTES } from "./instructions-DfzN-wVf.js";
2
2
  export { DEFAULT_MAX_BYTES, DEFAULT_MAX_SOURCE_BYTES, PLUGIN_SOURCE, apply, composeInstructionMessage, inject, touchedPathOfToolCall };
package/lib/lsp.js ADDED
@@ -0,0 +1,38 @@
1
+ import { t as aroundTool } from "./tool-context-CpgIFcCB.js";
2
+ import { n as routeWorkspacePath, t as wrapMethod } from "./method-wrapper-Zc-JlDP3.js";
3
+ import { AsyncLocalStorage } from "node:async_hooks";
4
+ //#region src/lsp.ts
5
+ const inject = ["multiRootCompat", "multiRootScope"];
6
+ function apply(ctx) {
7
+ const callers = new AsyncLocalStorage();
8
+ aroundTool(ctx, (exec, next) => exec.agent === void 0 ? next() : callers.run(exec.agent.session, next));
9
+ ctx.inject(["lsp", "fs"], (scope) => {
10
+ const service = scope.get("lsp");
11
+ scope.effect(() => wrapMethod(service, "query", (original) => async function(request, signal) {
12
+ const session = callers.getStore();
13
+ const primary = session === void 0 ? request.workspaceRoot : session.header.cwd ?? scope.get("sandboxPolicy")?.resolve({ session }).workspaceRoot;
14
+ if (primary === void 0) throw new Error("multi-root workspace: LSP caller has no session origin or deployment policy");
15
+ if (scope.multiRootScope.scopeOf(primary).length === 0 && session === void 0) return original.call(this, request, signal);
16
+ const route = await routeWorkspacePath(scope, primary, request.filePath, signal, request.workspaceRoot);
17
+ if (route === void 0) {
18
+ const target = await scope.fs.resolve(request.filePath, {
19
+ cwd: request.workspaceRoot,
20
+ ...signal === void 0 ? {} : { signal }
21
+ });
22
+ return original.call(this, {
23
+ ...request,
24
+ workspaceRoot: primary,
25
+ filePath: scope.fs.processPath(target)
26
+ }, signal);
27
+ }
28
+ if (session === void 0 && route.workspaceRoot === route.primaryRoot) return original.call(this, request, signal);
29
+ return original.call(this, {
30
+ ...request,
31
+ workspaceRoot: route.workspaceRoot,
32
+ filePath: route.absolutePath
33
+ }, signal);
34
+ }), "multi-root:lsp routing");
35
+ });
36
+ }
37
+ //#endregion
38
+ export { apply, inject };
@@ -0,0 +1,51 @@
1
+ import { canonicalPath } from "@deepseek-ai/dsh-sandbox";
2
+ //#region src/workspace-routing.ts
3
+ /** Resolve relative paths against the primary cwd; never search other roots by basename. */
4
+ async function routeWorkspacePath(ctx, primaryRoot, path, signal, cwd = primaryRoot) {
5
+ signal?.throwIfAborted();
6
+ const primary = canonicalPath(primaryRoot);
7
+ const roots = [primary, ...ctx.multiRootScope.scopeOf(primary)];
8
+ const target = await ctx.fs.resolve(path, {
9
+ cwd,
10
+ ...signal === void 0 ? {} : { signal }
11
+ });
12
+ for (const root of roots) {
13
+ const rootTarget = await ctx.fs.resolve(root, signal === void 0 ? {} : { signal });
14
+ if (!ctx.fs.contains(rootTarget, target)) continue;
15
+ if (root !== primary && !ctx.multiRootScope.scopeOf(primary).includes(root)) return void 0;
16
+ signal?.throwIfAborted();
17
+ return {
18
+ primaryRoot: primary,
19
+ workspaceRoot: root,
20
+ target,
21
+ absolutePath: ctx.fs.processPath(target)
22
+ };
23
+ }
24
+ }
25
+ //#endregion
26
+ //#region src/method-wrapper.ts
27
+ /** Wrap a public method on a Cordis service without capturing a caller-context proxy. */
28
+ function wrapMethod(target, key, wrap) {
29
+ const own = Object.getOwnPropertyDescriptor(target, key);
30
+ let prototype = target;
31
+ let descriptor;
32
+ while (prototype !== null && descriptor === void 0) {
33
+ descriptor = Object.getOwnPropertyDescriptor(prototype, key);
34
+ prototype = Object.getPrototypeOf(prototype);
35
+ }
36
+ if (typeof descriptor?.value !== "function") throw new Error(`multi-root: ${String(key)} is not a public method`);
37
+ const value = wrap(descriptor.value);
38
+ Object.defineProperty(target, key, {
39
+ configurable: true,
40
+ writable: true,
41
+ enumerable: own?.enumerable ?? false,
42
+ value
43
+ });
44
+ return () => {
45
+ if (Object.getOwnPropertyDescriptor(target, key)?.value !== value) return;
46
+ if (own === void 0) Reflect.deleteProperty(target, key);
47
+ else Object.defineProperty(target, key, own);
48
+ };
49
+ }
50
+ //#endregion
51
+ export { routeWorkspacePath as n, wrapMethod as t };