@dsh-electron/dsh-plugin-multi-root-workspace 0.1.11 → 0.1.13

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 (52) hide show
  1. package/CHANGELOG.en.md +25 -1
  2. package/CHANGELOG.md +25 -1
  3. package/README.en.md +10 -8
  4. package/README.md +10 -8
  5. package/cordis.patch.yml +7 -2
  6. package/lib/auto-review.js +1 -1
  7. package/lib/client.js +197 -74
  8. package/lib/client.js.map +1 -1
  9. package/lib/{command-BGdz7VBH.js → command-B9855H7y.js} +3 -2
  10. package/lib/command.js +1 -1
  11. package/lib/{compat-BiW-m_q0.js → compat-Bg2HFVMX.js} +3 -0
  12. package/lib/compat.js +1 -1
  13. package/lib/{sandbox-CXk5h-ct.js → dialects-Dr4rDR-H.js} +6 -264
  14. package/lib/{fs-BTXySwSG.js → fs-CfzJq2wd.js} +23 -8
  15. package/lib/fs.js +1 -1
  16. package/lib/index.js +7 -6
  17. package/lib/{instructions-MePoRigy.js → instructions-KsUwUFE0.js} +53 -37
  18. package/lib/instructions.js +1 -1
  19. package/lib/lsp.js +19 -16
  20. package/lib/native-win32-2waCRkPI.js +312 -0
  21. package/lib/process-fence.js +62 -12
  22. package/lib/protocol-CJOvAvOS.js +652 -0
  23. package/lib/{registry-KSsmU393.js → registry-Bv431TfO.js} +717 -74
  24. package/lib/registry-operation-C2ZV3Jhw.js +116 -0
  25. package/lib/registry.js +3 -2
  26. package/lib/{review-context-AH-SXHmj.js → review-context-NDpKeMCc.js} +1 -1
  27. package/lib/review-context.js +1 -1
  28. package/lib/sandbox-C-vCJCp3.js +278 -0
  29. package/lib/sandbox.js +1 -1
  30. package/lib/scope.js +101 -11
  31. package/lib/skills.js +48 -29
  32. package/lib/types/authority/native-win32.d.ts +7 -0
  33. package/lib/types/authority/native.d.ts +23 -0
  34. package/lib/types/authority/protocol.d.ts +70 -0
  35. package/lib/types/authority/recovery.d.ts +58 -0
  36. package/lib/types/client/RootSwitcher.d.ts +6 -3
  37. package/lib/types/client/files-tree/adapter.d.ts +63 -1
  38. package/lib/types/compat/authority-storage.d.ts +6 -0
  39. package/lib/types/compat/dsh-version.d.ts +4 -4
  40. package/lib/types/compat/open-in-app-device.d.ts +1 -1
  41. package/lib/types/compat/process-entrypoints.d.ts +2 -0
  42. package/lib/types/compat/process-profile.d.ts +3 -0
  43. package/lib/types/compat/sandbox-confine.d.ts +2 -0
  44. package/lib/types/fs.d.ts +4 -1
  45. package/lib/types/instructions.d.ts +0 -47
  46. package/lib/types/registry-operation.d.ts +31 -0
  47. package/lib/types/registry.d.ts +145 -6
  48. package/lib/types/scope.d.ts +28 -0
  49. package/lib/types/skills/catalog.d.ts +3 -0
  50. package/lib/types/skills/pool.d.ts +1 -1
  51. package/lib/workspace-files.js +63 -7
  52. package/package.json +38 -31
package/lib/skills.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { t as wrapMethod } from "./method-wrapper-CFY3p3j2.js";
2
+ import { c as withRegistryRead } from "./registry-operation-C2ZV3Jhw.js";
2
3
  import { r as isPackageInstalled, t as createInstructionMessage } from "./llm-message-fYORacvh.js";
3
4
  import { n as skillsMetadataValid } from "./metadata-DgqSdVX_.js";
4
5
  import { constants, statSync } from "node:fs";
@@ -6,8 +7,8 @@ import { createHash } from "node:crypto";
6
7
  import { isAbsolute, join, relative, resolve, sep } from "node:path";
7
8
  import { Service } from "@deepseek-ai/cordis";
8
9
  import { canonicalPath } from "@deepseek-ai/dsh-sandbox";
9
- import { lstat, open, readdir, stat } from "node:fs/promises";
10
10
  import { AsyncLocalStorage } from "node:async_hooks";
11
+ import { lstat, open, readdir, stat } from "node:fs/promises";
11
12
  import z from "@deepseek-ai/schemastery";
12
13
  //#region \0@oxc-project+runtime@0.149.0/helpers/esm/usingCtx.js
13
14
  function _usingCtx() {
@@ -191,9 +192,10 @@ var SkillReaderPool = class {
191
192
  return this.entries.size;
192
193
  }
193
194
  get roots() {
194
- return [...this.entries.keys()];
195
+ return [...new Set([...this.entries.values()].map((entry) => entry.root))];
195
196
  }
196
- async use(key, create, run, signal) {
197
+ async use(root, create, run, signal, identity = root) {
198
+ const key = identity;
197
199
  for (;;) {
198
200
  signal?.throwIfAborted();
199
201
  if (this.closing) throw new Error("multi-root skills: reader pool is closing");
@@ -225,6 +227,7 @@ var SkillReaderPool = class {
225
227
  const controller = new AbortController();
226
228
  entry = {
227
229
  key,
230
+ root,
228
231
  controller,
229
232
  reader: create(controller.signal),
230
233
  busy: 0
@@ -235,7 +238,7 @@ var SkillReaderPool = class {
235
238
  this.entries.set(key, entry);
236
239
  entry.busy++;
237
240
  try {
238
- if (entry.stamp === void 0) entry.stamp = await skillTreeStamp(key);
241
+ if (entry.stamp === void 0) entry.stamp = await skillTreeStamp(entry.root);
239
242
  signal?.throwIfAborted();
240
243
  return await run(await entry.reader);
241
244
  } finally {
@@ -249,7 +252,7 @@ var SkillReaderPool = class {
249
252
  this.polling ??= (async () => {
250
253
  let changed = false;
251
254
  for (const entry of [...this.entries.values()]) {
252
- const stamp = await skillTreeStamp(entry.key);
255
+ const stamp = await skillTreeStamp(entry.root);
253
256
  if (entry.stamp !== void 0 && entry.stamp !== stamp) changed = true;
254
257
  entry.stamp = stamp;
255
258
  }
@@ -285,10 +288,10 @@ var SkillReaderPool = class {
285
288
  /** No symlink target outside the Root is read or enumerated. Content reads stay on demand. */
286
289
  async function skillTreeStamp(root) {
287
290
  const rows = [canonicalPath(root)];
288
- const stamp = async (path) => {
291
+ const stamp = async (path, identityOnly = false) => {
289
292
  try {
290
293
  const link = await lstat(path, { bigint: true });
291
- rows.push(`${path}:${link.dev}:${link.ino}:${link.size}:${link.mtimeNs}:${link.ctimeNs}:${canonicalPath(path)}`);
294
+ rows.push(identityOnly ? `${path}:${link.dev}:${link.ino}:${link.mode}:${canonicalPath(path)}` : `${path}:${link.dev}:${link.ino}:${link.size}:${link.mtimeNs}:${link.ctimeNs}:${canonicalPath(path)}`);
292
295
  if (!containsPath(root, canonicalPath(path))) return void 0;
293
296
  return await stat(path);
294
297
  } catch {
@@ -296,7 +299,7 @@ async function skillTreeStamp(root) {
296
299
  return;
297
300
  }
298
301
  };
299
- await stamp(root);
302
+ await stamp(root, true);
300
303
  for (const directory of [".dsh/skills", ".agents/skills"]) {
301
304
  const path = join(root, directory);
302
305
  if (!(await stamp(path))?.isDirectory()) continue;
@@ -336,7 +339,7 @@ var AdditionalSkillCatalog = class {
336
339
  return this.generation();
337
340
  }
338
341
  current(primary, id) {
339
- if (this.signal.aborted || !this.enabled() || this.ctx.multiRootRegistry.authority.kind !== "active" || this.ctx.multiRootScope.resolve({
342
+ if (this.signal.aborted || !this.enabled() || !this.ctx.multiRootRegistry.available || this.ctx.multiRootScope.snapshot({
340
343
  mode: "read-only",
341
344
  workspaceRoot: primary
342
345
  }).authority !== "available") return void 0;
@@ -355,13 +358,17 @@ var AdditionalSkillCatalog = class {
355
358
  if (root === void 0) return await run(void 0);
356
359
  return await this.requests.run({
357
360
  primary,
358
- id
361
+ id,
362
+ epoch: this.ctx.multiRootRegistry.epoch
359
363
  }, () => this.pool.use(root.path, (signal) => rootReader(this.ctx, root.path, AbortSignal.any([signal, this.signal]), () => {
360
364
  const request = this.requests.getStore();
361
- return request !== void 0 && this.current(request.primary, request.id)?.path === root.path;
362
- }), run, options.signal));
365
+ return request !== void 0 && request.epoch === this.ctx.multiRootRegistry.epoch && this.current(request.primary, request.id)?.path === root.path;
366
+ }), run, options.signal, `${this.ctx.multiRootRegistry.epoch ?? "unavailable"}\0${root.path}`));
363
367
  }
364
368
  async list(primary, options) {
369
+ return withRegistryRead(this.ctx, primary, "skills", () => this.listAdmitted(primary, options));
370
+ }
371
+ async listAdmitted(primary, options) {
365
372
  options.signal?.throwIfAborted();
366
373
  if (!this.enabled()) return [];
367
374
  const roots = await this.ctx.multiRootRegistry.refresh(primary);
@@ -381,6 +388,7 @@ var AdditionalSkillCatalog = class {
381
388
  seen.add(candidate.name);
382
389
  const locator = {
383
390
  primary,
391
+ authorityEpoch: this.ctx.multiRootRegistry.epoch,
384
392
  rootId: root.id,
385
393
  aliasId: root.aliasId,
386
394
  candidate
@@ -408,8 +416,11 @@ var AdditionalSkillCatalog = class {
408
416
  }
409
417
  /** Locators are created here and are not accepted from a client path or object. */
410
418
  async load(primary, entry, options, user) {
419
+ return withRegistryRead(this.ctx, primary, "skills", () => this.loadAdmitted(primary, entry, options, user));
420
+ }
421
+ async loadAdmitted(primary, entry, options, user) {
411
422
  const locator = entry.locator;
412
- if (typeof locator !== "object" || locator === null || !this.locators.has(locator) || locator.primary !== primary) return void 0;
423
+ if (typeof locator !== "object" || locator === null || !this.locators.has(locator) || locator.primary !== primary || locator.authorityEpoch !== this.ctx.multiRootRegistry.epoch) return void 0;
413
424
  const root = this.current(primary, locator.rootId);
414
425
  if (root === void 0 || root.aliasId !== locator.aliasId || entry.name !== internalSkillName(primary, root.id, locator.candidate.name)) return void 0;
415
426
  const revision = this.generation();
@@ -446,14 +457,16 @@ var AdditionalSkillCatalog = class {
446
457
  }
447
458
  async admissionValid(definition) {
448
459
  const receipt = this.loadedReceipts.get(definition);
449
- if (receipt === void 0) return false;
450
- const current = this.current(receipt.locator.primary, receipt.locator.rootId);
451
- if (current === void 0 || current.aliasId !== receipt.locator.aliasId || receipt.revision !== this.generation()) return false;
452
- try {
453
- return receipt.stamp === await fileStamp(definition.path) && receipt.revision === this.generation() && this.current(receipt.locator.primary, receipt.locator.rootId)?.aliasId === receipt.locator.aliasId;
454
- } catch {
455
- return false;
456
- }
460
+ if (receipt === void 0 || receipt.locator.authorityEpoch !== this.ctx.multiRootRegistry.epoch) return false;
461
+ return withRegistryRead(this.ctx, receipt.locator.primary, "skills", async () => {
462
+ const current = this.current(receipt.locator.primary, receipt.locator.rootId);
463
+ if (current === void 0 || current.aliasId !== receipt.locator.aliasId || receipt.revision !== this.generation()) return false;
464
+ try {
465
+ return receipt.stamp === await fileStamp(definition.path) && receipt.revision === this.generation() && this.current(receipt.locator.primary, receipt.locator.rootId)?.aliasId === receipt.locator.aliasId;
466
+ } catch {
467
+ return false;
468
+ }
469
+ });
457
470
  }
458
471
  async dispose() {
459
472
  await this.pool.dispose();
@@ -574,16 +587,22 @@ function installSkillRouting(ctx) {
574
587
  fibers.push(ctx.inject(["skills"], (scope) => {
575
588
  const registry = scope.skills;
576
589
  scope.effect(() => wrapMethod(registry, "snapshot", (original) => async function(options = {}) {
577
- const result = await original.call(this, optionsFor(options));
578
- const primary = skillOrigin(scope, options);
579
- if (primary !== void 0) for (const skill of result.skills) {
580
- if (skill.provider === "multi-root-workspace") continue;
581
- if (scope.multiRootRegistry.list(primary).some((root) => skill.name.startsWith(internalSkillName(primary, root.id, "")))) scope.logger.warn(`multi-root skills: internal ID conflict; native winner retained: ${skill.name} (${skill.provider})`);
582
- }
583
- return result;
590
+ const primaryOrigin = skillOrigin(scope, options) ?? cold.getStore();
591
+ const run = async () => {
592
+ const result = await original.call(this, optionsFor(options));
593
+ const primary = skillOrigin(scope, options);
594
+ if (primary !== void 0) for (const skill of result.skills) {
595
+ if (skill.provider === "multi-root-workspace") continue;
596
+ if (scope.multiRootRegistry.list(primary).some((root) => skill.name.startsWith(internalSkillName(primary, root.id, "")))) scope.logger.warn(`multi-root skills: internal ID conflict; native winner retained: ${skill.name} (${skill.provider})`);
597
+ }
598
+ return result;
599
+ };
600
+ return primaryOrigin === void 0 ? run() : withRegistryRead(scope, primaryOrigin, "skills", run);
584
601
  }));
585
602
  scope.effect(() => wrapMethod(registry, "get", (original) => function(name, options = {}) {
586
- return original.call(this, name, optionsFor(options));
603
+ const primary = skillOrigin(scope, options) ?? cold.getStore();
604
+ const run = () => original.call(this, name, optionsFor(options));
605
+ return primary === void 0 ? run() : withRegistryRead(scope, primary, "skills", run);
587
606
  }));
588
607
  }));
589
608
  fibers.push(ctx.inject(["sessionSkillCatalog", "sessionQuery"], (scope) => {
@@ -0,0 +1,7 @@
1
+ import type { Channel, Listener } from './native.ts';
2
+ export declare function connectWindows(endpoint: string): Promise<Channel>;
3
+ export declare function listenWindows(endpoint: string, accepted: (channel: Channel) => void): Promise<Listener>;
4
+ export declare function windowsHostAvailability(): Promise<{
5
+ available: boolean;
6
+ reason?: string;
7
+ }>;
@@ -0,0 +1,23 @@
1
+ import type { EventEmitter } from 'node:events';
2
+ export interface Channel {
3
+ on: EventEmitter['on'];
4
+ readonly writableLength?: number;
5
+ write(data: string): boolean;
6
+ destroy(): unknown;
7
+ }
8
+ export interface Listener {
9
+ close(): void;
10
+ }
11
+ export declare function storeIdentity(root: string): {
12
+ root: string;
13
+ id: string;
14
+ endpoint: string;
15
+ };
16
+ export declare function connectNative(endpoint: string): Promise<Channel>;
17
+ /** Called only by the kernel lease holder. Never removes another epoch's socket. */
18
+ export declare function listenNative(endpoint: string, accepted: (channel: Channel) => void): Promise<Listener>;
19
+ /** Capability probe for native tests and diagnostics; not an authorization bypass. */
20
+ export declare function nativeHostAvailability(): Promise<{
21
+ available: boolean;
22
+ reason?: string;
23
+ }>;
@@ -0,0 +1,70 @@
1
+ import { z } from 'zod';
2
+ declare const methods: readonly ["hello", "snapshot", "admit", "add", "removeAt", "setAccess", "setAlias", "setSkillsEnabled", "setAliasId", "move", "recheck"];
3
+ export type Method = typeof methods[number];
4
+ export declare const snapshotSchema: z.ZodObject<{
5
+ protocol: z.ZodLiteral<1>;
6
+ store: z.ZodString;
7
+ epoch: z.ZodString;
8
+ revision: z.ZodNumber;
9
+ primary: z.ZodString;
10
+ roots: z.ZodArray<z.ZodObject<{
11
+ id: z.ZodString;
12
+ path: z.ZodString;
13
+ access: z.ZodEnum<{
14
+ inherit: "inherit";
15
+ "read-only": "read-only";
16
+ "read-write": "read-write";
17
+ }>;
18
+ addedAt: z.ZodString;
19
+ recordedPath: z.ZodOptional<z.ZodString>;
20
+ readOnlyRecordedPath: z.ZodOptional<z.ZodString>;
21
+ readWriteRecordedPath: z.ZodOptional<z.ZodString>;
22
+ alias: z.ZodOptional<z.ZodString>;
23
+ aliasId: z.ZodOptional<z.ZodUnknown>;
24
+ skillsEnabled: z.ZodOptional<z.ZodUnknown>;
25
+ state: z.ZodEnum<{
26
+ available: "available";
27
+ missing: "missing";
28
+ redirected: "redirected";
29
+ invalid: "invalid";
30
+ }>;
31
+ detail: z.ZodOptional<z.ZodString>;
32
+ invalidReason: z.ZodOptional<z.ZodLiteral<"membership-conflict">>;
33
+ }, z.core.$strict>>;
34
+ available: z.ZodBoolean;
35
+ }, z.core.$strict>;
36
+ export type Snapshot = z.infer<typeof snapshotSchema>;
37
+ export declare class AuthorityRpcError extends Error {
38
+ readonly code: string;
39
+ constructor(code: string, message: string);
40
+ }
41
+ export declare class AuthorityClient {
42
+ private readonly channel;
43
+ private readonly store;
44
+ private readonly limit;
45
+ private readonly pending;
46
+ private closed;
47
+ epoch: string | undefined;
48
+ private constructor();
49
+ static connect(endpoint: string, store: string, limit: number, onChange: (snapshot: Snapshot) => void, disconnected: () => void, signal?: AbortSignal): Promise<AuthorityClient>;
50
+ request(method: Method, args: readonly unknown[]): Promise<unknown>;
51
+ close(): void;
52
+ }
53
+ export declare class AuthorityServer {
54
+ private readonly store;
55
+ private readonly epoch;
56
+ private readonly limit;
57
+ private readonly dispatch;
58
+ private listener;
59
+ private readonly peers;
60
+ private readonly readyPeers;
61
+ private closed;
62
+ private inFlight;
63
+ private constructor();
64
+ static listen(endpoint: string, store: string, epoch: string, limit: number, dispatch: (method: Method, args: unknown[]) => Promise<unknown>): Promise<AuthorityServer>;
65
+ private accept;
66
+ changed(snapshot: Snapshot): void;
67
+ close(): void;
68
+ }
69
+ export declare function parseArguments(method: Method, args: unknown[]): unknown[];
70
+ export {};
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Bounded background retry for the registry authority: one pending timer and
3
+ * one in-flight attempt per instance, post-completion backoff, and local
4
+ * cancellation on dispose. The scheduler owns NO policy — the caller supplies
5
+ * `run` (one recovery attempt) and `eligible` (whether a degraded state still
6
+ * wants recovery), so the same object can drive contended, follower
7
+ * resync, and retryable storage failures.
8
+ *
9
+ * @module @dsh-electron/dsh-plugin-multi-root-workspace/authority/recovery
10
+ */
11
+ /** Injectable clock so fake-timer specs can drive the scheduler exactly. */
12
+ export interface RecoveryTimers {
13
+ setTimeout: typeof setTimeout;
14
+ clearTimeout: typeof clearTimeout;
15
+ random(): number;
16
+ now(): number;
17
+ }
18
+ /** What the scheduler is doing, for diagnostics surfaces. */
19
+ export type RecoveryPhase = 'idle' | 'waiting' | 'attempting' | 'disabled';
20
+ export declare class RecoveryScheduler {
21
+ private readonly run;
22
+ private readonly eligible;
23
+ private readonly enabled;
24
+ private readonly timers;
25
+ private timer;
26
+ private inFlight;
27
+ private disposed;
28
+ private attempts;
29
+ private retryAt;
30
+ constructor(options: {
31
+ run: () => Promise<void>;
32
+ eligible: () => boolean;
33
+ enabled: boolean;
34
+ timers?: RecoveryTimers;
35
+ });
36
+ /**
37
+ * Ask for a recovery attempt. Called after every `ensureAuthority()` and on
38
+ * every downgrade event. A healthy state (`!eligible()`) resets the backoff;
39
+ * a pending timer or in-flight attempt is never duplicated or reset.
40
+ */
41
+ arm(): void;
42
+ /**
43
+ * Progress event (a follower connection was just adopted): the backoff is
44
+ * earned, so reset it and run the next attempt immediately — still through
45
+ * the single pending timer.
46
+ */
47
+ kick(): void;
48
+ /** Stop scheduling. An in-flight attempt is left to finish on its own. */
49
+ dispose(): void;
50
+ get state(): {
51
+ phase: RecoveryPhase;
52
+ attempts: number;
53
+ nextRetryAt: number | undefined;
54
+ };
55
+ private clearTimer;
56
+ private delay;
57
+ private tick;
58
+ }
@@ -32,9 +32,12 @@
32
32
  * the same element's `closest('[data-dockkit-content]')` — the dockkit
33
33
  * tabHost section — with a generated fallback where the hook is absent.
34
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.
35
+ * The selected root lives in the inject product's session-scoped snapshot,
36
+ * keyed by that dockkit tab id — component state could not survive the
37
+ * remount every file open causes (the opened tab takes the pane, the Files
38
+ * body unmounts with the whole occupant subtree). One selection per tab;
39
+ * session switches mint a fresh inject product, so no selection crosses
40
+ * sessions, and a registry eviction reconciles it back to primary.
38
41
  *
39
42
  * @module @dsh-electron/dsh-plugin-multi-root-workspace/client/RootSwitcher
40
43
  */
@@ -25,19 +25,72 @@
25
25
  */
26
26
  import type { Context as ClientContext } from '@deepseek-ai/cordis';
27
27
  import type { ClientRemote } from '@deepseek-ai/dsh-api-remotes/client';
28
- import type { BoundActions } from '@deepseek-ai/dsh-client-store';
28
+ import { type BoundActions, type SnapshotSelectorHook } from '@deepseek-ai/dsh-client-store';
29
+ import type { HostObservable } from '@deepseek-ai/dsh-client-ui-slots';
30
+ import type { TabId } from '@deepseek-ai/dsh-client-ui-dockkit';
29
31
  import type { SessionId } from '@deepseek-ai/dsh-session/types';
30
32
  import { type FilesInjected } from './face.ts';
31
33
  import { createFilesStore } from './store.ts';
32
34
  import { filesTreeSurfaceSupported } from './primitives.ts';
33
35
  import type { FilesTreeProps } from './FilesTree.tsx';
36
+ import type { RootView } from '../../contract.ts';
34
37
  import { NS } from './locales.ts';
38
+ /**
39
+ * One session's per-tab additional-root selections, keyed by the pane's
40
+ * dockkit tab id. An absent entry means the primary root.
41
+ */
42
+ export type TreeSelection = Readonly<Record<string, RootView>>;
43
+ /**
44
+ * The lifetimes a selection runs under, keyed by tab like the store bucket:
45
+ * while an entry's signal is live the tree keeps its loaded levels, watches,
46
+ * expansion, and scroll — exactly what upstream's tab-record signal gives the
47
+ * native tree across a body remount (opening a file deactivates this tab,
48
+ * unmounting its body without ending the record).
49
+ */
50
+ export interface FilesTreeTakeover {
51
+ /**
52
+ * The live signal for (tabId, root); reuses the live entry so a remounted
53
+ * body finds its bucket and watches intact. A different root — or an entry
54
+ * already aborted — ends the previous controller before minting the next.
55
+ * @param tabId - the pane's dockkit tab id.
56
+ * @param root - absolute path of the root being browsed.
57
+ * @param section - the dockkit tabHost section the caller mounted under; a
58
+ * live entry keeps an observer on it that ends the takeover whenever the
59
+ * section leaves the document — the record's real end — including when the
60
+ * body that mounted it has long been unmounted itself.
61
+ * @returns the signal the forked tree lives under.
62
+ */
63
+ readonly signal: (tabId: TabId, root: string, section?: Element) => AbortSignal;
64
+ /**
65
+ * Abort and drop one tab's takeover: the reader deselected or switched
66
+ * roots, or the tab record itself went away.
67
+ * @param tabId - the pane's dockkit tab id.
68
+ */
69
+ readonly end: (tabId: TabId) => void;
70
+ }
35
71
  /** The inject face the registration composes onto the occupant's props. */
36
72
  export interface FilesTreeInjected {
37
73
  /** The verbatim files face (list/watch bookkeeping included). */
38
74
  readonly face: FilesInjected;
39
75
  /** Open one `dsh-resource://file` address in the host viewer. */
40
76
  readonly openFile: (address: string) => void;
77
+ /**
78
+ * The reserved inject compartment: the slot renderer binds each source to a
79
+ * `use<Name>` selector hook, so `treeSelection` reaches the component as
80
+ * `useTreeSelection`, never as a prop under its own key.
81
+ */
82
+ readonly hooks: {
83
+ readonly treeSelection: HostObservable<TreeSelection>;
84
+ };
85
+ /**
86
+ * Persist one pane's selected root; `'primary'` drops its entry. The
87
+ * session-scoped snapshot survives the body remount a file open causes.
88
+ * @param tabId - the pane's dockkit tab id.
89
+ * @param root - the selected root, or `'primary'` to browse the session root.
90
+ */
91
+ readonly selectRoot: (tabId: TabId, root: RootView | 'primary') => void;
92
+ /** The forked tree's lifetimes; see {@link FilesTreeTakeover}. */
93
+ readonly takeover: FilesTreeTakeover;
41
94
  }
42
95
  /** Everything `FilesTree` needs that the slot framework does not synthesize. */
43
96
  export interface FilesTreeSurface {
@@ -47,6 +100,12 @@ export interface FilesTreeSurface {
47
100
  readonly face: FilesInjected;
48
101
  readonly openFile: (address: string) => void;
49
102
  readonly t: FilesTreeProps['t'];
103
+ /** Selector hook over this session's per-tab root selections. */
104
+ readonly useTreeSelection: SnapshotSelectorHook<TreeSelection>;
105
+ /** The selection's write side; see {@link FilesTreeInjected.selectRoot}. */
106
+ readonly selectRoot: FilesTreeInjected['selectRoot'];
107
+ /** The forked tree's lifetimes; see {@link FilesTreeTakeover}. */
108
+ readonly takeover: FilesTreeTakeover;
50
109
  }
51
110
  /**
52
111
  * A `ClientRemote` stand-in that resolves the real service per call.
@@ -89,6 +148,9 @@ export declare function createFilesTreeSurface(props: {
89
148
  readonly actions: FilesTreeSurface['actions'];
90
149
  readonly face: FilesInjected | undefined;
91
150
  readonly openFile: ((address: string) => void) | undefined;
151
+ readonly useTreeSelection: FilesTreeSurface['useTreeSelection'] | undefined;
152
+ readonly selectRoot: FilesTreeSurface['selectRoot'] | undefined;
153
+ readonly takeover: FilesTreeSurface['takeover'] | undefined;
92
154
  }, t: FilesTreeProps['t']): FilesTreeSurface | undefined;
93
155
  export { filesTreeSurfaceSupported, NS as FILES_TREE_NS };
94
156
  export { FilesTree } from './FilesTree.tsx';
@@ -0,0 +1,6 @@
1
+ import type { Context } from '@deepseek-ai/cordis';
2
+ import type { DomainSpec } from '@deepseek-ai/dsh-storage-domain';
3
+ export declare function openAuthorityStorage<S extends DomainSpec>(ctx: Context, root: string, spec: S): Promise<{
4
+ table: <N extends keyof S["tables"] & string>(name: N) => import("@deepseek-ai/dsh-storage-domain").KvTable<import("@deepseek-ai/dsh-storage-domain").TableKeyOf<S, N>, import("@deepseek-ai/dsh-storage-domain").TableValueOf<S, N>>;
5
+ close(): Promise<void>;
6
+ }>;
@@ -28,7 +28,7 @@
28
28
  * the upgrade smoke passed on that exact version — a green CI run on an
29
29
  * unlisted release is evidence, not authorization (ADR-0009).
30
30
  */
31
- export declare const SUPPORTED_DSH_RELEASES: readonly ["0.1.5-rc.2", "0.1.6-alpha.1", "0.1.6-alpha.2", "0.1.7-alpha.1", "0.1.7-alpha.2", "0.1.7-rc.1", "0.1.7-rc.2", "0.2.0-rc.1", "0.2.0-rc.2", "0.2.1-alpha.1", "0.2.1-alpha.2", "0.2.1-alpha.3", "0.2.1-alpha.4", "0.2.1-alpha.5", "0.2.1-rc.1", "0.2.1-rc.2", "0.2.1-rc.3", "0.2.1-rc.4", "0.2.2-alpha.2"];
31
+ export declare const SUPPORTED_DSH_RELEASES: readonly ["0.1.5-rc.2", "0.1.6-alpha.1", "0.1.6-alpha.2", "0.1.7-alpha.1", "0.1.7-alpha.2", "0.1.7-rc.1", "0.1.7-rc.2", "0.2.0-rc.1", "0.2.0-rc.2", "0.2.1-alpha.1", "0.2.1-alpha.2", "0.2.1-alpha.3", "0.2.1-alpha.4", "0.2.1-alpha.5", "0.2.1-rc.1", "0.2.1-rc.2", "0.2.1-rc.3", "0.2.1-rc.4", "0.2.1-rc.5", "0.2.2-alpha.2"];
32
32
  /** One release this plugin claims to support. */
33
33
  export type SupportedDshRelease = typeof SUPPORTED_DSH_RELEASES[number];
34
34
  /**
@@ -42,7 +42,7 @@ export declare const REQUIRED_CORE_PACKAGES: readonly ["@deepseek-ai/dsh-fs", "@
42
42
  * Absence is fine; a version that DISAGREES with the required packages is not —
43
43
  * that is the mixed-installation failure this contract exists to catch.
44
44
  */
45
- export declare const OPTIONAL_CORE_PACKAGES: readonly ["@deepseek-ai/dsh-skill", "@deepseek-ai/dsh-skill-filesystem", "@deepseek-ai/dsh-settings", "@deepseek-ai/dsh-api-session-controller", "@deepseek-ai/dsh-session-query", "@deepseek-ai/dsh-tools", "@deepseek-ai/dsh-working-directory", "@deepseek-ai/dsh-lsp", "@deepseek-ai/dsh-lsp-stdio", "@deepseek-ai/dsh-api-workspace-files", "@deepseek-ai/dsh-typert-protocol", "@deepseek-ai/dsh-sandbox-windows-acl", "@deepseek-ai/dsh-agent", "@deepseek-ai/dsh-agent-instructions", "@deepseek-ai/dsh-client-connection", "@deepseek-ai/dsh-commands", "@deepseek-ai/dsh-llm", "@deepseek-ai/dsh-session", "@deepseek-ai/dsh-subprocess", "@deepseek-ai/dsh-system-prompt"];
45
+ export declare const OPTIONAL_CORE_PACKAGES: readonly ["@deepseek-ai/dsh-storage-json", "@deepseek-ai/dsh-skill", "@deepseek-ai/dsh-skill-filesystem", "@deepseek-ai/dsh-settings", "@deepseek-ai/dsh-api-session-controller", "@deepseek-ai/dsh-session-query", "@deepseek-ai/dsh-tools", "@deepseek-ai/dsh-working-directory", "@deepseek-ai/dsh-lsp", "@deepseek-ai/dsh-lsp-stdio", "@deepseek-ai/dsh-api-workspace-files", "@deepseek-ai/dsh-typert-protocol", "@deepseek-ai/dsh-sandbox-windows-acl", "@deepseek-ai/dsh-agent", "@deepseek-ai/dsh-agent-instructions", "@deepseek-ai/dsh-client-connection", "@deepseek-ai/dsh-commands", "@deepseek-ai/dsh-llm", "@deepseek-ai/dsh-session", "@deepseek-ai/dsh-subprocess", "@deepseek-ai/dsh-system-prompt"];
46
46
  /**
47
47
  * The optional packages this plugin loads itself at run time, through dynamic
48
48
  * `import()`s inside this package's own modules. The copy this module resolves
@@ -50,7 +50,7 @@ export declare const OPTIONAL_CORE_PACKAGES: readonly ["@deepseek-ai/dsh-skill",
50
50
  * private copy inside this package's own tree genuinely loads, and a version
51
51
  * that disagrees with the host is a real mixed installation, not a phantom.
52
52
  */
53
- export declare const RUNTIME_OPTIONAL_PACKAGES: readonly ["@deepseek-ai/dsh-skill", "@deepseek-ai/dsh-skill-filesystem", "@deepseek-ai/dsh-agent-instructions", "@deepseek-ai/dsh-llm", "@deepseek-ai/dsh-session", "@deepseek-ai/dsh-typert-protocol"];
53
+ export declare const RUNTIME_OPTIONAL_PACKAGES: readonly ["@deepseek-ai/dsh-storage-json", "@deepseek-ai/dsh-skill", "@deepseek-ai/dsh-skill-filesystem", "@deepseek-ai/dsh-agent-instructions", "@deepseek-ai/dsh-llm", "@deepseek-ai/dsh-session", "@deepseek-ai/dsh-typert-protocol"];
54
54
  /**
55
55
  * The optional packages this plugin consumes only as mounted host services or
56
56
  * ambient types — it never imports them, so a copy inside this package's own
@@ -79,7 +79,7 @@ export declare const HOST_SERVICE_PACKAGES: readonly ["@deepseek-ai/dsh-settings
79
79
  */
80
80
  export declare const RUNNER_ANCHORED_PACKAGES: readonly ["@deepseek-ai/dsh-sandbox-windows-acl"];
81
81
  /** Every package the contract inspects, required first. */
82
- export declare const CORE_PACKAGES: readonly ["@deepseek-ai/dsh-fs", "@deepseek-ai/dsh-fs-local", "@deepseek-ai/dsh-sandbox", "@deepseek-ai/dsh-sandbox-local", "@deepseek-ai/dsh-sandbox-policy", "@deepseek-ai/dsh-storage-domain", "@deepseek-ai/dsh-skill", "@deepseek-ai/dsh-skill-filesystem", "@deepseek-ai/dsh-settings", "@deepseek-ai/dsh-api-session-controller", "@deepseek-ai/dsh-session-query", "@deepseek-ai/dsh-tools", "@deepseek-ai/dsh-working-directory", "@deepseek-ai/dsh-lsp", "@deepseek-ai/dsh-lsp-stdio", "@deepseek-ai/dsh-api-workspace-files", "@deepseek-ai/dsh-typert-protocol", "@deepseek-ai/dsh-sandbox-windows-acl", "@deepseek-ai/dsh-agent", "@deepseek-ai/dsh-agent-instructions", "@deepseek-ai/dsh-client-connection", "@deepseek-ai/dsh-commands", "@deepseek-ai/dsh-llm", "@deepseek-ai/dsh-session", "@deepseek-ai/dsh-subprocess", "@deepseek-ai/dsh-system-prompt"];
82
+ export declare const CORE_PACKAGES: readonly ["@deepseek-ai/dsh-fs", "@deepseek-ai/dsh-fs-local", "@deepseek-ai/dsh-sandbox", "@deepseek-ai/dsh-sandbox-local", "@deepseek-ai/dsh-sandbox-policy", "@deepseek-ai/dsh-storage-domain", "@deepseek-ai/dsh-storage-json", "@deepseek-ai/dsh-skill", "@deepseek-ai/dsh-skill-filesystem", "@deepseek-ai/dsh-settings", "@deepseek-ai/dsh-api-session-controller", "@deepseek-ai/dsh-session-query", "@deepseek-ai/dsh-tools", "@deepseek-ai/dsh-working-directory", "@deepseek-ai/dsh-lsp", "@deepseek-ai/dsh-lsp-stdio", "@deepseek-ai/dsh-api-workspace-files", "@deepseek-ai/dsh-typert-protocol", "@deepseek-ai/dsh-sandbox-windows-acl", "@deepseek-ai/dsh-agent", "@deepseek-ai/dsh-agent-instructions", "@deepseek-ai/dsh-client-connection", "@deepseek-ai/dsh-commands", "@deepseek-ai/dsh-llm", "@deepseek-ai/dsh-session", "@deepseek-ai/dsh-subprocess", "@deepseek-ai/dsh-system-prompt"];
83
83
  /** How this plugin reacts to an installation it does not support. */
84
84
  export type CompatEnforcement = 'enforce' | 'warn';
85
85
  /** The environment variable that relaxes the gate. */
@@ -3,7 +3,7 @@
3
3
  * `@deepseek-ai/dsh-client-ui-primitives`.
4
4
  *
5
5
  * ```text
6
- * 0.1.5-rc.2 through 0.2.1-rc.4 no `isMobileDevice` export
6
+ * 0.1.5-rc.2 through 0.2.1-rc.5 no `isMobileDevice` export
7
7
  * 0.2.2-alpha.2 + `isMobileDevice` exported
8
8
  * ```
9
9
  *
@@ -29,3 +29,5 @@ export declare function shellEntrypoints(shell: object): readonly ProcessEntrypo
29
29
  * @returns the present entries — a structural probe only.
30
30
  */
31
31
  export declare function ptcEntrypoints(runtime: object): readonly ProcessEntrypoint[];
32
+ /** Async public entries can acquire admission before calling their implementation. */
33
+ export declare function isAsyncProcessEntry(entry: unknown): boolean;
@@ -0,0 +1,3 @@
1
+ export declare function hasConfinedProfile(argv: readonly string[]): boolean;
2
+ /** Agent PTYs are re-confined after their own fresh admission, never replayed. */
3
+ export declare function originalProcessArgv(argv: readonly string[]): readonly string[];
@@ -58,3 +58,5 @@ export declare function mapConfined<T>(value: UpstreamConfined, map: (confined:
58
58
  * @returns the widened result, in the shape the installed release uses.
59
59
  */
60
60
  export declare function widenConfined(base: ConfineCall, widen: (confined: ConfinedArgv) => ConfinedArgv, argv: readonly string[], policy: SandboxPolicy, signal?: AbortSignal): UpstreamConfined;
61
+ /** Public consumer call with the installed provider's arity and return shape. */
62
+ export declare function confineInstalled(provider: Pick<LocalSandboxProvider, 'confine'>, argv: readonly string[], policy: SandboxPolicy, signal?: AbortSignal): ConfinedArgv | Promise<ConfinedArgv>;
package/lib/types/fs.d.ts CHANGED
@@ -64,6 +64,8 @@ export declare class MultiRootFileSystem extends LocalFileSystem {
64
64
  editText(target: FsTarget, edit: FsEditRequest, expected?: {
65
65
  version: FsVersion;
66
66
  }, signal?: AbortSignal, sandboxPolicy?: SandboxExecutionPolicy): Promise<FsEditOutcome>;
67
+ /** The provider must also work without the gated registry (ADR-0009). */
68
+ private withOperation;
67
69
  /**
68
70
  * The writable roots of one confined call: the upstream derivation (primary
69
71
  * root plus the platform temp areas, canonical and deduplicated — empty
@@ -104,7 +106,8 @@ export declare class MultiRootFileSystem extends LocalFileSystem {
104
106
  * to primary-root-only writes (no shared temp or additional-root grants)
105
107
  * until the authority returns.
106
108
  * @param target - the resolved target about to be mutated.
107
- * @param sandboxPolicy - the per-call policy; omit for the deployment fallback.
109
+ * @param policy - the per-call policy captured before admission.
110
+ * @param scope - the admission snapshot shared by every fence check.
108
111
  * @returns the fresh canonical target the mutation must use.
109
112
  */
110
113
  private checkedTarget;
@@ -1,50 +1,3 @@
1
- /**
2
- * `multi-root-instructions`: the additional roots' own instruction files, made
3
- * visible to the model.
4
- *
5
- * Upstream `agent-instructions` discovers instruction files by walking UP from
6
- * the session cwd (plus the one user-global file). In a multi-root workspace
7
- * that walk never reaches an additional root, so `repo-b/AGENTS.md` is
8
- * invisible to a model that is nevertheless allowed to write `repo-b`. This row
9
- * closes exactly that gap, and nothing else: the primary root's chain — nested
10
- * files included — and the user-global file stay upstream's business.
11
- *
12
- * Four deliberate choices:
13
- *
14
- * 1. **Not a system-prompt contribution.** Instruction text is
15
- * producer-supplied context, not system authority, so it enters as a
16
- * user-role message tagged `form: 'instructions'`. Through session format 3
17
- * the source kind is `plugin`; format 4 retired that shared kind, so the
18
- * adapter persists this plugin's own (`multi-root-workspace`). It is also
19
- * the only channel that is logged, and model-visible content must be logged.
20
- * 2. **Injected from `agent/pre-step`, not a session lifecycle event.**
21
- * `pre-step` is an awaited waterfall, so discovery, reading and rendering all
22
- * complete BEFORE the step it feeds — deterministically, on the first step.
23
- * A synchronous prompt callback could not await them, and an emit-mode
24
- * lifecycle listener would race the first step. It is also the one hook whose
25
- * shape is identical across every supported release, which keeps the
26
- * instruction feature out of the compatibility matrix (ADR-0009).
27
- * 3. **Discovery is pinned to the root.** `projectRoot` is the additional root,
28
- * so the walk can never leave it, and every candidate is then required to be
29
- * canonically INSIDE that root. That is what keeps `$DSH_HOME/AGENTS.md`, the
30
- * primary root's `AGENTS.md`, and any ancestor's file from being injected a
31
- * second time by this plugin.
32
- * 4. **Nested files are driven by the session's own tool activity.** A
33
- * subdirectory's `AGENTS.md` only matters once the session has worked in that
34
- * subdirectory, so the directories worth examining are derived from the
35
- * session's persisted events: `tool/call` and `tool/result` are paired, and a
36
- * successful `read`/`write`/`edit` makes the path from the root down to the
37
- * touched file's directory relevant (ADR-0010). A file that disappears is
38
- * retracted explicitly, because the text it contributed earlier is still in
39
- * the conversation and silence does not withdraw it.
40
- *
41
- * The byte budget is shared: `maxBytes` bounds the WHOLE additional-root
42
- * snapshot, not each root, because ten roots must not quietly cost ten budgets
43
- * of context. Roots consume it in scope order. Retractions are exempt, because a
44
- * truncated withdrawal is worse than a slightly over-budget message.
45
- *
46
- * @module @dsh-electron/dsh-plugin-multi-root-workspace/instructions
47
- */
48
1
  import { Context } from '@deepseek-ai/cordis';
49
2
  import type { Agent } from '@deepseek-ai/dsh-agent';
50
3
  declare module '@deepseek-ai/cordis' {
@@ -0,0 +1,31 @@
1
+ import type { SandboxMode } from '@deepseek-ai/dsh-sandbox';
2
+ import type { FilesystemScope } from './scope.ts';
3
+ export type RegistryOperationKind = 'fs-write' | 'fs-edit' | 'process' | 'pty' | 'lsp' | 'files' | 'instructions' | 'skills';
4
+ export declare function currentRegistryOperation(proof: object): RegistryOperation | undefined;
5
+ export interface RegistryOperationIdentity {
6
+ readonly operationId: string;
7
+ readonly kind: RegistryOperationKind;
8
+ readonly mode: SandboxMode | undefined;
9
+ readonly primaryRoot: string;
10
+ /** Absent when the registry cannot grant additional authority. */
11
+ readonly authorityEpoch: string | undefined;
12
+ /** Effective publication revision, meaningful only within this epoch. */
13
+ readonly revision: number;
14
+ }
15
+ export interface RegistryOperation {
16
+ readonly identity: RegistryOperationIdentity;
17
+ /** Only readable during this operation's callback. */
18
+ readonly scope: FilesystemScope;
19
+ }
20
+ /** Capture by value: neither later publication nor a caller can mutate admission. */
21
+ export declare function freezeOperationScope(scope: FilesystemScope): FilesystemScope;
22
+ /** Execute outside the authority queue; completion also expires async descendants. */
23
+ export declare function runRegistryOperation<T>(scope: FilesystemScope, kind: RegistryOperationKind, authorityEpoch: string | undefined, revision: number, body: (operation: RegistryOperation) => T | Promise<T>, proof?: object, mode?: SandboxMode): Promise<T>;
24
+ /** Optional Registry seam for public consumers; the legacy composition stays exact. */
25
+ export declare function withRegistryRead<T>(ctx: import('@deepseek-ai/cordis').Context, primary: string, kind: RegistryOperationKind, body: () => T | Promise<T>): Promise<T>;
26
+ export declare function withRegistryAdmission<T>(ctx: import('@deepseek-ai/cordis').Context, policy: import('@deepseek-ai/dsh-sandbox').SandboxExecutionPolicy, kind: RegistryOperationKind, body: () => T | Promise<T>): Promise<T>;
27
+ /** Preserve a legacy synchronous entry while granting only the safe primary-root fallback. */
28
+ export declare function runPrimaryOperation<T>(scope: FilesystemScope, mode: SandboxMode, proof: object, body: () => T): T;
29
+ /** Only profiles produced within this exact admission can reach a sync spawn. */
30
+ export declare function rememberOperationProfile(proof: object, argv: readonly string[], capped: boolean): void;
31
+ export declare function operationPermitsProfile(proof: object, argv: readonly string[], capped: boolean): boolean;