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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.en.md CHANGED
@@ -8,6 +8,17 @@ This file records the user-visible changes of every **released** version.
8
8
  - For installation sources, the supported upstream runtimes, and known limitations, see the [README](./README.en.md).
9
9
  - The evidence, numbering, and release state of each version live in the [roadmap progress ledger](./docs/plans/active/2026-09-12-multi-root-workspace.md#进度总账). The body of each GitHub Release is generated from the commit history by `scripts/release-notes.mjs` and is the complete raw list behind this file.
10
10
 
11
+ ## [0.1.13] - 2026-10-31
12
+
13
+ ### Added
14
+
15
+ - **Background recovery for the shared Registry (on by default)**: when Hosts share a registry, retryable authority contention, disconnections, and storage-open failures are retried automatically with backoff and jitter. A follower is considered recovered only after it has resynchronized its tracked primary roots. Set `backgroundRecovery: false` on `multi-root-registry` to disable background retries; on-demand recovery remains available. Configuration errors and writes with an unknown state still require configuration correction or a restart. Recovery preserves deny boundaries, every new operation still requires admission, and mutations with an unknown result are never replayed automatically. Read-only diagnostics and deduplicated transition logs help diagnose failures ([ADR-0026](./docs/decisions/ADR-0026-registry-background-recovery.md)).
16
+
17
+ ### Fixed
18
+
19
+ - **Files tab retains its selected root when opening a file**: when opening a file in an additional root unmounts and remounts the Files tab, its selected root, expanded directories, and scroll position are retained. A pending or failed list request no longer resets the selection to the primary root; closing the tab cleans up watches and tree resources.
20
+ - **Shared-authority recovery safety and consistency**: a retry cannot bypass a failed storage-to-lock binding check. Recovery synchronizes root projections and publishes consistent state in a batch; empty snapshots clear stale boundaries, and failed recovery rounds no longer generate repeated notifications. On Windows, the request timer stays active during the connection handshake so the process cannot exit prematurely.
21
+
11
22
  ## [0.1.12] - 2026-10-30
12
23
 
13
24
  ### Added
@@ -203,7 +214,8 @@ The first release: the three MVP milestones (M1–M3).
203
214
 
204
215
  - All eight findings of the pre-release external review (three of them release-blocking) are fixed, each with a regression test: a registered directory replaced by a symlink transplanting its authority (which introduced `recordedPath` — **re-resolving a path is not re-authorizing it**), concurrent registry mutations losing a write or reviving a revoked grant (per-primary-root mutations are serialized), a refresh not re-checking directories, a typed path being overwritten by the directory picker, the `reveal` response contract and failure code, duplicate ids not validated, the primary root missing from nesting validation, and CI testing before building.
205
216
 
206
- [Unreleased]: https://github.com/cherrchen/dsh-plugin-multi-root-workspace/compare/v0.1.12...HEAD
217
+ [Unreleased]: https://github.com/cherrchen/dsh-plugin-multi-root-workspace/compare/v0.1.13...HEAD
218
+ [0.1.13]: https://github.com/cherrchen/dsh-plugin-multi-root-workspace/compare/v0.1.12...v0.1.13
207
219
  [0.1.12]: https://github.com/cherrchen/dsh-plugin-multi-root-workspace/compare/v0.1.11...v0.1.12
208
220
  [0.1.11]: https://github.com/cherrchen/dsh-plugin-multi-root-workspace/compare/v0.1.10...v0.1.11
209
221
  [0.1.10]: https://github.com/cherrchen/dsh-plugin-multi-root-workspace/compare/v0.1.9...v0.1.10
package/CHANGELOG.md CHANGED
@@ -8,6 +8,17 @@ English: [CHANGELOG.en.md](./CHANGELOG.en.md)
8
8
  - 安装方式、受支持的上游运行时与已知限制见 [README](./README.md)。
9
9
  - 每个版本的实施证据、编号口径与发布状态见[路线图 §进度总账](./docs/plans/active/2026-09-12-multi-root-workspace.md#进度总账);GitHub Release 的正文由 `scripts/release-notes.mjs` 从 commit 历史生成,是本文件之外的完整原始清单。
10
10
 
11
+ ## [0.1.13] - 2026-10-31
12
+
13
+ ### Added
14
+
15
+ - **共享 Registry 后台恢复(默认开启)**:多个 Host 共享登记表时,可重试的权威争用、断线或存储打开失败会以退避和抖动自动重试;follower 重新同步已跟踪主根后才视为恢复。`multi-root-registry` 的 `backgroundRecovery: false` 可关闭后台重试,按需恢复仍可用;配置错误和状态不明的写入仍需修正配置或重启。恢复期间继续保留拒绝边界,每个新操作仍须授权,结果不明的变更不会自动重放。只读诊断信息和去重的状态转换日志有助于排查故障([ADR-0026](./docs/decisions/ADR-0026-registry-background-recovery.md))。
16
+
17
+ ### Fixed
18
+
19
+ - **Files 页签打开文件后保留所选根**:打开附加根中的文件导致 Files 页签卸载并重新挂载时,保留该页签所选的根、目录展开状态与滚动位置;列表尚未应答或请求失败不会误将选择切回主根,关闭页签时会清理监听与目录树资源。
20
+ - **共享权威恢复的安全性和一致性**:介质与锁绑定校验失败后重试不能绕过校验;恢复时同步根投影并批量发布一致的状态,空快照能清除过期边界,失败的恢复轮次不再产生重复通知。Windows 连接握手期间保持请求计时器有效,避免进程提前退出。
21
+
11
22
  ## [0.1.12] - 2026-10-30
12
23
 
13
24
  ### Added
@@ -203,7 +214,8 @@ English: [CHANGELOG.en.md](./CHANGELOG.en.md)
203
214
 
204
215
  - 发布前外部评审提出的 8 项发现(其中 3 项发布阻断)全部修复,每项各带一个回归测试:登记目录被替换为符号链接后授权转移(引入 `recordedPath`,**重新解析路径不等于重新授权**)、并发修改注册表丢写或复活已撤销的授权(每个主根的变更串行)、刷新不重新检查目录、手输路径被目录选择器覆盖、`reveal` 的应答契约与失败码、重复 id 未校验、主根未纳入嵌套校验、CI 先测后构建。
205
216
 
206
- [Unreleased]: https://github.com/cherrchen/dsh-plugin-multi-root-workspace/compare/v0.1.12...HEAD
217
+ [Unreleased]: https://github.com/cherrchen/dsh-plugin-multi-root-workspace/compare/v0.1.13...HEAD
218
+ [0.1.13]: https://github.com/cherrchen/dsh-plugin-multi-root-workspace/compare/v0.1.12...v0.1.13
207
219
  [0.1.12]: https://github.com/cherrchen/dsh-plugin-multi-root-workspace/compare/v0.1.11...v0.1.12
208
220
  [0.1.11]: https://github.com/cherrchen/dsh-plugin-multi-root-workspace/compare/v0.1.10...v0.1.11
209
221
  [0.1.10]: https://github.com/cherrchen/dsh-plugin-multi-root-workspace/compare/v0.1.9...v0.1.10
package/README.en.md CHANGED
@@ -16,11 +16,12 @@ Not every project is a monorepo. A real development project is usually several i
16
16
 
17
17
  - **No new tools**: the agent keeps using the native `read` / `write` / `edit` / `bash`; only the granted directories grow.
18
18
  - **Sandbox confinement**: an additional root may inherit the session's (primary root's) permission setting, or be pinned to read-only or read-write; under read-only and read-write, confinement does not rest on system prompts — it is enforced by an in-process filesystem fence plus kernel-level sandboxes (macOS Seatbelt / Linux bwrap / Landlock / Windows ACL), with files and bash sharing one and the same scope.
19
+ - **Browse additional roots in the Files tab**: the Files tab header switches between the primary root and any registered additional root; an additional root's directory renders in the same native Files view, with open-file, open-in-app, and auto-refresh behaving identically.
19
20
  - **Optional Auto review (off by default)**: a fork of upstream's experimental auto-review that gives the reviewer the additional directories' context — the current workspace topology, each root's access level, and live instruction files; it ships disabled and only takes effect once the `@dsh-electron/dsh-plugin-multi-root-workspace/auto-review` row is explicitly enabled in the Plugin Manager row list, taking over the official bundle while on.
20
21
  - **Additional-root Skills**: discovers `.dsh/skills` and `.agents/skills` inside additional folders, invoked through the namespaced `/alias-id:skill-name` reference and the native `skill` tool; each root has an independent toggle and editable `aliasId`, with a global setting that defaults to on.
21
22
  - **Absent when absent**: with no additional root configured, its behavior is item-for-item identical to an uninstalled harness; every misconfiguration fails loudly instead of silently degrading.
22
23
 
23
- Current released version: `v0.1.12`. The user-visible changes of each version live in the [CHANGELOG](./CHANGELOG.en.md); progress and release state live in the [roadmap progress ledger](./docs/plans/active/2026-09-12-multi-root-workspace.md#进度总账).
24
+ Current released version: `v0.1.13`. The user-visible changes of each version live in the [CHANGELOG](./CHANGELOG.en.md); progress and release state live in the [roadmap progress ledger](./docs/plans/active/2026-09-12-multi-root-workspace.md#进度总账).
24
25
 
25
26
  ## Quick Start
26
27
 
@@ -93,8 +94,8 @@ This installs pre-built artifacts; the first install needs the `allowBuilds` add
93
94
  ```sh
94
95
  pnpm pack @dsh-electron/dsh-plugin-multi-root-workspace
95
96
  # or download the tgz from the GitHub Release assets, e.g.:
96
- # https://github.com/cherrchen/dsh-plugin-multi-root-workspace/releases/download/v0.1.12/dsh-electron-dsh-plugin-multi-root-workspace-0.1.12.tgz
97
- dsh plugin --profile web add ./dsh-electron-dsh-plugin-multi-root-workspace-0.1.12.tgz
97
+ # https://github.com/cherrchen/dsh-plugin-multi-root-workspace/releases/download/v0.1.13/dsh-electron-dsh-plugin-multi-root-workspace-0.1.13.tgz
98
+ dsh plugin --profile web add ./dsh-electron-dsh-plugin-multi-root-workspace-0.1.13.tgz
98
99
  ```
99
100
 
100
101
  Also pre-built, handy for air-gapped or offline delivery; the first install needs the same `allowBuilds` addition.
@@ -113,10 +114,10 @@ allowBuilds:
113
114
  koffi: true
114
115
  ```
115
116
 
116
- Pinning a version (e.g. `#v0.1.12`) is recommended so a later push cannot silently change what actually runs:
117
+ Pinning a version (e.g. `#v0.1.13`) is recommended so a later push cannot silently change what actually runs:
117
118
 
118
119
  ```sh
119
- dsh plugin --profile web add github:cherrchen/dsh-plugin-multi-root-workspace#v0.1.12
120
+ dsh plugin --profile web add github:cherrchen/dsh-plugin-multi-root-workspace#v0.1.13
120
121
  ```
121
122
 
122
123
  ### Install from a local clone
package/README.md CHANGED
@@ -16,11 +16,12 @@
16
16
 
17
17
  - **不新增工具**:Agent 照旧使用原生 `read` / `write` / `edit` / `bash`,只是授权的目录变多了。
18
18
  - **沙箱约束**:附加根可继承会话(主目录)的权限设置,也可单独固定为只读或读写;在只读与读写下,约束不靠系统提示词,而由进程内文件围栏 + 内核级沙箱(macOS Seatbelt / Linux bwrap / Landlock / Windows ACL)执行,文件与 bash 共享同一份授权范围。
19
+ - **Files 页签浏览附加根**:Files 页签头部可直接在主根与任一已登记附加根之间切换,附加根的目录树以与主根相同的原生 Files 视图呈现,打开文件、在外部应用打开、自动刷新等操作完全一致。
19
20
  - **可选 Auto review(默认关闭)**:fork 上游实验性「自动授权审查」,向 Reviewer 补充附加目录的审查上下文——当前工作区拓扑、各根访问级别与实时指令文件;出厂关闭,需在插件管理器的行列表中显式打开 `@dsh-electron/dsh-plugin-multi-root-workspace/auto-review` 行后生效,启用时接管官方同名 bundle。
20
21
  - **附属根 Skills**:发现附属目录的 `.dsh/skills` 与 `.agents/skills`,附属根经 `/alias-id:skill-name` 命名空间调用原生 `skill` 工具;每根独立开关、可编辑 `aliasId`,全局设置默认开启。
21
22
  - **不配等于没装**:未登记附加根时行为与未装插件逐项一致;配置错误一律明确报错,从不静默降级。
22
23
 
23
- 当前发布版本 `v0.1.12`。各版本的用户可见变更见 [CHANGELOG](./CHANGELOG.md);进度与发布状态见[路线图 §进度总账](./docs/plans/active/2026-09-12-multi-root-workspace.md#进度总账)。
24
+ 当前发布版本 `v0.1.13`。各版本的用户可见变更见 [CHANGELOG](./CHANGELOG.md);进度与发布状态见[路线图 §进度总账](./docs/plans/active/2026-09-12-multi-root-workspace.md#进度总账)。
24
25
 
25
26
  ## 快速开始
26
27
 
@@ -95,8 +96,8 @@ dsh plugin --profile web add @dsh-electron/dsh-plugin-multi-root-workspace
95
96
  ```sh
96
97
  pnpm pack @dsh-electron/dsh-plugin-multi-root-workspace
97
98
  # 或从 GitHub Release 资产下载,例如:
98
- # https://github.com/cherrchen/dsh-plugin-multi-root-workspace/releases/download/v0.1.12/dsh-electron-dsh-plugin-multi-root-workspace-0.1.12.tgz
99
- dsh plugin --profile web add ./dsh-electron-dsh-plugin-multi-root-workspace-0.1.12.tgz
99
+ # https://github.com/cherrchen/dsh-plugin-multi-root-workspace/releases/download/v0.1.13/dsh-electron-dsh-plugin-multi-root-workspace-0.1.13.tgz
100
+ dsh plugin --profile web add ./dsh-electron-dsh-plugin-multi-root-workspace-0.1.13.tgz
100
101
  ```
101
102
 
102
103
  同样是预构建产物,适合内网或离线环境交付;首次安装同样需要添加 `allowBuilds`。
@@ -115,10 +116,10 @@ allowBuilds:
115
116
  koffi: true
116
117
  ```
117
118
 
118
- 建议锁定版本(如 `#v0.1.12`),让后续推送无法悄悄改变实际运行的内容:
119
+ 建议锁定版本(如 `#v0.1.13`),让后续推送无法悄悄改变实际运行的内容:
119
120
 
120
121
  ```sh
121
- dsh plugin --profile web add github:cherrchen/dsh-plugin-multi-root-workspace#v0.1.12
122
+ dsh plugin --profile web add github:cherrchen/dsh-plugin-multi-root-workspace#v0.1.13
122
123
  ```
123
124
 
124
125
  ### 从本地源码安装
package/cordis.patch.yml CHANGED
@@ -88,6 +88,10 @@
88
88
  # already exceeds it keeps granting — only new adds are refused. Bench
89
89
  # evidence for the default: docs/reference/bench-root-scale-*.json.
90
90
  maxAdditionalRoots: 16
91
+ # Bounded background recovery of a degraded authority (contended,
92
+ # follower reconnect, retryable storage failure); on by default.
93
+ # Set false for on-demand recovery only (refresh still works).
94
+ backgroundRecovery: true
91
95
  # Upstream `agent-instructions` only discovers instruction files from the
92
96
  # session cwd upward, so an additional root's own AGENTS.md is invisible to
93
97
  # the model. This row closes that gap for the additional roots only; the
package/lib/client.js CHANGED
@@ -40,8 +40,8 @@ window.__ModuleLoader__.load({
40
40
  _deepseek_ai_dsh_client_ui_primitives = __toESM(_deepseek_ai_dsh_client_ui_primitives, 1);
41
41
  let react_jsx_runtime = require("react/jsx-runtime");
42
42
  let react_dom = require("react-dom");
43
- require("@deepseek-ai/cordis");
44
43
  let _deepseek_ai_dsh_client_store = require("@deepseek-ai/dsh-client-store");
44
+ require("@deepseek-ai/cordis");
45
45
  //#region src/compat/client-session.ts
46
46
  /**
47
47
  * The session the composed client is showing, when it can name one.
@@ -7902,12 +7902,59 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
7902
7902
  const store = createFilesStore();
7903
7903
  const remote = lazyFilesRemote(ctx);
7904
7904
  const face = filesFace(createList(remote), createWatch(remote));
7905
- const inject = (sessionId, actions) => ({
7906
- face: face(sessionId, actions),
7907
- openFile: (address) => {
7908
- ctx.get("sidebarRight")?.openResourceIn?.(sessionId, address);
7909
- }
7910
- });
7905
+ const inject = (sessionId, actions) => {
7906
+ const selection = (0, _deepseek_ai_dsh_client_store.createSnapshotStore)({});
7907
+ const takeovers = /* @__PURE__ */ new Map();
7908
+ const endTakeover = (tabId) => {
7909
+ const held = takeovers.get(tabId);
7910
+ held?.observer?.disconnect();
7911
+ held?.controller.abort();
7912
+ takeovers.delete(tabId);
7913
+ };
7914
+ return {
7915
+ face: face(sessionId, actions),
7916
+ openFile: (address) => {
7917
+ ctx.get("sidebarRight")?.openResourceIn?.(sessionId, address);
7918
+ },
7919
+ hooks: { treeSelection: selection },
7920
+ selectRoot: (tabId, root) => {
7921
+ const next = { ...selection.getSnapshot() };
7922
+ if (root === "primary") delete next[tabId];
7923
+ else next[tabId] = root;
7924
+ selection.set(next);
7925
+ },
7926
+ takeover: {
7927
+ signal: (tabId, root, section) => {
7928
+ let held = takeovers.get(tabId);
7929
+ if (held === void 0 || held.root !== root || held.controller.signal.aborted) {
7930
+ held?.observer?.disconnect();
7931
+ held?.controller.abort();
7932
+ held = {
7933
+ controller: new AbortController(),
7934
+ root,
7935
+ section: void 0,
7936
+ observer: void 0
7937
+ };
7938
+ takeovers.set(tabId, held);
7939
+ }
7940
+ if (section !== void 0 && held.section !== section) {
7941
+ held.section = section;
7942
+ held.observer?.disconnect();
7943
+ held.observer = new MutationObserver(() => {
7944
+ if (!section.isConnected) endTakeover(tabId);
7945
+ });
7946
+ held.observer.observe(document, {
7947
+ childList: true,
7948
+ subtree: true
7949
+ });
7950
+ }
7951
+ if (held.section !== void 0 && !held.section.isConnected) endTakeover(tabId);
7952
+ return held.controller.signal;
7953
+ },
7954
+ end: endTakeover
7955
+ }
7956
+ };
7957
+ };
7911
7958
  return {
7912
7959
  store,
7913
7960
  inject
@@ -7924,14 +7971,17 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
7924
7971
  */
7925
7972
  function createFilesTreeSurface(props, t) {
7926
7973
  if (!filesTreeSurfaceSupported()) return void 0;
7927
- const { sessionId, useStore, actions, face, openFile } = props;
7928
- if (face === void 0 || openFile === void 0 || useStore === void 0 || actions === void 0) return void 0;
7974
+ const { sessionId, useStore, actions, face, openFile, useTreeSelection, selectRoot, takeover } = props;
7975
+ 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;
7929
7976
  return {
7930
7977
  sessionId,
7931
7978
  useStore,
7932
7979
  actions,
7933
7980
  face,
7934
7981
  openFile,
7982
+ useTreeSelection,
7983
+ selectRoot,
7984
+ takeover,
7935
7985
  t
7936
7986
  };
7937
7987
  }
@@ -9574,9 +9624,12 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
9574
9624
  * the same element's `closest('[data-dockkit-content]')` — the dockkit
9575
9625
  * tabHost section — with a generated fallback where the hook is absent.
9576
9626
  *
9577
- * The selected root is component state on purpose: every tab render
9578
- * occurrence gets its own, and reopening the pane resets to the primary
9579
- * root. There is no cross-tab or persisted selection.
9627
+ * The selected root lives in the inject product's session-scoped snapshot,
9628
+ * keyed by that dockkit tab id — component state could not survive the
9629
+ * remount every file open causes (the opened tab takes the pane, the Files
9630
+ * body unmounts with the whole occupant subtree). One selection per tab;
9631
+ * session switches mint a fresh inject product, so no selection crosses
9632
+ * sessions, and a registry eviction reconciles it back to primary.
9580
9633
  *
9581
9634
  * @module @dsh-electron/dsh-plugin-multi-root-workspace/client/RootSwitcher
9582
9635
  */
@@ -9587,17 +9640,20 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
9587
9640
  * path stays readable and hoverable, only the dir/file typography differs.
9588
9641
  */
9589
9642
  const PathLabel = maybeComponent(["PathLabel"]);
9643
+ /** Selector hook for a composition without the fork: nothing was ever selectable. */
9644
+ const noTreeSelection = () => void 0;
9590
9645
  /** The Files-header switcher: root menu, dialog entry, and the portal tree. */
9591
9646
  function RootSwitcher(props) {
9592
9647
  const { panel, t, primaryPath, filesTree } = props;
9593
9648
  const [menuOpen, setMenuOpen] = (0, react.useState)(false);
9594
9649
  const [dialogOpen, setDialogOpen] = (0, react.useState)(false);
9595
9650
  const [view, setView] = (0, react.useState)(void 0);
9596
- const [selected, setSelected] = (0, react.useState)("primary");
9597
9651
  const [takeover, setTakeover] = (0, react.useState)(null);
9598
9652
  const wrapperRef = (0, react.useRef)(null);
9599
9653
  const fallbackTabId = (0, react.useRef)(void 0);
9654
+ const sectionRef = (0, react.useRef)(null);
9600
9655
  const [pane, setPane] = (0, react.useState)(null);
9656
+ const [tabId, setTabId] = (0, react.useState)(void 0);
9601
9657
  const mounted = (0, react.useRef)(true);
9602
9658
  const requestEpoch = (0, react.useRef)(0);
9603
9659
  (0, react.useEffect)(() => {
@@ -9608,8 +9664,18 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
9608
9664
  };
9609
9665
  }, []);
9610
9666
  (0, react.useEffect)(() => {
9611
- setPane(wrapperRef.current?.closest("[data-files-root]") ?? null);
9667
+ const wrapper = wrapperRef.current;
9668
+ sectionRef.current = wrapper?.closest("[data-dockkit-content]") ?? null;
9669
+ const content = sectionRef.current?.dataset.dockkitContent;
9670
+ setTabId(content !== void 0 && content !== "" ? content : fallbackTabId.current ??= `mrfw-${Math.random().toString(36).slice(2)}`);
9671
+ setPane(wrapper?.closest("[data-files-root]") ?? null);
9612
9672
  }, []);
9673
+ const selected = (filesTree?.useTreeSelection ?? noTreeSelection)((selection) => tabId === void 0 ? void 0 : selection[tabId]) ?? "primary";
9674
+ const selectRoot = filesTree?.selectRoot;
9675
+ const setSelected = (0, react.useCallback)((next) => {
9676
+ if (selectRoot === void 0 || tabId === void 0) return;
9677
+ selectRoot(tabId, next);
9678
+ }, [selectRoot, tabId]);
9613
9679
  const refresh = (0, react.useCallback)(async () => {
9614
9680
  if (panel === void 0) {
9615
9681
  setView(void 0);
@@ -9647,39 +9713,48 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
9647
9713
  }, [
9648
9714
  props.subscribeSession,
9649
9715
  props.sessionId,
9650
- refresh
9716
+ refresh,
9717
+ setSelected
9651
9718
  ]);
9652
9719
  (0, react.useEffect)(() => {
9653
- if (selected === "primary") return;
9654
- const current = view?.roots.find((root) => root.id === selected.id);
9720
+ if (selected === "primary" || view === void 0) return;
9721
+ const current = view.roots.find((root) => root.id === selected.id);
9655
9722
  if (current === void 0 || current.state !== "available") {
9656
9723
  setSelected("primary");
9657
9724
  return;
9658
9725
  }
9659
9726
  if (current !== selected) setSelected(current);
9660
- }, [view, selected]);
9727
+ }, [
9728
+ view,
9729
+ selected,
9730
+ setSelected
9731
+ ]);
9661
9732
  const roots = (0, react.useMemo)(() => view?.roots ?? [], [view]);
9662
9733
  const primaryLabel = view?.primaryName ?? basename(view?.primaryRoot ?? primaryPath);
9663
9734
  const triggerLabel = selected === "primary" ? primaryLabel : displayNameOf(selected);
9664
9735
  const triggerPath = selected === "primary" ? view?.primaryRoot ?? primaryPath : selected.path;
9665
9736
  const treeVisible = selected !== "primary" && panel !== void 0 && pane !== null && filesTree !== void 0;
9666
9737
  const selectedPath = selected === "primary" ? void 0 : selected.path;
9738
+ const takeoverCtl = filesTree?.takeover;
9667
9739
  (0, react.useEffect)(() => {
9668
- if (!treeVisible || selectedPath === void 0) {
9740
+ if (!treeVisible || selectedPath === void 0 || tabId === void 0 || takeoverCtl === void 0) {
9669
9741
  setTakeover(null);
9670
9742
  return;
9671
9743
  }
9672
- const controller = new AbortController();
9673
- const tabId = (wrapperRef.current?.closest("[data-dockkit-content]")?.dataset.dockkitContent ?? "") || (fallbackTabId.current ??= `mrfw-${Math.random().toString(36).slice(2)}`);
9674
9744
  setTakeover({
9675
- signal: controller.signal,
9745
+ signal: takeoverCtl.signal(tabId, selectedPath, sectionRef.current ?? void 0),
9676
9746
  tabId,
9677
9747
  root: selectedPath
9678
9748
  });
9679
9749
  return () => {
9680
- controller.abort();
9750
+ if (mounted.current || sectionRef.current === null) takeoverCtl.end(tabId);
9681
9751
  };
9682
- }, [treeVisible, selectedPath]);
9752
+ }, [
9753
+ treeVisible,
9754
+ selectedPath,
9755
+ tabId,
9756
+ takeoverCtl
9757
+ ]);
9683
9758
  const items = (0, react.useMemo)(() => [{
9684
9759
  id: "primary",
9685
9760
  label: `${primaryLabel} · ${t("panel.primary")}`,
@@ -9897,7 +9972,8 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
9897
9972
  inject: filesTreeSeats.inject
9898
9973
  }, function FilesActionsOccupant(props) {
9899
9974
  const absolutePath = props?.absolutePath;
9900
- const paneSessionId = () => props?.sessionId ?? sessionId();
9975
+ const composedSessionId = props?.sessionId;
9976
+ const paneSessionId = (0, react.useCallback)(() => composedSessionId ?? sessionId(), [composedSessionId]);
9901
9977
  const filesTree = createFilesTreeSurface(props, filesTreeT);
9902
9978
  return absolutePath === void 0 ? (0, react.createElement)(WorkspaceFoldersAction, {
9903
9979
  panel,