@dsh-electron/dsh-plugin-multi-root-workspace 0.1.14 → 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 (73) hide show
  1. package/CHANGELOG.en.md +32 -1
  2. package/CHANGELOG.md +32 -1
  3. package/README.en.md +25 -8
  4. package/README.md +25 -8
  5. package/cordis.patch.yml +5 -1
  6. package/lib/{llm-message-fYORacvh.js → agent-instructions-BXuKhOzr.js} +1 -84
  7. package/lib/auto-review.js +2 -2
  8. package/lib/client.js +969 -100
  9. package/lib/client.js.map +1 -1
  10. package/lib/{command-Dcz6nULo.js → command-DaVJ-mK3.js} +76 -11
  11. package/lib/command.js +1 -1
  12. package/lib/compat-CTJbpCxu.js +267 -0
  13. package/lib/compat.js +1 -1
  14. package/lib/{dialects-Dr4rDR-H.js → dialects-CdlG3RF9.js} +1 -1
  15. package/lib/{compat-veDqCOoa.js → dsh-version-DCwF7REk.js} +2 -264
  16. package/lib/index.js +10 -8
  17. package/lib/{instructions-KsUwUFE0.js → instructions-Ca80SkZq.js} +696 -27
  18. package/lib/instructions.js +1 -1
  19. package/lib/llm-message-CepyzEBQ.js +84 -0
  20. package/lib/lsp.js +4 -4
  21. package/lib/{metadata-DgqSdVX_.js → metadata-99jgViHQ.js} +1 -1
  22. package/lib/{method-wrapper-CFY3p3j2.js → method-wrapper-D4oUCsEF.js} +22 -2
  23. package/lib/{native-win32-CRNXr11U.js → native-win32-C5oMtt7f.js} +1 -1
  24. package/lib/ops-compatibility.js +58 -0
  25. package/lib/ops-settings-UN2oOKEp.js +40 -0
  26. package/lib/ops-tools-k4JYaOrM.js +152 -0
  27. package/lib/process-fence.js +193 -34
  28. package/lib/{protocol-CnR1KTXy.js → protocol-CwDnFEyK.js} +8 -4
  29. package/lib/{registry-DR_sMuvS.js → registry-BibiukWk.js} +236 -80
  30. package/lib/{registry-lease-win32-DDO-c7ig.js → registry-lease-win32-r4puac9m.js} +1 -1
  31. package/lib/{registry-operation-C2ZV3Jhw.js → registry-operation-CIlmt6_c.js} +5 -1
  32. package/lib/registry.js +2 -2
  33. package/lib/{review-context-NDpKeMCc.js → review-context-OxO-fMWR.js} +24 -7
  34. package/lib/review-context.js +1 -1
  35. package/lib/{roots-Ca_W1S9n.js → roots-CmaWxRVy.js} +112 -22
  36. package/lib/{sandbox-C-vCJCp3.js → sandbox-aobdyQGi.js} +7 -4
  37. package/lib/sandbox.js +1 -1
  38. package/lib/scope.js +162 -12
  39. package/lib/skills-utyuvmwr.js +104 -0
  40. package/lib/skills.js +5 -106
  41. package/lib/{tool-context-CpgIFcCB.js → tool-context-CCeY3mrK.js} +1 -1
  42. package/lib/types/authority/protocol.d.ts +5 -1
  43. package/lib/types/client/files-tree/FilesTree.d.ts +49 -6
  44. package/lib/types/client/files-tree/adapter.d.ts +69 -16
  45. package/lib/types/client/files-tree/face.d.ts +31 -2
  46. package/lib/types/client/files-tree/locate.d.ts +74 -0
  47. package/lib/types/client/files-tree/navigator.d.ts +98 -0
  48. package/lib/types/client/index.d.ts +7 -0
  49. package/lib/types/client/locales.d.ts +30 -0
  50. package/lib/types/client/ops-settings.d.ts +6 -0
  51. package/lib/types/client/skills-settings.d.ts +3 -2
  52. package/lib/types/client/styles.d.ts +1 -1
  53. package/lib/types/command.d.ts +3 -3
  54. package/lib/types/compat/files-tree.d.ts +8 -1
  55. package/lib/types/compat/forge-entrypoints.d.ts +44 -0
  56. package/lib/types/compat/instruction-activity.d.ts +32 -1
  57. package/lib/types/compat/ops-confirmation.d.ts +17 -0
  58. package/lib/types/compat/ops-settings.d.ts +11 -0
  59. package/lib/types/compat/ops-tools.d.ts +25 -0
  60. package/lib/types/compat/session-existence.d.ts +41 -0
  61. package/lib/types/compat/skills-client.d.ts +1 -0
  62. package/lib/types/compat/tool-context.d.ts +4 -0
  63. package/lib/types/compat.d.ts +2 -2
  64. package/lib/types/instructions.d.ts +6 -1
  65. package/lib/types/ops-compatibility.d.ts +34 -0
  66. package/lib/types/registry-operation.d.ts +7 -0
  67. package/lib/types/registry.d.ts +67 -8
  68. package/lib/types/review-context.d.ts +2 -0
  69. package/lib/types/roots.d.ts +116 -20
  70. package/lib/types/scope.d.ts +95 -3
  71. package/lib/workspace-files.js +4 -4
  72. package/lib/{workspace-routing-YPxaQ4V3.js → workspace-routing-D_qwMUh-.js} +4 -4
  73. package/package.json +5 -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);
@@ -5593,6 +5597,16 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
5593
5597
  "autoReview.loadFailed": "无法读取插件状态。",
5594
5598
  "autoReview.restartRequired": "更改将在重启后生效。",
5595
5599
  "autoReview.settingsFailed": "设置保存失败,请重试。",
5600
+ "ops.compatibilityEnable": "OPS 兼容模式",
5601
+ "ops.experimental": "实验性",
5602
+ "ops.compatibilityHint": "仅限 DSH 0.2.*。启用 OPS 写保护、实时活动与 Review 增强。受限模式可能拒绝附加根操作(包括读取),后台缓存可能残留;关闭时显式只读附加根可能被 OPS 写入。",
5603
+ "ops.settingsFailed": "设置保存失败,请重试。",
5604
+ "ops.confirmTitle": "开启 OPS 实验性兼容?",
5605
+ "ops.confirmDescription": "这仍是有限的实验性适配。开启后,附加根的合法读写及部分 PTC 操作可能被拒绝;Windows 存在只读边界时,OPS 读取和执行也可能被拒绝。Linux 批处理或结果读取、后台任务及清理可能不可用,并留下缓存。活动记录尚不持久化,恢复会话或审查动态操作时,Review 可能因证据不足而失败。目前尚未实现 OPS 全功能兼容,请仅在能接受这些限制时开启。",
5606
+ "ops.confirmAcknowledge": "我已了解上述限制,仍要开启实验性兼容",
5607
+ "ops.confirmCancel": "暂不开启",
5608
+ "ops.confirmClose": "关闭并保持未开启",
5609
+ "ops.confirmEnable": "确认开启",
5596
5610
  "skills.userOnly": "仅用户调用",
5597
5611
  "panel.skills": "目录技能",
5598
5612
  "panel.skillsEnabled": "此目录技能已启用,点击停用",
@@ -5637,6 +5651,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
5637
5651
  "panel.copied": "已复制",
5638
5652
  "panel.reveal": "在文件管理器中显示",
5639
5653
  "panel.reRegister": "重新登记(保留原访问级别)",
5654
+ "panel.redirectedHint": "目标已改变:请移除该登记后再显式添加新目录。",
5640
5655
  "panel.moveUp": "上移",
5641
5656
  "panel.moveDown": "下移",
5642
5657
  "access.inherit": "跟随会话",
@@ -5672,7 +5687,11 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
5672
5687
  "error.unavailable": "无法连接到 dsh 主进程。",
5673
5688
  "error.copy-failed": "无法写入剪贴板,请检查浏览器的剪贴板权限。",
5674
5689
  "error.session-not-found": "找不到该会话;请先打开一个工作区会话。",
5690
+ "error.session-not-live": "该会话存在但未在本实例激活;它可能正被另一个 DSH 实例持有、仍在恢复,或格式待迁移。若另一实例持有它,请退出那个实例;也可以切换到同一工作区的活动会话;格式待迁移时请在该会话完成迁移。否则请稍后重试。",
5675
5691
  "error.unsupported-path": "该路径是网络或设备路径,Windows ACL 隔离无法覆盖它。",
5692
+ "error.protected-path": "该目录与 DSH 主目录或根登记存储目录重叠,不能登记。",
5693
+ "error.protected-boundary-unknown": "暂时无法确定 DSH 主目录等受保护目录的位置;恢复之前不能新增登记。",
5694
+ "error.revive-target-changed": "该登记现在指向另一个目录;请先移除它,再显式添加新目录。",
5676
5695
  "error.fallback": "操作失败。"
5677
5696
  };
5678
5697
  /** English copy, pinned to the Chinese key set. */
@@ -5686,6 +5705,16 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
5686
5705
  "autoReview.loadFailed": "Could not read plugin state.",
5687
5706
  "autoReview.restartRequired": "The change takes effect after restart.",
5688
5707
  "autoReview.settingsFailed": "Could not save settings. Please try again.",
5708
+ "ops.compatibilityEnable": "OPS compatibility mode",
5709
+ "ops.experimental": "Experimental",
5710
+ "ops.compatibilityHint": "Requires DSH 0.2.*. Enables OPS write protection, live activity, and Review enhancements. Restricted modes may reject additional-root operations, including reads, and background caches may remain. When off, OPS may write to an explicitly read-only additional root.",
5711
+ "ops.settingsFailed": "Could not save settings. Please try again.",
5712
+ "ops.confirmTitle": "Enable experimental OPS compatibility?",
5713
+ "ops.confirmDescription": "This is a limited experimental adaptation. Valid additional-root reads/writes and some PTC calls may be rejected; Windows may also block OPS reads and execution when read-only boundaries apply. Linux batch/result operations, background tasks, and cleanup may fail or leave caches behind. Activity is not persisted: after resume, or when reviewing dynamic operations, Review may fail for lack of evidence. Full OPS compatibility is not available. Enable only if you accept these limitations.",
5714
+ "ops.confirmAcknowledge": "I understand these limitations and want to enable experimental compatibility",
5715
+ "ops.confirmCancel": "Keep disabled",
5716
+ "ops.confirmClose": "Close and keep disabled",
5717
+ "ops.confirmEnable": "Confirm and enable",
5689
5718
  "skills.userOnly": "User invocation only",
5690
5719
  "panel.skills": "Folder skills",
5691
5720
  "panel.skillsEnabled": "Skills enabled for this folder. Click to disable.",
@@ -5730,6 +5759,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
5730
5759
  "panel.copied": "Copied",
5731
5760
  "panel.reveal": "Reveal in file manager",
5732
5761
  "panel.reRegister": "Re-register (keeps its access level)",
5762
+ "panel.redirectedHint": "The target changed: remove this registration, then add the new folder explicitly.",
5733
5763
  "panel.moveUp": "Move up",
5734
5764
  "panel.moveDown": "Move down",
5735
5765
  "access.inherit": "Inherit",
@@ -5765,7 +5795,11 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
5765
5795
  "error.unavailable": "Could not reach the dsh host process.",
5766
5796
  "error.copy-failed": "Could not write to the clipboard; check the browser's clipboard permission.",
5767
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.",
5768
5799
  "error.unsupported-path": "That is a network or device path, which Windows ACL confinement cannot cover.",
5800
+ "error.protected-path": "That directory overlaps the DSH home or the registry storage directories; it cannot be registered.",
5801
+ "error.protected-boundary-unknown": "The protected DSH directories cannot be resolved right now; no folder can be registered until they can.",
5802
+ "error.revive-target-changed": "That registration now resolves to a different directory; remove it, then add the new folder explicitly.",
5769
5803
  "error.fallback": "The operation failed."
5770
5804
  };
5771
5805
  //#endregion
@@ -6095,6 +6129,27 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
6095
6129
  background: var(--dsw-alias-interactive-bg-hover);
6096
6130
  }
6097
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
+
6098
6153
  /* ── files-tree fork (verbatim rules, renamed classes) ─────────────────────
6099
6154
  Copied from upstream's FilesBody.module.css (ui-sidebar-files @
6100
6155
  0.2.2-alpha.2) with local names mapped to mrfw-ft* so the rules cannot
@@ -6682,10 +6737,13 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
6682
6737
  `;
6683
6738
  //#endregion
6684
6739
  //#region src/compat/skills-client.ts
6685
- function bindSkillsForm(ctx) {
6740
+ function bindSettingsForm(ctx, entryId) {
6686
6741
  const modern = ctx.get("configForms");
6687
- if (modern !== void 0) return modern.get("multi-root-skills");
6688
- return ctx.get("settingsScope")?.bind({ namespace: "multi-root-skills" });
6742
+ if (modern !== void 0) return modern.get(entryId);
6743
+ return ctx.get("settingsScope")?.bind({ namespace: entryId });
6744
+ }
6745
+ function bindSkillsForm(ctx) {
6746
+ return bindSettingsForm(ctx, "multi-root-skills");
6689
6747
  }
6690
6748
  function clientInject(ctx, services, callback) {
6691
6749
  if (typeof ctx.inject === "function") ctx.inject(services, callback);
@@ -7122,6 +7180,123 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
7122
7180
  });
7123
7181
  }
7124
7182
  //#endregion
7183
+ //#region src/compat/ops-confirmation.ts
7184
+ function opsRiskConfirmation() {
7185
+ return _deepseek_ai_dsh_client_ui_primitives.RiskConfirmation;
7186
+ }
7187
+ //#endregion
7188
+ //#region src/client/ops-settings.tsx
7189
+ function OpsCompatibilitySettingsCard({ form, t }) {
7190
+ const snapshot = (0, react.useSyncExternalStore)((listener) => form.subscribe(listener), () => form.getSnapshot());
7191
+ const [busy, setBusy] = (0, react.useState)(false), [error, setError] = (0, react.useState)();
7192
+ const [confirmation, setConfirmation] = (0, react.useState)();
7193
+ const [acknowledged, setAcknowledged] = (0, react.useState)(false);
7194
+ const saving = (0, react.useRef)(false);
7195
+ const mounted = (0, react.useRef)(true);
7196
+ const RiskConfirmation = opsRiskConfirmation();
7197
+ const canConfirm = confirmation !== void 0 && confirmation.form === form && confirmation.revision === snapshot.revision && snapshot.status === "ready" && snapshot.writable && snapshot.value?.enabled !== true && RiskConfirmation !== void 0;
7198
+ const dismiss = () => {
7199
+ setConfirmation(void 0);
7200
+ setAcknowledged(false);
7201
+ };
7202
+ (0, react.useEffect)(() => {
7203
+ mounted.current = true;
7204
+ return () => {
7205
+ mounted.current = false;
7206
+ };
7207
+ }, []);
7208
+ (0, react.useEffect)(() => {
7209
+ if (confirmation !== void 0 && !canConfirm) {
7210
+ setConfirmation(void 0);
7211
+ setAcknowledged(false);
7212
+ }
7213
+ }, [confirmation, canConfirm]);
7214
+ const save = async (enabled, revision) => {
7215
+ if (saving.current) return;
7216
+ saving.current = true;
7217
+ setBusy(true);
7218
+ setError(void 0);
7219
+ try {
7220
+ const ok = await form.mutate([{
7221
+ op: "set",
7222
+ path: ["enabled"],
7223
+ value: enabled
7224
+ }], revision);
7225
+ if (mounted.current && !ok) setError(t("ops.settingsFailed"));
7226
+ } catch {
7227
+ if (mounted.current) setError(t("ops.settingsFailed"));
7228
+ } finally {
7229
+ saving.current = false;
7230
+ if (mounted.current) setBusy(false);
7231
+ }
7232
+ };
7233
+ if (snapshot.status === "unavailable") return null;
7234
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
7235
+ className: "mrfw-skillsSettings",
7236
+ children: [
7237
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
7238
+ className: "mrfw-skillsSettingsText",
7239
+ children: [
7240
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
7241
+ className: "mrfw-settingsTitleRow",
7242
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
7243
+ className: "mrfw-skillsSettingsTitle",
7244
+ children: t("ops.compatibilityEnable")
7245
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Tag, {
7246
+ tone: "info",
7247
+ children: t("ops.experimental")
7248
+ })]
7249
+ }),
7250
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
7251
+ className: "mrfw-skillsSettingsDescription",
7252
+ children: t("ops.compatibilityHint")
7253
+ }),
7254
+ error ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
7255
+ role: "alert",
7256
+ children: error
7257
+ }) : null
7258
+ ]
7259
+ }),
7260
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Switch, {
7261
+ label: t("ops.compatibilityEnable"),
7262
+ checked: snapshot.value?.enabled === true,
7263
+ disabled: busy || confirmation !== void 0 || snapshot.status !== "ready" || !snapshot.writable || snapshot.value?.enabled !== true && RiskConfirmation === void 0,
7264
+ onChange: (enabled) => {
7265
+ if (!enabled) {
7266
+ save(false, snapshot.revision);
7267
+ return;
7268
+ }
7269
+ setError(void 0);
7270
+ setAcknowledged(false);
7271
+ setConfirmation({
7272
+ form,
7273
+ revision: snapshot.revision
7274
+ });
7275
+ }
7276
+ }),
7277
+ RiskConfirmation === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(RiskConfirmation, {
7278
+ open: canConfirm,
7279
+ title: t("ops.confirmTitle"),
7280
+ description: t("ops.confirmDescription"),
7281
+ acknowledgeLabel: t("ops.confirmAcknowledge"),
7282
+ cancelLabel: t("ops.confirmCancel"),
7283
+ closeLabel: t("ops.confirmClose"),
7284
+ confirmLabel: t("ops.confirmEnable"),
7285
+ acknowledged,
7286
+ disabled: busy || !canConfirm,
7287
+ onAcknowledgedChange: setAcknowledged,
7288
+ onCancel: dismiss,
7289
+ onConfirm: () => {
7290
+ if (!canConfirm || !acknowledged || saving.current) return;
7291
+ const revision = confirmation.revision;
7292
+ dismiss();
7293
+ save(true, revision);
7294
+ }
7295
+ })
7296
+ ]
7297
+ });
7298
+ }
7299
+ //#endregion
7125
7300
  //#region src/client/skills-settings.tsx
7126
7301
  const BUNDLE_KEY = "@dsh-electron/dsh-plugin-multi-root-workspace";
7127
7302
  function SkillsSettingsCard({ form, t }) {
@@ -7189,42 +7364,55 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
7189
7364
  /**
7190
7365
  * The bundle's one `plugins.bundle.config` cell: every plugin-level switch the
7191
7366
  * package offers, in display order. Each row owns its own form and write path —
7192
- * Skills through its settings namespace, Auto Review through the Plugin
7367
+ * Skills and OPS through their settings namespaces, Auto Review through the Plugin
7193
7368
  * Manager's Loader-row enablement — so one cell never serializes their state.
7194
7369
  */
7195
- function BundleSettingsCard({ form, autoReviewForm, t }) {
7196
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [form !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SkillsSettingsCard, {
7197
- form,
7198
- t
7199
- }) : null, autoReviewForm !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AutoReviewSettingsCard, {
7200
- form: autoReviewForm,
7201
- t
7202
- }) : null] });
7370
+ function BundleSettingsCard({ form, autoReviewForm, opsForm, t }) {
7371
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
7372
+ form !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SkillsSettingsCard, {
7373
+ form,
7374
+ t
7375
+ }) : null,
7376
+ autoReviewForm !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AutoReviewSettingsCard, {
7377
+ form: autoReviewForm,
7378
+ t
7379
+ }) : null,
7380
+ opsForm !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(OpsCompatibilitySettingsCard, {
7381
+ form: opsForm,
7382
+ t
7383
+ }) : null
7384
+ ] });
7203
7385
  }
7204
7386
  function installSkillsSettingsCard(ctx, t) {
7205
7387
  let currentSkills;
7388
+ let currentOps;
7206
7389
  let currentAutoReview = false;
7207
7390
  let stop;
7208
7391
  const candidates = /* @__PURE__ */ new Map();
7209
7392
  const update = () => {
7210
- const skillsForm = [...candidates.entries()].sort(([a], [b]) => Number(b.get("configForms") !== void 0) - Number(a.get("configForms") !== void 0))[0]?.[1];
7393
+ const chosen = [...candidates.entries()].sort(([a], [b]) => Number(b.get("configForms") !== void 0) - Number(a.get("configForms") !== void 0))[0];
7394
+ const skillsForm = chosen?.[1].skills;
7395
+ const opsCandidate = chosen?.[1].ops;
7396
+ const opsForm = opsCandidate?.getSnapshot().status === "unavailable" ? void 0 : opsCandidate;
7211
7397
  const autoSnapshot = autoForm.getSnapshot();
7212
7398
  const autoAvailable = autoSnapshot.status === "loading" || autoSnapshot.status === "error" || autoSnapshot.status === "ready" && autoSnapshot.row !== void 0;
7213
- if (skillsForm === void 0 && !autoAvailable) {
7214
- if (currentSkills !== void 0 || currentAutoReview) {
7399
+ if (skillsForm === void 0 && opsForm === void 0 && !autoAvailable) {
7400
+ if (currentSkills !== void 0 || currentOps !== void 0 || currentAutoReview) {
7215
7401
  stop?.();
7216
7402
  stop = void 0;
7217
7403
  currentSkills = void 0;
7404
+ currentOps = void 0;
7218
7405
  currentAutoReview = false;
7219
7406
  }
7220
7407
  return;
7221
7408
  }
7222
- if (currentSkills === skillsForm && currentAutoReview === autoAvailable) return;
7409
+ if (currentSkills === skillsForm && currentOps === opsForm && currentAutoReview === autoAvailable) return;
7223
7410
  const slots = ctx.slots;
7224
7411
  if (slots === void 0) return;
7225
7412
  stop?.();
7226
7413
  stop = void 0;
7227
7414
  currentSkills = skillsForm;
7415
+ currentOps = opsForm;
7228
7416
  currentAutoReview = autoAvailable;
7229
7417
  const disposers = [slots.inject("plugins.bundle.config", () => slots.register({
7230
7418
  name: "plugins.bundle.config",
@@ -7232,6 +7420,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
7232
7420
  inject: () => ({
7233
7421
  form: skillsForm,
7234
7422
  autoReviewForm: autoForm,
7423
+ opsForm,
7235
7424
  t
7236
7425
  })
7237
7426
  }, BundleSettingsCard))];
@@ -7243,6 +7432,14 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
7243
7432
  t
7244
7433
  })
7245
7434
  }, SkillsSettingsCard)));
7435
+ if (opsForm !== void 0) disposers.push(slots.inject("settings.plugin.item", () => slots.register({
7436
+ name: "settings.plugin.item",
7437
+ key: "multi-root-ops-compatibility",
7438
+ inject: () => ({
7439
+ form: opsForm,
7440
+ t
7441
+ })
7442
+ }, OpsCompatibilitySettingsCard)));
7246
7443
  stop = () => {
7247
7444
  for (const dispose of disposers) dispose();
7248
7445
  };
@@ -7251,11 +7448,17 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
7251
7448
  const stopAutoWatch = autoForm.subscribe(update);
7252
7449
  update();
7253
7450
  for (const service of ["settingsScope", "configForms"]) clientInject(ctx, [service], (scope) => {
7254
- const form = bindSkillsForm(scope);
7255
- if (form === void 0) return;
7256
- candidates.set(scope, form);
7451
+ const skills = bindSkillsForm(scope);
7452
+ const ops = scope.get("configForms")?.get("multi-root-ops-compatibility");
7453
+ if (skills === void 0 && ops === void 0) return;
7454
+ const stopOpsWatch = ops?.subscribe(update);
7455
+ candidates.set(scope, {
7456
+ skills,
7457
+ ops
7458
+ });
7257
7459
  update();
7258
7460
  return () => {
7461
+ stopOpsWatch?.();
7259
7462
  candidates.delete(scope);
7260
7463
  update();
7261
7464
  };
@@ -7571,6 +7774,10 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
7571
7774
  /** Per tab, per absolute path: the listing generation a settlement must match; the latest request wins. */
7572
7775
  const generations = /* @__PURE__ */ new Map();
7573
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();
7574
7781
  const nextGeneration = (tabId, path) => {
7575
7782
  const byPath = generations.get(tabId) ?? /* @__PURE__ */ new Map();
7576
7783
  generations.set(tabId, byPath);
@@ -7589,6 +7796,39 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
7589
7796
  return result.ok ? result.value : void 0;
7590
7797
  });
7591
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
+ };
7592
7832
  return {
7593
7833
  refresh: (tabId) => {
7594
7834
  roots.get(tabId)?.refreshTree();
@@ -7597,17 +7837,14 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
7597
7837
  actions.autoRefresh(tabId, enabled);
7598
7838
  roots.get(tabId)?.setAutomatic(enabled);
7599
7839
  },
7600
- start(tabId, root, signal) {
7601
- actions.start(tabId, root);
7602
- signal.addEventListener("abort", () => {
7603
- roots.get(tabId)?.close();
7604
- roots.delete(tabId);
7605
- generations.delete(tabId);
7606
- actions.forget(tabId);
7607
- }, { once: true });
7608
- roots.set(tabId, new DirectoryNode(root, (path, lifetime) => load(tabId, path, lifetime), (path, lifetime) => watch(sessionId, path, lifetime), (path, error) => {
7609
- if (!signal.aborted) actions.failed(tabId, path, new RemoteError("gateway/internal", error instanceof Error ? error.message : String(error), {}));
7610
- }, 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);
7611
7848
  },
7612
7849
  load: (tabId, path, signal) => {
7613
7850
  load(tabId, path, signal);
@@ -7856,6 +8093,10 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
7856
8093
  function encodePath(path) {
7857
8094
  return path.split("/").map(encodeSegment).join("/");
7858
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
+ }
7859
8100
  /**
7860
8101
  * Build the address of a file read through one Session.
7861
8102
  * @param sessionId - the Session whose Host workspace resolves the path.
@@ -7867,6 +8108,44 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
7867
8108
  return `${FILE_ADDRESS_PREFIX}session/${encodeSegment(sessionId)}/${encodePath(normalized)}`;
7868
8109
  }
7869
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
+ /**
7870
8149
  * Browser-safe Workspace path and display helpers.
7871
8150
  * @module @deepseek-ai/dsh-util-workspace-path
7872
8151
  */
@@ -7901,6 +8180,110 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
7901
8180
  return sessionFileAddress(sessionId, normalized);
7902
8181
  }
7903
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
7904
8287
  //#region src/client/files-tree/FilesBody.module.css.ts
7905
8288
  /**
7906
8289
  * Shim for the CSS-module virtual module `./FilesBody.module.css`.
@@ -8006,26 +8389,32 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
8006
8389
  })]
8007
8390
  });
8008
8391
  }
8009
- if (entry.type === "file") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", {
8010
- className: css$1.item,
8011
- "data-files-entry": "file",
8012
- "data-files-path": path,
8013
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
8014
- type: "button",
8015
- className: css$1.row,
8016
- onClick: () => {
8017
- tree.onOpen(path);
8018
- },
8019
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(FileTypeIcon, {
8020
- kind: classifyFileType(entry.name),
8021
- size: 16,
8022
- className: css$1.fileIcon
8023
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
8024
- className: css$1.name,
8025
- children: entry.name
8026
- })]
8027
- })
8028
- });
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
+ }
8029
8418
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", {
8030
8419
  className: css$1.item,
8031
8420
  "data-files-entry": "other",
@@ -8085,46 +8474,181 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
8085
8474
  * has opened under it — the upstream body's scroll, seed, and render logic
8086
8475
  * verbatim, with the root arriving as a prop instead of `useSessions(cwd)`.
8087
8476
  */
8088
- 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 }) {
8089
8478
  const state = useStore((store) => store.byTab[tabId]);
8090
8479
  const bodyRef = (0, react.useRef)(null);
8091
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
+ });
8092
8558
  const seeded = state !== void 0;
8093
8559
  (0, react.useLayoutEffect)(() => {
8094
8560
  const body = bodyRef.current;
8095
8561
  if (seeded && body !== null) {
8096
8562
  body.scrollTop = state.scrollTop;
8097
8563
  scrollTopRef.current = body.scrollTop;
8564
+ if ((body.parentElement?.querySelector("[data-files-body]"))?.contains(body.ownerDocument.activeElement) === true) body.focus();
8098
8565
  }
8099
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]);
8100
8594
  (0, react.useEffect)(() => () => {
8101
8595
  if (seeded && !signal.aborted) actions.scrolled(tabId, scrollTopRef.current);
8596
+ onScrollOffset?.(scrollTopRef.current);
8102
8597
  }, [
8103
8598
  seeded,
8104
8599
  signal,
8105
8600
  tabId,
8106
- actions
8601
+ actions,
8602
+ onScrollOffset
8107
8603
  ]);
8108
8604
  (0, react.useEffect)(() => {
8109
- if (state !== void 0 || signal.aborted) return;
8110
- 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
+ };
8111
8620
  }, [
8112
- state,
8113
8621
  root,
8114
8622
  tabId,
8115
8623
  signal,
8116
- start
8624
+ hold,
8625
+ release,
8626
+ toggle
8117
8627
  ]);
8118
8628
  if (state === void 0) return null;
8119
8629
  const tree = {
8120
8630
  state,
8121
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
+ }
8122
8644
  toggle(tabId, parent, path, state.expanded, signal);
8123
8645
  },
8124
8646
  onOpen: (path) => {
8125
8647
  onOpen(fileAddressFor(sessionId, void 0, path));
8126
8648
  },
8127
- t
8649
+ t,
8650
+ current,
8651
+ currentRef
8128
8652
  };
8129
8653
  const legacy = /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
8130
8654
  className: css$1.level,
@@ -8133,13 +8657,14 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
8133
8657
  tree
8134
8658
  })
8135
8659
  });
8136
- 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 });
8137
8664
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
8138
8665
  ref: bodyRef,
8139
8666
  className: css$1.body,
8140
- onScroll: (event) => {
8141
- scrollTopRef.current = event.currentTarget.scrollTop;
8142
- },
8667
+ tabIndex: -1,
8143
8668
  children: content
8144
8669
  });
8145
8670
  }
@@ -8219,9 +8744,10 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
8219
8744
  /**
8220
8745
  * The registration's `store`/`inject` declarations, built once in `apply`.
8221
8746
  *
8222
- * The inject face nests the verbatim files face under `face` and adds the
8223
- * session-bound `openFile` — keeping `FilesInjected` untouched means the
8224
- * 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).
8225
8751
  * @param ctx - the client root context (the remote/sidebarRight lookups).
8226
8752
  * @returns the store handle and the inject factory for `register`.
8227
8753
  */
@@ -8232,17 +8758,47 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
8232
8758
  const inject = (sessionId, actions) => {
8233
8759
  const selection = (0, _deepseek_ai_dsh_client_store.createSnapshotStore)({});
8234
8760
  const takeovers = /* @__PURE__ */ new Map();
8235
- const endTakeover = (tabId) => {
8236
- const held = takeovers.get(tabId);
8237
- held?.observer?.disconnect();
8238
- held?.controller.abort();
8239
- 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();
8240
8793
  };
8241
8794
  return {
8242
8795
  face: face(sessionId, actions),
8243
8796
  openFile: (address) => {
8244
8797
  ctx.get("sidebarRight")?.openResourceIn?.(sessionId, address);
8245
8798
  },
8799
+ openFileIn: (tabId, address) => {
8800
+ ctx.get("sidebarRight")?.openResourceIn?.(sessionId, address, { replaceTab: tabId });
8801
+ },
8246
8802
  hooks: { treeSelection: selection },
8247
8803
  selectRoot: (tabId, root) => {
8248
8804
  const next = { ...selection.getSnapshot() };
@@ -8251,34 +8807,37 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
8251
8807
  selection.set(next);
8252
8808
  },
8253
8809
  takeover: {
8254
- signal: (tabId, root, section) => {
8255
- let held = takeovers.get(tabId);
8810
+ signal: (key, root, section) => {
8811
+ let held = takeovers.get(key);
8256
8812
  if (held === void 0 || held.root !== root || held.controller.signal.aborted) {
8257
- held?.observer?.disconnect();
8258
8813
  held?.controller.abort();
8259
8814
  held = {
8260
8815
  controller: new AbortController(),
8261
- root,
8262
- section: void 0,
8263
- observer: void 0
8816
+ root
8264
8817
  };
8265
- takeovers.set(tabId, held);
8266
- }
8267
- if (section !== void 0 && held.section !== section) {
8268
- held.section = section;
8269
- held.observer?.disconnect();
8270
- held.observer = new MutationObserver(() => {
8271
- if (!section.isConnected) endTakeover(tabId);
8272
- });
8273
- held.observer.observe(document, {
8274
- childList: true,
8275
- subtree: true
8276
- });
8818
+ takeovers.set(key, held);
8277
8819
  }
8278
- if (held.section !== void 0 && !held.section.isConnected) endTakeover(tabId);
8820
+ if (section !== void 0) armDeathWatch(key, typeof section === "function" ? section : () => section.isConnected);
8279
8821
  return held.controller.signal;
8280
8822
  },
8281
- 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
+ }
8282
8841
  }
8283
8842
  };
8284
8843
  };
@@ -8298,14 +8857,15 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
8298
8857
  */
8299
8858
  function createFilesTreeSurface(props, t) {
8300
8859
  if (!filesTreeSurfaceSupported()) return void 0;
8301
- const { sessionId, useStore, actions, face, openFile, useTreeSelection, selectRoot, takeover } = props;
8302
- 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;
8303
8862
  return {
8304
8863
  sessionId,
8305
8864
  useStore,
8306
8865
  actions,
8307
8866
  face,
8308
8867
  openFile,
8868
+ openFileIn,
8309
8869
  useTreeSelection,
8310
8870
  selectRoot,
8311
8871
  takeover,
@@ -9394,12 +9954,14 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
9394
9954
  accessChosen
9395
9955
  ]);
9396
9956
  /**
9397
- * Re-register one withheld or unusable root in place. The request carries
9398
- * the stored path and deliberately NO access: `registry.add` treats the call
9399
- * as a revive — it keeps the record's access, alias, and identity instead of
9957
+ * Re-register one MISSING root in place. The request carries the stored
9958
+ * path and deliberately NO access: `registry.add` treats the call as a
9959
+ * revive — it keeps the record's access, alias, and identity instead of
9400
9960
  * creating a new registration — which is also why the root limit must not
9401
- * gate this button: a revive consumes no slot, and it is the panel's only
9402
- * repair route for a redirected entry.
9961
+ * gate this button: a revive consumes no slot. Only `missing` rows get it:
9962
+ * a `redirected` registration now names a different directory, and revive
9963
+ * never changes the grant's target — replacing it is the explicit
9964
+ * remove-then-add flow the redirected hint describes (ADR-0027).
9403
9965
  */
9404
9966
  const reRegister = (0, react.useCallback)(async (root) => {
9405
9967
  await mutate("add", { path: root.path });
@@ -9627,14 +10189,18 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
9627
10189
  className: "mrfw-stateWarn",
9628
10190
  children: t(stateKey(root.state))
9629
10191
  }),
9630
- root.state === "available" ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(IconButton, {
10192
+ root.state === "missing" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(IconButton, {
9631
10193
  pending: state.busy,
9632
10194
  onClick: () => {
9633
10195
  reRegister(root);
9634
10196
  },
9635
10197
  icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(IconRefresh, {}),
9636
10198
  label: t("panel.reRegister")
9637
- }),
10199
+ }) : null,
10200
+ root.state === "redirected" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
10201
+ className: "mrfw-note",
10202
+ children: t("panel.redirectedHint")
10203
+ }) : null,
9638
10204
  root.skillsEnabled === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(IconButton, {
9639
10205
  pending: state.busy,
9640
10206
  pressed: root.skillsEnabled,
@@ -10191,7 +10757,8 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
10191
10757
  signal: takeover.signal,
10192
10758
  useStore: filesTree.useStore,
10193
10759
  actions: filesTree.actions,
10194
- start: filesTree.face.start,
10760
+ hold: filesTree.face.hold,
10761
+ release: filesTree.face.release,
10195
10762
  toggle: filesTree.face.toggle,
10196
10763
  onOpen: filesTree.openFile,
10197
10764
  t: filesTree.t,
@@ -10211,9 +10778,293 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
10211
10778
  ] });
10212
10779
  }
10213
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
10214
11063
  //#region src/client/index.ts
10215
11064
  /** The footer-action cell this plugin owns. */
10216
11065
  const SLOT_ID = "multi-root-folders";
11066
+ /** The document-preview navigator retarget's occupant id. */
11067
+ const NAVIGATOR_SLOT_ID = "multi-root-navigator";
10217
11068
  /** The id of the `<style>` element carrying this plugin's sheet. */
10218
11069
  const STYLE_ELEMENT_ID = "multi-root-workspace-styles";
10219
11070
  /** Services the client half needs before it may activate. */
@@ -10322,6 +11173,24 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
10322
11173
  nativeTree: nativeTreeRenderer(props.renderFactorySlot)
10323
11174
  });
10324
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
+ }));
10325
11194
  }
10326
11195
  /**
10327
11196
  * Read one optional service by name.