dsh-over-ssh 0.2.1 → 0.2.2

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/README.md CHANGED
@@ -35,6 +35,15 @@ shell:
35
35
 
36
36
  不配也能用,只是 Web 终端永远开在本地(远程文件、远端执行、`@` 补全不受影响)。
37
37
 
38
+ ## 0.2.2 更新
39
+
40
+ - 「添加目录」按会话作用域显示与删除,fork 子会话继承父会话的目录且立即可见。
41
+ - 会话面板:删除/还原/清理都有确认与部分失败说明;运行中的会话拒绝删除;回收站条目显示标题与删除时间。
42
+ - SSH 传输:连接闪断后自动重建(不再永久超时);版本守卫会校验摘要来源,损坏/缺失的摘要不再当守卫。
43
+ - 宿主 HTTP:请求体超限、路径含 NUL、根外访问返回 400 而不是 500;`?workspace=` 单独给出时也生效。
44
+ - 性能:会话面板刷新的日志头读取加签名缓存(31 会话冷 17 ms → 热 1.3–3.7 ms);`GET /roots` 与
45
+ `user-roots.json` 读取走 stat 短路;`?session=` 过滤先过滤后投影。
46
+
38
47
  ## 发布说明
39
48
 
40
49
  包里带了两个平台的 Tier 2 代理二进制(纯静态,远端零依赖)。源码在配套的 Go 仓库
@@ -45,10 +54,10 @@ shell:
45
54
 
46
55
  | 平台 | 字节 | sha256 |
47
56
  |---|---|---|
48
- | linux-amd64 | 4,944,032 | `fd5a5d6f8be07e786cc7c34a1aad8e2f040f1de3b7f124e739d2209df5ab5038` |
49
- | linux-arm64 | 4,653,216 | `202f4be3d1c1d98ccae27b9ccaa4879fa148c741981063f8be98689674b3ad2e` |
57
+ | linux-amd64 | 4,944,032 | `d45237ef1bfec1955fb5e6a2391bd651ffeaffb89e781e1c518e9e9836eec974` |
58
+ | linux-arm64 | 4,653,216 | `237a3cc9f5ae57f752f62eee4f8a57bd8c4684fd4d3fa6ab5851115baa844ccd` |
50
59
 
51
- - 代理版本:`0.2.1-e019f2d`
60
+ - 代理版本:`0.2.2-e019f2d`
52
61
  - 源码提交:`e019f2d`(Go 仓 `dsh-over-ssh-agent` 的这个提交)
53
62
  - 清单原文:包内 `agents/agent-manifest.json`
54
63
  - 核对:`sha256sum agents/<平台>/dsh-over-ssh-agent` 与上表比对;按上面的提交检出 Go 仓后
@@ -1,19 +1,19 @@
1
1
  {
2
2
  "version": 1,
3
- "generatedAt": "2026-11-05T23:45:29.528Z",
4
- "agentVersion": "0.2.1-e019f2d",
3
+ "generatedAt": "2026-11-06T16:15:01.747Z",
4
+ "agentVersion": "0.2.2-e019f2d",
5
5
  "sourceCommit": "e019f2d",
6
6
  "artifacts": [
7
7
  {
8
8
  "platform": "linux-amd64",
9
9
  "path": "linux-amd64/dsh-over-ssh-agent",
10
- "sha256": "fd5a5d6f8be07e786cc7c34a1aad8e2f040f1de3b7f124e739d2209df5ab5038",
10
+ "sha256": "d45237ef1bfec1955fb5e6a2391bd651ffeaffb89e781e1c518e9e9836eec974",
11
11
  "bytes": 4944032
12
12
  },
13
13
  {
14
14
  "platform": "linux-arm64",
15
15
  "path": "linux-arm64/dsh-over-ssh-agent",
16
- "sha256": "202f4be3d1c1d98ccae27b9ccaa4879fa148c741981063f8be98689674b3ad2e",
16
+ "sha256": "237a3cc9f5ae57f752f62eee4f8a57bd8c4684fd4d3fa6ab5851115baa844ccd",
17
17
  "bytes": 4653216
18
18
  }
19
19
  ]
package/lib/client.js CHANGED
@@ -1698,8 +1698,8 @@ function parseRootsSnapshot(value) {
1698
1698
  const row = toRootRow(entry, "config", false);
1699
1699
  const identity = row === void 0 ? void 0 : rootKeyIdentity(row.key);
1700
1700
  if (row !== void 0 && identity !== void 0 && !seen.has(identity)) {
1701
- const scoped = userWorkspaces.get(identity);
1702
- const scopedSessions = userSessions.get(identity);
1701
+ const scoped = row.workspaces ?? userWorkspaces.get(identity);
1702
+ const scopedSessions = row.sessions ?? userSessions.get(identity);
1703
1703
  roots.push({
1704
1704
  ...row,
1705
1705
  inUserFile: userKeys.has(identity),
@@ -1723,7 +1723,14 @@ function parseRootsSnapshot(value) {
1723
1723
  }
1724
1724
  const warnings = (Array.isArray(payload.warnings) ? payload.warnings : []).filter((item) => typeof item === "string");
1725
1725
  const anchorRoot = readString(payload, "anchorRoot");
1726
- return { roots, connections, warnings, ...anchorRoot === void 0 ? {} : { anchorRoot } };
1726
+ const scopedForSession = readString(payload, "scopedForSession");
1727
+ return {
1728
+ roots,
1729
+ connections,
1730
+ warnings,
1731
+ ...anchorRoot === void 0 ? {} : { anchorRoot },
1732
+ ...scopedForSession === void 0 ? {} : { scopedForSession }
1733
+ };
1727
1734
  }
1728
1735
  function isAbsoluteRemotePath(path) {
1729
1736
  if (!path.startsWith("/")) return false;
@@ -1912,13 +1919,27 @@ async function requestSnapshot(fetchImpl, path, init) {
1912
1919
  const reason = typeof body?.error === "string" ? body.error : transportFailureReason(response.status, path);
1913
1920
  return { ok: false, error: reason };
1914
1921
  }
1922
+ if (typeof body === "object" && body !== null && body["ok"] === false && typeof body["error"] === "string") {
1923
+ return { ok: false, error: body["error"] };
1924
+ }
1915
1925
  return { ok: true, value: parseRootsSnapshot(body) };
1916
1926
  } catch (error) {
1917
1927
  return { ok: false, error: error instanceof Error ? error.name : "network" };
1918
1928
  }
1919
1929
  }
1920
1930
  function createRootsApi(fetchImpl, path = ROOTS_API_PATH) {
1921
- const load = async (signal) => requestSnapshot(fetchImpl, path, { method: "GET", headers: { accept: "application/json" }, ...signal === void 0 ? {} : { signal } });
1931
+ const load = async (options = {}) => {
1932
+ const normalized = options instanceof AbortSignal ? { signal: options } : options;
1933
+ const query = new URLSearchParams();
1934
+ if (normalized.session !== void 0 && normalized.session !== "") query.set("session", normalized.session);
1935
+ if (normalized.workspace !== void 0 && normalized.workspace !== "") query.set("workspace", normalized.workspace);
1936
+ const suffix = query.size === 0 ? "" : `?${query.toString()}`;
1937
+ return requestSnapshot(fetchImpl, `${path}${suffix}`, {
1938
+ method: "GET",
1939
+ headers: { accept: "application/json" },
1940
+ ...normalized.signal === void 0 ? {} : { signal: normalized.signal }
1941
+ });
1942
+ };
1922
1943
  const send = async (payload) => requestSnapshot(fetchImpl, path, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(payload) });
1923
1944
  const sendRaw = async (payload) => {
1924
1945
  try {
@@ -2435,7 +2456,16 @@ function createTransferProgress(options) {
2435
2456
  setState(transferPanelPollStarted(state, showLoading));
2436
2457
  const options_ = mode === "park" ? { waitMs, signal: controller.signal } : { signal: controller.signal };
2437
2458
  issued += 1;
2438
- const result = await options.io.list(options_);
2459
+ let result;
2460
+ try {
2461
+ result = await options.io.list(options_);
2462
+ } catch (error) {
2463
+ if (controller.signal.aborted || disposed) {
2464
+ if (inFlight === controller) inFlight = void 0;
2465
+ continue;
2466
+ }
2467
+ result = { ok: false, error: error instanceof Error ? error.message : "list-failed" };
2468
+ }
2439
2469
  if (inFlight === controller) inFlight = void 0;
2440
2470
  if (disposed) return;
2441
2471
  if (mine !== generation) {
@@ -2476,6 +2506,7 @@ function createTransferProgress(options) {
2476
2506
  },
2477
2507
  clear: () => {
2478
2508
  if (disposed) return;
2509
+ if (state.clearing) return;
2479
2510
  setState(transferPanelClearing(state));
2480
2511
  void options.io.clear().then((result) => {
2481
2512
  if (disposed) return;
@@ -2574,7 +2605,9 @@ var ROOTS_EDIT_BACKDROP_CLASS = classes(CLASS.backdrop, CLASS.backdropSettings);
2574
2605
  function createRootsPanelController(options) {
2575
2606
  let state = initialRootsPanelState();
2576
2607
  const listeners = /* @__PURE__ */ new Set();
2577
- let token = 0;
2608
+ let readToken = 0;
2609
+ let writeToken = 0;
2610
+ let epoch = 0;
2578
2611
  let disposed = false;
2579
2612
  const requests = createAbortGate();
2580
2613
  const dispatch = (action) => {
@@ -2583,22 +2616,30 @@ function createRootsPanelController(options) {
2583
2616
  state = next;
2584
2617
  for (const listener of [...listeners]) listener();
2585
2618
  };
2586
- const latest = (mine) => !disposed && mine === token;
2619
+ const latestRead = (mine, issuedEpoch) => !disposed && mine === readToken && issuedEpoch === epoch;
2620
+ const latestWrite = (mine) => !disposed && mine === writeToken;
2587
2621
  const reload = async () => {
2588
- const mine = ++token;
2622
+ const mine = ++readToken;
2623
+ const issuedEpoch = epoch;
2589
2624
  const signal = requests.begin();
2590
2625
  dispatch({ type: "load" });
2591
2626
  const result = await options.api.load(signal);
2592
- if (!latest(mine)) return;
2627
+ if (!latestRead(mine, issuedEpoch)) return;
2628
+ if (result.ok && result.value.warnings.includes("roots-invalid-answer")) {
2629
+ dispatch({ type: "failed", error: "roots-invalid-answer" });
2630
+ return;
2631
+ }
2593
2632
  if (result.ok) dispatch({ type: "loaded", snapshot: result.value });
2594
2633
  else dispatch({ type: "failed", error: result.error });
2595
2634
  };
2596
2635
  const settle = async (mine, scope, result, resetDrafts) => {
2597
- if (!latest(mine)) return;
2636
+ if (!latestWrite(mine)) return;
2598
2637
  if (!result.ok) {
2599
2638
  dispatch({ type: "failed", error: result.error, scope });
2600
2639
  return;
2601
2640
  }
2641
+ epoch += 1;
2642
+ void result.value;
2602
2643
  dispatch({ type: "succeeded", scope, ...resetDrafts ? { resetDrafts: true } : {} });
2603
2644
  await reload();
2604
2645
  };
@@ -2611,7 +2652,7 @@ function createRootsPanelController(options) {
2611
2652
  dispatch({ type: "refused", scope: "roots", message: validated.message });
2612
2653
  return;
2613
2654
  }
2614
- const mine = ++token;
2655
+ const mine = ++writeToken;
2615
2656
  dispatch({ type: "submitted" });
2616
2657
  const result = await options.api.send({ action: "add-root", ...validated.value });
2617
2658
  await settle(mine, "roots", result, true);
@@ -2637,7 +2678,7 @@ function createRootsPanelController(options) {
2637
2678
  dispatch({ type: "refused", scope: "connection", message: validated.message });
2638
2679
  return;
2639
2680
  }
2640
- const mine = ++token;
2681
+ const mine = ++writeToken;
2641
2682
  dispatch({ type: "submitted" });
2642
2683
  const result = await options.api.send({ action: "add-connection", ...validated.value });
2643
2684
  await settle(mine, "connection", result, true);
@@ -2646,7 +2687,7 @@ function createRootsPanelController(options) {
2646
2687
  const row = selectRootsView(state).rows.find((candidate) => candidate.key === key);
2647
2688
  if (row === void 0 || !row.removable) return;
2648
2689
  if (!canEdit(state)) return;
2649
- const mine = ++token;
2690
+ const mine = ++writeToken;
2650
2691
  dispatch({ type: "remove", key });
2651
2692
  const result = await options.api.send({ action: "remove-root", key });
2652
2693
  await settle(mine, "roots", result, false);
@@ -2702,7 +2743,7 @@ function createRootsPanelController(options) {
2702
2743
  dispatch({ type: "refused", scope: "edit", message: validated.message });
2703
2744
  return;
2704
2745
  }
2705
- const mine = ++token;
2746
+ const mine = ++writeToken;
2706
2747
  dispatch({ type: "submitted" });
2707
2748
  const result = await options.api.send({ action: "update-connection", id: editingId, ...validated.value });
2708
2749
  await settle(mine, "edit", result, true);
@@ -2716,16 +2757,16 @@ function createRootsPanelController(options) {
2716
2757
  const confirmRemoveConnection = async (id) => {
2717
2758
  if (state.confirmingRemove !== id) return;
2718
2759
  if (!canEdit(state)) return;
2719
- const mine = ++token;
2760
+ const mine = ++writeToken;
2720
2761
  dispatch({ type: "submitted" });
2721
2762
  const result = await options.api.send({ action: "remove-connection", id });
2722
2763
  await settle(mine, "edit", result, true);
2723
2764
  };
2724
2765
  const loadImport = async () => {
2725
- const mine = ++token;
2766
+ const mine = ++readToken;
2726
2767
  dispatch({ type: "import-busy" });
2727
2768
  const result = await options.api.sendRaw({ action: "list-ssh-config" });
2728
- if (!latest(mine)) return;
2769
+ if (disposed || mine !== readToken) return;
2729
2770
  if (!result.ok) {
2730
2771
  dispatch({ type: "import-failed", error: result.error });
2731
2772
  return;
@@ -2739,10 +2780,10 @@ function createRootsPanelController(options) {
2739
2780
  const submitImport = async () => {
2740
2781
  const selected = state.importSelected ?? [];
2741
2782
  if (selected.length === 0) return;
2742
- const mine = ++token;
2783
+ const mine = ++writeToken;
2743
2784
  dispatch({ type: "import-busy" });
2744
2785
  const result = await options.api.sendRaw({ action: "import-ssh-config", aliases: selected });
2745
- if (!latest(mine)) return;
2786
+ if (!latestWrite(mine)) return;
2746
2787
  if (!result.ok) {
2747
2788
  dispatch({ type: "import-failed", error: result.error });
2748
2789
  return;
@@ -3625,10 +3666,10 @@ function buildPanelModel(copy, options = {}) {
3625
3666
  // src/shared/remote-identity.ts
3626
3667
  var SSH_DEV_REMOTE_NAMESPACE = "sshdev";
3627
3668
  var SSH_DEV_REMOTE_SERVICE = "sshDevRemote";
3628
-
3629
- // src/client/core/remote.ts
3630
3669
  var SSH_DEV_REMOTE_METHOD = "connections";
3631
3670
  var SSH_DEV_REMOTE_PACKAGE = "dsh-over-ssh";
3671
+
3672
+ // src/client/core/remote.ts
3632
3673
  var SSH_DEV_CONNECTIONS_DESCRIPTOR = {
3633
3674
  id: `${SSH_DEV_REMOTE_PACKAGE}#${SSH_DEV_REMOTE_NAMESPACE}/${SSH_DEV_REMOTE_METHOD}`,
3634
3675
  service: SSH_DEV_REMOTE_SERVICE,
@@ -4526,6 +4567,7 @@ function createSessionRowActionStore(deps) {
4526
4567
  };
4527
4568
  });
4528
4569
  let cancelAutoClose;
4570
+ let postInFlight = false;
4529
4571
  const stopAutoClose = () => {
4530
4572
  cancelAutoClose?.();
4531
4573
  cancelAutoClose = void 0;
@@ -4550,18 +4592,21 @@ function createSessionRowActionStore(deps) {
4550
4592
  const confirm = async () => {
4551
4593
  const request = state.request;
4552
4594
  const row = state.row;
4553
- if (disposed || request === void 0 || state.busy) return;
4595
+ if (disposed || request === void 0 || state.busy || postInFlight) return;
4554
4596
  if (state.phase !== "ready" || row === void 0 || !row.deletable) return;
4555
4597
  const stillPending = () => {
4556
4598
  const current = state.request;
4557
4599
  return current !== void 0 && current.kind === request.kind && current.sessionId === request.sessionId;
4558
4600
  };
4601
+ postInFlight = true;
4559
4602
  dispatch({ ...state, busy: true, outcome: void 0 });
4560
4603
  let result;
4561
4604
  try {
4562
4605
  result = await deleteSessions(deps.io, [request.sessionId], request.kind === "delete" ? { permanent: true } : {});
4563
4606
  } catch {
4564
4607
  result = { kind: "error", message: { key: "sessionResultActionFailed" } };
4608
+ } finally {
4609
+ postInFlight = false;
4565
4610
  }
4566
4611
  if (disposed || !stillPending()) return;
4567
4612
  dispatch({ ...state, busy: false, outcome: result });
@@ -4584,7 +4629,7 @@ function createSessionRowActionStore(deps) {
4584
4629
  request: (request) => {
4585
4630
  if (disposed) return;
4586
4631
  stopAutoClose();
4587
- dispatch({ request, phase: "loading", row: void 0, busy: false, outcome: void 0 });
4632
+ dispatch({ request, phase: "loading", row: void 0, busy: postInFlight, outcome: void 0 });
4588
4633
  },
4589
4634
  load,
4590
4635
  confirm,
@@ -5281,7 +5326,7 @@ function DirectoryBrowserSection({
5281
5326
  listRef
5282
5327
  }) {
5283
5328
  const view = selectDirectoryBrowserView(state);
5284
- const gate = disabled;
5329
+ const gate = disabled || state.pending;
5285
5330
  const connectionFieldId = `${idPrefix}-connection`;
5286
5331
  const modeButton = (mode, label) => Button({
5287
5332
  // The active world is the brand-filled one: the switch reads without an extra attribute, and both
@@ -5503,12 +5548,18 @@ function createSessionDirectoryIo(options) {
5503
5548
  });
5504
5549
  const rootsApi = createRootsApi(options.fetchImpl, path);
5505
5550
  const loadRoots = async (workspacePath, sessionId) => {
5506
- const result = await rootsApi.load();
5551
+ const result = await rootsApi.load({
5552
+ ...sessionId === void 0 || sessionId === "" ? {} : { session: sessionId },
5553
+ ...workspacePath === void 0 || workspacePath === "" ? {} : { workspace: workspacePath }
5554
+ });
5507
5555
  if (!result.ok) return result;
5508
5556
  if (result.value.warnings.includes("roots-invalid-answer")) {
5509
5557
  return { ok: false, error: "roots-invalid-answer" };
5510
5558
  }
5511
5559
  const all = result.value.roots;
5560
+ if (sessionId !== void 0 && sessionId !== "" && result.value.scopedForSession === sessionId) {
5561
+ return { ok: true, value: all };
5562
+ }
5512
5563
  if (workspacePath === void 0 || workspacePath === "") return { ok: true, value: [] };
5513
5564
  return {
5514
5565
  ok: true,
@@ -5651,7 +5702,11 @@ function selectSessionDirectoryView(state, copy) {
5651
5702
  title: row.label ?? (row.location === "remote" ? `${row.connectionId ?? ""}:${row.path}` : row.path),
5652
5703
  subtitle: row.label === void 0 ? "" : row.path,
5653
5704
  badge: row.location === "remote" ? copy["rootsBadgeRemote"] : copy["rootsBadgeLocal"],
5654
- removable: row.inUserFile
5705
+ // fork 继承来的行没有删除按钮:它在父亲那里才是"本会话添加的",在这里点删除会把父会话的
5706
+ // 目录拿走(宿主也会拒绝)。没有 sessions 字段的历史条目按文件权限判定,保持旧行为。
5707
+ removable: row.inUserFile && // 会话型条目只有**本会话**能删;`state.sessionId` 还没到也不能放行(那会把父会话的目录
5708
+ // 交给 fork 去删,宿主会拒,用户看到的是一次"按了没反应")。历史条目(无 sessions)按文件权限。
5709
+ (row.sessions === void 0 || state.sessionId !== void 0 && row.sessions.includes(state.sessionId))
5655
5710
  }))
5656
5711
  ],
5657
5712
  rootsPending: state.rootsPending,
@@ -5706,11 +5761,14 @@ function createSessionDirectory(deps) {
5706
5761
  const commit = async (path, connectionId) => {
5707
5762
  if (disposed || state.pending) return;
5708
5763
  const location = connectionId === void 0 ? "local" : "remote";
5709
- const validated = validateRootDraft({
5710
- location,
5711
- path,
5712
- ...connectionId === void 0 ? {} : { connectionId }
5713
- });
5764
+ const validated = validateRootDraft(
5765
+ {
5766
+ location,
5767
+ path,
5768
+ ...connectionId === void 0 ? {} : { connectionId }
5769
+ },
5770
+ state.roots
5771
+ );
5714
5772
  if (!validated.ok) {
5715
5773
  dispatch({ type: "refused", message: validated.message });
5716
5774
  return;
@@ -5819,7 +5877,10 @@ function SessionDirectorySection({
5819
5877
  { className: CLASS.block },
5820
5878
  (0, import_react11.createElement)("h4", { className: CLASS.blockTitle }, copy.sessionRootsTitle),
5821
5879
  (0, import_react11.createElement)("p", { className: CLASS.hint }, copy.sessionRootsHint),
5822
- view.roots.filter((row) => row.workspace !== true).length === 0 ? view.rootsReading ? Notice({ role: "status", tone: "info", children: copy.sessionRootsLoading }) : (
5880
+ // **有行就画列表**:工作区那一行是合成行,它不属于"已添加目录",但它是这张卡片最重要的一行。
5881
+ // 旧条件按"有没有已添加目录"决定画不画整张列表,结果是新会话只看到「还没有添加目录」,连会话
5882
+ // 自己在哪个工作区都不显示,必须先去添加一个目录才冒出来(审计 2026-11-06,用户截图)。
5883
+ view.roots.length === 0 ? view.rootsReading ? Notice({ role: "status", tone: "info", children: copy.sessionRootsLoading }) : (
5823
5884
  // A failed read is not an empty list: the error notice below is the whole truth there, and
5824
5885
  // 「还没有添加目录」 next to it would contradict it (same rule as the import dialog, ui P2-2/P2-3).
5825
5886
  view.notice === "" ? (0, import_react11.createElement)("p", { className: CLASS.empty }, copy.sessionRootsEmpty) : null
@@ -5839,7 +5900,9 @@ function SessionDirectorySection({
5839
5900
  Button({
5840
5901
  variant: "ghost",
5841
5902
  size: "compact",
5842
- disabled: view.rootsPending,
5903
+ // 刷新在飞时也禁用:否则一次点击会叠加"删除 + 一次刷新"两次远端写
5904
+ // (审计 2026-11-06 客户端 P2)。
5905
+ disabled: view.rootsPending || view.rootsReading,
5843
5906
  onClick: () => actions.onRemoveRoot(row.key),
5844
5907
  children: copy.rootsRemove
5845
5908
  })
@@ -5847,6 +5910,8 @@ function SessionDirectorySection({
5847
5910
  )
5848
5911
  )
5849
5912
  ),
5913
+ // 列表里只剩工作区行时,"还没有添加目录"仍然是有用的下一步提示;但它不再替换列表。
5914
+ view.roots.length > 0 && view.roots.filter((row) => row.workspace !== true).length === 0 && !view.rootsReading && view.notice === "" ? (0, import_react11.createElement)("p", { className: CLASS.empty }, copy.sessionRootsEmpty) : null,
5850
5915
  (0, import_react11.createElement)(
5851
5916
  "div",
5852
5917
  { className: CLASS.actions },
@@ -5886,20 +5951,22 @@ function readWorkspacePath(props) {
5886
5951
  const fromWorkspace = workspaces === void 0 ? void 0 : workspaces(
5887
5952
  (snapshot) => sessionId === void 0 || sessionId === "" ? void 0 : snapshot?.items?.find((item) => item.sessionIds?.includes(sessionId) === true)?.path
5888
5953
  );
5889
- if (typeof fromSession === "string" && fromSession !== "") return fromSession;
5890
5954
  if (typeof fromWorkspace === "string" && fromWorkspace !== "") return fromWorkspace;
5955
+ if (typeof fromSession === "string" && fromSession !== "") return fromSession;
5891
5956
  return void 0;
5892
5957
  }
5893
5958
  function rootsQueryKey(state) {
5894
5959
  if (!state.open) return "closed";
5895
- if (state.workspacePath === void 0 || state.workspacePath === "") return "open:";
5896
- return `open:${state.workspacePath}\0${state.sessionId ?? ""}`;
5960
+ const workspace = state.workspacePath === void 0 || state.workspacePath === "" ? "unknown" : state.workspacePath;
5961
+ const session = state.sessionId === void 0 || state.sessionId === "" ? "unknown" : state.sessionId;
5962
+ return `open:${workspace}\0${session}`;
5897
5963
  }
5898
5964
  function SessionDirectoryContainer(props) {
5899
5965
  const { copy, io } = props;
5900
5966
  const copyRef = (0, import_react11.useRef)(copy);
5901
5967
  copyRef.current = copy;
5902
- const [controller] = (0, import_react11.useState)(() => createSessionDirectory({ io, copy: () => copyRef.current }));
5968
+ const injected = props.controller;
5969
+ const [controller] = (0, import_react11.useState)(() => injected ?? createSessionDirectory({ io, copy: () => copyRef.current }));
5903
5970
  const state = (0, import_react11.useSyncExternalStore)(controller.subscribe, controller.getState);
5904
5971
  const workspacePath = readWorkspacePath(props);
5905
5972
  const titleId = (0, import_react11.useId)();
@@ -5924,8 +5991,10 @@ function SessionDirectoryContainer(props) {
5924
5991
  useCardTabTrap(state.open, portal.container);
5925
5992
  (0, import_react11.useEffect)(() => {
5926
5993
  controller.activate();
5927
- return () => controller.dispose();
5928
- }, [controller]);
5994
+ return () => {
5995
+ if (injected === void 0) controller.dispose();
5996
+ };
5997
+ }, [controller, injected]);
5929
5998
  (0, import_react11.useLayoutEffect)(() => {
5930
5999
  if (!state.open) return;
5931
6000
  focusOrigin.current = captureFocusOrigin(browserPortalDocument());
@@ -6889,6 +6958,7 @@ function apply(ctx) {
6889
6958
  return await chooser.pickDirectory();
6890
6959
  }
6891
6960
  });
6961
+ const controller = createSessionDirectory({ io, copy: () => copy() });
6892
6962
  const registerSessionEntry = () => ctx.slots.inject(
6893
6963
  SESSION_DIRECTORY_SLOT,
6894
6964
  () => ctx.slots.register(
@@ -6896,7 +6966,7 @@ function apply(ctx) {
6896
6966
  name: SESSION_DIRECTORY_SLOT,
6897
6967
  id: SESSION_DIRECTORY_ID,
6898
6968
  order: SESSION_DIRECTORY_ORDER,
6899
- inject: () => ({ copy: copy(), io })
6969
+ inject: () => ({ copy: copy(), io, controller })
6900
6970
  },
6901
6971
  SessionDirectoryContainer
6902
6972
  )
@@ -6909,6 +6979,7 @@ function apply(ctx) {
6909
6979
  return () => {
6910
6980
  offLocaleChange();
6911
6981
  disposeEntry();
6982
+ controller.dispose();
6912
6983
  };
6913
6984
  });
6914
6985
  ctx.effect(() => {
@@ -154,11 +154,28 @@ function normalizeLocalPath2(path, options = {}) {
154
154
  return normalizeLocalPath(path, { caseInsensitive: options.caseInsensitive ?? process.platform === "win32" });
155
155
  }
156
156
 
157
+ // src/host/integration/session-fork-pending.ts
158
+ var pending = /* @__PURE__ */ new Map();
159
+ function pendingForkSources(sessionId) {
160
+ if (sessionId === void 0 || sessionId === "") return [];
161
+ const source = pending.get(sessionId);
162
+ return source === void 0 ? [] : [source];
163
+ }
164
+
157
165
  // src/host/integration/session-scope.ts
158
166
  var MAX_FORK_DEPTH = 32;
159
167
  var ancestorCache = /* @__PURE__ */ new Map();
160
168
  var ANCESTOR_CACHE_MAX = 256;
161
169
  function readSessionAncestors(ctx, sessionId) {
170
+ const pending2 = pendingForkSources(sessionId);
171
+ if (pending2.length > 0) {
172
+ const inherited = /* @__PURE__ */ new Set();
173
+ for (const source of pending2) {
174
+ inherited.add(source);
175
+ for (const ancestor of walkAncestors(ctx, source).ancestors) inherited.add(ancestor);
176
+ }
177
+ return [...inherited];
178
+ }
162
179
  const cached = ancestorCache.get(sessionId);
163
180
  if (cached !== void 0) {
164
181
  ancestorCache.delete(sessionId);