@dsh-electron/dsh-plugin-multi-root-workspace 0.1.15 → 0.1.16

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 (39) hide show
  1. package/CHANGELOG.en.md +13 -1
  2. package/CHANGELOG.md +13 -1
  3. package/README.en.md +6 -6
  4. package/README.md +6 -6
  5. package/lib/auto-review.js +1 -1
  6. package/lib/client.js +761 -74
  7. package/lib/client.js.map +1 -1
  8. package/lib/{command-y5tfcAeC.js → command-DaVJ-mK3.js} +55 -1
  9. package/lib/command.js +1 -1
  10. package/lib/index.js +4 -4
  11. package/lib/{instructions-Ck9wp4J4.js → instructions-Ca80SkZq.js} +1 -1
  12. package/lib/instructions.js +1 -1
  13. package/lib/lsp.js +1 -1
  14. package/lib/process-fence.js +95 -12
  15. package/lib/{registry-DyDNDSJZ.js → registry-BibiukWk.js} +1 -1
  16. package/lib/{registry-operation-C2ZV3Jhw.js → registry-operation-CIlmt6_c.js} +5 -1
  17. package/lib/registry.js +1 -1
  18. package/lib/{review-context-B-oHkU6a.js → review-context-OxO-fMWR.js} +1 -1
  19. package/lib/review-context.js +1 -1
  20. package/lib/{sandbox-CZElTNPz.js → sandbox-aobdyQGi.js} +6 -3
  21. package/lib/sandbox.js +1 -1
  22. package/lib/scope.js +91 -4
  23. package/lib/skills.js +1 -1
  24. package/lib/types/client/files-tree/FilesTree.d.ts +49 -6
  25. package/lib/types/client/files-tree/adapter.d.ts +69 -16
  26. package/lib/types/client/files-tree/face.d.ts +31 -2
  27. package/lib/types/client/files-tree/locate.d.ts +74 -0
  28. package/lib/types/client/files-tree/navigator.d.ts +98 -0
  29. package/lib/types/client/index.d.ts +7 -0
  30. package/lib/types/client/locales.d.ts +2 -0
  31. package/lib/types/client/styles.d.ts +1 -1
  32. package/lib/types/compat/files-tree.d.ts +8 -1
  33. package/lib/types/compat/forge-entrypoints.d.ts +44 -0
  34. package/lib/types/compat/session-existence.d.ts +41 -0
  35. package/lib/types/registry-operation.d.ts +7 -0
  36. package/lib/types/roots.d.ts +3 -1
  37. package/lib/types/scope.d.ts +67 -1
  38. package/lib/workspace-files.js +1 -1
  39. package/package.json +1 -1
package/lib/client.js CHANGED
@@ -128,7 +128,7 @@ window.__ModuleLoader__.load({
128
128
  return typeof value === "function" ? value : void 0;
129
129
  }
130
130
  /** The factory consumes absolute, slash-separated keys and already-localized errors. */
131
- function nativeTreeInput(state, failureLine, toggle, open) {
131
+ function nativeTreeInput(state, failureLine, toggle, open, current) {
132
132
  const root = state.root.replace(/[/\\]+$/, "");
133
133
  const keyOf = (path) => path === state.root ? root : path;
134
134
  const entries = [];
@@ -155,6 +155,10 @@ window.__ModuleLoader__.load({
155
155
  entries,
156
156
  directories,
157
157
  expanded: state.expanded.map(keyOf),
158
+ ...current === void 0 ? {} : {
159
+ current: current.path,
160
+ currentRef: current.ref
161
+ },
158
162
  onToggle: (entry) => {
159
163
  const parent = entry.path.slice(0, entry.path.lastIndexOf("/"));
160
164
  toggle(parent === root ? state.root : parent, entry.path);
@@ -5683,6 +5687,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
5683
5687
  "error.unavailable": "无法连接到 dsh 主进程。",
5684
5688
  "error.copy-failed": "无法写入剪贴板,请检查浏览器的剪贴板权限。",
5685
5689
  "error.session-not-found": "找不到该会话;请先打开一个工作区会话。",
5690
+ "error.session-not-live": "该会话存在但未在本实例激活;它可能正被另一个 DSH 实例持有、仍在恢复,或格式待迁移。若另一实例持有它,请退出那个实例;也可以切换到同一工作区的活动会话;格式待迁移时请在该会话完成迁移。否则请稍后重试。",
5686
5691
  "error.unsupported-path": "该路径是网络或设备路径,Windows ACL 隔离无法覆盖它。",
5687
5692
  "error.protected-path": "该目录与 DSH 主目录或根登记存储目录重叠,不能登记。",
5688
5693
  "error.protected-boundary-unknown": "暂时无法确定 DSH 主目录等受保护目录的位置;恢复之前不能新增登记。",
@@ -5790,6 +5795,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
5790
5795
  "error.unavailable": "Could not reach the dsh host process.",
5791
5796
  "error.copy-failed": "Could not write to the clipboard; check the browser's clipboard permission.",
5792
5797
  "error.session-not-found": "That session is not active; open a workspace session first.",
5798
+ "error.session-not-live": "That session exists but is not active in this instance — it may be held by another DSH instance, still resuming, or awaiting a format migration. If another instance holds it, quit that instance; alternatively, switch to an active session in the same workspace; if a migration is pending, complete it in that session. Otherwise retry shortly.",
5793
5799
  "error.unsupported-path": "That is a network or device path, which Windows ACL confinement cannot cover.",
5794
5800
  "error.protected-path": "That directory overlaps the DSH home or the registry storage directories; it cannot be registered.",
5795
5801
  "error.protected-boundary-unknown": "The protected DSH directories cannot be resolved right now; no folder can be registered until they can.",
@@ -6123,6 +6129,27 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
6123
6129
  background: var(--dsw-alias-interactive-bg-hover);
6124
6130
  }
6125
6131
 
6132
+ /* The document preview's navigator aside, while the retarget holds it: the
6133
+ native body hides (it lists the session's primary root regardless of the
6134
+ previewed file), and the forked tree — portaled in as the aside's sibling
6135
+ child — takes the same stretch item in the aside's column flex, right
6136
+ down to the body's own padding and 2px scrollbar offset. The aside's
6137
+ chrome is untouched: width, push/overlay/fill modes, and the resize
6138
+ separator all stay native. The marker attribute is set only while a
6139
+ takeover actually holds the aside, so a navigator we never matched — or
6140
+ one whose plugin sheet is gone — renders exactly as upstream drew it. */
6141
+ aside[data-mrfw-navigator] > [data-files-body] {
6142
+ display: none;
6143
+ }
6144
+
6145
+ /* The reveal's current row, the fork's spelling of upstream's .current:
6146
+ the interactive-active fill, kept under hover exactly as the native
6147
+ tree's current row keeps it. */
6148
+ .mrfw-ftRow[data-files-current],
6149
+ .mrfw-ftRow[data-files-current]:hover {
6150
+ background: var(--dsw-alias-interactive-bg-active);
6151
+ }
6152
+
6126
6153
  /* ── files-tree fork (verbatim rules, renamed classes) ─────────────────────
6127
6154
  Copied from upstream's FilesBody.module.css (ui-sidebar-files @
6128
6155
  0.2.2-alpha.2) with local names mapped to mrfw-ft* so the rules cannot
@@ -7747,6 +7774,10 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
7747
7774
  /** Per tab, per absolute path: the listing generation a settlement must match; the latest request wins. */
7748
7775
  const generations = /* @__PURE__ */ new Map();
7749
7776
  const roots = /* @__PURE__ */ new Map();
7777
+ /** The root each tab's store bucket belongs to; kept while a hold is released. */
7778
+ const buckets = /* @__PURE__ */ new Map();
7779
+ /** The record signal each tab's store bucket answers to. */
7780
+ const bindings = /* @__PURE__ */ new Map();
7750
7781
  const nextGeneration = (tabId, path) => {
7751
7782
  const byPath = generations.get(tabId) ?? /* @__PURE__ */ new Map();
7752
7783
  generations.set(tabId, byPath);
@@ -7765,6 +7796,39 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
7765
7796
  return result.ok ? result.value : void 0;
7766
7797
  });
7767
7798
  };
7799
+ const closeNode = (tabId) => {
7800
+ const root = roots.get(tabId);
7801
+ roots.delete(tabId);
7802
+ generations.delete(tabId);
7803
+ root?.close();
7804
+ };
7805
+ const forget = (tabId) => {
7806
+ closeNode(tabId);
7807
+ buckets.delete(tabId);
7808
+ bindings.delete(tabId);
7809
+ actions.forget(tabId);
7810
+ };
7811
+ const bindRecord = (tabId, signal) => {
7812
+ if (bindings.get(tabId) === signal) return;
7813
+ bindings.set(tabId, signal);
7814
+ signal.addEventListener("abort", () => {
7815
+ if (bindings.get(tabId) !== signal) return;
7816
+ forget(tabId);
7817
+ }, { once: true });
7818
+ };
7819
+ const open = (tabId, root, signal, restore, reset) => {
7820
+ if (signal.aborted) return;
7821
+ closeNode(tabId);
7822
+ bindRecord(tabId, signal);
7823
+ if (reset || buckets.get(tabId) !== root) {
7824
+ actions.start(tabId, root);
7825
+ buckets.set(tabId, root);
7826
+ restore = [];
7827
+ }
7828
+ roots.set(tabId, new DirectoryNode(root, (path, lifetime) => load(tabId, path, lifetime), (path, lifetime) => watch(sessionId, path, lifetime), (path, error) => {
7829
+ if (!signal.aborted) actions.failed(tabId, path, new RemoteError("gateway/internal", error instanceof Error ? error.message : String(error), {}));
7830
+ }, signal, restore).open());
7831
+ };
7768
7832
  return {
7769
7833
  refresh: (tabId) => {
7770
7834
  roots.get(tabId)?.refreshTree();
@@ -7773,17 +7837,14 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
7773
7837
  actions.autoRefresh(tabId, enabled);
7774
7838
  roots.get(tabId)?.setAutomatic(enabled);
7775
7839
  },
7776
- start(tabId, root, signal) {
7777
- actions.start(tabId, root);
7778
- signal.addEventListener("abort", () => {
7779
- roots.get(tabId)?.close();
7780
- roots.delete(tabId);
7781
- generations.delete(tabId);
7782
- actions.forget(tabId);
7783
- }, { once: true });
7784
- roots.set(tabId, new DirectoryNode(root, (path, lifetime) => load(tabId, path, lifetime), (path, lifetime) => watch(sessionId, path, lifetime), (path, error) => {
7785
- if (!signal.aborted) actions.failed(tabId, path, new RemoteError("gateway/internal", error instanceof Error ? error.message : String(error), {}));
7786
- }, signal).open());
7840
+ start: (tabId, root, signal) => {
7841
+ open(tabId, root, signal, [], true);
7842
+ },
7843
+ hold: (tabId, root, signal, restore = []) => {
7844
+ open(tabId, root, signal, restore, false);
7845
+ },
7846
+ release: (tabId) => {
7847
+ closeNode(tabId);
7787
7848
  },
7788
7849
  load: (tabId, path, signal) => {
7789
7850
  load(tabId, path, signal);
@@ -8032,6 +8093,10 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
8032
8093
  function encodePath(path) {
8033
8094
  return path.split("/").map(encodeSegment).join("/");
8034
8095
  }
8096
+ /** Whether a decoded first path segment is a Windows drive (`C:`). */
8097
+ function isDriveSegment(segment) {
8098
+ return segment !== void 0 && /^[A-Za-z]:$/.test(segment);
8099
+ }
8035
8100
  /**
8036
8101
  * Build the address of a file read through one Session.
8037
8102
  * @param sessionId - the Session whose Host workspace resolves the path.
@@ -8043,6 +8108,44 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
8043
8108
  return `${FILE_ADDRESS_PREFIX}session/${encodeSegment(sessionId)}/${encodePath(normalized)}`;
8044
8109
  }
8045
8110
  /**
8111
+ * Read a file address back into its parts without resolving `.` or `..`.
8112
+ * Query and fragment suffixes are ignored; encoded path segments are decoded.
8113
+ * @param address - a candidate address.
8114
+ * @returns the parts, or `undefined` when the string is not a `dsh-resource://file/` URI in a known scope with a path, or a segment is not validly encoded.
8115
+ */
8116
+ function parseFileAddress(address) {
8117
+ try {
8118
+ if (!address.startsWith(FILE_ADDRESS_PREFIX)) return void 0;
8119
+ const end = address.search(/[?#]/);
8120
+ const [scope, ...rest] = address.slice(20, end === -1 ? void 0 : end).split("/");
8121
+ if (scope === "session") {
8122
+ const [id, ...segments] = rest;
8123
+ if (id === void 0 || id === "" || segments.length === 0) return void 0;
8124
+ return {
8125
+ scope,
8126
+ sessionId: decodeURIComponent(id),
8127
+ path: segments.map(decodeURIComponent).join("/")
8128
+ };
8129
+ }
8130
+ if (scope === "absolute") {
8131
+ const unc = rest[0] === "" && rest.length > 1;
8132
+ const segments = (unc ? rest.slice(1) : rest).map(decodeURIComponent);
8133
+ if (segments.length === 0 || segments[0] === "") return void 0;
8134
+ if (unc) return {
8135
+ scope,
8136
+ path: `//${segments.join("/")}`
8137
+ };
8138
+ return {
8139
+ scope,
8140
+ path: isDriveSegment(segments[0]) ? segments.join("/") : `/${segments.join("/")}`
8141
+ };
8142
+ }
8143
+ return;
8144
+ } catch {
8145
+ return;
8146
+ }
8147
+ }
8148
+ /**
8046
8149
  * Browser-safe Workspace path and display helpers.
8047
8150
  * @module @deepseek-ai/dsh-util-workspace-path
8048
8151
  */
@@ -8077,6 +8180,110 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
8077
8180
  return sessionFileAddress(sessionId, normalized);
8078
8181
  }
8079
8182
  //#endregion
8183
+ //#region src/client/files-tree/locate.ts
8184
+ /**
8185
+ * Whether a root is a Windows drive or UNC path, in either separator
8186
+ * spelling, where names compare regardless of case.
8187
+ * @param root - the tree's root, or a prefix built from it.
8188
+ * @returns `true` for a drive letter followed by a separator, or a leading
8189
+ * double separator.
8190
+ */
8191
+ function windowsRoot(root) {
8192
+ const path = root.replace(/\\/g, "/");
8193
+ return /^[A-Za-z]:\//.test(path) || path.startsWith("//");
8194
+ }
8195
+ /**
8196
+ * An absolute `/`-separated path relative to a tree's root.
8197
+ * @param path - the absolute path, in either separator spelling.
8198
+ * @param root - the tree's root, in either separator spelling.
8199
+ * @returns the path under the root, or `undefined` outside it.
8200
+ */
8201
+ function underRoot(path, root) {
8202
+ const normalized = path.replace(/\\/g, "/");
8203
+ const prefix = `${root.replace(/\\/g, "/").replace(/\/+$/, "")}/`;
8204
+ const head = normalized.slice(0, prefix.length);
8205
+ return (windowsRoot(prefix) ? head.toLowerCase() === prefix.toLowerCase() : head === prefix) ? normalized.slice(prefix.length) : void 0;
8206
+ }
8207
+ /**
8208
+ * The workspace-relative path of a file under a tree's root, validated the
8209
+ * way upstream's `relativeFileOf` tail validates it: nothing absolute, and
8210
+ * no empty or dot segments.
8211
+ * @param path - the file's absolute path, in either separator spelling.
8212
+ * @param root - the root it must sit under.
8213
+ * @returns the `/`-separated relative path, or `undefined` outside the root.
8214
+ */
8215
+ function relativeUnderRoot(path, root) {
8216
+ const normalized = underRoot(path, root);
8217
+ if (normalized === void 0 || normalized === "" || /^([A-Za-z]:)?\//.test(normalized)) return void 0;
8218
+ return normalized.split("/").some((part) => part === "" || part === "." || part === "..") ? void 0 : normalized;
8219
+ }
8220
+ /** One path's comparison spelling: separators unified, trailing slashes dropped. */
8221
+ function normalized(path) {
8222
+ return path.replace(/\\/g, "/").replace(/\/+$/, "");
8223
+ }
8224
+ /**
8225
+ * The additional root that owns one absolute file path: available roots
8226
+ * only, the longest matching root path winning when registrations nest.
8227
+ * @param view - the session's registry view.
8228
+ * @param path - the file's absolute path, in either separator spelling.
8229
+ * @returns the owning root, or `undefined` when the file is outside every
8230
+ * available additional root (the primary root included).
8231
+ */
8232
+ function rootForPath(view, path) {
8233
+ let best;
8234
+ const primary = view.primaryRoot === void 0 ? void 0 : normalized(view.primaryRoot);
8235
+ for (const root of view.roots) {
8236
+ if (root.state !== "available" || primary !== void 0 && normalized(root.path) === primary) continue;
8237
+ if (underRoot(path, root.path) === void 0) continue;
8238
+ if (best === void 0 || root.path.length > best.path.length) best = root;
8239
+ }
8240
+ return best;
8241
+ }
8242
+ /**
8243
+ * Walk the listed levels to a workspace-relative file, keeping the listing's
8244
+ * spelling of each name; a Windows root also matches names regardless of
8245
+ * case. A verbatim port of upstream's navigator helper — the fork's
8246
+ * `LevelState` carries no `refreshing` flag, so a stale level reports
8247
+ * `pending: false` like a settled native one.
8248
+ * @param tree - the tab's tree.
8249
+ * @param relativePath - the file's path under the tree's root, `/`-separated.
8250
+ * @returns the ancestors found so far, the file's tree key, and whether a
8251
+ * level is pending.
8252
+ */
8253
+ function locateFile(tree, relativePath) {
8254
+ const caseless = windowsRoot(tree.root);
8255
+ const parts = relativePath.split("/");
8256
+ const ancestors = [];
8257
+ let parent = tree.root;
8258
+ for (const [index, part] of parts.entries()) {
8259
+ const level = tree.levels[parent];
8260
+ if (level?.kind !== "ready") return {
8261
+ ancestors,
8262
+ current: void 0,
8263
+ pending: level === void 0 || level.kind === "loading"
8264
+ };
8265
+ const type = index === parts.length - 1 ? "file" : "directory";
8266
+ const candidates = level.level.entries.filter((entry) => entry.type === type);
8267
+ const entry = candidates.find((candidate) => candidate.name === part) ?? (caseless ? candidates.find((candidate) => candidate.name.toLowerCase() === part.toLowerCase()) : void 0);
8268
+ if (entry === void 0) return {
8269
+ ancestors,
8270
+ current: void 0,
8271
+ pending: false
8272
+ };
8273
+ const path = childPath(parent, entry.name);
8274
+ if (type === "directory") ancestors.push({
8275
+ parent,
8276
+ path
8277
+ });
8278
+ parent = path;
8279
+ }
8280
+ return {
8281
+ ancestors,
8282
+ current: parent,
8283
+ pending: false
8284
+ };
8285
+ }
8286
+ //#endregion
8080
8287
  //#region src/client/files-tree/FilesBody.module.css.ts
8081
8288
  /**
8082
8289
  * Shim for the CSS-module virtual module `./FilesBody.module.css`.
@@ -8182,26 +8389,32 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
8182
8389
  })]
8183
8390
  });
8184
8391
  }
8185
- if (entry.type === "file") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", {
8186
- className: css$1.item,
8187
- "data-files-entry": "file",
8188
- "data-files-path": path,
8189
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
8190
- type: "button",
8191
- className: css$1.row,
8192
- onClick: () => {
8193
- tree.onOpen(path);
8194
- },
8195
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(FileTypeIcon, {
8196
- kind: classifyFileType(entry.name),
8197
- size: 16,
8198
- className: css$1.fileIcon
8199
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
8200
- className: css$1.name,
8201
- children: entry.name
8202
- })]
8203
- })
8204
- });
8392
+ if (entry.type === "file") {
8393
+ const current = tree.current === path;
8394
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", {
8395
+ className: css$1.item,
8396
+ "data-files-entry": "file",
8397
+ "data-files-path": path,
8398
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
8399
+ type: "button",
8400
+ className: css$1.row,
8401
+ onClick: () => {
8402
+ tree.onOpen(path);
8403
+ },
8404
+ ref: current ? tree.currentRef : void 0,
8405
+ "data-files-current": current ? "" : void 0,
8406
+ "aria-current": current ? true : void 0,
8407
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(FileTypeIcon, {
8408
+ kind: classifyFileType(entry.name),
8409
+ size: 16,
8410
+ className: css$1.fileIcon
8411
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
8412
+ className: css$1.name,
8413
+ children: entry.name
8414
+ })]
8415
+ })
8416
+ });
8417
+ }
8205
8418
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", {
8206
8419
  className: css$1.item,
8207
8420
  "data-files-entry": "other",
@@ -8261,46 +8474,181 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
8261
8474
  * has opened under it — the upstream body's scroll, seed, and render logic
8262
8475
  * verbatim, with the root arriving as a prop instead of `useSessions(cwd)`.
8263
8476
  */
8264
- function FilesTree({ tabId, sessionId, root, signal, useStore, actions, start, toggle, onOpen, t, nativeTree }) {
8477
+ function FilesTree({ tabId, sessionId, root, signal, useStore, actions, hold, release, toggle, onOpen, t, nativeTree, reveal, saved, onScrollOffset }) {
8265
8478
  const state = useStore((store) => store.byTab[tabId]);
8266
8479
  const bodyRef = (0, react.useRef)(null);
8267
8480
  const scrollTopRef = (0, react.useRef)(0);
8481
+ const [current, setCurrent] = (0, react.useState)(void 0);
8482
+ const walk = (0, react.useRef)({
8483
+ key: void 0,
8484
+ settled: false,
8485
+ canceled: false
8486
+ });
8487
+ const revealSeed = (0, react.useRef)(saved?.revealed);
8488
+ const seededKey = (0, react.useRef)(void 0);
8489
+ const wantScroll = (0, react.useRef)(false);
8490
+ const currentRef = (0, react.useCallback)((row) => {
8491
+ if (row instanceof HTMLElement && wantScroll.current) {
8492
+ wantScroll.current = false;
8493
+ row.scrollIntoView({
8494
+ block: "nearest",
8495
+ inline: "nearest"
8496
+ });
8497
+ }
8498
+ }, []);
8499
+ const revealPath = reveal?.path;
8500
+ const revealSaved = saved?.revealed;
8501
+ const revealOnSettled = reveal?.onSettled;
8502
+ const revealKey = revealPath === void 0 || state === void 0 ? void 0 : JSON.stringify([state.root, revealPath]);
8503
+ (0, react.useEffect)(() => {
8504
+ if (revealPath === void 0 || revealKey === void 0 || state === void 0 || signal.aborted) {
8505
+ seededKey.current = void 0;
8506
+ setCurrent(void 0);
8507
+ return;
8508
+ }
8509
+ if (walk.current.key !== revealKey || seededKey.current !== revealKey) {
8510
+ walk.current = {
8511
+ key: revealKey,
8512
+ settled: false,
8513
+ canceled: false
8514
+ };
8515
+ revealSeed.current = revealSaved;
8516
+ seededKey.current = revealKey;
8517
+ wantScroll.current = false;
8518
+ }
8519
+ const relative = relativeUnderRoot(revealPath, state.root);
8520
+ if (relative === void 0) {
8521
+ setCurrent(void 0);
8522
+ return;
8523
+ }
8524
+ const location = locateFile(state, relative);
8525
+ if (walk.current.settled || walk.current.canceled || revealSeed.current === revealPath) {
8526
+ setCurrent(location?.current);
8527
+ return;
8528
+ }
8529
+ const next = location?.ancestors.find((ancestor) => !state.expanded.includes(ancestor.path));
8530
+ if (location !== void 0 && next !== void 0) {
8531
+ toggle(tabId, next.parent, next.path, state.expanded, signal);
8532
+ setCurrent(void 0);
8533
+ return;
8534
+ }
8535
+ if (location?.pending === true) return;
8536
+ walk.current.settled = true;
8537
+ revealSeed.current = void 0;
8538
+ revealOnSettled?.(revealPath);
8539
+ if (location?.current !== void 0) wantScroll.current = true;
8540
+ setCurrent(location?.current);
8541
+ }, [
8542
+ revealKey,
8543
+ state,
8544
+ tabId,
8545
+ signal,
8546
+ toggle,
8547
+ revealPath,
8548
+ revealOnSettled,
8549
+ revealSaved
8550
+ ]);
8551
+ const pendingScroll = (0, react.useRef)(0);
8552
+ const stateRef = (0, react.useRef)(state);
8553
+ const savedRef = (0, react.useRef)(saved);
8554
+ (0, react.useLayoutEffect)(() => {
8555
+ stateRef.current = state;
8556
+ savedRef.current = saved;
8557
+ });
8268
8558
  const seeded = state !== void 0;
8269
8559
  (0, react.useLayoutEffect)(() => {
8270
8560
  const body = bodyRef.current;
8271
8561
  if (seeded && body !== null) {
8272
8562
  body.scrollTop = state.scrollTop;
8273
8563
  scrollTopRef.current = body.scrollTop;
8564
+ if ((body.parentElement?.querySelector("[data-files-body]"))?.contains(body.ownerDocument.activeElement) === true) body.focus();
8274
8565
  }
8275
8566
  }, [seeded]);
8567
+ (0, react.useLayoutEffect)(() => {
8568
+ const body = bodyRef.current;
8569
+ if (body === null || pendingScroll.current <= 0) return;
8570
+ const seededIdentity = seededKey.current === revealKey;
8571
+ const seed = seededIdentity ? revealSeed.current : revealSaved;
8572
+ const inFlight = revealPath !== void 0 && !(seededIdentity && (walk.current.settled || walk.current.canceled)) && seed !== revealPath;
8573
+ if (inFlight || body.scrollHeight > body.clientHeight) {
8574
+ if (!inFlight) {
8575
+ body.scrollTop = pendingScroll.current;
8576
+ scrollTopRef.current = body.scrollTop;
8577
+ }
8578
+ pendingScroll.current = 0;
8579
+ }
8580
+ });
8581
+ (0, react.useLayoutEffect)(() => {
8582
+ const body = bodyRef.current;
8583
+ if (body === null) return;
8584
+ const scroll = () => {
8585
+ if (body.scrollTop !== scrollTopRef.current && pendingScroll.current > 0) pendingScroll.current = 0;
8586
+ scrollTopRef.current = body.scrollTop;
8587
+ onScrollOffset?.(scrollTopRef.current);
8588
+ };
8589
+ body.addEventListener("scroll", scroll, { passive: true });
8590
+ return () => {
8591
+ body.removeEventListener("scroll", scroll);
8592
+ };
8593
+ }, [seeded, onScrollOffset]);
8276
8594
  (0, react.useEffect)(() => () => {
8277
8595
  if (seeded && !signal.aborted) actions.scrolled(tabId, scrollTopRef.current);
8596
+ onScrollOffset?.(scrollTopRef.current);
8278
8597
  }, [
8279
8598
  seeded,
8280
8599
  signal,
8281
8600
  tabId,
8282
- actions
8601
+ actions,
8602
+ onScrollOffset
8283
8603
  ]);
8284
8604
  (0, react.useEffect)(() => {
8285
- if (state !== void 0 || signal.aborted) return;
8286
- start(tabId, root, signal);
8605
+ if (signal.aborted) return;
8606
+ const current = stateRef.current;
8607
+ const resumed = current !== void 0 && current.root === root;
8608
+ hold(tabId, root, signal, resumed ? current.expanded : void 0);
8609
+ if (!resumed) {
8610
+ const snapshot = savedRef.current;
8611
+ if (snapshot?.expanded !== void 0) for (const path of snapshot.expanded) {
8612
+ if (path === root || relativeUnderRoot(path, root) === void 0) continue;
8613
+ toggle(tabId, path.slice(0, path.lastIndexOf("/")), path, snapshot.expanded.filter((value) => value !== path), signal);
8614
+ }
8615
+ if (snapshot?.scrollTop !== void 0) pendingScroll.current = snapshot.scrollTop;
8616
+ }
8617
+ return () => {
8618
+ release(tabId);
8619
+ };
8287
8620
  }, [
8288
- state,
8289
8621
  root,
8290
8622
  tabId,
8291
8623
  signal,
8292
- start
8624
+ hold,
8625
+ release,
8626
+ toggle
8293
8627
  ]);
8294
8628
  if (state === void 0) return null;
8295
8629
  const tree = {
8296
8630
  state,
8297
8631
  onToggle: (parent, path) => {
8632
+ const seededIdentity = seededKey.current === revealKey;
8633
+ const seed = seededIdentity ? revealSeed.current : revealSaved;
8634
+ if (revealPath !== void 0 && revealKey !== void 0 && seed !== revealPath && state.expanded.includes(path) && (!seededIdentity || !walk.current.settled && !walk.current.canceled)) {
8635
+ if (!seededIdentity) walk.current = {
8636
+ key: revealKey,
8637
+ settled: false,
8638
+ canceled: true
8639
+ };
8640
+ else walk.current.canceled = true;
8641
+ revealSeed.current = void 0;
8642
+ revealOnSettled?.(revealPath);
8643
+ }
8298
8644
  toggle(tabId, parent, path, state.expanded, signal);
8299
8645
  },
8300
8646
  onOpen: (path) => {
8301
8647
  onOpen(fileAddressFor(sessionId, void 0, path));
8302
8648
  },
8303
- t
8649
+ t,
8650
+ current,
8651
+ currentRef
8304
8652
  };
8305
8653
  const legacy = /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
8306
8654
  className: css$1.level,
@@ -8309,13 +8657,14 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
8309
8657
  tree
8310
8658
  })
8311
8659
  });
8312
- const content = nativeTree === void 0 ? legacy : nativeTree("sidebar.files.tree", nativeTreeInput(state, (failure) => failureLine(t, failure), tree.onToggle, tree.onOpen), { fallback: legacy });
8660
+ const content = nativeTree === void 0 ? legacy : nativeTree("sidebar.files.tree", nativeTreeInput(state, (failure) => failureLine(t, failure), tree.onToggle, tree.onOpen, current === void 0 ? void 0 : {
8661
+ path: current,
8662
+ ref: currentRef
8663
+ }), { fallback: legacy });
8313
8664
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
8314
8665
  ref: bodyRef,
8315
8666
  className: css$1.body,
8316
- onScroll: (event) => {
8317
- scrollTopRef.current = event.currentTarget.scrollTop;
8318
- },
8667
+ tabIndex: -1,
8319
8668
  children: content
8320
8669
  });
8321
8670
  }
@@ -8395,9 +8744,10 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
8395
8744
  /**
8396
8745
  * The registration's `store`/`inject` declarations, built once in `apply`.
8397
8746
  *
8398
- * The inject face nests the verbatim files face under `face` and adds the
8399
- * session-bound `openFile` — keeping `FilesInjected` untouched means the
8400
- * fork's call sites and the sync diff stay upstream-shaped.
8747
+ * The inject face nests the files face under `face` and adds the
8748
+ * session-bound `openFile`; the fork's call sites stay upstream-shaped, the
8749
+ * one lifecycle delta being the hold/release pair the preview navigator needs
8750
+ * (see FORK.md).
8401
8751
  * @param ctx - the client root context (the remote/sidebarRight lookups).
8402
8752
  * @returns the store handle and the inject factory for `register`.
8403
8753
  */
@@ -8408,17 +8758,47 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
8408
8758
  const inject = (sessionId, actions) => {
8409
8759
  const selection = (0, _deepseek_ai_dsh_client_store.createSnapshotStore)({});
8410
8760
  const takeovers = /* @__PURE__ */ new Map();
8411
- const endTakeover = (tabId) => {
8412
- const held = takeovers.get(tabId);
8413
- held?.observer?.disconnect();
8414
- held?.controller.abort();
8415
- takeovers.delete(tabId);
8761
+ const savedTrees = /* @__PURE__ */ new Map();
8762
+ const watchers = /* @__PURE__ */ new Map();
8763
+ let watcherObserver;
8764
+ const endTakeover = (key, dropSaved = false) => {
8765
+ takeovers.get(key)?.controller.abort();
8766
+ takeovers.delete(key);
8767
+ if (dropSaved) savedTrees.delete(key);
8768
+ };
8769
+ const dropDeadOwners = () => {
8770
+ for (const [key, alive] of watchers) if (!alive()) {
8771
+ watchers.delete(key);
8772
+ endTakeover(key, true);
8773
+ }
8774
+ if (watchers.size === 0) {
8775
+ watcherObserver?.disconnect();
8776
+ watcherObserver = void 0;
8777
+ }
8778
+ };
8779
+ const armDeathWatch = (key, alive) => {
8780
+ watchers.set(key, alive);
8781
+ if (watcherObserver === void 0) {
8782
+ watcherObserver = new MutationObserver((mutations) => {
8783
+ if (mutations.some((mutation) => mutation.type === "attributes" || mutation.addedNodes.length !== 0 || mutation.removedNodes.length !== 0)) dropDeadOwners();
8784
+ });
8785
+ watcherObserver.observe(document, {
8786
+ childList: true,
8787
+ subtree: true,
8788
+ attributes: true,
8789
+ attributeFilter: ["data-dockkit-pane", "data-dockkit-float"]
8790
+ });
8791
+ }
8792
+ dropDeadOwners();
8416
8793
  };
8417
8794
  return {
8418
8795
  face: face(sessionId, actions),
8419
8796
  openFile: (address) => {
8420
8797
  ctx.get("sidebarRight")?.openResourceIn?.(sessionId, address);
8421
8798
  },
8799
+ openFileIn: (tabId, address) => {
8800
+ ctx.get("sidebarRight")?.openResourceIn?.(sessionId, address, { replaceTab: tabId });
8801
+ },
8422
8802
  hooks: { treeSelection: selection },
8423
8803
  selectRoot: (tabId, root) => {
8424
8804
  const next = { ...selection.getSnapshot() };
@@ -8427,34 +8807,37 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
8427
8807
  selection.set(next);
8428
8808
  },
8429
8809
  takeover: {
8430
- signal: (tabId, root, section) => {
8431
- let held = takeovers.get(tabId);
8810
+ signal: (key, root, section) => {
8811
+ let held = takeovers.get(key);
8432
8812
  if (held === void 0 || held.root !== root || held.controller.signal.aborted) {
8433
- held?.observer?.disconnect();
8434
8813
  held?.controller.abort();
8435
8814
  held = {
8436
8815
  controller: new AbortController(),
8437
- root,
8438
- section: void 0,
8439
- observer: void 0
8816
+ root
8440
8817
  };
8441
- takeovers.set(tabId, held);
8818
+ takeovers.set(key, held);
8442
8819
  }
8443
- if (section !== void 0 && held.section !== section) {
8444
- held.section = section;
8445
- held.observer?.disconnect();
8446
- held.observer = new MutationObserver(() => {
8447
- if (!section.isConnected) endTakeover(tabId);
8448
- });
8449
- held.observer.observe(document, {
8450
- childList: true,
8451
- subtree: true
8452
- });
8453
- }
8454
- if (held.section !== void 0 && !held.section.isConnected) endTakeover(tabId);
8820
+ if (section !== void 0) armDeathWatch(key, typeof section === "function" ? section : () => section.isConnected);
8455
8821
  return held.controller.signal;
8456
8822
  },
8457
- end: endTakeover
8823
+ end: (key) => {
8824
+ endTakeover(key);
8825
+ },
8826
+ saved: (key, root) => savedTrees.get(key)?.get(root),
8827
+ save: (key, snapshot) => {
8828
+ if (!takeovers.has(key) && !watchers.has(key)) return;
8829
+ let perRoot = savedTrees.get(key);
8830
+ if (perRoot === void 0) savedTrees.set(key, perRoot = /* @__PURE__ */ new Map());
8831
+ const previous = perRoot.get(snapshot.root);
8832
+ const merged = { root: snapshot.root };
8833
+ const expanded = snapshot.expanded ?? previous?.expanded;
8834
+ const scrollTop = snapshot.scrollTop ?? previous?.scrollTop;
8835
+ const revealed = snapshot.revealed ?? previous?.revealed;
8836
+ if (expanded !== void 0) merged.expanded = expanded;
8837
+ if (scrollTop !== void 0) merged.scrollTop = scrollTop;
8838
+ if (revealed !== void 0) merged.revealed = revealed;
8839
+ perRoot.set(snapshot.root, merged);
8840
+ }
8458
8841
  }
8459
8842
  };
8460
8843
  };
@@ -8474,14 +8857,15 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
8474
8857
  */
8475
8858
  function createFilesTreeSurface(props, t) {
8476
8859
  if (!filesTreeSurfaceSupported()) return void 0;
8477
- const { sessionId, useStore, actions, face, openFile, useTreeSelection, selectRoot, takeover } = props;
8478
- if (face === void 0 || openFile === void 0 || useStore === void 0 || actions === void 0 || useTreeSelection === void 0 || selectRoot === void 0 || takeover === void 0) return void 0;
8860
+ const { sessionId, useStore, actions, face, openFile, openFileIn, useTreeSelection, selectRoot, takeover } = props;
8861
+ if (face === void 0 || openFile === void 0 || openFileIn === void 0 || useStore === void 0 || actions === void 0 || useTreeSelection === void 0 || selectRoot === void 0 || takeover === void 0) return void 0;
8479
8862
  return {
8480
8863
  sessionId,
8481
8864
  useStore,
8482
8865
  actions,
8483
8866
  face,
8484
8867
  openFile,
8868
+ openFileIn,
8485
8869
  useTreeSelection,
8486
8870
  selectRoot,
8487
8871
  takeover,
@@ -10373,7 +10757,8 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
10373
10757
  signal: takeover.signal,
10374
10758
  useStore: filesTree.useStore,
10375
10759
  actions: filesTree.actions,
10376
- start: filesTree.face.start,
10760
+ hold: filesTree.face.hold,
10761
+ release: filesTree.face.release,
10377
10762
  toggle: filesTree.face.toggle,
10378
10763
  onOpen: filesTree.openFile,
10379
10764
  t: filesTree.t,
@@ -10393,9 +10778,293 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
10393
10778
  ] });
10394
10779
  }
10395
10780
  //#endregion
10781
+ //#region src/client/files-tree/navigator.tsx
10782
+ /**
10783
+ * The document-preview navigator's additional-root retarget.
10784
+ *
10785
+ * Upstream's `sidebar.files.navigator` Factory (`ui-sidebar-files`, added at
10786
+ * `0.2.2-alpha.3`) is the Folder-toggle tree the document preview draws
10787
+ * beside its body. Its root is hard-wired to the session's `cwd` — the
10788
+ * previewed file's absolute path is located against that root, so a file
10789
+ * that lives under an additional root lands outside every tree it can draw:
10790
+ * the navigator opens on the primary root, unexpanded and unmarked.
10791
+ *
10792
+ * The Factory itself cannot be retargeted — it is a registration upstream
10793
+ * already owns, and its root is not an input. What CAN move is what its
10794
+ * `aside[data-files-navigator]` draws: this occupant mounts in the preview
10795
+ * header's `sidebar.right.tab.document.actions` seat (owner prop
10796
+ * `absolutePath` — the previewed file's host path), watches the preview's
10797
+ * `[data-textpreview-content]` row for the aside, and when the file sits
10798
+ * under an available additional root it lends the aside's body area to the
10799
+ * forked tree, the same takeover the Files page's root switcher performs.
10800
+ * The aside itself — its width, its push/overlay/fill modes, its resize
10801
+ * separator — stays native; only the tree inside is the fork's.
10802
+ *
10803
+ * Everything is keyed by the dockkit PANE id (`data-files-navigator` on the
10804
+ * aside, `data-dockkit-pane` on the content section), never by the preview
10805
+ * tab: upstream's navigator records live in `store.byPane`, and the pane
10806
+ * survives what the tab does not — `replaceTab` mints a fresh tab id, so
10807
+ * opening a file from the retargeted tree destroys the preview tab that
10808
+ * opened it. The inject-side takeover map holds the pane-record memory
10809
+ * (`expanded`/`scrollTop`/`revealed`) across that death; the component's
10810
+ * own refs only ever carry the live copy.
10811
+ *
10812
+ * Three upstream behaviors are reproduced, not approximated:
10813
+ *
10814
+ * - **Hold lifetime.** The native navigator holds its pane tree only while
10815
+ * it is open (`hold(paneId, cwd)` gated on `state.open`): closing the
10816
+ * navigator closes the directory node — watches and listings end — while
10817
+ * the pane record keeps `expanded`/`scrollTop`/`revealed` for the next
10818
+ * open. Here the takeover `signal` is requested when the aside appears
10819
+ * and ended when it leaves. The saved snapshot is written into the
10820
+ * inject-side map continuously — the bucket's expansion set mirrors into
10821
+ * it, the forked body reports its live scroll offset, and a settled
10822
+ * reveal records its file — so no cleanup ordering ever decides what is
10823
+ * remembered. A dockkit pane has no stable element (a non-empty pane's
10824
+ * id rides on its selected tab's host, which `replaceTab` swaps out), so
10825
+ * the owner's death is checked by query: the pane lives while some
10826
+ * connected element still carries its id, and the watch that drops the
10827
+ * memory outlives every `end` because the body's cleanup always wins
10828
+ * the unmount race.
10829
+ * - **Reveal.** The native navigator seeds `pane.revealing` with the drawn
10830
+ * file's address — unless `pane.revealed` already names it — and steps
10831
+ * the walk open (ancestors expanded one listing at a time, then the
10832
+ * file's row marked current and scrolled into view). The forked tree's
10833
+ * `reveal` prop drives the same sequence against the additional root,
10834
+ * with the saved `revealed` suppressing a repeat of a completed walk, so
10835
+ * collapsing a directory and reopening the navigator keeps the reader's
10836
+ * collapse exactly as upstream's pane record does.
10837
+ * - **Replace-tab open.** The native navigator's `onOpen` is
10838
+ * `tab.actions.openResource(address, { replaceTab: true })` — the opened
10839
+ * file takes the preview's own tab. The forked tree's opens route through
10840
+ * `openFileIn`, which spells the same intent to `openResourceIn` as
10841
+ * `{ replaceTab: tabId }` against the preview's dockkit tab id.
10842
+ *
10843
+ * Every step degrades silently: no `[data-textpreview-url]` ancestor, no
10844
+ * dockkit pane id, no `panel`, an address that is not a session file of
10845
+ * THIS session, a file outside every available additional root (the
10846
+ * primary root's own files included), or a fork that cannot render — each
10847
+ * leaves the native navigator completely untouched. Where the upstream
10848
+ * hooks do not exist (a runtime without this navigator), the marker span
10849
+ * is the only trace the occupant leaves, and it draws nothing.
10850
+ *
10851
+ * @module @dsh-electron/dsh-plugin-multi-root-workspace/client/files-tree/navigator
10852
+ */
10853
+ /**
10854
+ * A dockkit pane has no stable element: a non-empty pane's id rides on its
10855
+ * SELECTED tab's host (`data-dockkit-pane`), an empty pane's on its
10856
+ * `TabPanel`, and a floating pane's host carries `data-dockkit-float` —
10857
+ * `replaceTab` swaps the host while the pane lives on. The owner is
10858
+ * therefore alive while some connected element still carries its id.
10859
+ * @param id - the dockkit pane id the aside announced.
10860
+ * @returns whether the document still contains a carrier of the id.
10861
+ */
10862
+ function paneAlive(id) {
10863
+ for (const element of document.querySelectorAll("[data-dockkit-pane],[data-dockkit-float]")) if (element.dataset.dockkitPane === id || element.dataset.dockkitFloat === id) return true;
10864
+ return false;
10865
+ }
10866
+ /**
10867
+ * The marker that lives in the preview header and, while the file it names
10868
+ * sits under an additional root, lends the navigator aside to the forked
10869
+ * tree. It draws nothing of its own.
10870
+ */
10871
+ function NavigatorRetarget(props) {
10872
+ const { absolutePath, sessionId, panel, filesTree } = props;
10873
+ const anchorRef = (0, react.useRef)(null);
10874
+ const sectionRef = (0, react.useRef)(null);
10875
+ const fallbackPaneId = (0, react.useRef)(void 0);
10876
+ const paneIdentified = (0, react.useRef)(false);
10877
+ const mounted = (0, react.useRef)(true);
10878
+ const [preview, setPreview] = (0, react.useState)(null);
10879
+ const [tabId, setTabId] = (0, react.useState)(void 0);
10880
+ const [aside, setAside] = (0, react.useState)(null);
10881
+ const [paneId, setPaneId] = (0, react.useState)(void 0);
10882
+ const [root, setRoot] = (0, react.useState)(void 0);
10883
+ const [resolvedPath, setResolvedPath] = (0, react.useState)(void 0);
10884
+ const [takeover, setTakeover] = (0, react.useState)(null);
10885
+ (0, react.useEffect)(() => {
10886
+ mounted.current = true;
10887
+ return () => {
10888
+ mounted.current = false;
10889
+ };
10890
+ }, []);
10891
+ (0, react.useEffect)(() => {
10892
+ const anchor = anchorRef.current;
10893
+ const previewEl = anchor?.closest("[data-textpreview-url]") ?? null;
10894
+ sectionRef.current = anchor?.closest("[data-dockkit-content]") ?? null;
10895
+ const content = sectionRef.current?.dataset.dockkitContent;
10896
+ setTabId(content !== void 0 && content !== "" ? content : fallbackPaneId.current ??= `mrfw-nav-${Math.random().toString(36).slice(2)}`);
10897
+ setPreview(previewEl);
10898
+ }, []);
10899
+ const asideRef = (0, react.useRef)(null);
10900
+ (0, react.useEffect)(() => {
10901
+ if (preview === null) {
10902
+ setAside(null);
10903
+ return;
10904
+ }
10905
+ const content = preview.querySelector("[data-textpreview-content]") ?? preview;
10906
+ const query = () => {
10907
+ const current = asideRef.current;
10908
+ if (current !== null && current.isConnected) return;
10909
+ const found = content.querySelector("aside[data-files-navigator]");
10910
+ if (found !== current) {
10911
+ asideRef.current = found;
10912
+ setAside(found);
10913
+ }
10914
+ };
10915
+ query();
10916
+ const observer = new MutationObserver(query);
10917
+ observer.observe(content, {
10918
+ childList: true,
10919
+ subtree: true
10920
+ });
10921
+ return () => {
10922
+ observer.disconnect();
10923
+ asideRef.current = null;
10924
+ };
10925
+ }, [preview]);
10926
+ (0, react.useEffect)(() => {
10927
+ if (aside === null) return;
10928
+ const read = () => {
10929
+ const identified = aside.dataset.filesNavigator ?? sectionRef.current?.dataset.dockkitPane;
10930
+ paneIdentified.current = identified !== void 0 && identified !== "";
10931
+ setPaneId(identified !== void 0 && identified !== "" ? identified : fallbackPaneId.current ??= `mrfw-nav-${Math.random().toString(36).slice(2)}`);
10932
+ };
10933
+ read();
10934
+ const observer = new MutationObserver(read);
10935
+ observer.observe(aside, {
10936
+ attributes: true,
10937
+ attributeFilter: ["data-files-navigator"]
10938
+ });
10939
+ const section = sectionRef.current;
10940
+ if (section !== null) observer.observe(section, {
10941
+ attributes: true,
10942
+ attributeFilter: ["data-dockkit-pane"]
10943
+ });
10944
+ return () => {
10945
+ observer.disconnect();
10946
+ };
10947
+ }, [aside]);
10948
+ (0, react.useEffect)(() => {
10949
+ if (preview === null || aside === null || sessionId === void 0 || sessionId === "" || panel === void 0) {
10950
+ setRoot(void 0);
10951
+ setResolvedPath(void 0);
10952
+ return;
10953
+ }
10954
+ const address = preview.dataset.textpreviewUrl === void 0 ? void 0 : parseFileAddress(preview.dataset.textpreviewUrl);
10955
+ if (address?.scope !== "session" || address.sessionId !== sessionId) {
10956
+ setRoot(void 0);
10957
+ setResolvedPath(void 0);
10958
+ return;
10959
+ }
10960
+ const path = absolutePath;
10961
+ const controller = new AbortController();
10962
+ panel.call("list", { sessionId }, controller.signal).then((view) => {
10963
+ if (controller.signal.aborted || !mounted.current) return;
10964
+ setRoot(rootForPath(view, path));
10965
+ setResolvedPath(path);
10966
+ }).catch(() => {
10967
+ if (controller.signal.aborted || !mounted.current) return;
10968
+ setRoot(void 0);
10969
+ setResolvedPath(path);
10970
+ });
10971
+ return () => {
10972
+ controller.abort();
10973
+ };
10974
+ }, [
10975
+ preview,
10976
+ aside,
10977
+ sessionId,
10978
+ panel,
10979
+ absolutePath
10980
+ ]);
10981
+ const rootPath = root?.path;
10982
+ const takeoverCtl = filesTree.takeover;
10983
+ const bucket = filesTree.useStore((store) => paneId === void 0 ? void 0 : store.byTab[paneId]);
10984
+ (0, react.useEffect)(() => {
10985
+ if (bucket !== void 0 && takeover !== null && takeover.paneId === paneId && takeover.root === rootPath) takeoverCtl.save(takeover.paneId, {
10986
+ root: takeover.root,
10987
+ expanded: bucket.expanded
10988
+ });
10989
+ }, [
10990
+ bucket,
10991
+ takeover,
10992
+ paneId,
10993
+ rootPath,
10994
+ takeoverCtl
10995
+ ]);
10996
+ const onScrollOffset = (0, react.useCallback)((offset) => {
10997
+ if (takeover !== null) takeoverCtl.save(takeover.paneId, {
10998
+ root: takeover.root,
10999
+ scrollTop: offset
11000
+ });
11001
+ }, [takeover, takeoverCtl]);
11002
+ const savedTree = takeover === null ? void 0 : takeoverCtl.saved(takeover.paneId, takeover.root);
11003
+ (0, react.useEffect)(() => {
11004
+ if (aside === null || rootPath === void 0 || paneId === void 0 || takeoverCtl === void 0) {
11005
+ setTakeover(null);
11006
+ return;
11007
+ }
11008
+ setTakeover({
11009
+ signal: takeoverCtl.signal(paneId, rootPath, paneIdentified.current ? () => paneAlive(paneId) : void 0),
11010
+ paneId,
11011
+ root: rootPath
11012
+ });
11013
+ aside.setAttribute("data-mrfw-navigator", "");
11014
+ const capturedPaneId = paneId;
11015
+ const identified = paneIdentified.current;
11016
+ return () => {
11017
+ aside.removeAttribute("data-mrfw-navigator");
11018
+ if (!identified) takeoverCtl.end(capturedPaneId);
11019
+ };
11020
+ }, [
11021
+ aside,
11022
+ rootPath,
11023
+ paneId,
11024
+ takeoverCtl
11025
+ ]);
11026
+ const onRevealSettled = (0, react.useCallback)((path) => {
11027
+ if (takeover !== null) takeoverCtl.save(takeover.paneId, {
11028
+ root: takeover.root,
11029
+ revealed: path
11030
+ });
11031
+ }, [takeover, takeoverCtl]);
11032
+ const revealLive = takeover !== null && rootPath !== void 0 && resolvedPath === absolutePath && takeover.root === rootPath;
11033
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
11034
+ ref: anchorRef,
11035
+ "data-mrfw-navigator-anchor": true,
11036
+ style: { display: "none" }
11037
+ }), aside !== null && takeover !== null ? (0, react_dom.createPortal)(/* @__PURE__ */ (0, react_jsx_runtime.jsx)(FilesTree, {
11038
+ tabId: takeover.paneId,
11039
+ sessionId: filesTree.sessionId,
11040
+ root: takeover.root,
11041
+ signal: takeover.signal,
11042
+ useStore: filesTree.useStore,
11043
+ actions: filesTree.actions,
11044
+ hold: filesTree.face.hold,
11045
+ release: filesTree.face.release,
11046
+ toggle: filesTree.face.toggle,
11047
+ onOpen: (address) => {
11048
+ if (tabId === void 0) filesTree.openFile(address);
11049
+ else filesTree.openFileIn(tabId, address);
11050
+ },
11051
+ t: filesTree.t,
11052
+ nativeTree: props.nativeTree,
11053
+ onScrollOffset,
11054
+ reveal: revealLive ? {
11055
+ path: absolutePath,
11056
+ revealed: savedTree?.revealed,
11057
+ onSettled: onRevealSettled
11058
+ } : void 0,
11059
+ saved: savedTree
11060
+ }), aside) : null] });
11061
+ }
11062
+ //#endregion
10396
11063
  //#region src/client/index.ts
10397
11064
  /** The footer-action cell this plugin owns. */
10398
11065
  const SLOT_ID = "multi-root-folders";
11066
+ /** The document-preview navigator retarget's occupant id. */
11067
+ const NAVIGATOR_SLOT_ID = "multi-root-navigator";
10399
11068
  /** The id of the `<style>` element carrying this plugin's sheet. */
10400
11069
  const STYLE_ELEMENT_ID = "multi-root-workspace-styles";
10401
11070
  /** Services the client half needs before it may activate. */
@@ -10504,6 +11173,24 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
10504
11173
  nativeTree: nativeTreeRenderer(props.renderFactorySlot)
10505
11174
  });
10506
11175
  }));
11176
+ ctx.slots.inject("sidebar.right.tab.document.actions", () => ctx.slots.register({
11177
+ name: "sidebar.right.tab.document.actions",
11178
+ id: NAVIGATOR_SLOT_ID,
11179
+ order: 50,
11180
+ store: filesTreeSeats.store,
11181
+ inject: filesTreeSeats.inject
11182
+ }, function DocumentActionsOccupant(props) {
11183
+ const absolutePath = props?.absolutePath;
11184
+ const filesTree = createFilesTreeSurface(props, filesTreeT);
11185
+ if (absolutePath === void 0 || filesTree === void 0) return null;
11186
+ return (0, react.createElement)(NavigatorRetarget, {
11187
+ absolutePath,
11188
+ sessionId: props?.sessionId,
11189
+ panel,
11190
+ filesTree,
11191
+ nativeTree: nativeTreeRenderer(props.renderFactorySlot)
11192
+ });
11193
+ }));
10507
11194
  }
10508
11195
  /**
10509
11196
  * Read one optional service by name.