@dsh-electron/dsh-plugin-multi-root-workspace 0.1.6 → 0.1.7

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 (93) hide show
  1. package/CHANGELOG.en.md +44 -1
  2. package/CHANGELOG.md +44 -1
  3. package/README.en.md +155 -92
  4. package/README.md +156 -91
  5. package/cordis.patch.yml +57 -0
  6. package/lib/client.js +2678 -276
  7. package/lib/client.js.map +1 -1
  8. package/lib/command-CESuwlVt.js +1306 -0
  9. package/lib/command.js +2 -2
  10. package/lib/{compat-B6ZqbQBy.js → compat-DF_yL1bB.js} +160 -2
  11. package/lib/compat.js +1 -1
  12. package/lib/contract-DZ4zczDT.js +238 -0
  13. package/lib/{fs-D_Sw3o5z.js → fs-9JIzGaf3.js} +55 -19
  14. package/lib/fs.js +2 -1
  15. package/lib/index.js +10 -8
  16. package/lib/{instructions-DfzN-wVf.js → instructions-D19jRxN0.js} +3 -2
  17. package/lib/instructions.js +1 -1
  18. package/lib/lsp.js +7 -2
  19. package/lib/method-wrapper-CFY3p3j2.js +26 -0
  20. package/lib/process-fence.js +280 -0
  21. package/lib/providers.js +87 -0
  22. package/lib/{registry-DySMbjgW.js → registry-DCxC5Lay.js} +256 -35
  23. package/lib/registry.js +2 -2
  24. package/lib/{roots-DasWEBJk.js → roots-B-0l8mNG.js} +117 -20
  25. package/lib/sandbox-Kup4_ZIb.js +747 -0
  26. package/lib/sandbox.js +2 -1
  27. package/lib/scope.js +229 -20
  28. package/lib/types/client/RootSwitcher.d.ts +63 -0
  29. package/lib/types/client/WorkspaceFoldersAction.d.ts +39 -1
  30. package/lib/types/client/files-tree/FilesBody.module.css.d.ts +24 -0
  31. package/lib/types/client/files-tree/FilesTree.d.ts +47 -0
  32. package/lib/types/client/files-tree/adapter.d.ts +94 -0
  33. package/lib/types/client/files-tree/directory-node.d.ts +62 -0
  34. package/lib/types/client/files-tree/face.d.ts +107 -0
  35. package/lib/types/client/files-tree/locales.d.ts +10 -0
  36. package/lib/types/client/files-tree/primitives.d.ts +33 -0
  37. package/lib/types/client/files-tree/store.d.ts +81 -0
  38. package/lib/types/client/locales.d.ts +56 -10
  39. package/lib/types/client/open-in-app/OpenInAppAction.d.ts +27 -0
  40. package/lib/types/client/open-in-app/OpenTargetButton.d.ts +52 -0
  41. package/lib/types/client/open-in-app/OpenTargetButton.module.css.d.ts +29 -0
  42. package/lib/types/client/open-in-app/adapter.d.ts +46 -0
  43. package/lib/types/client/open-in-app/applications.d.ts +4 -0
  44. package/lib/types/client/open-in-app/controller.d.ts +53 -0
  45. package/lib/types/client/open-in-app/locales.d.ts +10 -0
  46. package/lib/types/client/open-in-app/open-failure-toast.d.ts +11 -0
  47. package/lib/types/client/open-in-app/open-path.d.ts +13 -0
  48. package/lib/types/client/panel-client.d.ts +2 -1
  49. package/lib/types/client/styles.d.ts +1 -1
  50. package/lib/types/command.d.ts +16 -3
  51. package/lib/types/compat/client-icons.d.ts +18 -0
  52. package/lib/types/compat/dsh-version.d.ts +1 -1
  53. package/lib/types/compat/open-in-app-routes.d.ts +38 -0
  54. package/lib/types/compat/process-entrypoints.d.ts +31 -0
  55. package/lib/types/compat/process-result.d.ts +14 -0
  56. package/lib/types/compat/pty-marker.d.ts +83 -0
  57. package/lib/types/compat/sandbox-confine.d.ts +2 -0
  58. package/lib/types/compat/storage-zod.d.ts +65 -0
  59. package/lib/types/compat/windows-deny-preflight.d.ts +21 -0
  60. package/lib/types/compat/windows-native.d.ts +68 -0
  61. package/lib/types/compat/windows-path-guard.d.ts +56 -0
  62. package/lib/types/compat/workspace-files.d.ts +2 -0
  63. package/lib/types/contract.d.ts +31 -19
  64. package/lib/types/dialects.d.ts +99 -3
  65. package/lib/types/fs.d.ts +32 -10
  66. package/lib/types/index.d.ts +7 -2
  67. package/lib/types/process-fence.d.ts +54 -0
  68. package/lib/types/profile-isolation.d.ts +178 -0
  69. package/lib/types/providers.d.ts +70 -0
  70. package/lib/types/registry.d.ts +132 -31
  71. package/lib/types/roots.d.ts +100 -13
  72. package/lib/types/sandbox.d.ts +59 -3
  73. package/lib/types/scope.d.ts +200 -14
  74. package/lib/types/windows-broker.d.ts +17 -0
  75. package/lib/types/windows-deny.d.ts +1 -0
  76. package/lib/types/windows-profile.d.ts +10 -13
  77. package/lib/types/windows-runner-contract.d.ts +97 -0
  78. package/lib/windows-runner-contract-CEfmZzu0.js +141 -0
  79. package/lib/windows-runner.js +1256 -13
  80. package/lib/workspace-files.js +138 -3
  81. package/lib/workspace-routing-YPxaQ4V3.js +26 -0
  82. package/package.json +84 -62
  83. package/scripts/windows-broker/install.ps1 +89 -0
  84. package/scripts/windows-broker/rights.cs +58 -0
  85. package/scripts/windows-broker/service.cs +323 -0
  86. package/scripts/windows-broker/verify-install.ps1 +80 -0
  87. package/lib/command-CTdNvwt1.js +0 -791
  88. package/lib/method-wrapper-Zc-JlDP3.js +0 -51
  89. package/lib/sandbox-BRXl_FpF.js +0 -132
  90. package/lib/types/client/RootFileTree.d.ts +0 -12
  91. package/lib/types/panel-files.d.ts +0 -3
  92. package/lib/windows-profile-CiFsFYIV.js +0 -216
  93. package/lib/{registry-lease-win32-dy8TQMD9.js → registry-lease-win32-DTO6QpT9.js} +2 -2
package/lib/client.js CHANGED
@@ -11,6 +11,15 @@ window.__ModuleLoader__.load({
11
11
  var __getOwnPropNames = Object.getOwnPropertyNames;
12
12
  var __getProtoOf = Object.getPrototypeOf;
13
13
  var __hasOwnProp = Object.prototype.hasOwnProperty;
14
+ var __exportAll = (all, no_symbols) => {
15
+ let target = {};
16
+ for (var name in all) __defProp(target, name, {
17
+ get: all[name],
18
+ enumerable: true
19
+ });
20
+ if (!no_symbols) __defProp(target, Symbol.toStringTag, { value: "Module" });
21
+ return target;
22
+ };
14
23
  var __copyProps = (to, from, except, desc) => {
15
24
  if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
16
25
  key = keys[i];
@@ -27,8 +36,11 @@ window.__ModuleLoader__.load({
27
36
  }) : target, mod));
28
37
  //#endregion
29
38
  let react = require("react");
39
+ let react_dom = require("react-dom");
30
40
  let _deepseek_ai_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives");
31
41
  _deepseek_ai_dsh_client_ui_primitives = __toESM(_deepseek_ai_dsh_client_ui_primitives, 1);
42
+ require("@deepseek-ai/cordis");
43
+ let _deepseek_ai_dsh_client_store = require("@deepseek-ai/dsh-client-store");
32
44
  let react_jsx_runtime = require("react/jsx-runtime");
33
45
  //#region src/compat/client-session.ts
34
46
  /**
@@ -5252,7 +5264,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
5252
5264
  * from endpoint to response type, so the panel cannot silently decode one
5253
5265
  * endpoint's answer as another's. `reveal` answers with
5254
5266
  * {@link RevealedView}; root-management endpoints answer with
5255
- * {@link RootsView}, and browsing returns {@link FilesView} or {@link FilePreview}.
5267
+ * {@link RootsView}.
5256
5268
  * The host builds those answers in `command.ts` and the
5257
5269
  * panel parses them here — one definition, two consumers.
5258
5270
  * - **Runtime validation at the boundary.** The payload arrives from a browser
@@ -5280,6 +5292,11 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
5280
5292
  id: string().optional(),
5281
5293
  path: string().optional(),
5282
5294
  alias: string().optional(),
5295
+ access: _enum([
5296
+ "inherit",
5297
+ "read-write",
5298
+ "read-only"
5299
+ ]).optional(),
5283
5300
  beforeEntry: object({
5284
5301
  ordinal: number().int().positive(),
5285
5302
  id: string(),
@@ -5305,52 +5322,36 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
5305
5322
  "redirected",
5306
5323
  "invalid"
5307
5324
  ]),
5308
- detail: string().optional()
5325
+ access: _enum([
5326
+ "inherit",
5327
+ "read-write",
5328
+ "read-only"
5329
+ ]).optional(),
5330
+ detail: string().optional(),
5331
+ effectiveAccess: _enum([
5332
+ "read-only",
5333
+ "read-write",
5334
+ "full-access",
5335
+ "unavailable"
5336
+ ]).optional()
5309
5337
  });
5310
5338
  const rootsViewSchema = object({
5311
5339
  primaryRoot: string(),
5312
5340
  primaryName: string().optional(),
5313
5341
  effectiveCwd: string().optional(),
5342
+ mode: _enum([
5343
+ "read-only",
5344
+ "workspace-write",
5345
+ "danger-full-access"
5346
+ ]).optional(),
5347
+ cappedTools: array(string()).optional(),
5348
+ toolAccessInvalid: boolean().optional(),
5314
5349
  roots: array(rootViewSchema),
5350
+ limit: number().int().nonnegative().optional(),
5351
+ count: number().int().nonnegative().optional(),
5315
5352
  unavailable: string().optional()
5316
5353
  });
5317
5354
  const revealedViewSchema = object({ revealed: string() });
5318
- const filesViewSchema = object({
5319
- path: string(),
5320
- entries: array(object({
5321
- name: string().min(1).refine((name) => name !== "." && name !== ".." && !/[\\/]/.test(name) && !name.includes(String.fromCharCode(0))),
5322
- type: _enum([
5323
- "file",
5324
- "directory",
5325
- "other"
5326
- ])
5327
- })),
5328
- truncated: boolean()
5329
- });
5330
- const filePreviewSchema = object({
5331
- text: string(),
5332
- eof: boolean()
5333
- });
5334
- function parseFilesView(value) {
5335
- const parsed = filesViewSchema.safeParse(value);
5336
- return parsed.success ? {
5337
- ok: true,
5338
- value: parsed.data
5339
- } : {
5340
- ok: false,
5341
- message: firstIssue(parsed.error)
5342
- };
5343
- }
5344
- function parseFilePreview(value) {
5345
- const parsed = filePreviewSchema.safeParse(value);
5346
- return parsed.success ? {
5347
- ok: true,
5348
- value: parsed.data
5349
- } : {
5350
- ok: false,
5351
- message: firstIssue(parsed.error)
5352
- };
5353
- }
5354
5355
  const errorViewSchema = object({
5355
5356
  code: string(),
5356
5357
  message: string(),
@@ -5374,7 +5375,12 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
5374
5375
  primaryRoot: view.primaryRoot,
5375
5376
  ...view.primaryName === void 0 ? {} : { primaryName: view.primaryName },
5376
5377
  ...view.effectiveCwd === void 0 ? {} : { effectiveCwd: view.effectiveCwd },
5378
+ ...view.mode === void 0 ? {} : { mode: view.mode },
5379
+ ...view.cappedTools === void 0 ? {} : { cappedTools: view.cappedTools },
5380
+ ...view.toolAccessInvalid === void 0 ? {} : { toolAccessInvalid: view.toolAccessInvalid },
5377
5381
  roots: view.roots.map(narrowRootView),
5382
+ limit: view.limit ?? Number.MAX_SAFE_INTEGER,
5383
+ count: view.count ?? view.roots.length,
5378
5384
  ...view.unavailable === void 0 ? {} : { unavailable: view.unavailable }
5379
5385
  }
5380
5386
  };
@@ -5416,6 +5422,8 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
5416
5422
  path: value.path,
5417
5423
  addedAt: value.addedAt,
5418
5424
  state: value.state,
5425
+ access: value.access ?? "inherit",
5426
+ ...value.effectiveAccess === void 0 ? {} : { effectiveAccess: value.effectiveAccess },
5419
5427
  ...value.alias === void 0 ? {} : { alias: value.alias },
5420
5428
  ...value.detail === void 0 ? {} : { detail: value.detail }
5421
5429
  };
@@ -5439,23 +5447,31 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
5439
5447
  * @module @dsh-electron/dsh-plugin-multi-root-workspace/client/locales
5440
5448
  */
5441
5449
  /** The locale namespace this plugin owns. */
5442
- const NS = "multiRootWorkspace";
5450
+ const NS$2 = "multiRootWorkspace";
5443
5451
  /** Chinese copy (the canonical shape). */
5444
5452
  const zh = {
5445
- "files.browse": "浏览文件",
5446
- "files.reload": "刷新文件树",
5447
- "files.empty": "目录为空",
5448
- "files.truncated": "条目过多,列表已截断",
5449
- "files.previewTruncated": "仅显示前 200 行",
5453
+ "panel.mode": "当前会话权限:",
5454
+ "panel.effectiveAccess": "当前有效:",
5455
+ "panel.toolCaps": "工具 {tools} 对附属根始终只读。",
5456
+ "panel.invalidCap": "工具权限配置无效,附属根写入全部被拒绝。",
5457
+ "mode.read-only": "只读",
5458
+ "mode.workspace-write": "读写",
5459
+ "mode.danger-full-access": "完全权限(Full-access)",
5460
+ "effective.read-only": "只读",
5461
+ "effective.read-write": "读写",
5462
+ "effective.full-access": "完全权限(Full-access)",
5463
+ "effective.unavailable": "不可用",
5450
5464
  "action.label": "工作区目录",
5451
5465
  "action.title": "管理本会话的附属工作目录",
5466
+ "switcher.manage": "管理工作区目录…",
5452
5467
  "panel.title": "工作区目录",
5453
- "panel.subtitle": "主目录和附加目录遵循当前沙箱模式;workspace-write 可写,read-only 下都不可写。",
5468
+ "panel.subtitle": "主目录与「跟随会话」的附加目录遵循当前沙箱模式;目录也可固定为只读或读写。",
5454
5469
  "panel.primary": "主根",
5455
5470
  "panel.primaryNote": "会话原始项目,不可移除",
5456
5471
  "panel.cwd": "当前执行目录",
5457
5472
  "panel.cwdNote": "切换目录不会增加授权",
5458
5473
  "panel.additional": "附属工作目录",
5474
+ "panel.limitReached": "已达到附加目录数量上限,请先移除一个再添加。",
5459
5475
  "panel.empty": "还没有附属工作目录。",
5460
5476
  "panel.emptyHint": "添加后,Agent 可在同一会话中读写该目录,受限 shell 和终端同步生效。",
5461
5477
  "panel.noSession": "当前没有活动会话",
@@ -5475,9 +5491,20 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
5475
5491
  "panel.copyPath": "复制路径",
5476
5492
  "panel.copied": "已复制",
5477
5493
  "panel.reveal": "在文件管理器中显示",
5494
+ "panel.reRegister": "重新登记(保留原访问级别)",
5478
5495
  "panel.moveUp": "上移",
5479
5496
  "panel.moveDown": "下移",
5480
- "state.available": "可写",
5497
+ "access.inherit": "跟随会话",
5498
+ "access.readWrite": "固定读写",
5499
+ "access.readOnly": "只读",
5500
+ "panel.accessLabel": "访问权限",
5501
+ "panel.accessInherit": "设为跟随会话",
5502
+ "panel.accessReadOnly": "设为只读",
5503
+ "panel.accessReadWrite": "设为固定读写",
5504
+ "panel.accessUpgradeConfirm": "将该目录的权限改为「{access}」?在某些沙箱模式下,这将允许 Agent 修改其中的文件。",
5505
+ "panel.accessUpgradeTitle": "扩大访问权限",
5506
+ "panel.accessUpgradeDo": "修改",
5507
+ "state.available": "已授予",
5481
5508
  "state.missing": "目录不存在,暂不授予",
5482
5509
  "state.redirected": "登记目录已被替换(现在指向别处),暂不授予",
5483
5510
  "state.invalid": "登记无效",
@@ -5493,28 +5520,40 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
5493
5520
  "error.invalid-ref": "请求缺少必要的标识。",
5494
5521
  "error.storage-unavailable": "根目录登记的存储不可用,请检查 $DSH_HOME/storages 下的文件。",
5495
5522
  "error.registry-contended": "根目录登记正由另一个 DSH 进程占用;本进程不会授予附加根。关闭另一个进程后点重试。",
5523
+ "error.root-limit-reached": "已达到附加目录数量上限,请先移除一个再添加。",
5524
+ "error.config-invalid": "根目录登记的配置无效;本进程不会授予附加根,请修正配置后重启。",
5525
+ "error.root-unavailable": "该目录当前不可用,只能设为只读;请先恢复目录再调整权限。",
5496
5526
  "error.reveal-unavailable": "无法打开系统的文件管理器;登记项不受影响,路径仍可复制。",
5497
5527
  "error.unavailable": "无法连接到 dsh 主进程。",
5498
5528
  "error.copy-failed": "无法写入剪贴板,请检查浏览器的剪贴板权限。",
5499
5529
  "error.session-not-found": "找不到该会话;请先打开一个工作区会话。",
5530
+ "error.unsupported-path": "该路径是网络或设备路径,Windows ACL 隔离无法覆盖它。",
5500
5531
  "error.fallback": "操作失败。"
5501
5532
  };
5502
5533
  /** English copy, pinned to the Chinese key set. */
5503
5534
  const en = {
5504
- "files.browse": "Browse files",
5505
- "files.reload": "Refresh files",
5506
- "files.empty": "Empty directory",
5507
- "files.truncated": "More entries were omitted",
5508
- "files.previewTruncated": "Showing the first 200 lines",
5535
+ "panel.mode": "Current session access:",
5536
+ "panel.effectiveAccess": "Effective:",
5537
+ "panel.toolCaps": "The tools {tools} always have read-only access to additional roots.",
5538
+ "panel.invalidCap": "Invalid tool access configuration refuses all additional-root writes.",
5539
+ "mode.read-only": "Read-only",
5540
+ "mode.workspace-write": "Read-write",
5541
+ "mode.danger-full-access": "Full-access",
5542
+ "effective.read-only": "Read-only",
5543
+ "effective.read-write": "Read-write",
5544
+ "effective.full-access": "Full-access",
5545
+ "effective.unavailable": "Unavailable",
5509
5546
  "action.label": "Folders",
5510
5547
  "action.title": "Manage this session's secondary working directories",
5548
+ "switcher.manage": "Manage workspace folders…",
5511
5549
  "panel.title": "Workspace folders",
5512
- "panel.subtitle": "The primary and additional folders follow the current sandbox mode: writable in workspace-write and read-only in read-only.",
5550
+ "panel.subtitle": "The primary root and inherit folders follow the current sandbox mode; a folder can also be pinned read-only or read-write.",
5513
5551
  "panel.primary": "Primary root",
5514
5552
  "panel.primaryNote": "The session's original project; it cannot be removed",
5515
5553
  "panel.cwd": "Current working directory",
5516
5554
  "panel.cwdNote": "Changing directory does not grant access",
5517
5555
  "panel.additional": "Secondary working directories",
5556
+ "panel.limitReached": "The additional-folder limit is reached; remove one before adding another.",
5518
5557
  "panel.empty": "No secondary working directories yet.",
5519
5558
  "panel.emptyHint": "Once added, the agent can use the folder in this session, including confined shells and terminals.",
5520
5559
  "panel.noSession": "No active session",
@@ -5534,9 +5573,20 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
5534
5573
  "panel.copyPath": "Copy path",
5535
5574
  "panel.copied": "Copied",
5536
5575
  "panel.reveal": "Reveal in file manager",
5576
+ "panel.reRegister": "Re-register (keeps its access level)",
5537
5577
  "panel.moveUp": "Move up",
5538
5578
  "panel.moveDown": "Move down",
5539
- "state.available": "Writable",
5579
+ "access.inherit": "Inherit",
5580
+ "access.readWrite": "Read-write",
5581
+ "access.readOnly": "Read-only",
5582
+ "panel.accessLabel": "Access",
5583
+ "panel.accessInherit": "Make inherit",
5584
+ "panel.accessReadOnly": "Make read-only",
5585
+ "panel.accessReadWrite": "Make read-write",
5586
+ "panel.accessUpgradeConfirm": "Change this folder's access to \"{access}\"? Under some sandbox modes this lets the agent modify files in it.",
5587
+ "panel.accessUpgradeTitle": "Widen access",
5588
+ "panel.accessUpgradeDo": "Change",
5589
+ "state.available": "Granted",
5540
5590
  "state.missing": "Directory is absent; not granted",
5541
5591
  "state.redirected": "Registration was replaced (it now points elsewhere); not granted",
5542
5592
  "state.invalid": "Registration is unusable",
@@ -5552,10 +5602,14 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
5552
5602
  "error.invalid-ref": "The request is missing a required identity.",
5553
5603
  "error.storage-unavailable": "The root registry store is unavailable; check the files under $DSH_HOME/storages.",
5554
5604
  "error.registry-contended": "The root registry is owned by another DSH process; this process grants no additional roots. Close the other process, then retry.",
5605
+ "error.root-limit-reached": "The additional-root limit is reached; remove one before adding another.",
5606
+ "error.config-invalid": "The root registry configuration is invalid; this process grants no additional roots. Fix the configuration and restart.",
5607
+ "error.root-unavailable": "That folder is not available right now and cannot be made writable; restore the directory first.",
5555
5608
  "error.reveal-unavailable": "Could not open the system file manager; the registration is untouched, and the path can still be copied.",
5556
5609
  "error.unavailable": "Could not reach the dsh host process.",
5557
5610
  "error.copy-failed": "Could not write to the clipboard; check the browser's clipboard permission.",
5558
5611
  "error.session-not-found": "That session is not active; open a workspace session first.",
5612
+ "error.unsupported-path": "That is a network or device path, which Windows ACL confinement cannot cover.",
5559
5613
  "error.fallback": "The operation failed."
5560
5614
  };
5561
5615
  //#endregion
@@ -5589,10 +5643,10 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
5589
5643
  * @returns the client, with every transport failure normalized to a `PanelError`.
5590
5644
  */
5591
5645
  function createPanelClient(connection) {
5592
- return { async call(endpoint, payload) {
5646
+ return { async call(endpoint, payload, signal) {
5593
5647
  let result;
5594
5648
  try {
5595
- result = await connection.rpc.call(PANEL_CHANNEL, endpoint, payload);
5649
+ result = await connection.rpc.call(PANEL_CHANNEL, endpoint, payload, signal);
5596
5650
  } catch (error) {
5597
5651
  throw new PanelError("unavailable", error instanceof Error ? error.message : String(error));
5598
5652
  }
@@ -5613,7 +5667,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
5613
5667
  * @throws {PanelError} `panel/bad-response` when the value is not that shape.
5614
5668
  */
5615
5669
  function expectShape(endpoint, value) {
5616
- const parsed = endpoint === "reveal" ? parseRevealedView(value) : endpoint === "files" ? parseFilesView(value) : endpoint === "readFile" ? parseFilePreview(value) : parseRootsView(value);
5670
+ const parsed = endpoint === "reveal" ? parseRevealedView(value) : parseRootsView(value);
5617
5671
  if (!parsed.ok) throw new PanelError("panel/bad-response", parsed.message);
5618
5672
  return parsed.value;
5619
5673
  }
@@ -5700,6 +5754,367 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
5700
5754
  justify-content: center;
5701
5755
  }
5702
5756
 
5757
+ /* Icon-only launcher inside the upstream Files tab header. Upstream's
5758
+ FilesBody renders .header as one flat flex row — PathLabel, then the
5759
+ reload button, then the files.actions slot occupants — and each of those
5760
+ children carries a stable data-files-* attribute on the flex item itself
5761
+ (data-files-root on the pane, data-files-path, data-files-reload).
5762
+ A registration order sorts only INSIDE the actions slot and can never
5763
+ overtake the reload button, so the launcher is moved with the flex order
5764
+ property instead: this wrapper is the slot's root element and therefore a
5765
+ direct flex item of .header, and order: -1 puts it ahead of every
5766
+ order-0 sibling while the :has() rule below keeps the path first. */
5767
+ .mrfw-fileLauncher {
5768
+ order: -1;
5769
+ flex: none;
5770
+ display: flex;
5771
+ align-items: center;
5772
+ }
5773
+
5774
+ /* Pin the upstream path label ahead of the launcher — but only while this
5775
+ plugin is in the row: the :has() guard keeps a rule that names an
5776
+ upstream element inert whenever the launcher is absent. The
5777
+ :not([data-files-entry]) qualifier is load-bearing, not decorative:
5778
+ upstream puts data-files-path on EVERY tree row too (the row's own
5779
+ absolute path), so an unqualified selector would match the whole tree,
5780
+ not just the header's PathLabel. Both selectors depend on upstream's DOM
5781
+ shape, which is not part of the slot contract; if it changes, the rules
5782
+ simply stop matching and the launcher falls back to its in-slot position —
5783
+ a silent degradation, never a broken header. */
5784
+ [data-files-root]:has(.mrfw-fileLauncher) [data-files-path]:not([data-files-entry]) {
5785
+ order: -2;
5786
+ }
5787
+
5788
+ /* The root switcher is the launcher's grown-up form on runtimes whose
5789
+ files.actions owner carries absolutePath. It takes the path label's flex
5790
+ position and stretch: order: -1 ahead of every order-0 sibling, and a
5791
+ growing min-width:0 item so both the trigger label and the PathLabel
5792
+ inside it can ellipsize instead of pushing the header controls out. */
5793
+ .mrfw-rootSwitcher {
5794
+ order: -1;
5795
+ flex: 1 1 auto;
5796
+ min-width: 0;
5797
+ display: flex;
5798
+ align-items: center;
5799
+ /* Same 4px the upstream .header puts between its own flex items, so the
5800
+ takeover controls (reload, open-in-app fork) sit at the same offsets the
5801
+ hidden native ones did — no drift between primary and additional state. */
5802
+ gap: 4px;
5803
+ }
5804
+
5805
+ /* While the switcher exists it hides the upstream path label — the
5806
+ :has() guard again keeps the upstream-naming rule inert unless this
5807
+ plugin is actually in the row. The switcher draws its own PathLabel of
5808
+ the selected root next to the trigger (className mrfw-switcherPath, no
5809
+ data-files-* attributes), so the header keeps a path in the same spot.
5810
+ :not([data-files-entry]) is what confines this rule to the header:
5811
+ every tree row carries data-files-path with its own path as the value,
5812
+ and only the PathLabel carries it bare. Without the qualifier this
5813
+ display:none would blank the entire native tree. */
5814
+ [data-files-root]:has(.mrfw-rootSwitcher) [data-files-path]:not([data-files-entry]) {
5815
+ display: none;
5816
+ }
5817
+
5818
+ /* While the switcher names an additional root, the native tree body and its
5819
+ reload hide: the reload refreshes the very tree that is no longer
5820
+ visible, and the plugin's forked tree is portaled in as the body's sibling.
5821
+ The header still offers a reload — the switcher's own .mrfw-switcherReload
5822
+ calls the forked face's refresh. The native tree keeps watching in the
5823
+ background, so switching back to the primary root restores it instantly. */
5824
+ [data-files-root]:has(.mrfw-rootSwitcher[data-root-kind="additional"]) [data-files-body],
5825
+ [data-files-root]:has(.mrfw-rootSwitcher[data-root-kind="additional"]) [data-files-reload] {
5826
+ display: none;
5827
+ }
5828
+
5829
+ /* While the switcher names an additional root, the native open-in-app split
5830
+ button hides too: its slot props carry the pane's primary root, so it
5831
+ would always open the primary root no matter what the switcher selected.
5832
+ The header gets the plugin's own fork of that same control instead (the
5833
+ open-in-app directory), wired to the selected root.
5834
+
5835
+ The slot's render site is wrapped in the renderer's own anchor —
5836
+ <div data-slot="sidebar.right.tab.files.actions" style="display:contents">
5837
+ — so the header's flex items inside it are the occupants themselves, and
5838
+ the native item is the ui-primitives Menu anchor span wrapping the marked
5839
+ .split. The first selector hides that occupant item through the data-slot
5840
+ anchor: hiding only the inner control would leave the empty anchor span
5841
+ as a zero-width flex item whose header gap pushes the fork 4px left of
5842
+ the native position. The wrapper itself can never be the target — its
5843
+ inline display:contents beats any stylesheet rule, and hiding it would
5844
+ take the switcher with it. The second selector is the fallback for an
5845
+ upstream that moves the attribute off the item. The fork survives both:
5846
+ the only data-open-target inside .mrfw-rootSwitcher is its own
5847
+ .mrfw-openSplit, which :not() excludes. */
5848
+ [data-files-root]:has(.mrfw-rootSwitcher[data-root-kind="additional"]) [data-slot="sidebar.right.tab.files.actions"] > *:has([data-open-target="directory"]:not(.mrfw-openSplit)),
5849
+ [data-files-root]:has(.mrfw-rootSwitcher[data-root-kind="additional"]) [data-open-target="directory"]:not(.mrfw-openSplit) {
5850
+ display: none;
5851
+ }
5852
+
5853
+ /* The trigger borrows the path label's look: quiet text with a folder
5854
+ glyph, the hover of the neighboring header buttons, and the overflow
5855
+ behavior PathLabel itself uses in a narrow header. It may shrink but
5856
+ never grows — the PathLabel after it owns the free header space. */
5857
+ .mrfw-rootSwitchBtn {
5858
+ flex: 0 1 auto;
5859
+ min-width: 0;
5860
+ display: inline-flex;
5861
+ align-items: center;
5862
+ gap: 4px;
5863
+ height: 28px;
5864
+ margin: 0;
5865
+ padding: 0 4px;
5866
+ border: none;
5867
+ border-radius: 8px;
5868
+ background: transparent;
5869
+ color: var(--dsw-alias-label-primary);
5870
+ font-family: inherit;
5871
+ font-size: inherit;
5872
+ line-height: 20px;
5873
+ text-align: left;
5874
+ cursor: pointer;
5875
+ transition: background-color var(--ds-transition-duration-fast) var(--ds-ease-in-out);
5876
+ }
5877
+
5878
+ .mrfw-rootSwitchBtn:hover {
5879
+ background: var(--dsw-alias-interactive-bg-hover);
5880
+ }
5881
+
5882
+ .mrfw-rootSwitchBtn .mrfw-triggerIcon {
5883
+ color: var(--dsw-alias-label-secondary);
5884
+ }
5885
+
5886
+ .mrfw-rootSwitchLabel {
5887
+ min-width: 0;
5888
+ overflow: hidden;
5889
+ text-overflow: ellipsis;
5890
+ white-space: nowrap;
5891
+ }
5892
+
5893
+ .mrfw-rootSwitchChevron {
5894
+ margin-left: auto;
5895
+ color: var(--dsw-alias-label-tertiary);
5896
+ }
5897
+
5898
+ /* The selected root's full path sits between the trigger and the reload —
5899
+ the very PathLabel upstream draws there, so it brings its own grow,
5900
+ left-clip and hover-title behavior; only the gutters are ours. The right
5901
+ margin plus the container gap reproduces upstream's .path margin-right
5902
+ 12px + header gap 4px; the left gutter comes from the same container gap. */
5903
+ .mrfw-switcherPath {
5904
+ margin: 0 12px 0 0;
5905
+ }
5906
+
5907
+ /* The takeover's own reload, standing exactly where the hidden native one
5908
+ did: the upstream .tool recipe verbatim (28px hit area, 15px glyph,
5909
+ secondary voice, hover fills). It calls the forked face's refresh — an
5910
+ in-place re-list, not a remount. */
5911
+ .mrfw-switcherReload {
5912
+ flex: none;
5913
+ display: inline-flex;
5914
+ align-items: center;
5915
+ justify-content: center;
5916
+ width: 28px;
5917
+ height: 28px;
5918
+ padding: 6px;
5919
+ border: none;
5920
+ border-radius: var(--dsw-radius-sm);
5921
+ background: transparent;
5922
+ color: var(--dsw-alias-label-secondary);
5923
+ line-height: 1;
5924
+ cursor: pointer;
5925
+ }
5926
+
5927
+ .mrfw-switcherReload:hover {
5928
+ color: var(--dsw-alias-label-primary);
5929
+ background: var(--dsw-alias-interactive-bg-hover);
5930
+ }
5931
+
5932
+ /* ── files-tree fork (verbatim rules, renamed classes) ─────────────────────
5933
+ Copied from upstream's FilesBody.module.css (ui-sidebar-files @
5934
+ 0.2.1-alpha.2) with local names mapped to mrfw-ft* so the rules cannot
5935
+ collide with host DOM — and so the fork's own body (which must NOT carry
5936
+ data-files-body) stays visible while the rules above hide the native one.
5937
+ The header-only rules (root/header/path/tool/status/statusLine/titleIcon)
5938
+ belong to the upstream shell this fork drops and are not mapped. Fork sync
5939
+ re-diffs the upstream file and re-applies the rename — see
5940
+ src/client/files-tree/FORK.md. */
5941
+
5942
+ /* The pane's own .root already supplies the flex column, color, and font;
5943
+ the fork's body takes the same stretch item the hidden native body held,
5944
+ including the workspace list's 2px scrollbar offset and stable gutter. */
5945
+ .mrfw-ftBody {
5946
+ flex: 1 1 auto;
5947
+ min-height: 0;
5948
+ margin-right: 2px;
5949
+ padding: 8px 0 8px 8px;
5950
+ overflow: auto;
5951
+ scrollbar-gutter: stable;
5952
+ }
5953
+
5954
+ .mrfw-ftBody::-webkit-scrollbar-track {
5955
+ margin: 2px;
5956
+ }
5957
+
5958
+ .mrfw-ftLevel {
5959
+ margin: 0;
5960
+ padding: 0;
5961
+ list-style: none;
5962
+ }
5963
+
5964
+ .mrfw-ftLevel .mrfw-ftLevel {
5965
+ padding-left: 18px;
5966
+ }
5967
+
5968
+ .mrfw-ftItem {
5969
+ margin: 0;
5970
+ padding: 0;
5971
+ }
5972
+
5973
+ /* Rows abut, so the hover fill is the whole row; the tree's air is this
5974
+ inset, not a gap between rows. */
5975
+ .mrfw-ftRow {
5976
+ display: flex;
5977
+ gap: 6px;
5978
+ align-items: center;
5979
+ width: 100%;
5980
+ min-width: 0;
5981
+ padding: 5px 10px;
5982
+ color: inherit;
5983
+ font: inherit;
5984
+ text-align: left;
5985
+ background: transparent;
5986
+ border: 0;
5987
+ border-radius: var(--dsw-radius-md);
5988
+ cursor: pointer;
5989
+ }
5990
+
5991
+ .mrfw-ftRow:hover {
5992
+ background: var(--dsw-alias-interactive-bg-hover);
5993
+ }
5994
+
5995
+ /* The folder glyph rides the tertiary ink, as the workspace tree's does. */
5996
+ .mrfw-ftIcon {
5997
+ flex: 0 0 auto;
5998
+ color: var(--dsw-alias-label-tertiary);
5999
+ }
6000
+
6001
+ /* The type's coloured sheet at the folder glyph's 16px; its colour is its own. */
6002
+ .mrfw-ftFileIcon {
6003
+ flex: 0 0 auto;
6004
+ }
6005
+
6006
+ .mrfw-ftName {
6007
+ min-width: 0;
6008
+ overflow: hidden;
6009
+ white-space: nowrap;
6010
+ text-overflow: ellipsis;
6011
+ }
6012
+
6013
+ /* Neither a file nor a directory: shown so the directory is reported whole,
6014
+ greyed so nobody tries to open it. */
6015
+ .mrfw-ftOther {
6016
+ color: var(--dsw-alias-label-tertiary);
6017
+ cursor: default;
6018
+ }
6019
+
6020
+ .mrfw-ftOther:hover {
6021
+ background: transparent;
6022
+ }
6023
+
6024
+ .mrfw-ftNote {
6025
+ margin: 0;
6026
+ padding: 3px 10px;
6027
+ color: var(--dsw-alias-label-tertiary);
6028
+ font-size: 12px;
6029
+ }
6030
+
6031
+ /* ── open-in-app fork (verbatim rules, renamed classes) ───────────────────
6032
+ Copied from upstream's OpenTargetButton.module.css
6033
+ (dsh-client-ui-open-in-app @ 0.2.1-alpha.2) with local names mapped to
6034
+ mrfw-open* so the rules cannot collide with host DOM. Fork sync re-diffs
6035
+ the upstream file and re-applies the rename — see
6036
+ src/client/open-in-app/FORK.md. */
6037
+
6038
+ /* Sized to sit in the document header's 24px control row beside its round
6039
+ icon buttons: a hairline pill with a 12px label and a narrow chevron half. */
6040
+ .mrfw-openMenuAnchor {
6041
+ display: inline-flex;
6042
+ flex: none;
6043
+ align-self: center;
6044
+ }
6045
+
6046
+ .mrfw-openSplit {
6047
+ display: inline-flex;
6048
+ align-items: stretch;
6049
+ box-sizing: border-box;
6050
+ height: 24px;
6051
+ overflow: hidden;
6052
+ border: 0.5px solid var(--dsw-alias-border-l4);
6053
+ border-radius: var(--dsw-radius-sm);
6054
+ font-family: var(--dsw-font-family);
6055
+ }
6056
+
6057
+ .mrfw-openMain,
6058
+ .mrfw-openChevron {
6059
+ display: inline-flex;
6060
+ align-items: center;
6061
+ justify-content: center;
6062
+ border: 0;
6063
+ background: none;
6064
+ color: var(--dsw-alias-label-primary);
6065
+ font-size: 11px;
6066
+ line-height: 16px;
6067
+ white-space: nowrap;
6068
+ cursor: pointer;
6069
+ }
6070
+
6071
+ .mrfw-openMain {
6072
+ gap: 4px;
6073
+ padding: 3px 5px;
6074
+ }
6075
+
6076
+ .mrfw-openChevron {
6077
+ padding: 3px 4px 3px 3px;
6078
+ border-left: 0.5px solid var(--dsw-alias-border-l4);
6079
+ color: var(--dsw-alias-label-secondary);
6080
+ }
6081
+
6082
+ .mrfw-openMain:hover:not(:disabled),
6083
+ .mrfw-openMain:focus-visible,
6084
+ .mrfw-openChevron:hover:not(:disabled),
6085
+ .mrfw-openChevron:focus-visible {
6086
+ background: var(--dsw-alias-interactive-bg-hover);
6087
+ }
6088
+
6089
+ /* Pending disables clicks without a restyle; only hover feedback pauses. */
6090
+ .mrfw-openMain:disabled,
6091
+ .mrfw-openChevron:disabled {
6092
+ cursor: default;
6093
+ }
6094
+
6095
+ .mrfw-openAppIcon {
6096
+ object-fit: contain;
6097
+ }
6098
+
6099
+ /* The unavailable preview emphasizes the same actions with a labeled button. */
6100
+ .mrfw-openSplit[data-size='large'] {
6101
+ height: 36px;
6102
+ border-radius: var(--dsw-radius-md);
6103
+ border-color: var(--dsw-alias-border-l2);
6104
+ }
6105
+
6106
+ .mrfw-openSplit[data-size='large'] .mrfw-openMain {
6107
+ gap: 6px;
6108
+ padding: 6px 14px;
6109
+ font-size: 14px;
6110
+ }
6111
+
6112
+ .mrfw-openSplit[data-size='large'] .mrfw-openChevron {
6113
+ padding: 6px 10px;
6114
+ }
6115
+
6116
+ .mrfw-openSkeleton { display: inline-block; flex: none; border-radius: var(--dsw-radius-xs); background: var(--dsw-alias-interactive-bg-hover); }
6117
+
5703
6118
  .mrfw-overlay {
5704
6119
  position: fixed;
5705
6120
  inset: 0;
@@ -5844,16 +6259,27 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
5844
6259
  min-width: 0;
5845
6260
  }
5846
6261
 
6262
+ /* The name line is a flex row: the truncating name text first, the access
6263
+ badge beside it never shrinking — baseline-aligned so the badge's smaller
6264
+ voice sits on the name's baseline rather than its middle. */
5847
6265
  .mrfw-rootName {
6266
+ display: flex;
6267
+ align-items: baseline;
6268
+ gap: 6px;
6269
+ min-width: 0;
5848
6270
  overflow: hidden;
5849
- text-overflow: ellipsis;
5850
- white-space: nowrap;
5851
6271
  font-size: 14px;
5852
6272
  line-height: 20px;
5853
6273
  font-weight: 500;
5854
6274
  color: var(--dsw-alias-label-primary);
5855
6275
  }
5856
6276
 
6277
+ .mrfw-rootNameText {
6278
+ overflow: hidden;
6279
+ text-overflow: ellipsis;
6280
+ white-space: nowrap;
6281
+ }
6282
+
5857
6283
  .mrfw-rootPath {
5858
6284
  font-family: var(--ds-font-family-code);
5859
6285
  font-size: 12px;
@@ -5862,6 +6288,35 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
5862
6288
  color: var(--dsw-alias-label-tertiary);
5863
6289
  }
5864
6290
 
6291
+ /* The path doubles as the copy control: an unstyled button that keeps the
6292
+ text recipe and makes the whole string clickable — the whole region, not
6293
+ just the icon, copies on click. */
6294
+ .mrfw-pathCopy {
6295
+ display: block;
6296
+ width: 100%;
6297
+ padding: 0;
6298
+ border: 0;
6299
+ background: transparent;
6300
+ text-align: left;
6301
+ cursor: pointer;
6302
+ }
6303
+
6304
+ /* The trailing icon stays in flow but invisible until the path is hovered
6305
+ or focused: appearing never re-wraps the text, and the icon inherits the
6306
+ path's own tertiary ink. */
6307
+ .mrfw-pathCopyIcon {
6308
+ display: inline-flex;
6309
+ margin-left: 4px;
6310
+ vertical-align: -2px;
6311
+ opacity: 0;
6312
+ transition: opacity var(--ds-transition-duration-fast) var(--ds-ease-in-out);
6313
+ }
6314
+
6315
+ .mrfw-pathCopy:hover .mrfw-pathCopyIcon,
6316
+ .mrfw-pathCopy:focus-visible .mrfw-pathCopyIcon {
6317
+ opacity: 1;
6318
+ }
6319
+
5865
6320
  .mrfw-note {
5866
6321
  margin: 0;
5867
6322
  font-size: 12px;
@@ -5907,8 +6362,19 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
5907
6362
  background: var(--dsw-alias-interactive-bg-active);
5908
6363
  }
5909
6364
 
6365
+ /* The solid destructive action of the capsule family: the upgrade confirm's
6366
+ red button. state-error-primary is the error ink upstream also uses as a
6367
+ fill (StateDot); the hover mixes in the host's primary label token to
6368
+ follow its theme and contrast choices. Neither color is a guessed token:
6369
+ an undefined var() would fall back
6370
+ to transparent and white-out the whole button. */
6371
+ .mrfw-btnDanger {
6372
+ background: var(--dsw-alias-state-error-primary);
6373
+ color: var(--dsw-alias-label-primary-foreground);
6374
+ }
6375
+
5910
6376
  .mrfw-btnDanger:hover:not(:disabled) {
5911
- background: var(--dsw-alias-interactive-bg-hover-danger);
6377
+ background: color-mix(in srgb, var(--dsw-alias-state-error-primary) 85%, var(--dsw-alias-label-primary));
5912
6378
  }
5913
6379
 
5914
6380
  .mrfw-btnOutline {
@@ -5966,6 +6432,24 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
5966
6432
  flex: 1 1 auto;
5967
6433
  }
5968
6434
 
6435
+ .mrfw-accessSelect {
6436
+ flex: 0 0 auto;
6437
+ }
6438
+
6439
+ /* The access dropdown's capsule trigger: the Menu anchor wrapper is the flex
6440
+ item, the button inside keeps the control row's 32px capsule recipe. */
6441
+ .mrfw-accessTrigger {
6442
+ padding: 0 12px;
6443
+ }
6444
+
6445
+ .mrfw-accessBadge,
6446
+ .mrfw-effectiveAccess {
6447
+ flex: none;
6448
+ font-size: 12px;
6449
+ line-height: 18px;
6450
+ color: var(--dsw-alias-label-tertiary);
6451
+ }
6452
+
5969
6453
  .mrfw-aliasEditor {
5970
6454
  display: flex;
5971
6455
  flex-wrap: wrap;
@@ -5989,11 +6473,6 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
5989
6473
  border-radius: 16px;
5990
6474
  }
5991
6475
 
5992
- .mrfw-fileTree { flex-basis: 100%; min-width: 0; }
5993
- .mrfw-fileLevel { list-style: none; padding-left: 16px; margin: 4px 0; }
5994
- .mrfw-fileEntry { display: block; border: 0; background: transparent; color: var(--dsw-alias-label-primary); font: inherit; text-align: left; cursor: pointer; padding: 4px; overflow-wrap: anywhere; }
5995
- .mrfw-fileEntry:focus-visible { outline: 1px solid var(--dsw-alias-brand-primary); }
5996
- .mrfw-filePreview { max-height: 320px; overflow: auto; padding: 8px; background: var(--dsw-alias-bg-layer-1); color: var(--dsw-alias-label-primary); font-size: 12px; }
5997
6476
  .mrfw-actions .mrfw-iconBtn {
5998
6477
  width: 32px;
5999
6478
  }
@@ -6006,7 +6485,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
6006
6485
  * Named imports cannot spell both releases: each name is absent from the
6007
6486
  * other release's declaration, so the compiler is told once, here.
6008
6487
  */
6009
- const installed = _deepseek_ai_dsh_client_ui_primitives;
6488
+ const installed$1 = _deepseek_ai_dsh_client_ui_primitives;
6010
6489
  /**
6011
6490
  * Pick the first export name this release actually provides.
6012
6491
  * @param module - the icon module to read.
@@ -6022,9 +6501,29 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
6022
6501
  }
6023
6502
  throw new Error(`multi-root workspace: @deepseek-ai/dsh-client-ui-primitives exports none of ${names.join(", ")}; the workspace-folders panel cannot render its icons. Adapt src/compat/client-icons.ts to the installed release.`);
6024
6503
  }
6504
+ /**
6505
+ * The same by-name probe for a decorative glyph a release may simply not
6506
+ * carry: absence degrades to no icon, not to a boot error. Required icons go
6507
+ * through `selectIcon` and fail loud; optional ones go through here.
6508
+ */
6509
+ function maybeIcon(names) {
6510
+ for (const name of names) {
6511
+ const found = installed$1[name];
6512
+ if (found !== void 0) return found;
6513
+ }
6514
+ }
6515
+ /**
6516
+ * The same by-name probe for a whole component a release may not carry:
6517
+ * `PathLabel` arrived in `0.1.7-alpha.1`, so pixel-era runtimes degrade to a
6518
+ * plain span where the header invites one. Absence degrades to no component,
6519
+ * not to a boot error.
6520
+ */
6521
+ function maybeComponent(names) {
6522
+ return maybeIcon(names);
6523
+ }
6025
6524
  /** Resolved once, against the installed module. */
6026
6525
  function icon(pixelName, regularName) {
6027
- return selectIcon(installed, [pixelName, regularName]);
6526
+ return selectIcon(installed$1, [pixelName, regularName]);
6028
6527
  }
6029
6528
  const IconChevronDown = icon("IconChevronDownOutline14", "IconChevronDownOutlineRegular");
6030
6529
  const IconChevronUp = icon("IconChevronUpOutline14", "IconChevronUpOutlineRegular");
@@ -6034,149 +6533,1505 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
6034
6533
  const IconFolderClose = icon("IconFolderClose16", "IconFolderCloseRegular");
6035
6534
  const IconFolderOpen = icon("IconFolderOpenOutline16", "IconFolderOpenOutlineRegular");
6036
6535
  const IconPlus = icon("IconPlusOutline16", "IconPlusOutlineRegular");
6536
+ const IconRefresh = icon("IconRefreshOutline16", "IconRefreshOutlineRegular");
6537
+ const IconSettings = icon("IconSettingsOutline16", "IconSettingsOutlineRegular");
6037
6538
  const IconTrash = icon("IconTrashOutline16", "IconTrashOutlineRegular");
6539
+ const IconAccessReadWrite = maybeIcon(["IconWorkspaceWriteRegular"]);
6540
+ const IconAccessReadOnly = maybeIcon(["IconReadOnlyRegular"]);
6541
+ const IconAccessInherit = maybeIcon(["IconWorkspaceTreeOutline16", "IconWorkspaceTreeOutlineRegular"]);
6038
6542
  //#endregion
6039
- //#region src/client/RootFileTree.tsx
6040
- /** Lazy additional-root tree and bounded text previews; requests carry a session and exact root. */
6041
- function RootFileTree(props) {
6042
- const [open, setOpen] = (0, react.useState)(false);
6043
- const [generation, setGeneration] = (0, react.useState)(0);
6044
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
6045
- className: "mrfw-fileTree",
6046
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
6047
- className: "mrfw-btn mrfw-btnGhost",
6048
- type: "button",
6049
- "aria-expanded": open,
6050
- onClick: () => {
6051
- setOpen((value) => !value);
6052
- },
6053
- children: props.t("files.browse")
6054
- }), open && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
6055
- className: "mrfw-btn mrfw-btnGhost",
6056
- type: "button",
6057
- onClick: () => {
6058
- setGeneration((value) => value + 1);
6059
- },
6060
- children: props.t("files.reload")
6061
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Directory, {
6062
- ...props,
6063
- path: "."
6064
- }, generation)] })]
6065
- });
6066
- }
6067
- function Directory(props) {
6068
- const [listing, setListing] = (0, react.useState)();
6069
- const [error, setError] = (0, react.useState)();
6070
- (0, react.useEffect)(() => {
6071
- let active = true;
6072
- setListing(void 0);
6073
- setError(void 0);
6074
- const session = props.sessionId();
6075
- if (session === void 0) {
6076
- setError(props.t("panel.noSession"));
6077
- return;
6543
+ //#region node_modules/.pnpm/@deepseek-ai+dsh-typert-protocol@0.2.1-rc.2_@deepseek-ai+cordis@4.0.5/node_modules/@deepseek-ai/dsh-typert-protocol/lib/index.js
6544
+ /** The one Remote failure class shared by owners, the Gateway, and consumers. */
6545
+ /**
6546
+ * One Remote call failure: a real Error carrying its stable code and typed
6547
+ * details. Owners throw it at the failure point; the Host Gateway encodes it
6548
+ * onto the wire unchanged; the Client face rebuilds an instance for the
6549
+ * `RemoteResult` error branch, so `throw result.error` keeps throw semantics.
6550
+ * Discrimination is always by `code`, never by instanceof.
6551
+ */
6552
+ var RemoteError = class extends Error {
6553
+ code;
6554
+ details;
6555
+ /** Structural marker: cross-realm/bundle identification never uses instanceof. */
6556
+ isDSHRemoteError = true;
6557
+ /**
6558
+ * @param code - stable failure code declared in {@link RemoteErrorDetailsMap}.
6559
+ * @param message - human diagnostic carried across the wire.
6560
+ * @param details - structured payload typed by the code.
6561
+ * @param options - standard Error options (`cause` survives in-process only).
6562
+ */
6563
+ constructor(code, message, details, options) {
6564
+ super(message, options);
6565
+ this.code = code;
6566
+ this.details = details;
6567
+ this.name = "RemoteError";
6568
+ }
6569
+ };
6570
+ //#endregion
6571
+ //#region src/client/files-tree/directory-node.ts
6572
+ /** One open directory and its active child nodes; cached view preferences live in the store. */
6573
+ var DirectoryNode = class DirectoryNode {
6574
+ path;
6575
+ load;
6576
+ watch;
6577
+ failed;
6578
+ restore;
6579
+ /** Open direct-child directories, keyed by absolute path. */
6580
+ children = /* @__PURE__ */ new Map();
6581
+ controller = new AbortController();
6582
+ signal;
6583
+ task;
6584
+ reading;
6585
+ dirty = false;
6586
+ initialized = false;
6587
+ automatic = true;
6588
+ constructor(path, load, watch, failed, lifetime, restore = []) {
6589
+ this.path = path;
6590
+ this.load = load;
6591
+ this.watch = watch;
6592
+ this.failed = failed;
6593
+ this.restore = restore;
6594
+ this.signal = AbortSignal.any([lifetime, this.controller.signal]);
6595
+ }
6596
+ /**
6597
+ * Start observation once; readiness triggers the initial listing.
6598
+ * @returns this node.
6599
+ */
6600
+ open() {
6601
+ this.task ??= this.follow();
6602
+ return this;
6603
+ }
6604
+ /**
6605
+ * Find an active node in this subtree.
6606
+ * @param path - absolute directory path.
6607
+ * @returns the matching node, or undefined when that directory is closed.
6608
+ */
6609
+ find(path) {
6610
+ if (path === this.path) return this;
6611
+ for (const child of this.children.values()) {
6612
+ const found = child.find(path);
6613
+ if (found !== void 0) return found;
6078
6614
  }
6079
- props.panel.call("files", {
6080
- sessionId: session,
6081
- entry: props.entry,
6082
- path: props.path
6083
- }).then((value) => {
6084
- if (active && props.sessionId() === session) setListing(value);
6085
- }, (failure) => {
6086
- if (active) setError(failure instanceof Error ? failure.message : String(failure));
6087
- });
6088
- return () => {
6089
- active = false;
6090
- };
6091
- }, [
6092
- props.panel,
6093
- props.entry,
6094
- props.path,
6095
- props.sessionId,
6096
- props.t
6097
- ]);
6098
- if (error !== void 0) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
6099
- className: "mrfw-alert",
6100
- role: "alert",
6101
- children: error
6615
+ }
6616
+ /**
6617
+ * Update the expansion preferences used by pending directory listings.
6618
+ * @param expanded - latest expansion preferences from the store.
6619
+ */
6620
+ setExpanded(expanded) {
6621
+ this.restore = expanded;
6622
+ for (const child of this.children.values()) child.setExpanded(expanded);
6623
+ }
6624
+ /**
6625
+ * Open a direct child using this node's lifetime and automatic-refresh setting.
6626
+ * @param path - absolute direct-child directory path.
6627
+ * @param restore - descendant expansion preferences to restore after listing.
6628
+ * @returns the active child, or undefined after cancellation.
6629
+ */
6630
+ expand(path, restore = []) {
6631
+ if (this.signal.aborted) return void 0;
6632
+ let child = this.children.get(path);
6633
+ if (child === void 0) {
6634
+ child = new DirectoryNode(path, this.load, this.watch, this.failed, this.signal, restore);
6635
+ child.automatic = this.automatic;
6636
+ this.children.set(path, child);
6637
+ }
6638
+ return child.open();
6639
+ }
6640
+ /**
6641
+ * Remove a child and its pending restoration preferences.
6642
+ * @param path - absolute direct-child directory path.
6643
+ * @returns once the child subtree's reads and watches have ended.
6644
+ */
6645
+ async collapse(path) {
6646
+ this.restore = this.restore.filter((value) => value !== path && !value.startsWith(`${path}/`));
6647
+ const child = this.children.get(path);
6648
+ this.children.delete(path);
6649
+ await child?.close();
6650
+ }
6651
+ /**
6652
+ * Control subtree rereads without ending subscriptions.
6653
+ * @param enabled - refresh dirty nodes automatically.
6654
+ */
6655
+ setAutomatic(enabled) {
6656
+ this.automatic = enabled;
6657
+ if (enabled && this.dirty) this.refresh();
6658
+ for (const child of this.children.values()) child.setAutomatic(enabled);
6659
+ }
6660
+ /** Queue a reread, coalescing with active work. @returns completion of the active read and its coalesced rereads. */
6661
+ refresh() {
6662
+ this.dirty = true;
6663
+ this.reading ??= this.read();
6664
+ return this.reading;
6665
+ }
6666
+ /** Refresh this node and its open descendants. @returns once their listings settle. */
6667
+ async refreshTree() {
6668
+ await this.refresh();
6669
+ await Promise.all([...this.children.values()].map((child) => child.refreshTree()));
6670
+ }
6671
+ /** Cancel the active subtree. @returns once all owned reads and watches have ended. */
6672
+ async close() {
6673
+ this.controller.abort();
6674
+ await Promise.all([
6675
+ this.task,
6676
+ this.reading,
6677
+ ...[...this.children.values()].map((child) => child.close())
6678
+ ]);
6679
+ this.children.clear();
6680
+ }
6681
+ async follow() {
6682
+ try {
6683
+ for await (const _event of this.watch(this.path, this.signal)) {
6684
+ this.dirty = true;
6685
+ if (!this.initialized || this.automatic) this.refresh();
6686
+ }
6687
+ } catch (error) {
6688
+ if (!this.signal.aborted) {
6689
+ if (!this.initialized) await this.refresh();
6690
+ if (!(typeof error === "object" && error !== null && "code" in error && error.code === "workspace-file/watch-unsupported")) this.failed(this.path, error);
6691
+ }
6692
+ }
6693
+ }
6694
+ async read() {
6695
+ const readAgain = () => this.dirty && this.automatic && !this.signal.aborted;
6696
+ try {
6697
+ do {
6698
+ this.dirty = false;
6699
+ const level = await this.load(this.path, this.signal);
6700
+ if (this.signal.aborted || level === void 0) return;
6701
+ this.initialized = true;
6702
+ const directories = new Set(level.entries.filter((entry) => entry.type === "directory").map((entry) => `${this.path.replace(/[/\\]+$/, "")}/${entry.name}`));
6703
+ for (const path of this.children.keys()) if (!directories.has(path)) await this.collapse(path);
6704
+ for (const path of directories) if (this.restore.includes(path)) this.expand(path, this.restore);
6705
+ this.restore = [];
6706
+ } while (readAgain());
6707
+ } finally {
6708
+ this.reading = void 0;
6709
+ }
6710
+ }
6711
+ };
6712
+ //#endregion
6713
+ //#region src/client/files-tree/face.ts
6714
+ /**
6715
+ * Bind directory observation to the Remote stream supervisor.
6716
+ * @param remote - Client Remote with workspace file streams.
6717
+ * @returns a watcher that awaits stream disposal when its node ends.
6718
+ */
6719
+ function createWatch(remote) {
6720
+ return async function* (sessionId, path, signal) {
6721
+ const aborted = () => signal.aborted;
6722
+ if (aborted()) return;
6723
+ const stream = remote.$stream({
6724
+ name: `directory ${path}`,
6725
+ open: (lifetime) => remote.workspaceFiles.changes(sessionId, path, lifetime),
6726
+ ended: () => /* @__PURE__ */ new Error(`Directory watch ended: ${path}`)
6727
+ });
6728
+ const abort = () => {
6729
+ stream.dispose();
6730
+ };
6731
+ signal.addEventListener("abort", abort, { once: true });
6732
+ try {
6733
+ for await (const item of stream) {
6734
+ if (aborted()) return;
6735
+ if (item.value.kind === "ready") item.accept();
6736
+ yield item.value.kind;
6737
+ }
6738
+ } finally {
6739
+ signal.removeEventListener("abort", abort);
6740
+ await stream.dispose();
6741
+ }
6742
+ };
6743
+ }
6744
+ /**
6745
+ * Bind the listing to one Remote face, keeping only what the tree stores.
6746
+ * @param remote - the Client Remote face carrying the `workspaceFiles` namespace.
6747
+ * @returns the listing the tree's face performs.
6748
+ */
6749
+ function createList(remote) {
6750
+ return async (sessionId, path, signal) => {
6751
+ const result = await remote.workspaceFiles.list(sessionId, path, signal);
6752
+ if (!result.ok) return result;
6753
+ return {
6754
+ ok: true,
6755
+ value: {
6756
+ entries: result.value.entries,
6757
+ truncated: result.value.truncated
6758
+ }
6759
+ };
6760
+ };
6761
+ }
6762
+ /**
6763
+ * The absolute path of one child entry.
6764
+ *
6765
+ * Joined with `/` whatever the parent's separators: the Host resolves mixed
6766
+ * separators, and the tree only needs a stable key.
6767
+ * @param parent - absolute path of the listed directory.
6768
+ * @param name - the entry's basename.
6769
+ * @returns the child's absolute path.
6770
+ */
6771
+ function childPath(parent, name) {
6772
+ return `${parent.replace(/[/\\]+$/, "")}/${name}`;
6773
+ }
6774
+ /**
6775
+ * Bind the tree's face to one directory listing.
6776
+ * @param list - the bound `workspaceFiles.list` call.
6777
+ * @param watch - target-scoped directory observation.
6778
+ * @returns the Slot `inject` factory: session and bound actions in, face out.
6779
+ */
6780
+ function filesFace(list, watch) {
6781
+ return (sessionId, actions) => {
6782
+ /** Per tab, per absolute path: the listing generation a settlement must match; the latest request wins. */
6783
+ const generations = /* @__PURE__ */ new Map();
6784
+ const roots = /* @__PURE__ */ new Map();
6785
+ const nextGeneration = (tabId, path) => {
6786
+ const byPath = generations.get(tabId) ?? /* @__PURE__ */ new Map();
6787
+ generations.set(tabId, byPath);
6788
+ const generation = (byPath.get(path) ?? 0) + 1;
6789
+ byPath.set(path, generation);
6790
+ return generation;
6791
+ };
6792
+ const load = async (tabId, path, signal) => {
6793
+ if (signal.aborted) return;
6794
+ const generation = nextGeneration(tabId, path);
6795
+ actions.loading(tabId, path);
6796
+ return list(sessionId, path, signal).then((result) => {
6797
+ if (signal.aborted || generations.get(tabId)?.get(path) !== generation) return;
6798
+ if (result.ok) actions.loaded(tabId, path, result.value);
6799
+ else actions.failed(tabId, path, result.error);
6800
+ return result.ok ? result.value : void 0;
6801
+ });
6802
+ };
6803
+ return {
6804
+ refresh: (tabId) => {
6805
+ roots.get(tabId)?.refreshTree();
6806
+ },
6807
+ setAutoRefresh: (tabId, enabled) => {
6808
+ actions.autoRefresh(tabId, enabled);
6809
+ roots.get(tabId)?.setAutomatic(enabled);
6810
+ },
6811
+ start(tabId, root, signal) {
6812
+ actions.start(tabId, root);
6813
+ signal.addEventListener("abort", () => {
6814
+ roots.get(tabId)?.close();
6815
+ roots.delete(tabId);
6816
+ generations.delete(tabId);
6817
+ actions.forget(tabId);
6818
+ }, { once: true });
6819
+ roots.set(tabId, new DirectoryNode(root, (path, lifetime) => load(tabId, path, lifetime), (path, lifetime) => watch(sessionId, path, lifetime), (path, error) => {
6820
+ if (!signal.aborted) actions.failed(tabId, path, new RemoteError("gateway/internal", error instanceof Error ? error.message : String(error), {}));
6821
+ }, signal).open());
6822
+ },
6823
+ load: (tabId, path, signal) => {
6824
+ load(tabId, path, signal);
6825
+ },
6826
+ toggle(tabId, parentPath, path, expanded, signal) {
6827
+ if (signal.aborted) return;
6828
+ const root = roots.get(tabId);
6829
+ if (root === void 0) return;
6830
+ const parent = root.find(parentPath);
6831
+ if (parent === void 0 && !expanded.includes(parentPath)) return;
6832
+ const collapsing = expanded.includes(path);
6833
+ const next = collapsing ? expanded.filter((value) => value !== path) : [...expanded, path];
6834
+ root.setExpanded(next);
6835
+ if (collapsing) parent?.collapse(path);
6836
+ else parent?.expand(path, next);
6837
+ actions.toggled(tabId, path);
6838
+ }
6839
+ };
6840
+ };
6841
+ }
6842
+ //#endregion
6843
+ //#region src/client/files-tree/store.ts
6844
+ /**
6845
+ * The file tree's view state: which directories are expanded, what each
6846
+ * loaded level contains, and where the body is scrolled to.
6847
+ *
6848
+ * The tree is not one resource. A directory listing per level, expanded lazily,
6849
+ * is state the type owns — so it lives in a Slot-standard exclusive store
6850
+ * (one instance per session), bucketed by tab id because two tabs of this kind
6851
+ * in one session expand independently.
6852
+ *
6853
+ * Writers run between `start` and `forget`: the owner's `signal` is what ends a
6854
+ * bucket's life, and the face stops dispatching once it aborts.
6855
+ */
6856
+ /**
6857
+ * One tab's bucket, which every writer after `start` relies on: the face only
6858
+ * dispatches while the record's signal is live, and `forget` runs on its abort.
6859
+ * @param state - the draft.
6860
+ * @param tabId - the tab being written.
6861
+ * @returns the tab's tree.
6862
+ */
6863
+ function bucket(state, tabId) {
6864
+ const tree = state.byTab[tabId];
6865
+ if (tree === void 0) throw new Error(`ui-sidebar-files: no tree for tab "${tabId}"`);
6866
+ return tree;
6867
+ }
6868
+ /**
6869
+ * Declare the file tree's store.
6870
+ *
6871
+ * A factory rather than a shared handle: the registration declares it as an
6872
+ * exclusive store, so the framework mints one instance per session.
6873
+ * @returns the store handle to declare on the registration.
6874
+ */
6875
+ function createFilesStore() {
6876
+ return (0, _deepseek_ai_dsh_client_store.defineStore)({
6877
+ init: () => ({ byTab: {} }),
6878
+ actions: {
6879
+ autoRefresh: (d, tabId, enabled) => {
6880
+ bucket(d, tabId).autoRefresh = enabled;
6881
+ },
6882
+ /**
6883
+ * Seed one tab's tree at its workspace root, with the root expanded.
6884
+ * @param d - draft state.
6885
+ * @param tabId - the tab being drawn.
6886
+ * @param root - absolute path of the workspace root.
6887
+ */
6888
+ start: (d, tabId, root) => {
6889
+ d.byTab[tabId] = {
6890
+ root,
6891
+ levels: {},
6892
+ expanded: [root],
6893
+ scrollTop: 0,
6894
+ autoRefresh: true
6895
+ };
6896
+ },
6897
+ /**
6898
+ * Mark one directory as being listed.
6899
+ * @param d - draft state.
6900
+ * @param tabId - the tab being drawn.
6901
+ * @param path - absolute directory path.
6902
+ */
6903
+ loading: (d, tabId, path) => {
6904
+ const state = bucket(d, tabId);
6905
+ if (state.levels[path]?.kind !== "ready") state.levels[path] = { kind: "loading" };
6906
+ },
6907
+ /**
6908
+ * Record one directory's contents.
6909
+ * @param d - draft state.
6910
+ * @param tabId - the tab being drawn.
6911
+ * @param path - absolute directory path.
6912
+ * @param level - the listing to show under it.
6913
+ */
6914
+ loaded: (d, tabId, path, level) => {
6915
+ const state = bucket(d, tabId);
6916
+ const previous = state.levels[path];
6917
+ if (previous?.kind === "ready") {
6918
+ const directories = new Set(level.entries.filter((entry) => entry.type === "directory").map((entry) => entry.name));
6919
+ for (const entry of previous.level.entries) {
6920
+ if (entry.type !== "directory" || directories.has(entry.name)) continue;
6921
+ const removed = `${path.replace(/[/\\]+$/, "")}/${entry.name}`;
6922
+ state.expanded = state.expanded.filter((value) => value !== removed && !value.startsWith(`${removed}/`));
6923
+ state.levels = Object.fromEntries(Object.entries(state.levels).filter(([key]) => key !== removed && !key.startsWith(`${removed}/`)));
6924
+ }
6925
+ }
6926
+ state.levels[path] = {
6927
+ kind: "ready",
6928
+ level
6929
+ };
6930
+ },
6931
+ /**
6932
+ * Record why one directory could not be listed.
6933
+ * @param d - draft state.
6934
+ * @param tabId - the tab being drawn.
6935
+ * @param path - absolute directory path.
6936
+ * @param failure - the settled Remote failure.
6937
+ */
6938
+ failed: (d, tabId, path, failure) => {
6939
+ const level = bucket(d, tabId).levels[path];
6940
+ bucket(d, tabId).levels[path] = level?.kind === "ready" ? {
6941
+ ...level,
6942
+ failure
6943
+ } : {
6944
+ kind: "failed",
6945
+ failure
6946
+ };
6947
+ },
6948
+ /**
6949
+ * Open a collapsed directory, or collapse an open one.
6950
+ *
6951
+ * A collapsed level keeps what it loaded, so reopening it draws at once.
6952
+ * @param d - draft state.
6953
+ * @param tabId - the tab being drawn.
6954
+ * @param path - absolute directory path.
6955
+ */
6956
+ toggled: (d, tabId, path) => {
6957
+ const state = bucket(d, tabId);
6958
+ const at = state.expanded.indexOf(path);
6959
+ if (at >= 0) state.expanded.splice(at, 1);
6960
+ else state.expanded.push(path);
6961
+ },
6962
+ /**
6963
+ * Record where one tab's body is scrolled to.
6964
+ * @param d - draft state.
6965
+ * @param tabId - the tab being drawn.
6966
+ * @param scrollTop - the body's scroll offset, in px.
6967
+ */
6968
+ scrolled: (d, tabId, scrollTop) => {
6969
+ bucket(d, tabId).scrollTop = scrollTop;
6970
+ },
6971
+ /**
6972
+ * Drop every loaded level, keeping what is expanded.
6973
+ *
6974
+ * This is the reload gesture's first half: the expanded set says which
6975
+ * levels to fetch again.
6976
+ * @param d - draft state.
6977
+ * @param tabId - the tab being drawn.
6978
+ */
6979
+ reset: (d, tabId) => {
6980
+ bucket(d, tabId).levels = {};
6981
+ },
6982
+ /**
6983
+ * Forget one tab's tree, for a tab record that is gone.
6984
+ * @param d - draft state.
6985
+ * @param tabId - the tab that went away.
6986
+ */
6987
+ forget: (d, tabId) => {
6988
+ d.byTab = Object.fromEntries(Object.entries(d.byTab).filter(([id]) => id !== tabId));
6989
+ }
6990
+ }
6991
+ });
6992
+ }
6993
+ //#endregion
6994
+ //#region src/client/files-tree/primitives.ts
6995
+ /**
6996
+ * Runtime probes for the `@deepseek-ai/dsh-client-ui-primitives` exports the
6997
+ * verbatim Files-tree files spell.
6998
+ *
6999
+ * The upstream imports name the `*Regular` glyph family (0.1.7+; the pixel
7000
+ * names are kept as fallbacks for the same reason `compat/client-icons`
7001
+ * dual-names them) plus `FileTypeIcon` and `classifyFileType`, which every
7002
+ * supported release carries. A named import cannot spell two release eras at
7003
+ * once, so the resolution goes through `maybeIcon`/`maybeComponent` — the
7004
+ * same probe the open-in-app fork uses.
7005
+ *
7006
+ * The exported bindings are typed definite because the adapter mounts the
7007
+ * tree only after {@link filesTreeSurfaceSupported} passes; reaching for a
7008
+ * binding on a composition that failed the gate is a caller bug, and the
7009
+ * gate — not this module — is what a missing export degrades into.
7010
+ *
7011
+ * @module @dsh-electron/dsh-plugin-multi-root-workspace/client/files-tree/primitives
7012
+ */
7013
+ /** Exports the fork's row rendering needs, keyed by their upstream names. */
7014
+ const probed = {
7015
+ folderClose: maybeIcon(["IconFolderCloseRegular", "IconFolderClose16"]),
7016
+ folderOpen: maybeIcon(["IconFolderOpenRegular", "IconFolderOpen16"]),
7017
+ fileTypeIcon: maybeComponent(["FileTypeIcon"]),
7018
+ classifyFileType: maybeComponent(["classifyFileType"])
7019
+ };
7020
+ /**
7021
+ * Whether this composition's primitives carry everything the forked rows
7022
+ * spell. The adapter consults this once: `false` means the additional-root
7023
+ * menu stays menu-only and no fork mount is attempted — the same absence
7024
+ * upstream's own no-such-export state would produce.
7025
+ */
7026
+ function filesTreeSurfaceSupported() {
7027
+ return probed.folderClose !== void 0 && probed.folderOpen !== void 0 && probed.fileTypeIcon !== void 0 && probed.classifyFileType !== void 0;
7028
+ }
7029
+ const IconFolderCloseRegular = probed.folderClose;
7030
+ const IconFolderOpenRegular = probed.folderOpen;
7031
+ const FileTypeIcon = probed.fileTypeIcon;
7032
+ const classifyFileType = probed.classifyFileType;
7033
+ //#endregion
7034
+ //#region src/client/files-tree/locales.ts
7035
+ /** Dictionary namespace owned by upstream's plugin (verbatim). */
7036
+ const NS$1 = "sidebarFiles";
7037
+ //#endregion
7038
+ //#region node_modules/.pnpm/clsx@2.1.1/node_modules/clsx/dist/clsx.mjs
7039
+ function r(e) {
7040
+ var t, f, n = "";
7041
+ if ("string" == typeof e || "number" == typeof e) n += e;
7042
+ else if ("object" == typeof e) if (Array.isArray(e)) {
7043
+ var o = e.length;
7044
+ for (t = 0; t < o; t++) e[t] && (f = r(e[t])) && (n && (n += " "), n += f);
7045
+ } else for (f in e) e[f] && (n && (n += " "), n += f);
7046
+ return n;
7047
+ }
7048
+ function clsx() {
7049
+ for (var e, t, f = 0, n = "", o = arguments.length; f < o; f++) (e = arguments[f]) && (t = r(e)) && (n && (n += " "), n += t);
7050
+ return n;
7051
+ }
7052
+ //#endregion
7053
+ //#region node_modules/.pnpm/@deepseek-ai+dsh-util-workspace-path@0.2.1-rc.2_@deepseek-ai+cordis@4.0.5/node_modules/@deepseek-ai/dsh-util-workspace-path/lib/index.js
7054
+ /**
7055
+ * The `dsh-resource://file/…` address grammar: how a file is named across the
7056
+ * Sidebar and the resource model, built and parsed without touching a
7057
+ * filesystem.
7058
+ * @module
7059
+ */
7060
+ /** The scheme and type every file address opens with. */
7061
+ const FILE_ADDRESS_PREFIX = "dsh-resource://file/";
7062
+ /** Component-encode one id or path segment, keeping `:` literal for drive letters. */
7063
+ function encodeSegment(segment) {
7064
+ return encodeURIComponent(segment).replace(/%3A/gi, ":");
7065
+ }
7066
+ /** Encode a `/`-separated path segment by segment. */
7067
+ function encodePath(path) {
7068
+ return path.split("/").map(encodeSegment).join("/");
7069
+ }
7070
+ /**
7071
+ * Build the address of a file read through one Session.
7072
+ * @param sessionId - the Session whose Host workspace resolves the path.
7073
+ * @param path - absolute or workspace-relative path; backslashes are normalized to `/`, and leading `./` prefixes are dropped.
7074
+ * @returns the `dsh-resource://file/session/<sessionId>/<path>` address.
7075
+ */
7076
+ function sessionFileAddress(sessionId, path) {
7077
+ const normalized = path.replace(/\\/g, "/").replace(/^(?:\.\/)+/, "");
7078
+ return `${FILE_ADDRESS_PREFIX}session/${encodeSegment(sessionId)}/${encodePath(normalized)}`;
7079
+ }
7080
+ /**
7081
+ * Browser-safe Workspace path and display helpers.
7082
+ * @module @deepseek-ai/dsh-util-workspace-path
7083
+ */
7084
+ /** Whether a path uses a Windows drive or UNC prefix. */
7085
+ function isWindowsStylePath(value) {
7086
+ return /^[A-Za-z]:[/\\]/.test(value) || value.startsWith("\\\\");
7087
+ }
7088
+ /**
7089
+ * Whether a path is absolute in either spelling the Host accepts: POSIX (`/a/b`) or Windows drive or UNC.
7090
+ * @param path - the path to classify.
7091
+ * @returns `true` for an absolute path; `false` for a Workspace-relative one.
7092
+ */
7093
+ function isAbsoluteWorkspacePath(path) {
7094
+ return path.startsWith("/") || isWindowsStylePath(path);
7095
+ }
7096
+ /**
7097
+ * The address for a path as a caller holds it: a relative path, or an absolute
7098
+ * path inside the Session's workspace, becomes a `session`-scoped address; an
7099
+ * absolute path outside it, or one whose workspace root is unknown, keeps its
7100
+ * absolute path in that Session's address.
7101
+ * @param sessionId - the Session the path is read in.
7102
+ * @param cwd - that Session's workspace root, when known.
7103
+ * @param path - absolute or workspace-relative path, in either separator spelling.
7104
+ * @returns the `dsh-resource://file/…` address.
7105
+ */
7106
+ function fileAddressFor(sessionId, cwd, path) {
7107
+ const normalized = path.replace(/\\/g, "/");
7108
+ if (!isAbsoluteWorkspacePath(normalized)) return sessionFileAddress(sessionId, normalized);
7109
+ const root = cwd === void 0 ? "" : cwd.replace(/\\/g, "/").replace(/\/+$/, "");
7110
+ if (root !== "" && normalized === root) return sessionFileAddress(sessionId, "");
7111
+ if (root !== "" && normalized.startsWith(`${root}/`)) return sessionFileAddress(sessionId, normalized.slice(root.length + 1));
7112
+ return sessionFileAddress(sessionId, normalized);
7113
+ }
7114
+ //#endregion
7115
+ //#region src/client/files-tree/FilesBody.module.css.ts
7116
+ /**
7117
+ * Shim for the CSS-module virtual module `./FilesBody.module.css`.
7118
+ *
7119
+ * This toolchain resolves real files, not CSS-module virtual modules — so
7120
+ * the forked body imports this file instead (the one-line specifier delta
7121
+ * recorded in FORK.md). It exports the same `css.<name>` map, pointed at the
7122
+ * plugin-prefixed `mrfw-ft*` classes whose rules live in `styles.ts`; the
7123
+ * upstream `FilesBody.module.css` sits beside it verbatim as the diffable
7124
+ * source. Only the names the fork's JSX actually reads are mapped — the
7125
+ * header-only entries (`root`, `header`, `path`, `tool`, `status`,
7126
+ * `statusLine`, `titleIcon`) belong to the shell this fork drops.
7127
+ */
7128
+ const css$1 = {
7129
+ body: "mrfw-ftBody",
7130
+ level: "mrfw-ftLevel",
7131
+ item: "mrfw-ftItem",
7132
+ row: "mrfw-ftRow",
7133
+ icon: "mrfw-ftIcon",
7134
+ fileIcon: "mrfw-ftFileIcon",
7135
+ name: "mrfw-ftName",
7136
+ other: "mrfw-ftOther",
7137
+ note: "mrfw-ftNote"
7138
+ };
7139
+ //#endregion
7140
+ //#region src/client/files-tree/FilesTree.tsx
7141
+ /**
7142
+ * The file tree's body, forked to a caller-supplied root: one additional
7143
+ * workspace root, listed one level at a time.
7144
+ *
7145
+ * Everything the tree keeps lives in its store, keyed by tab; everything it
7146
+ * asks for goes through its injected face. The component itself only decides
7147
+ * what to draw for each absolute path and what a click means: a directory
7148
+ * toggles, a file opens through the owner's open callback for a `file:`
7149
+ * viewer to claim, and anything else is shown but refuses to open.
7150
+ *
7151
+ * The rows below the exported shell are byte-identical to upstream's
7152
+ * `FilesBody.tsx`; the shell is the adaptation — upstream's version derives
7153
+ * the root from the session's `cwd`, takes its tab identity and lifetime from
7154
+ * `useTabInfo()`, opens resources through `tab.actions.openResource`, and
7155
+ * draws the header (PathLabel, auto-refresh, reload, actions slot) that this
7156
+ * plugin's switcher already owns. This fork takes all four as props and
7157
+ * draws only the body. See FORK.md.
7158
+ */
7159
+ /** Natural, case-insensitive name order, so `file2` precedes `file10`. */
7160
+ const byName = new Intl.Collator(void 0, {
7161
+ numeric: true,
7162
+ sensitivity: "base"
7163
+ });
7164
+ /**
7165
+ * Order one level's entries for display: directories first, then everything
7166
+ * else, each group by name. The endpoint's order is a listing fact; this is the
7167
+ * reader's.
7168
+ * @param entries - the listing as the endpoint returned it.
7169
+ * @returns a new array, directories first, then by name within each group.
7170
+ */
7171
+ function orderEntries(entries) {
7172
+ return [...entries].sort((left, right) => {
7173
+ const group = Number(right.type === "directory") - Number(left.type === "directory");
7174
+ return group !== 0 ? group : byName.compare(left.name, right.name);
6102
7175
  });
6103
- if (listing === void 0) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
6104
- className: "mrfw-note",
6105
- children: props.t("panel.loading")
7176
+ }
7177
+ /**
7178
+ * Say why a directory could not be listed, in terms of the directory.
7179
+ * @param t - namespace-bound translate.
7180
+ * @param failure - the settled Remote failure.
7181
+ * @returns the line to show under the directory.
7182
+ */
7183
+ function failureLine(t, failure) {
7184
+ switch (failure.code) {
7185
+ case "workspace-file/not-found": return t("error.notFound");
7186
+ case "workspace-file/outside-workspace": return t("error.outsideWorkspace");
7187
+ case "workspace-file/not-directory": return t("error.notDirectory");
7188
+ default: return t("error.unavailable", { message: failure.message });
7189
+ }
7190
+ }
7191
+ /** One entry's row, and its children when it is an expanded directory. */
7192
+ function Entry({ parent, entry, tree }) {
7193
+ const path = childPath(parent, entry.name);
7194
+ if (entry.type === "directory") {
7195
+ const expanded = tree.state.expanded.includes(path);
7196
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
7197
+ className: css$1.item,
7198
+ "data-files-entry": "directory",
7199
+ "data-files-path": path,
7200
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
7201
+ type: "button",
7202
+ className: css$1.row,
7203
+ "aria-expanded": expanded,
7204
+ onClick: () => {
7205
+ tree.onToggle(parent, path);
7206
+ },
7207
+ children: [expanded ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(IconFolderOpenRegular, { className: css$1.icon }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(IconFolderCloseRegular, { className: css$1.icon }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
7208
+ className: css$1.name,
7209
+ children: entry.name
7210
+ })]
7211
+ }), expanded && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
7212
+ className: css$1.level,
7213
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Level, {
7214
+ path,
7215
+ tree
7216
+ })
7217
+ })]
7218
+ });
7219
+ }
7220
+ if (entry.type === "file") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", {
7221
+ className: css$1.item,
7222
+ "data-files-entry": "file",
7223
+ "data-files-path": path,
7224
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
7225
+ type: "button",
7226
+ className: css$1.row,
7227
+ onClick: () => {
7228
+ tree.onOpen(path);
7229
+ },
7230
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(FileTypeIcon, {
7231
+ kind: classifyFileType(entry.name),
7232
+ size: 16,
7233
+ className: css$1.fileIcon
7234
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
7235
+ className: css$1.name,
7236
+ children: entry.name
7237
+ })]
7238
+ })
6106
7239
  });
6107
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("ul", {
6108
- className: "mrfw-fileLevel",
6109
- children: [
6110
- listing.entries.length === 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", {
6111
- className: "mrfw-note",
6112
- children: props.t("files.empty")
6113
- }),
6114
- [...listing.entries].sort((a, b) => Number(b.type === "directory") - Number(a.type === "directory") || a.name.localeCompare(b.name, void 0, { numeric: true })).map((entry) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(FileEntry, {
6115
- ...props,
6116
- path: props.path === "." ? entry.name : `${props.path}/${entry.name}`,
6117
- name: entry.name,
6118
- kind: entry.type
6119
- }, entry.name)),
6120
- listing.truncated && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", {
6121
- className: "mrfw-note",
6122
- children: props.t("files.truncated")
7240
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", {
7241
+ className: css$1.item,
7242
+ "data-files-entry": "other",
7243
+ "data-files-path": path,
7244
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
7245
+ className: clsx(css$1.row, css$1.other),
7246
+ "aria-disabled": "true",
7247
+ title: tree.t("entry.other"),
7248
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
7249
+ className: css$1.name,
7250
+ children: entry.name
6123
7251
  })
6124
- ]
7252
+ })
6125
7253
  });
6126
7254
  }
6127
- function FileEntry(props) {
6128
- const [open, setOpen] = (0, react.useState)(false);
6129
- const [preview, setPreview] = (0, react.useState)();
6130
- const [error, setError] = (0, react.useState)();
6131
- const epoch = (0, react.useRef)(0);
7255
+ /** One directory's rows: its state while listing, its entries once listed. */
7256
+ function Level({ path, tree }) {
7257
+ const { state, t } = tree;
7258
+ const level = state.levels[path];
7259
+ if (level === void 0 || level.kind === "loading") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", {
7260
+ className: css$1.note,
7261
+ "data-files-row": "loading",
7262
+ children: t("loading")
7263
+ });
7264
+ if (level.kind === "failed") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", {
7265
+ className: css$1.note,
7266
+ "data-files-row": "failed",
7267
+ "data-files-code": level.failure.code,
7268
+ children: failureLine(t, level.failure)
7269
+ });
7270
+ const entries = orderEntries(level.level.entries);
7271
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
7272
+ level.failure !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", {
7273
+ className: css$1.note,
7274
+ "data-files-row": "failed",
7275
+ children: failureLine(t, level.failure)
7276
+ }),
7277
+ entries.length === 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", {
7278
+ className: css$1.note,
7279
+ "data-files-row": "empty",
7280
+ children: t("empty")
7281
+ }),
7282
+ entries.map((entry) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Entry, {
7283
+ parent: path,
7284
+ entry,
7285
+ tree
7286
+ }, entry.name)),
7287
+ level.level.truncated && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", {
7288
+ className: css$1.note,
7289
+ "data-files-row": "truncated",
7290
+ children: t("truncated")
7291
+ })
7292
+ ] });
7293
+ }
7294
+ /**
7295
+ * The additional-root tree's body: the selected root and whatever the reader
7296
+ * has opened under it — the upstream body's scroll, seed, and render logic
7297
+ * verbatim, with the root arriving as a prop instead of `useSessions(cwd)`.
7298
+ */
7299
+ function FilesTree({ tabId, sessionId, root, signal, useStore, actions, start, toggle, onOpen, t }) {
7300
+ const state = useStore((store) => store.byTab[tabId]);
7301
+ const bodyRef = (0, react.useRef)(null);
7302
+ const scrollTopRef = (0, react.useRef)(0);
7303
+ const seeded = state !== void 0;
7304
+ (0, react.useLayoutEffect)(() => {
7305
+ const body = bodyRef.current;
7306
+ if (seeded && body !== null) {
7307
+ body.scrollTop = state.scrollTop;
7308
+ scrollTopRef.current = body.scrollTop;
7309
+ }
7310
+ }, [seeded]);
6132
7311
  (0, react.useEffect)(() => () => {
6133
- epoch.current += 1;
7312
+ if (seeded && !signal.aborted) actions.scrolled(tabId, scrollTopRef.current);
7313
+ }, [
7314
+ seeded,
7315
+ signal,
7316
+ tabId,
7317
+ actions
7318
+ ]);
7319
+ (0, react.useEffect)(() => {
7320
+ if (state !== void 0 || signal.aborted) return;
7321
+ start(tabId, root, signal);
7322
+ }, [
7323
+ state,
7324
+ root,
7325
+ tabId,
7326
+ signal,
7327
+ start
7328
+ ]);
7329
+ if (state === void 0) return null;
7330
+ const tree = {
7331
+ state,
7332
+ onToggle: (parent, path) => {
7333
+ toggle(tabId, parent, path, state.expanded, signal);
7334
+ },
7335
+ onOpen: (path) => {
7336
+ onOpen(fileAddressFor(sessionId, void 0, path));
7337
+ },
7338
+ t
7339
+ };
7340
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
7341
+ ref: bodyRef,
7342
+ className: css$1.body,
7343
+ onScroll: (event) => {
7344
+ scrollTopRef.current = event.currentTarget.scrollTop;
7345
+ },
7346
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
7347
+ className: css$1.level,
7348
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Level, {
7349
+ path: state.root,
7350
+ tree
7351
+ })
7352
+ })
7353
+ });
7354
+ }
7355
+ //#endregion
7356
+ //#region src/client/files-tree/adapter.tsx
7357
+ /** The failure every lazy-remote path reports when the carrier is absent. */
7358
+ const unavailable = () => new RemoteError("gateway/internal", "the remote service is not available in this composition", {});
7359
+ /**
7360
+ * Fold an unexpected exception into the Remote failure a dead carrier would
7361
+ * produce. A generated Remote call cannot reject — but the lazy indirection
7362
+ * can (a `remote` whose `workspaceFiles` namespace is not mounted leaves
7363
+ * `.list` throwing a TypeError). The verbatim `load` awaits `list` through
7364
+ * `.then` without `.catch`, so an escaped rejection would die inside
7365
+ * `DirectoryNode.task` and pin the level at `loading` forever; folding it
7366
+ * here keeps the failure a settled Remote result — the visible failure row.
7367
+ */
7368
+ const failureOf = (error) => typeof error === "object" && error !== null && "code" in error ? error : new RemoteError("gateway/internal", error instanceof Error ? error.message : String(error), {});
7369
+ /** An AsyncIterable whose first read throws the absent-carrier failure. */
7370
+ const deadStream = (error = unavailable()) => ({
7371
+ [Symbol.asyncIterator]: () => ({ next: () => Promise.reject(failureOf(error)) }),
7372
+ dispose: () => Promise.resolve()
7373
+ });
7374
+ /**
7375
+ * A `ClientRemote` stand-in that resolves the real service per call.
7376
+ *
7377
+ * The verbatim `createList`/`createWatch` hold one remote face for the
7378
+ * session's life; resolving lazily keeps the fork correct on compositions
7379
+ * that acquire `remote` after this plugin activates, and a composition that
7380
+ * never acquires it still answers every call — with the same failure kind a
7381
+ * dead carrier returns — instead of crashing the slot. Every method upholds
7382
+ * the Remote contract the face relies on: `list` resolves a `RemoteResult`
7383
+ * and never rejects; `changes`/`$stream` surface synchronous lookup failures
7384
+ * through the stream's own failure path.
7385
+ */
7386
+ function lazyFilesRemote(ctx) {
7387
+ const get = (name) => ctx.get(name);
7388
+ const namespace = () => get("remote.workspaceFiles");
7389
+ return {
7390
+ workspaceFiles: {
7391
+ list: (sessionId, path, signal) => {
7392
+ try {
7393
+ const ns = namespace();
7394
+ if (ns === void 0) return Promise.resolve({
7395
+ ok: false,
7396
+ error: unavailable()
7397
+ });
7398
+ return Promise.resolve(ns.list(sessionId, path, signal)).catch((error) => ({
7399
+ ok: false,
7400
+ error: failureOf(error)
7401
+ }));
7402
+ } catch (error) {
7403
+ return Promise.resolve({
7404
+ ok: false,
7405
+ error: failureOf(error)
7406
+ });
7407
+ }
7408
+ },
7409
+ changes: (sessionId, path, signal) => {
7410
+ try {
7411
+ const ns = namespace();
7412
+ return ns === void 0 ? deadStream() : ns.changes(sessionId, path, signal);
7413
+ } catch (error) {
7414
+ return deadStream(error);
7415
+ }
7416
+ }
7417
+ },
7418
+ $stream: ((options) => {
7419
+ try {
7420
+ const remote = get("remote");
7421
+ return remote === void 0 ? deadStream() : remote.$stream(options);
7422
+ } catch (error) {
7423
+ return deadStream(error);
7424
+ }
7425
+ })
7426
+ };
7427
+ }
7428
+ /**
7429
+ * The registration's `store`/`inject` declarations, built once in `apply`.
7430
+ *
7431
+ * The inject face nests the verbatim files face under `face` and adds the
7432
+ * session-bound `openFile` — keeping `FilesInjected` untouched means the
7433
+ * fork's call sites and the sync diff stay upstream-shaped.
7434
+ * @param ctx - the client root context (the remote/sidebarRight lookups).
7435
+ * @returns the store handle and the inject factory for `register`.
7436
+ */
7437
+ function createFilesTreeSeats(ctx) {
7438
+ const store = createFilesStore();
7439
+ const remote = lazyFilesRemote(ctx);
7440
+ const face = filesFace(createList(remote), createWatch(remote));
7441
+ const inject = (sessionId, actions) => ({
7442
+ face: face(sessionId, actions),
7443
+ openFile: (address) => {
7444
+ ctx.get("sidebarRight")?.openResourceIn?.(sessionId, address);
7445
+ }
7446
+ });
7447
+ return {
7448
+ store,
7449
+ inject
7450
+ };
7451
+ }
7452
+ /**
7453
+ * Assemble the props `RootSwitcher` portals into `FilesTree`, or `undefined`
7454
+ * when this composition cannot render the fork — the caller then keeps the
7455
+ * additional-root rows out of the menu entirely, which is the same absence
7456
+ * upstream produces when its own tree cannot draw.
7457
+ * @param props - the occupant's composed props (store, inject, session shares).
7458
+ * @param t - the `sidebarFiles`-bound translate.
7459
+ * @returns the stable surface, or `undefined` when the fork cannot mount.
7460
+ */
7461
+ function createFilesTreeSurface(props, t) {
7462
+ if (!filesTreeSurfaceSupported()) return void 0;
7463
+ const { sessionId, useStore, actions, face, openFile } = props;
7464
+ if (face === void 0 || openFile === void 0 || useStore === void 0 || actions === void 0) return void 0;
7465
+ return {
7466
+ sessionId,
7467
+ useStore,
7468
+ actions,
7469
+ face,
7470
+ openFile,
7471
+ t
7472
+ };
7473
+ }
7474
+ //#endregion
7475
+ //#region node_modules/.pnpm/@deepseek-ai+dsh-host-open-in-app@0.2.1-rc.2_@deepseek-ai+cordis@4.0.5_@deepseek-ai+dsh_90dfa79a7f5aae0c8b5a66b41d5c318b/node_modules/@deepseek-ai/dsh-host-open-in-app/lib/types/shared.js
7476
+ var shared_exports = /* @__PURE__ */ __exportAll({
7477
+ OPEN_IN_APP_APPS_PATH: () => OPEN_IN_APP_APPS_PATH,
7478
+ OPEN_IN_APP_APPS_ROUTE: () => OPEN_IN_APP_APPS_ROUTE,
7479
+ OPEN_IN_APP_ICON_PREFIX_PATH: () => OPEN_IN_APP_ICON_PREFIX_PATH,
7480
+ OPEN_IN_APP_ICON_PREFIX_ROUTE: () => OPEN_IN_APP_ICON_PREFIX_ROUTE,
7481
+ OPEN_IN_APP_OPEN_PATH: () => OPEN_IN_APP_OPEN_PATH,
7482
+ OPEN_IN_APP_OPEN_ROUTE: () => OPEN_IN_APP_OPEN_ROUTE
7483
+ });
7484
+ /**
7485
+ * Route paths and wire payloads shared verbatim by the host routes and the
7486
+ * browser package (`@deepseek-ai/dsh-client-ui-open-in-app`), published as
7487
+ * the `./shared` subpath. Browser-safe: constants and types only. Each route
7488
+ * carries the absolute pathname the Host registers beside the
7489
+ * document-relative form the browser addresses; see
7490
+ * .agents/notes/implemented/architecture/2026-09-14-web-document-relative-app-routes.md.
7491
+ */
7492
+ /** GET route path serving the probed application ids. */
7493
+ const OPEN_IN_APP_APPS_PATH = "/open-in-app/apps";
7494
+ /** Browser-relative form of {@link OPEN_IN_APP_APPS_PATH}. */
7495
+ const OPEN_IN_APP_APPS_ROUTE = OPEN_IN_APP_APPS_PATH.slice(1);
7496
+ /** GET prefix path serving one PNG bundle icon per application id. */
7497
+ const OPEN_IN_APP_ICON_PREFIX_PATH = "/open-in-app/icon";
7498
+ /** Browser-relative form of {@link OPEN_IN_APP_ICON_PREFIX_PATH}. */
7499
+ const OPEN_IN_APP_ICON_PREFIX_ROUTE = OPEN_IN_APP_ICON_PREFIX_PATH.slice(1);
7500
+ /** POST route path launching one application on one workspace directory. */
7501
+ const OPEN_IN_APP_OPEN_PATH = "/open-in-app/open";
7502
+ /** Browser-relative form of {@link OPEN_IN_APP_OPEN_PATH}. */
7503
+ const OPEN_IN_APP_OPEN_ROUTE = OPEN_IN_APP_OPEN_PATH.slice(1);
7504
+ //#endregion
7505
+ //#region src/compat/open-in-app-routes.ts
7506
+ /**
7507
+ * Compatibility adapter for the route constants of
7508
+ * `@deepseek-ai/dsh-host-open-in-app/shared`.
7509
+ *
7510
+ * ```text
7511
+ * 0.1.5-rc.2 / 0.1.6 OPEN_IN_APP_ICON_PREFIX = '/open-in-app/icon' (absolute pathname)
7512
+ * 0.1.7-alpha.1 + OPEN_IN_APP_ICON_PREFIX_ROUTE = 'open-in-app/icon' (document-relative)
7513
+ * ```
7514
+ *
7515
+ * `0.1.7-alpha.1` split every shared route constant into a `*_PATH` spelling
7516
+ * the Host registers and a `*_ROUTE` spelling the browser addresses, retiring
7517
+ * the old names. The fork's `iconUrl` needs the browser-facing form — which a
7518
+ * pixel-era release spells without the `_ROUTE` suffix, so a named import of
7519
+ * the new name does not even BUILD against those manifests (MISSING_EXPORT at
7520
+ * bundle time, not a runtime undefined). The lookup is by export name, not by
7521
+ * release string.
7522
+ *
7523
+ * @module @dsh-electron/dsh-plugin-multi-root-workspace/compat/open-in-app-routes
7524
+ */
7525
+ /**
7526
+ * The installed shared module, addressed by export name.
7527
+ *
7528
+ * Named imports cannot spell both eras: each name is absent from the other
7529
+ * era's declaration, so the compiler is told once, here.
7530
+ */
7531
+ const installed = shared_exports;
7532
+ /**
7533
+ * Pick the first export name this release actually provides.
7534
+ * @param module - the shared module to read.
7535
+ * @param names - candidate export names, preferred first.
7536
+ * @returns the route string.
7537
+ * @throws {Error} when none of the names is exported. That is a compatibility
7538
+ * break to adapt here, not a missing optional peer.
7539
+ */
7540
+ function selectSharedRoute(module, names) {
7541
+ for (const name of names) {
7542
+ const found = module[name];
7543
+ if (found !== void 0) return found;
7544
+ }
7545
+ throw new Error(`multi-root workspace: @deepseek-ai/dsh-host-open-in-app/shared exports none of ${names.join(", ")}; the open-in-app fork cannot address its icon route. Adapt src/compat/open-in-app-routes.ts to the installed release.`);
7546
+ }
7547
+ /**
7548
+ * The browser-facing icon-prefix route, whichever spelling this release
7549
+ * exports. Resolved once at module load: the fork's adapter only reaches
7550
+ * `iconUrl` on runtimes whose glyph gate already passed, and on the pixel era
7551
+ * the absolute form is exactly what upstream's own client fetched.
7552
+ */
7553
+ const OPEN_IN_APP_ICON_URL_PREFIX = selectSharedRoute(installed, ["OPEN_IN_APP_ICON_PREFIX_ROUTE", "OPEN_IN_APP_ICON_PREFIX"]);
7554
+ //#endregion
7555
+ //#region src/client/open-in-app/applications.ts
7556
+ /** Label keys for applications supported by this client. */
7557
+ const APP_LABEL_KEY = {
7558
+ finder: "app.finder",
7559
+ explorer: "app.explorer",
7560
+ filemanager: "app.filemanager",
7561
+ cursor: "app.cursor",
7562
+ vscode: "app.vscode",
7563
+ vscodeinsiders: "app.vscodeinsiders",
7564
+ windsurf: "app.windsurf",
7565
+ zed: "app.zed",
7566
+ sublimetext: "app.sublimetext",
7567
+ xcode: "app.xcode",
7568
+ androidstudio: "app.androidstudio",
7569
+ intellij: "app.intellij",
7570
+ pycharm: "app.pycharm",
7571
+ webstorm: "app.webstorm",
7572
+ phpstorm: "app.phpstorm",
7573
+ goland: "app.goland",
7574
+ rider: "app.rider",
7575
+ rustrover: "app.rustrover",
7576
+ fork: "app.fork",
7577
+ sourcetree: "app.sourcetree",
7578
+ github: "app.github",
7579
+ tower: "app.tower",
7580
+ gitkraken: "app.gitkraken",
7581
+ smartgit: "app.smartgit",
7582
+ sublimemerge: "app.sublimemerge",
7583
+ ghostty: "app.ghostty",
7584
+ warp: "app.warp",
7585
+ iterm: "app.iterm",
7586
+ kitty: "app.kitty",
7587
+ terminal: "app.terminal",
7588
+ windowsterminal: "app.windowsterminal",
7589
+ gitbash: "app.gitbash",
7590
+ gnometerminal: "app.gnometerminal",
7591
+ konsole: "app.konsole"
7592
+ };
7593
+ //#endregion
7594
+ //#region src/client/open-in-app/controller.ts
7595
+ /** Browser availability/choice state and the launch carrier for the split button. */
7596
+ /**
7597
+ * Owns the once-per-page availability read, the persisted last choice, and
7598
+ * the launch POST. Availability and choice publish through uSES-safe sources
7599
+ * so every Session header shares one truth.
7600
+ */
7601
+ var OpenInAppController = class {
7602
+ fetcher;
7603
+ /** Installed app ids in host menu order; null until the host answered. */
7604
+ apps = (0, _deepseek_ai_dsh_client_store.createSnapshotStore)(null);
7605
+ /** Last chosen app id, or empty before the first choice, shared across sessions and browser restarts. */
7606
+ choice = (0, _deepseek_ai_dsh_client_store.createSnapshotStore)("", { persist: { name: "dsh.open-in-app.choice" } });
7607
+ /** Current launch, shared by pointer and keyboard gestures. */
7608
+ operation = (0, _deepseek_ai_dsh_client_store.createSnapshotStore)({
7609
+ phase: "idle",
7610
+ path: null
7611
+ });
7612
+ /**
7613
+ * Resolve the remembered nameable installed application, with the button's first-app fallback.
7614
+ * @returns the installed app id, or undefined while unavailable.
7615
+ */
7616
+ currentApp() {
7617
+ const apps = (this.apps.getSnapshot() ?? []).filter((id) => APP_LABEL_KEY[id] !== void 0);
7618
+ const choice = this.choice.getSnapshot();
7619
+ return apps.includes(choice) ? choice : apps[0];
7620
+ }
7621
+ loading;
7622
+ /**
7623
+ * @param fetcher - HTTP carrier for the apps read and the launch POST.
7624
+ */
7625
+ constructor(fetcher = (input, init) => fetch(input, init)) {
7626
+ this.fetcher = fetcher;
7627
+ }
7628
+ /**
7629
+ * Read availability once per controller life; concurrent calls share the read.
7630
+ * A failed read publishes an empty list, which renders no button at all.
7631
+ * @returns after availability is published.
7632
+ */
7633
+ load() {
7634
+ this.loading ??= this.run();
7635
+ return this.loading;
7636
+ }
7637
+ /**
7638
+ * Remember one picked app id.
7639
+ * @param appId - catalog id from the availability list.
7640
+ */
7641
+ choose(appId) {
7642
+ if (this.operation.getSnapshot().phase !== "busy") this.choice.set(appId);
7643
+ }
7644
+ /**
7645
+ * Launch one installed app on a workspace directory.
7646
+ * @param appId - catalog id from the availability list.
7647
+ * @param path - the session's absolute workspace directory.
7648
+ * Concurrent gestures are ignored until the current Host request settles.
7649
+ * @returns after the host acknowledged the launch; rejects on any failure.
7650
+ */
7651
+ async launch(appId, path) {
7652
+ if (this.operation.getSnapshot().phase === "busy") return;
7653
+ this.operation.set({
7654
+ phase: "busy",
7655
+ path
7656
+ });
7657
+ const body = {
7658
+ app: appId,
7659
+ path
7660
+ };
7661
+ try {
7662
+ const response = await this.fetcher(OPEN_IN_APP_OPEN_ROUTE, {
7663
+ method: "POST",
7664
+ headers: { "content-type": "application/json" },
7665
+ body: JSON.stringify(body)
7666
+ });
7667
+ if (!response.ok) throw new Error(`open failed: HTTP ${String(response.status)}`);
7668
+ this.operation.set({
7669
+ phase: "idle",
7670
+ path
7671
+ });
7672
+ } catch (error) {
7673
+ this.operation.set({
7674
+ phase: "error",
7675
+ path
7676
+ });
7677
+ throw error;
7678
+ }
7679
+ }
7680
+ async run() {
7681
+ let apps = [];
7682
+ try {
7683
+ const response = await this.fetcher(OPEN_IN_APP_APPS_ROUTE, { headers: { accept: "application/json" } });
7684
+ if (response.ok) {
7685
+ const payload = await response.json();
7686
+ if (Array.isArray(payload.apps)) apps = payload.apps.filter((id) => typeof id === "string");
7687
+ }
7688
+ } catch {}
7689
+ this.apps.set(apps);
7690
+ }
7691
+ };
7692
+ //#endregion
7693
+ //#region src/client/open-in-app/locales.ts
7694
+ /** Dictionary namespace owned by upstream's plugin (verbatim). */
7695
+ const NS = "open-in-app";
7696
+ //#endregion
7697
+ //#region src/client/open-in-app/open-failure-toast.tsx
7698
+ /** Per-control failure banner for path gestures. */
7699
+ /**
7700
+ * Transient failure banner owned by the control that initiated the gesture,
7701
+ * so one failed request announces once, from the control the user pressed.
7702
+ * @returns `toast` owned by the control and `show` to announce one
7703
+ * failure with resolved copy; announcing again replays the banner.
7704
+ */
7705
+ function useOpenFailureToast() {
7706
+ const seq = (0, react.useRef)(0);
7707
+ const [banner, setBanner] = (0, react.useState)(null);
7708
+ const dismiss = (0, react.useCallback)(() => {
7709
+ setBanner(null);
6134
7710
  }, []);
6135
- const toggle = async () => {
6136
- setOpen(!open);
6137
- const current = ++epoch.current;
6138
- if (open || props.kind !== "file") return;
6139
- setPreview(void 0);
6140
- setError(void 0);
6141
- const session = props.sessionId();
6142
- if (session === void 0) {
6143
- setError(props.t("panel.noSession"));
7711
+ return {
7712
+ toast: banner === null ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Toast, {
7713
+ text: banner.text,
7714
+ icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconWarningOutlineRegular, {}),
7715
+ onDone: dismiss
7716
+ }, banner.seq),
7717
+ show: (text) => {
7718
+ seq.current += 1;
7719
+ setBanner({
7720
+ seq: seq.current,
7721
+ text
7722
+ });
7723
+ }
7724
+ };
7725
+ }
7726
+ //#endregion
7727
+ //#region src/client/open-in-app/OpenTargetButton.module.css.ts
7728
+ /**
7729
+ * Fork-facing shim for upstream `OpenTargetButton.module.css`.
7730
+ *
7731
+ * This plugin's bundle has no CSS-module pipeline: the `<style>` element it
7732
+ * installs carries one flat sheet (`src/client/styles.ts`). The upstream
7733
+ * component reads `css.<name>` so the forked file needs one importable
7734
+ * module exporting that same key map — this file, reached through the
7735
+ * verbatim `./OpenTargetButton.module.css.ts` specifier (the one sanctioned
7736
+ * fork edit: the specifier gains the `.ts` this toolchain resolves
7737
+ * everywhere — tsc, tsdown, vitest). The specifier text is spelled without
7738
+ * its `import` clause on purpose: the packed-artifact check scans every
7739
+ * bundle comment too, and quoting the full statement would read as a
7740
+ * relative import the tarball does not carry.
7741
+ *
7742
+ * Each upstream local class maps to an `mrfw-open*` name so the copied
7743
+ * rules cannot collide with host DOM; the rules themselves live in
7744
+ * styles.ts verbatim apart from that rename. `data-open-target` and the
7745
+ * other data attributes are literals in the component and stay byte-exact.
7746
+ * See FORK.md.
7747
+ */
7748
+ const css = {
7749
+ menuAnchor: "mrfw-openMenuAnchor",
7750
+ split: "mrfw-openSplit",
7751
+ main: "mrfw-openMain",
7752
+ chevron: "mrfw-openChevron",
7753
+ appIcon: "mrfw-openAppIcon",
7754
+ skeleton: "mrfw-openSkeleton"
7755
+ };
7756
+ //#endregion
7757
+ //#region src/client/open-in-app/OpenTargetButton.tsx
7758
+ /** Shared file and directory opener: one default action, application menu, and per-gesture feedback. */
7759
+ /**
7760
+ * Serialize gestures and announce their failures through the initiating control's toast.
7761
+ * @param execute - target adapter that returns the failure to announce, or null.
7762
+ * @param t - localized control copy.
7763
+ * @returns the pending state, feedback, and guarded action callback.
7764
+ */
7765
+ function useOpenTargetGesture(execute, t) {
7766
+ const [pending, setPending] = (0, react.useState)(false);
7767
+ const inFlight = (0, react.useRef)(false);
7768
+ const { toast, show } = useOpenFailureToast();
7769
+ return {
7770
+ pending,
7771
+ toast,
7772
+ act: (operation) => {
7773
+ if (inFlight.current) return;
7774
+ inFlight.current = true;
7775
+ setPending(true);
7776
+ execute(operation).then((failure) => {
7777
+ if (failure !== null) show(t(`path.${failure}`));
7778
+ }).finally(() => {
7779
+ inFlight.current = false;
7780
+ setPending(false);
7781
+ });
7782
+ }
7783
+ };
7784
+ }
7785
+ /** One application image with a per-image fallback, shared by main and menu buttons. */
7786
+ function ApplicationIcon({ source, size = 14 }) {
7787
+ const [failed, setFailed] = (0, react.useState)(false);
7788
+ return source === null || failed ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconRightUpOutlineRegular, { size }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("img", {
7789
+ src: source,
7790
+ width: size,
7791
+ height: size,
7792
+ className: css.appIcon,
7793
+ alt: "",
7794
+ draggable: false,
7795
+ onError: () => {
7796
+ setFailed(true);
7797
+ }
7798
+ });
7799
+ }
7800
+ /**
7801
+ * Render identical split buttons for files and directories. File reveal always
7802
+ * stays last; it is the default only when no application is registered.
7803
+ * @param props - target applications, default selection, and operations.
7804
+ * @returns the control and its transient failure feedback.
7805
+ */
7806
+ function OpenTargetButton(props) {
7807
+ const { applications, defaultId, kind, t } = props;
7808
+ const [menuOpen, setMenuOpen] = (0, react.useState)(false);
7809
+ const { pending, toast, act } = useOpenTargetGesture(props.execute, t);
7810
+ const preferred = kind === "file" ? applications.find((app) => app.id === defaultId) ?? applications[0] : applications.find((app) => app.id === defaultId);
7811
+ const disabled = pending || props.busy === true || props.loading === true;
7812
+ const hasMenu = props.loading === true || props.failed || applications.length + (kind === "file" ? 1 : 0) > 1;
7813
+ const revealDefault = kind === "file" && preferred === void 0 && props.loading !== true;
7814
+ const primaryLabel = preferred === void 0 ? t("path.reveal") : t("open.title", { app: preferred.name });
7815
+ const run = (operation) => {
7816
+ setMenuOpen(false);
7817
+ act(operation);
7818
+ };
7819
+ const primary = () => {
7820
+ if (revealDefault) {
7821
+ run({ kind: "reveal" });
6144
7822
  return;
6145
7823
  }
6146
- try {
6147
- const value = await props.panel.call("readFile", {
6148
- sessionId: session,
6149
- entry: props.entry,
6150
- path: props.path
7824
+ if (preferred !== void 0 && preferred.id !== defaultId) {
7825
+ run({
7826
+ kind: "application",
7827
+ id: preferred.id
7828
+ });
7829
+ return;
7830
+ }
7831
+ run({ kind: "default" });
7832
+ };
7833
+ const icon = props.loading === true && preferred === void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
7834
+ className: css.skeleton,
7835
+ "data-open-target-skeleton": true,
7836
+ "aria-hidden": "true",
7837
+ style: {
7838
+ width: props.prominent ? 18 : 13,
7839
+ height: props.prominent ? 18 : 13
7840
+ }
7841
+ }) : revealDefault ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconFolderOpenOutlineRegular, { size: props.prominent ? 18 : 13 }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ApplicationIcon, {
7842
+ source: preferred?.icon ?? null,
7843
+ size: props.prominent ? 18 : 13
7844
+ }, preferred?.icon);
7845
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Menu, {
7846
+ className: css.menuAnchor,
7847
+ open: menuOpen && !disabled && hasMenu,
7848
+ autoFocus: true,
7849
+ portal: true,
7850
+ dense: true,
7851
+ align: "end",
7852
+ onClose: () => {
7853
+ setMenuOpen(false);
7854
+ },
7855
+ items: [...applications.map((app) => ({
7856
+ id: `app:${app.id}`,
7857
+ icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ApplicationIcon, { source: app.icon }, app.icon),
7858
+ label: app.id === preferred?.id ? t("path.appDefault", { app: app.name }) : app.name
7859
+ })), ...props.failed ? [{
7860
+ id: "unavailable",
7861
+ label: t("path.appsError"),
7862
+ disabled: true
7863
+ }] : []],
7864
+ footer: kind === "file" ? [{
7865
+ id: "reveal",
7866
+ icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconFolderOpenOutlineRegular, {}),
7867
+ label: revealDefault ? t("path.appDefault", { app: t("path.reveal") }) : t("path.reveal")
7868
+ }] : [],
7869
+ onSelect: (id) => {
7870
+ run(id === "reveal" ? { kind: "reveal" } : {
7871
+ kind: "application",
7872
+ id: id.slice(4)
6151
7873
  });
6152
- if (current === epoch.current && props.sessionId() === session) setPreview(value);
6153
- } catch (failure) {
6154
- if (current === epoch.current) setError(failure instanceof Error ? failure.message : String(failure));
7874
+ },
7875
+ anchor: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
7876
+ className: css.split,
7877
+ "data-open-target": kind,
7878
+ "data-size": props.prominent ? "large" : "compact",
7879
+ "data-open-path": kind === "file" && !props.prominent ? "" : void 0,
7880
+ "data-state": disabled ? "busy" : "idle",
7881
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Tooltip, {
7882
+ portal: true,
7883
+ label: primaryLabel,
7884
+ shortcutKeys: props.shortcut?.keys,
7885
+ side: "bottom",
7886
+ delayMs: 500,
7887
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
7888
+ type: "button",
7889
+ className: css.main,
7890
+ disabled,
7891
+ "aria-label": props.prominent ? void 0 : primaryLabel,
7892
+ "aria-keyshortcuts": props.shortcut?.aria,
7893
+ "data-open-path-open": kind === "file" && !props.prominent ? "" : void 0,
7894
+ "data-open-path-unpreviewable": props.prominent ? "" : void 0,
7895
+ onClick: primary,
7896
+ children: [icon, props.prominent && (revealDefault ? t("path.reveal") : t("path.open"))]
7897
+ })
7898
+ }), hasMenu && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
7899
+ type: "button",
7900
+ className: css.chevron,
7901
+ disabled,
7902
+ "aria-haspopup": "menu",
7903
+ "aria-expanded": menuOpen && !disabled,
7904
+ "aria-label": t("path.more"),
7905
+ "data-open-path-more": kind === "file" && !props.prominent ? "" : void 0,
7906
+ onClick: () => {
7907
+ if (!menuOpen) props.refresh?.();
7908
+ setMenuOpen((value) => !value);
7909
+ },
7910
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronDownOutlineRegular, { size: props.prominent ? 14 : 10 })
7911
+ })]
7912
+ })
7913
+ }), toast] });
7914
+ }
7915
+ //#endregion
7916
+ //#region src/client/open-in-app/OpenInAppAction.tsx
7917
+ /**
7918
+ * Adapt the installed directory catalog to the shared opening control.
7919
+ * @param props - displayed directory, installed catalog, and launch operations.
7920
+ * @returns the shared control, or null without an eligible application.
7921
+ */
7922
+ function OpenInAppAction(props) {
7923
+ const { absolutePath, useOpenInAppApps, useOpenInAppChoice, t } = props;
7924
+ const available = useOpenInAppApps((apps) => apps);
7925
+ const choice = useOpenInAppChoice((id) => id);
7926
+ const operation = props.useOpenInAppLaunch((value) => value);
7927
+ const shortcut = props.useShortcuts((rows) => rows.find((row) => row.id === "workspace.openLocal"));
7928
+ const apps = (available ?? []).flatMap((id) => {
7929
+ const key = APP_LABEL_KEY[id];
7930
+ return key === void 0 ? [] : [{
7931
+ id,
7932
+ name: t(key),
7933
+ icon: props.iconUrl(id)
7934
+ }];
7935
+ });
7936
+ const preferred = apps.find((app) => app.id === choice) ?? apps[0];
7937
+ if (preferred === void 0) return null;
7938
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(OpenTargetButton, {
7939
+ kind: "directory",
7940
+ applications: apps,
7941
+ defaultId: preferred.id,
7942
+ failed: false,
7943
+ t,
7944
+ busy: operation.phase === "busy",
7945
+ shortcut,
7946
+ execute: async (operation) => {
7947
+ const id = operation.kind === "application" ? operation.id : preferred.id;
7948
+ try {
7949
+ await props.launch(id, absolutePath);
7950
+ } catch (_error) {
7951
+ return "openError";
7952
+ }
7953
+ if (operation.kind === "application") props.choose(id);
7954
+ return null;
6155
7955
  }
7956
+ }, absolutePath);
7957
+ }
7958
+ //#endregion
7959
+ //#region src/client/open-in-app/adapter.tsx
7960
+ /**
7961
+ * The plugin-owned wiring around the verbatim open-in-app fork.
7962
+ *
7963
+ * Upstream builds the same props in its `apply` (`directoryInjected`): one
7964
+ * controller per page, its SnapshotStores bound by the slot renderer into
7965
+ * `use*` props, and a `t` bound to the `open-in-app` namespace. This module
7966
+ * reproduces exactly that — minus slot machinery the fork never sees —
7967
+ * plus the two degradations only a fork needs:
7968
+ *
7969
+ * - the glyph set the fork spells is the `*Regular` family (0.1.7+); a
7970
+ * pixel-name runtime (0.1.5/0.1.6) leaves the adapter undefined, so the
7971
+ * header simply keeps the native button, mirroring upstream's own
7972
+ * "no apps → no button" absence;
7973
+ * - `ctx.locale.bind('open-in-app')` consumes upstream's registered
7974
+ * dictionary verbatim — same strings, upstream's updates included;
7975
+ * - the icon route comes through `compat/open-in-app-routes`: the `*_ROUTE`
7976
+ * spelling is another 0.1.7+ name, so the constant is probed by export
7977
+ * name rather than imported (a pixel-era manifest lacks it entirely —
7978
+ * a named import would fail the bundle's own build).
7979
+ *
7980
+ * Everything here is ours; nothing in this file needs upstream sync.
7981
+ *
7982
+ * @module @dsh-electron/dsh-plugin-multi-root-workspace/client/open-in-app/adapter
7983
+ */
7984
+ /**
7985
+ * Turn one observable into the renderer-bound `use*` prop shape:
7986
+ * `useSyncExternalStore` under the hood, exactly like the slot renderer's
7987
+ * own binding. Built once per store so `subscribe` stays referentially
7988
+ * stable across renders.
7989
+ * @param source - the observable to bind.
7990
+ * @returns the bound selector hook.
7991
+ */
7992
+ function bindSnapshot(source) {
7993
+ const subscribe = (listener) => source.subscribe(listener);
7994
+ const getSnapshot = () => source.getSnapshot();
7995
+ return function useBound(selector) {
7996
+ return (0, react.useSyncExternalStore)(subscribe, () => selector(getSnapshot()));
6156
7997
  };
6157
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
6158
- className: "mrfw-fileEntry",
6159
- type: "button",
6160
- disabled: props.kind === "other",
6161
- "aria-expanded": open,
6162
- onClick: () => {
6163
- toggle();
7998
+ }
7999
+ /** Glyphs the forked components spell verbatim — all `*Regular`, all probed. */
8000
+ const REQUIRED_GLYPHS = [
8001
+ "IconChevronDownOutlineRegular",
8002
+ "IconRightUpOutlineRegular",
8003
+ "IconFolderOpenOutlineRegular",
8004
+ "IconWarningOutlineRegular"
8005
+ ];
8006
+ /** The empty catalog stand-in when the composition carries no shortcuts service. */
8007
+ const NO_SHORTCUTS = {
8008
+ getSnapshot: () => [],
8009
+ subscribe: () => () => {}
8010
+ };
8011
+ /**
8012
+ * Build the open-in-app props upstream hands its own registration.
8013
+ * @param deps - the namespace-bound translate for `open-in-app`, and the
8014
+ * composition's shortcuts catalog when it exists.
8015
+ * @returns the adapter, or `undefined` when this runtime cannot render the
8016
+ * fork (pixel-era icon names — the forked files would crash on missing
8017
+ * exports, so absence mirrors upstream's own no-button path).
8018
+ */
8019
+ function createOpenInAppAdapter(deps) {
8020
+ if (!REQUIRED_GLYPHS.every((name) => maybeIcon([name]) !== void 0)) return void 0;
8021
+ const controller = new OpenInAppController();
8022
+ controller.load();
8023
+ return {
8024
+ useOpenInAppApps: bindSnapshot(controller.apps),
8025
+ useOpenInAppChoice: bindSnapshot(controller.choice),
8026
+ useOpenInAppLaunch: bindSnapshot(controller.operation),
8027
+ useShortcuts: bindSnapshot(deps.shortcuts ?? NO_SHORTCUTS),
8028
+ launch: (appId, path) => controller.launch(appId, path),
8029
+ choose: (appId) => {
8030
+ controller.choose(appId);
6164
8031
  },
6165
- children: [props.kind === "directory" ? open ? "▾ " : "▸ " : "", props.name]
6166
- }), open && (props.kind === "directory" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Directory, { ...props }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_jsx_runtime.Fragment, { children: error !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
6167
- role: "alert",
6168
- className: "mrfw-alert",
6169
- children: error
6170
- }) : preview === void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
6171
- className: "mrfw-note",
6172
- children: props.t("panel.loading")
6173
- }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("pre", {
6174
- className: "mrfw-filePreview",
6175
- children: preview.text
6176
- }), !preview.eof && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
6177
- className: "mrfw-note",
6178
- children: props.t("files.previewTruncated")
6179
- })] }) }))] });
8032
+ iconUrl: (appId) => `${OPEN_IN_APP_ICON_URL_PREFIX}/${appId}`,
8033
+ t: deps.t
8034
+ };
6180
8035
  }
6181
8036
  //#endregion
6182
8037
  //#region src/client/WorkspaceFoldersAction.tsx
@@ -6198,6 +8053,10 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
6198
8053
  * native look — sidebar trigger, modal card, capsule buttons — in both themes
6199
8054
  * without naming a single color here (see ADR-0006).
6200
8055
  *
8056
+ * The dialog itself is also exported: the Files-tab root switcher opens the
8057
+ * same manager through its trailing menu item instead of carrying a second
8058
+ * dialog.
8059
+ *
6201
8060
  * @module @dsh-electron/dsh-plugin-multi-root-workspace/client/WorkspaceFoldersAction
6202
8061
  */
6203
8062
  const VARIANT_CLASS = {
@@ -6247,6 +8106,27 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
6247
8106
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M4 4l8 8M12 4l-8 8" })
6248
8107
  });
6249
8108
  }
8109
+ /**
8110
+ * One path that copies itself: text that paints like the surrounding copy
8111
+ * and reveals a trailing icon on hover — clicking the text or the icon
8112
+ * copies the same canonical path. The accessible name swaps to "copied"
8113
+ * for the feedback window, the icon button's old recipe. Shared by the
8114
+ * primary row, the current-directory line, and every additional root.
8115
+ */
8116
+ function PathCopy(props) {
8117
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
8118
+ type: "button",
8119
+ className: "mrfw-rootPath mrfw-pathCopy",
8120
+ title: props.path,
8121
+ "aria-label": props.copied ? props.t("panel.copied") : props.t("panel.copyPath"),
8122
+ onClick: props.onCopy,
8123
+ children: [props.path, /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
8124
+ className: "mrfw-pathCopyIcon",
8125
+ "aria-hidden": "true",
8126
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(IconCopy, { size: 14 })
8127
+ })]
8128
+ });
8129
+ }
6250
8130
  /** The locale key for one reported root state. */
6251
8131
  function stateKey(state) {
6252
8132
  switch (state) {
@@ -6256,6 +8136,25 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
6256
8136
  case "invalid": return "state.invalid";
6257
8137
  }
6258
8138
  }
8139
+ /** The locale key for one registration's access level. */
8140
+ function accessKey(access) {
8141
+ switch (access) {
8142
+ case "inherit": return "access.inherit";
8143
+ case "read-only": return "access.readOnly";
8144
+ case "read-write": return "access.readWrite";
8145
+ }
8146
+ }
8147
+ /**
8148
+ * Whether moving FROM `from` TO `to` could widen the agent's write authority
8149
+ * under some session mode. The three levels are not a total order — `inherit`
8150
+ * is weaker than `read-write` under a read-only session but never weaker
8151
+ * under the others — so the check is spelled out per pair rather than
8152
+ * computed from a rank. Only a widening change asks for confirmation.
8153
+ */
8154
+ function widensAccess(from, to) {
8155
+ if (from === "read-only") return to !== "read-only";
8156
+ return from === "inherit" && to === "read-write" || from === "read-write" && to === "inherit";
8157
+ }
6259
8158
  /** The final segment of an absolute path, on either platform separator. */
6260
8159
  function basename(path) {
6261
8160
  const segments = path.split(/[\\/]/).filter((segment) => segment !== "");
@@ -6276,8 +8175,9 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
6276
8175
  className: props.compact ? "mrfw-fileLauncher" : rail ? "mrfw-triggerRow mrfw-railRow" : "mrfw-triggerRow",
6277
8176
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
6278
8177
  type: "button",
6279
- className: props.compact ? "mrfw-btn mrfw-btnGhost" : rail ? "mrfw-trigger mrfw-triggerRail" : "mrfw-trigger",
8178
+ className: props.compact ? "mrfw-btn mrfw-btnGhost mrfw-iconBtn" : rail ? "mrfw-trigger mrfw-triggerRail" : "mrfw-trigger",
6280
8179
  title: props.t("action.title"),
8180
+ "aria-label": props.compact === true ? props.t("action.title") : void 0,
6281
8181
  "aria-haspopup": "dialog",
6282
8182
  "aria-expanded": open,
6283
8183
  onClick: () => {
@@ -6286,8 +8186,8 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
6286
8186
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
6287
8187
  className: "mrfw-triggerIcon",
6288
8188
  "aria-hidden": "true",
6289
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(IconFolderClose, { size: rail ? 18 : 16 })
6290
- }), rail ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
8189
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(IconSettings, { size: rail ? 18 : 16 })
8190
+ }), rail || props.compact === true ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
6291
8191
  className: "mrfw-triggerLabel",
6292
8192
  children: props.t("action.label")
6293
8193
  })]
@@ -6299,6 +8199,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
6299
8199
  }
6300
8200
  }) : null] });
6301
8201
  }
8202
+ /** The exact row reference the browsing endpoints and mutations carry. */
6302
8203
  function entryOf(root) {
6303
8204
  return {
6304
8205
  ordinal: root.ordinal,
@@ -6328,6 +8229,18 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
6328
8229
  noSession: false
6329
8230
  });
6330
8231
  const [manualPath, setManualPath] = (0, react.useState)("");
8232
+ /** The access the Add controls register new roots with; `inherit` is the default. */
8233
+ const [addAccess, setAddAccess] = (0, react.useState)("inherit");
8234
+ /**
8235
+ * Whether the operator actually picked a level. The select shows its default
8236
+ * before they do, and sending that default would be an explicit value the
8237
+ * operator never named: on a revive the registry upgrades a read-only record
8238
+ * to it. Only a chosen level may travel in the request.
8239
+ */
8240
+ const [accessChosen, setAccessChosen] = (0, react.useState)(false);
8241
+ const [accessMenuOpen, setAccessMenuOpen] = (0, react.useState)(false);
8242
+ /** The widening access change waiting on the confirmation's red button. */
8243
+ const [upgradeFor, setUpgradeFor] = (0, react.useState)(void 0);
6331
8244
  const [aliasFor, setAliasFor] = (0, react.useState)(void 0);
6332
8245
  const [aliasDraft, setAliasDraft] = (0, react.useState)("");
6333
8246
  const [copiedId, setCopiedId] = (0, react.useState)(void 0);
@@ -6335,9 +8248,12 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
6335
8248
  const dialogRef = (0, react.useRef)(null);
6336
8249
  const requestEpoch = (0, react.useRef)(0);
6337
8250
  const mounted = (0, react.useRef)(true);
6338
- (0, react.useEffect)(() => () => {
6339
- mounted.current = false;
6340
- requestEpoch.current += 1;
8251
+ (0, react.useEffect)(() => {
8252
+ mounted.current = true;
8253
+ return () => {
8254
+ mounted.current = false;
8255
+ requestEpoch.current += 1;
8256
+ };
6341
8257
  }, []);
6342
8258
  const requestBase = (0, react.useCallback)(() => {
6343
8259
  const current = props.sessionId?.();
@@ -6348,15 +8264,16 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
6348
8264
  }, []);
6349
8265
  const onDialogKeyDown = (event) => {
6350
8266
  if (event.key === "Escape") {
6351
- if (menuFor !== void 0) return;
8267
+ if (menuFor !== void 0 || accessMenuOpen || upgradeFor !== void 0) return;
6352
8268
  event.stopPropagation();
6353
8269
  props.onClose();
6354
8270
  return;
6355
8271
  }
6356
8272
  if (event.key !== "Tab") return;
8273
+ if (upgradeFor !== void 0) return;
6357
8274
  const dialog = dialogRef.current;
6358
8275
  if (dialog === null) return;
6359
- const focusable = Array.from(dialog.querySelectorAll("button, input, [href], [tabindex]:not([tabindex=\"-1\"])")).filter((element) => !element.hasAttribute("disabled"));
8276
+ const focusable = Array.from(dialog.querySelectorAll("button, input, select, [href], [tabindex]:not([tabindex=\"-1\"])")).filter((element) => !element.hasAttribute("disabled"));
6360
8277
  if (focusable.length === 0) {
6361
8278
  event.preventDefault();
6362
8279
  return;
@@ -6507,8 +8424,16 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
6507
8424
  return;
6508
8425
  }
6509
8426
  if (picked === null || picked.trim() === "") return;
6510
- await mutate("add", { path: picked });
6511
- }, [mutate, props]);
8427
+ await mutate("add", {
8428
+ path: picked,
8429
+ ...accessChosen ? { access: addAccess } : {}
8430
+ });
8431
+ }, [
8432
+ mutate,
8433
+ props,
8434
+ addAccess,
8435
+ accessChosen
8436
+ ]);
6512
8437
  /**
6513
8438
  * Add the path in the manual field — the one the operator typed. The picker
6514
8439
  * plays no part here: this is the entry point the confirm button and the
@@ -6517,8 +8442,27 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
6517
8442
  const addManualPath = (0, react.useCallback)(async () => {
6518
8443
  const path = manualPath.trim();
6519
8444
  if (path === "") return;
6520
- if (await mutate("add", { path })) setManualPath("");
6521
- }, [manualPath, mutate]);
8445
+ if (await mutate("add", {
8446
+ path,
8447
+ ...accessChosen ? { access: addAccess } : {}
8448
+ })) setManualPath("");
8449
+ }, [
8450
+ manualPath,
8451
+ mutate,
8452
+ addAccess,
8453
+ accessChosen
8454
+ ]);
8455
+ /**
8456
+ * Re-register one withheld or unusable root in place. The request carries
8457
+ * the stored path and deliberately NO access: `registry.add` treats the call
8458
+ * as a revive — it keeps the record's access, alias, and identity instead of
8459
+ * creating a new registration — which is also why the root limit must not
8460
+ * gate this button: a revive consumes no slot, and it is the panel's only
8461
+ * repair route for a redirected entry.
8462
+ */
8463
+ const reRegister = (0, react.useCallback)(async (root) => {
8464
+ await mutate("add", { path: root.path });
8465
+ }, [mutate]);
6522
8466
  /** Reveal one root: the answer is the revealed path, not a new view. */
6523
8467
  const reveal = (0, react.useCallback)(async (root) => {
6524
8468
  if (panel === void 0) return;
@@ -6558,10 +8502,9 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
6558
8502
  }));
6559
8503
  }
6560
8504
  }, [panel, requestBase]);
6561
- const copyPath = (0, react.useCallback)(async (root) => {
8505
+ const copyPath = (0, react.useCallback)(async (key, path) => {
6562
8506
  try {
6563
- await navigator.clipboard.writeText(root.path);
6564
- const key = rowKey(root);
8507
+ await navigator.clipboard.writeText(path);
6565
8508
  setCopiedId(key);
6566
8509
  setState((previous) => ({
6567
8510
  ...previous,
@@ -6585,7 +8528,11 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
6585
8528
  });
6586
8529
  }, [mutate]);
6587
8530
  const roots = state.view?.roots ?? [];
6588
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
8531
+ const atLimit = state.view !== void 0 && state.view.count >= state.view.limit;
8532
+ const IconAccessTrigger = addAccess === "read-only" ? IconAccessReadOnly : addAccess === "read-write" ? IconAccessReadWrite : IconAccessInherit;
8533
+ const primaryPath = state.view?.primaryRoot;
8534
+ const cwdPath = state.view?.effectiveCwd;
8535
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
6589
8536
  className: "mrfw-overlay",
6590
8537
  role: "presentation",
6591
8538
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
@@ -6625,6 +8572,21 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
6625
8572
  className: "mrfw-description",
6626
8573
  children: t("panel.subtitle")
6627
8574
  }),
8575
+ state.view?.mode === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
8576
+ className: "mrfw-note",
8577
+ children: [
8578
+ t("panel.mode"),
8579
+ " ",
8580
+ t(`mode.${state.view.mode}`)
8581
+ ]
8582
+ }),
8583
+ state.view?.toolAccessInvalid ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
8584
+ className: "mrfw-note",
8585
+ children: t("panel.invalidCap")
8586
+ }) : (state.view?.cappedTools?.length ?? 0) > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
8587
+ className: "mrfw-note",
8588
+ children: t("panel.toolCaps", { tools: state.view.cappedTools.join(", ") })
8589
+ }) : null,
6628
8590
  state.error === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
6629
8591
  role: "alert",
6630
8592
  className: "mrfw-alert",
@@ -6652,15 +8614,25 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
6652
8614
  className: "mrfw-rootText",
6653
8615
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
6654
8616
  className: "mrfw-rootName",
6655
- children: state.view === void 0 ? "…" : state.view.primaryName ?? basename(state.view.primaryRoot)
6656
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
8617
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
8618
+ className: "mrfw-rootNameText",
8619
+ children: state.view === void 0 ? "…" : state.view.primaryName ?? basename(state.view.primaryRoot)
8620
+ })
8621
+ }), primaryPath === void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
6657
8622
  className: "mrfw-rootPath",
6658
- children: state.view?.primaryRoot ?? "…"
8623
+ children: "…"
8624
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(PathCopy, {
8625
+ path: primaryPath,
8626
+ copied: copiedId === "primary",
8627
+ onCopy: () => {
8628
+ copyPath("primary", primaryPath);
8629
+ },
8630
+ t
6659
8631
  })]
6660
8632
  })
6661
8633
  })]
6662
8634
  }),
6663
- state.view?.effectiveCwd !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
8635
+ cwdPath !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
6664
8636
  className: "mrfw-section",
6665
8637
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
6666
8638
  className: "mrfw-sectionTitleRow",
@@ -6671,9 +8643,13 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
6671
8643
  className: "mrfw-sectionNote",
6672
8644
  children: t("panel.cwdNote")
6673
8645
  })]
6674
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
6675
- className: "mrfw-rootPath",
6676
- children: state.view.effectiveCwd
8646
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(PathCopy, {
8647
+ path: cwdPath,
8648
+ copied: copiedId === "cwd",
8649
+ onCopy: () => {
8650
+ copyPath("cwd", cwdPath);
8651
+ },
8652
+ t
6677
8653
  })]
6678
8654
  }),
6679
8655
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
@@ -6705,12 +8681,33 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
6705
8681
  children: [
6706
8682
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
6707
8683
  className: "mrfw-rootText",
6708
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
8684
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
6709
8685
  className: "mrfw-rootName",
6710
- children: displayNameOf(root)
6711
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
6712
- className: "mrfw-rootPath",
6713
- children: root.path
8686
+ children: [
8687
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
8688
+ className: "mrfw-rootNameText",
8689
+ children: displayNameOf(root)
8690
+ }),
8691
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
8692
+ className: "mrfw-accessBadge",
8693
+ children: t(accessKey(root.access))
8694
+ }),
8695
+ root.effectiveAccess === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
8696
+ className: "mrfw-effectiveAccess",
8697
+ children: [
8698
+ t("panel.effectiveAccess"),
8699
+ " ",
8700
+ t(`effective.${root.effectiveAccess}`)
8701
+ ]
8702
+ })
8703
+ ]
8704
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(PathCopy, {
8705
+ path: root.path,
8706
+ copied: copiedId === key,
8707
+ onCopy: () => {
8708
+ copyPath(key, root.path);
8709
+ },
8710
+ t
6714
8711
  })]
6715
8712
  }),
6716
8713
  root.state === "available" ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
@@ -6718,12 +8715,13 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
6718
8715
  className: "mrfw-stateWarn",
6719
8716
  children: t(stateKey(root.state))
6720
8717
  }),
6721
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(IconButton, {
8718
+ root.state === "available" ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(IconButton, {
8719
+ disabled: state.busy,
6722
8720
  onClick: () => {
6723
- copyPath(root);
8721
+ reRegister(root);
6724
8722
  },
6725
- icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(IconCopy, {}),
6726
- label: copiedId === key ? t("panel.copied") : t("panel.copyPath")
8723
+ icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(IconRefresh, {}),
8724
+ label: t("panel.reRegister")
6727
8725
  }),
6728
8726
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(IconButton, {
6729
8727
  disabled: state.busy,
@@ -6754,23 +8752,55 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
6754
8752
  onClose: () => {
6755
8753
  setMenuFor(void 0);
6756
8754
  },
6757
- items: [{
6758
- id: "rename",
6759
- label: t("panel.rename"),
6760
- icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(IconEdit, {})
6761
- }, {
6762
- id: "remove",
6763
- label: t("panel.remove"),
6764
- icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(IconTrash, {}),
6765
- danger: true,
6766
- disabled: state.busy
6767
- }],
8755
+ items: [
8756
+ {
8757
+ id: "rename",
8758
+ label: t("panel.rename"),
8759
+ icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(IconEdit, {})
8760
+ },
8761
+ {
8762
+ id: "access:inherit",
8763
+ label: t("panel.accessInherit"),
8764
+ disabled: state.busy || root.access === "inherit"
8765
+ },
8766
+ {
8767
+ id: "access:read-only",
8768
+ label: t("panel.accessReadOnly"),
8769
+ disabled: state.busy || root.access === "read-only"
8770
+ },
8771
+ {
8772
+ id: "access:read-write",
8773
+ label: t("panel.accessReadWrite"),
8774
+ disabled: state.busy || root.access === "read-write"
8775
+ },
8776
+ {
8777
+ id: "remove",
8778
+ label: t("panel.remove"),
8779
+ icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(IconTrash, {}),
8780
+ danger: true,
8781
+ disabled: state.busy
8782
+ }
8783
+ ],
6768
8784
  onSelect: (id) => {
6769
8785
  setMenuFor(void 0);
6770
8786
  if (id === "rename") {
6771
8787
  setAliasFor(key);
6772
8788
  setAliasDraft(root.alias ?? "");
6773
8789
  }
8790
+ if (id.startsWith("access:")) {
8791
+ const access = id.slice(7);
8792
+ if (widensAccess(root.access, access)) {
8793
+ setUpgradeFor({
8794
+ root,
8795
+ access
8796
+ });
8797
+ return;
8798
+ }
8799
+ mutate("setAccess", {
8800
+ entry: entryOf(root),
8801
+ access
8802
+ });
8803
+ }
6774
8804
  if (id === "remove") mutate("remove", { entry: entryOf(root) });
6775
8805
  },
6776
8806
  portal: true,
@@ -6815,13 +8845,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
6815
8845
  children: t("panel.aliasCancel")
6816
8846
  })
6817
8847
  ]
6818
- }) : null,
6819
- root.state === "available" && panel !== void 0 && props.sessionId !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(RootFileTree, {
6820
- panel,
6821
- entry: entryOf(root),
6822
- sessionId: props.sessionId,
6823
- t
6824
- })
8848
+ }) : null
6825
8849
  ]
6826
8850
  }, key);
6827
8851
  })
@@ -6832,31 +8856,92 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
6832
8856
  children: [
6833
8857
  state.noSession || props.pickDirectory === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(IconButton, {
6834
8858
  variant: "outline",
6835
- disabled: state.busy,
8859
+ disabled: state.busy || atLimit,
6836
8860
  onClick: () => {
6837
8861
  addViaPicker();
6838
8862
  },
6839
8863
  icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(IconPlus, {}),
6840
- label: t("panel.add")
8864
+ label: atLimit ? t("panel.limitReached") : t("panel.add")
6841
8865
  }),
6842
- state.noSession ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
6843
- className: "mrfw-input",
6844
- value: manualPath,
6845
- placeholder: t("panel.addManual"),
6846
- onChange: (event) => {
6847
- setManualPath(event.target.value);
6848
- },
6849
- onKeyDown: (event) => {
6850
- if (event.key === "Enter") addManualPath();
6851
- }
6852
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Action, {
6853
- onClick: () => {
6854
- addManualPath();
6855
- },
6856
- disabled: state.busy || manualPath.trim() === "",
6857
- variant: "primary",
6858
- children: t("panel.addConfirm")
6859
- })] }),
8866
+ state.noSession ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
8867
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
8868
+ className: "mrfw-input",
8869
+ value: manualPath,
8870
+ placeholder: t("panel.addManual"),
8871
+ disabled: atLimit,
8872
+ onChange: (event) => {
8873
+ setManualPath(event.target.value);
8874
+ },
8875
+ onKeyDown: (event) => {
8876
+ if (event.key === "Enter") addManualPath();
8877
+ }
8878
+ }),
8879
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Menu, {
8880
+ open: accessMenuOpen,
8881
+ onClose: () => {
8882
+ setAccessMenuOpen(false);
8883
+ },
8884
+ className: "mrfw-accessSelect",
8885
+ items: [
8886
+ {
8887
+ id: "inherit",
8888
+ label: t("access.inherit"),
8889
+ icon: IconAccessInherit === void 0 ? void 0 : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(IconAccessInherit, {})
8890
+ },
8891
+ {
8892
+ id: "read-write",
8893
+ label: t("access.readWrite"),
8894
+ icon: IconAccessReadWrite === void 0 ? void 0 : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(IconAccessReadWrite, {})
8895
+ },
8896
+ {
8897
+ id: "read-only",
8898
+ label: t("access.readOnly"),
8899
+ icon: IconAccessReadOnly === void 0 ? void 0 : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(IconAccessReadOnly, {})
8900
+ }
8901
+ ],
8902
+ selectedId: addAccess,
8903
+ onSelect: (id) => {
8904
+ setAccessMenuOpen(false);
8905
+ if (id === "inherit" || id === "read-write" || id === "read-only") {
8906
+ setAccessChosen(true);
8907
+ setAddAccess(id);
8908
+ }
8909
+ },
8910
+ portal: true,
8911
+ side: "top",
8912
+ align: "end",
8913
+ closeOnPointerLeave: true,
8914
+ anchor: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
8915
+ type: "button",
8916
+ className: "mrfw-btn mrfw-btnOutline mrfw-accessTrigger",
8917
+ title: t("panel.accessLabel"),
8918
+ "aria-label": t("panel.accessLabel"),
8919
+ "aria-haspopup": "menu",
8920
+ "aria-expanded": accessMenuOpen,
8921
+ disabled: state.busy || atLimit,
8922
+ onClick: () => {
8923
+ setAccessMenuOpen((open) => !open);
8924
+ },
8925
+ children: [
8926
+ IconAccessTrigger === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(IconAccessTrigger, {}),
8927
+ t(accessKey(addAccess)),
8928
+ accessMenuOpen ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(IconChevronUp, { size: 14 }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(IconChevronDown, { size: 14 })
8929
+ ]
8930
+ })
8931
+ }),
8932
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Action, {
8933
+ onClick: () => {
8934
+ addManualPath();
8935
+ },
8936
+ disabled: state.busy || atLimit || manualPath.trim() === "",
8937
+ variant: "primary",
8938
+ children: t("panel.addConfirm")
8939
+ })
8940
+ ] }),
8941
+ atLimit ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
8942
+ className: "mrfw-note",
8943
+ children: t("panel.limitReached")
8944
+ }) : null,
6860
8945
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Action, {
6861
8946
  onClick: () => {
6862
8947
  refresh();
@@ -6869,7 +8954,33 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
6869
8954
  })]
6870
8955
  })]
6871
8956
  })]
6872
- });
8957
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Modal, {
8958
+ open: upgradeFor !== void 0,
8959
+ onClose: () => {
8960
+ setUpgradeFor(void 0);
8961
+ },
8962
+ title: t("panel.accessUpgradeTitle"),
8963
+ description: upgradeFor === void 0 ? "" : t("panel.accessUpgradeConfirm", { access: t(accessKey(upgradeFor.access)) }),
8964
+ closeLabel: t("panel.close"),
8965
+ footer: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Action, {
8966
+ onClick: () => {
8967
+ setUpgradeFor(void 0);
8968
+ },
8969
+ children: t("panel.aliasCancel")
8970
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Action, {
8971
+ variant: "danger",
8972
+ disabled: state.busy,
8973
+ onClick: () => {
8974
+ const pending = upgradeFor;
8975
+ setUpgradeFor(void 0);
8976
+ if (pending !== void 0) mutate("setAccess", {
8977
+ entry: entryOf(pending.root),
8978
+ access: pending.access
8979
+ });
8980
+ },
8981
+ children: t("panel.accessUpgradeDo")
8982
+ })] })
8983
+ })] });
6873
8984
  }
6874
8985
  /** Normalize any thrown value into a `PanelError` the dialog can render. */
6875
8986
  function asPanelError(error) {
@@ -6877,6 +8988,275 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
6877
8988
  return new PanelError("fallback", error instanceof Error ? error.message : String(error));
6878
8989
  }
6879
8990
  //#endregion
8991
+ //#region src/client/RootSwitcher.tsx
8992
+ /**
8993
+ * The Files-tab root switcher — the entry that took over the header position
8994
+ * the P1 gear launcher first claimed. It poses as the pane's path label: a
8995
+ * trigger that opens a menu of this session's roots (primary first, the
8996
+ * registered additional roots next, and the Workspace Folders manager last),
8997
+ * followed by a `PathLabel` of the selected root's full path — the same
8998
+ * component upstream draws in that spot, so both primary and additional
8999
+ * selections read exactly like the native header. While an additional root
9000
+ * is selected it also lends the pane's body area to this plugin's verbatim
9001
+ * fork of the native Files tree (`files-tree/`), so an additional root reads
9002
+ * and behaves exactly like the primary one.
9003
+ *
9004
+ * Two upstream DOM hooks make the takeover work, and both degrade silently:
9005
+ *
9006
+ * - The stylesheet rules: while `.mrfw-rootSwitcher` exists inside a
9007
+ * `[data-files-root]` pane the upstream `data-files-path` is hidden and the
9008
+ * switcher takes its flex position; while the switcher carries
9009
+ * `data-root-kind="additional"` the native `data-files-body`,
9010
+ * `data-files-reload`, and `data-open-target="directory"` split button
9011
+ * hide too (the reload refreshes the very tree that is no longer
9012
+ * visible, and the native open-in-app button only ever receives the
9013
+ * primary root through its slot props — the header gets a
9014
+ * `.mrfw-switcherReload` replacement and the plugin's verbatim fork of
9015
+ * the open-in-app control, pointed at the selected root). Upstream
9016
+ * changing that DOM shape simply stops the selectors from matching — the
9017
+ * native header keeps working and the switcher is just another actions
9018
+ * occupant.
9019
+ * - `closest('[data-files-root]')` from the switcher's own element locates
9020
+ * the pane the forked Files tree is portaled into, as a sibling of
9021
+ * `data-files-body`. No match → no portal: the component keeps working as
9022
+ * a menu and the native tree keeps its area. The tree's tab id comes from
9023
+ * the same element's `closest('[data-dockkit-content]')` — the dockkit
9024
+ * tabHost section — with a generated fallback where the hook is absent.
9025
+ *
9026
+ * The selected root is component state on purpose: every tab render
9027
+ * occurrence gets its own, and reopening the pane resets to the primary
9028
+ * root. There is no cross-tab or persisted selection.
9029
+ *
9030
+ * @module @dsh-electron/dsh-plugin-multi-root-workspace/client/RootSwitcher
9031
+ */
9032
+ /**
9033
+ * The subdued full-path label upstream's Files header draws — it exists from
9034
+ * `0.1.7-alpha.1` on. A pixel-era runtime has no such export, so the probe
9035
+ * returns `undefined` and the header falls back to a plain titled span: the
9036
+ * path stays readable and hoverable, only the dir/file typography differs.
9037
+ */
9038
+ const PathLabel = maybeComponent(["PathLabel"]);
9039
+ /** The Files-header switcher: root menu, dialog entry, and the portal tree. */
9040
+ function RootSwitcher(props) {
9041
+ const { panel, t, primaryPath, filesTree } = props;
9042
+ const [menuOpen, setMenuOpen] = (0, react.useState)(false);
9043
+ const [dialogOpen, setDialogOpen] = (0, react.useState)(false);
9044
+ const [view, setView] = (0, react.useState)(void 0);
9045
+ const [selected, setSelected] = (0, react.useState)("primary");
9046
+ const [takeover, setTakeover] = (0, react.useState)(null);
9047
+ const wrapperRef = (0, react.useRef)(null);
9048
+ const fallbackTabId = (0, react.useRef)(void 0);
9049
+ const [pane, setPane] = (0, react.useState)(null);
9050
+ const mounted = (0, react.useRef)(true);
9051
+ const requestEpoch = (0, react.useRef)(0);
9052
+ (0, react.useEffect)(() => {
9053
+ mounted.current = true;
9054
+ return () => {
9055
+ mounted.current = false;
9056
+ requestEpoch.current += 1;
9057
+ };
9058
+ }, []);
9059
+ (0, react.useEffect)(() => {
9060
+ setPane(wrapperRef.current?.closest("[data-files-root]") ?? null);
9061
+ }, []);
9062
+ const refresh = (0, react.useCallback)(async () => {
9063
+ if (panel === void 0) {
9064
+ setView(void 0);
9065
+ return;
9066
+ }
9067
+ const session = props.sessionId?.();
9068
+ if (session === void 0 || session === "") {
9069
+ setView(void 0);
9070
+ return;
9071
+ }
9072
+ const epoch = ++requestEpoch.current;
9073
+ try {
9074
+ const next = await panel.call("list", { sessionId: session });
9075
+ if (!mounted.current || epoch !== requestEpoch.current || props.sessionId?.() !== session) return;
9076
+ setView(next);
9077
+ } catch {
9078
+ if (!mounted.current || epoch !== requestEpoch.current || props.sessionId?.() !== session) return;
9079
+ setView(void 0);
9080
+ }
9081
+ }, [panel, props.sessionId]);
9082
+ (0, react.useEffect)(() => {
9083
+ refresh();
9084
+ }, [refresh]);
9085
+ (0, react.useEffect)(() => {
9086
+ let previous = props.sessionId?.();
9087
+ return props.subscribeSession?.(() => {
9088
+ const current = props.sessionId?.();
9089
+ if (current === previous) return;
9090
+ previous = current;
9091
+ requestEpoch.current += 1;
9092
+ setSelected("primary");
9093
+ setView(void 0);
9094
+ refresh();
9095
+ });
9096
+ }, [
9097
+ props.subscribeSession,
9098
+ props.sessionId,
9099
+ refresh
9100
+ ]);
9101
+ (0, react.useEffect)(() => {
9102
+ if (selected === "primary") return;
9103
+ const current = view?.roots.find((root) => root.id === selected.id);
9104
+ if (current === void 0 || current.state !== "available") {
9105
+ setSelected("primary");
9106
+ return;
9107
+ }
9108
+ if (current !== selected) setSelected(current);
9109
+ }, [view, selected]);
9110
+ const roots = (0, react.useMemo)(() => view?.roots ?? [], [view]);
9111
+ const primaryLabel = view?.primaryName ?? basename(view?.primaryRoot ?? primaryPath);
9112
+ const triggerLabel = selected === "primary" ? primaryLabel : displayNameOf(selected);
9113
+ const triggerPath = selected === "primary" ? view?.primaryRoot ?? primaryPath : selected.path;
9114
+ const treeVisible = selected !== "primary" && panel !== void 0 && pane !== null && filesTree !== void 0;
9115
+ const selectedPath = selected === "primary" ? void 0 : selected.path;
9116
+ (0, react.useEffect)(() => {
9117
+ if (!treeVisible || selectedPath === void 0) {
9118
+ setTakeover(null);
9119
+ return;
9120
+ }
9121
+ const controller = new AbortController();
9122
+ const tabId = (wrapperRef.current?.closest("[data-dockkit-content]")?.dataset.dockkitContent ?? "") || (fallbackTabId.current ??= `mrfw-${Math.random().toString(36).slice(2)}`);
9123
+ setTakeover({
9124
+ signal: controller.signal,
9125
+ tabId,
9126
+ root: selectedPath
9127
+ });
9128
+ return () => {
9129
+ controller.abort();
9130
+ };
9131
+ }, [treeVisible, selectedPath]);
9132
+ const items = (0, react.useMemo)(() => [{
9133
+ id: "primary",
9134
+ label: `${primaryLabel} · ${t("panel.primary")}`,
9135
+ icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(IconFolderOpen, {})
9136
+ }, ...(filesTree === void 0 ? [] : roots).map((root) => ({
9137
+ id: `root:${root.id}`,
9138
+ label: root.state === "available" ? displayNameOf(root) : `${displayNameOf(root)} · ${t(stateKey(root.state))}`,
9139
+ icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(IconFolderClose, {}),
9140
+ disabled: root.state !== "available"
9141
+ }))], [
9142
+ roots,
9143
+ primaryLabel,
9144
+ t,
9145
+ filesTree
9146
+ ]);
9147
+ const onSelect = (id) => {
9148
+ setMenuOpen(false);
9149
+ if (id === "manage") {
9150
+ setDialogOpen(true);
9151
+ return;
9152
+ }
9153
+ if (id === "primary") {
9154
+ setSelected("primary");
9155
+ return;
9156
+ }
9157
+ const root = roots.find((candidate) => `root:${candidate.id}` === id);
9158
+ if (root !== void 0) setSelected(root);
9159
+ };
9160
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
9161
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
9162
+ ref: wrapperRef,
9163
+ className: "mrfw-rootSwitcher",
9164
+ "data-root-kind": treeVisible ? "additional" : "primary",
9165
+ children: [
9166
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Menu, {
9167
+ open: menuOpen,
9168
+ onClose: () => {
9169
+ setMenuOpen(false);
9170
+ },
9171
+ items,
9172
+ footer: [{
9173
+ id: "manage",
9174
+ label: t("switcher.manage"),
9175
+ icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(IconSettings, {})
9176
+ }],
9177
+ selectedId: selected === "primary" ? "primary" : `root:${selected.id}`,
9178
+ onSelect,
9179
+ portal: true,
9180
+ align: "start",
9181
+ anchor: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
9182
+ type: "button",
9183
+ className: "mrfw-rootSwitchBtn",
9184
+ title: triggerPath,
9185
+ "aria-haspopup": "menu",
9186
+ "aria-expanded": menuOpen,
9187
+ onClick: () => {
9188
+ const opening = !menuOpen;
9189
+ setMenuOpen(opening);
9190
+ if (opening) refresh();
9191
+ },
9192
+ children: [
9193
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
9194
+ className: "mrfw-triggerIcon",
9195
+ "aria-hidden": "true",
9196
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(IconFolderClose, { size: 16 })
9197
+ }),
9198
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
9199
+ className: "mrfw-rootSwitchLabel",
9200
+ children: triggerLabel
9201
+ }),
9202
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
9203
+ className: "mrfw-triggerIcon mrfw-rootSwitchChevron",
9204
+ "aria-hidden": "true",
9205
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(IconChevronDown, { size: 14 })
9206
+ })
9207
+ ]
9208
+ })
9209
+ }),
9210
+ PathLabel === void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
9211
+ className: "mrfw-switcherPath",
9212
+ title: triggerPath,
9213
+ children: triggerPath
9214
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(PathLabel, {
9215
+ path: triggerPath,
9216
+ className: "mrfw-switcherPath"
9217
+ }),
9218
+ treeVisible && filesTree !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
9219
+ type: "button",
9220
+ className: "mrfw-switcherReload",
9221
+ "aria-label": filesTree.t("reload"),
9222
+ title: filesTree.t("reload"),
9223
+ onClick: () => {
9224
+ if (takeover !== null) filesTree.face.refresh(takeover.tabId);
9225
+ },
9226
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(IconRefresh, { size: 15 })
9227
+ }) : null,
9228
+ treeVisible && props.openInApp !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(OpenInAppAction, {
9229
+ ...props.openInApp,
9230
+ absolutePath: triggerPath
9231
+ }) : null
9232
+ ]
9233
+ }),
9234
+ treeVisible && takeover !== null && filesTree !== void 0 ? (0, react_dom.createPortal)(/* @__PURE__ */ (0, react_jsx_runtime.jsx)(FilesTree, {
9235
+ tabId: takeover.tabId,
9236
+ sessionId: filesTree.sessionId,
9237
+ root: takeover.root,
9238
+ signal: takeover.signal,
9239
+ useStore: filesTree.useStore,
9240
+ actions: filesTree.actions,
9241
+ start: filesTree.face.start,
9242
+ toggle: filesTree.face.toggle,
9243
+ onOpen: filesTree.openFile,
9244
+ t: filesTree.t
9245
+ }), pane) : null,
9246
+ dialogOpen ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(WorkspaceFoldersDialog, {
9247
+ panel: props.panel,
9248
+ pickDirectory: props.pickDirectory,
9249
+ ...props.sessionId === void 0 ? {} : { sessionId: props.sessionId },
9250
+ ...props.subscribeSession === void 0 ? {} : { subscribeSession: props.subscribeSession },
9251
+ t,
9252
+ onClose: () => {
9253
+ setDialogOpen(false);
9254
+ refresh();
9255
+ }
9256
+ }) : null
9257
+ ] });
9258
+ }
9259
+ //#endregion
6880
9260
  //#region src/client/index.ts
6881
9261
  /** The footer-action cell this plugin owns. */
6882
9262
  const SLOT_ID = "multi-root-folders";
@@ -6893,7 +9273,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
6893
9273
  * @param ctx - the client root context.
6894
9274
  */
6895
9275
  function apply(ctx) {
6896
- ctx.effect(() => ctx.locale.register(NS, {
9276
+ ctx.effect(() => ctx.locale.register(NS$2, {
6897
9277
  zh,
6898
9278
  en
6899
9279
  }), "multi-root-workspace: dictionaries");
@@ -6920,13 +9300,19 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
6920
9300
  };
6921
9301
  const sessionId = () => currentSessionIdOf(readService(ctx, "sessions"));
6922
9302
  const subscribeSession = (listener) => subscribeSessions(readService(ctx, "sessions"), listener);
6923
- const t = ctx.locale.bind(NS);
9303
+ const t = ctx.locale.bind(NS$2);
9304
+ const openInApp = createOpenInAppAdapter({
9305
+ t: ctx.locale.bind(NS),
9306
+ shortcuts: readService(ctx, "shortcuts")?.catalog
9307
+ });
9308
+ const filesTreeSeats = createFilesTreeSeats(ctx);
9309
+ const filesTreeT = ctx.locale.bind(NS$1);
6924
9310
  ctx.slots.inject("sidebar.footer.action", () => ctx.slots.register({
6925
9311
  name: "sidebar.footer.action",
6926
9312
  id: SLOT_ID,
6927
9313
  order: 50,
6928
9314
  label: () => t("action.label"),
6929
- locale: NS
9315
+ locale: NS$2
6930
9316
  }, (owner) => {
6931
9317
  const wide = owner?.wide;
6932
9318
  return (0, react.createElement)(WorkspaceFoldersAction, {
@@ -6941,15 +9327,31 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
6941
9327
  ctx.slots.inject("sidebar.right.tab.files.actions", () => ctx.slots.register({
6942
9328
  name: "sidebar.right.tab.files.actions",
6943
9329
  id: SLOT_ID,
6944
- order: 50
6945
- }, () => (0, react.createElement)(WorkspaceFoldersAction, {
6946
- panel,
6947
- pickDirectory,
6948
- sessionId,
6949
- subscribeSession,
6950
- t,
6951
- compact: true
6952
- })));
9330
+ order: 50,
9331
+ store: filesTreeSeats.store,
9332
+ inject: filesTreeSeats.inject
9333
+ }, function FilesActionsOccupant(props) {
9334
+ const absolutePath = props?.absolutePath;
9335
+ const paneSessionId = () => props?.sessionId ?? sessionId();
9336
+ const filesTree = createFilesTreeSurface(props, filesTreeT);
9337
+ return absolutePath === void 0 ? (0, react.createElement)(WorkspaceFoldersAction, {
9338
+ panel,
9339
+ pickDirectory,
9340
+ sessionId: paneSessionId,
9341
+ subscribeSession,
9342
+ t,
9343
+ compact: true
9344
+ }) : (0, react.createElement)(RootSwitcher, {
9345
+ primaryPath: absolutePath,
9346
+ panel,
9347
+ pickDirectory,
9348
+ sessionId: paneSessionId,
9349
+ subscribeSession,
9350
+ t,
9351
+ openInApp,
9352
+ filesTree
9353
+ });
9354
+ }));
6953
9355
  }
6954
9356
  /**
6955
9357
  * Read one optional service by name.