@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.
- package/CHANGELOG.en.md +44 -1
- package/CHANGELOG.md +44 -1
- package/README.en.md +155 -92
- package/README.md +156 -91
- package/cordis.patch.yml +57 -0
- package/lib/client.js +2678 -276
- package/lib/client.js.map +1 -1
- package/lib/command-CESuwlVt.js +1306 -0
- package/lib/command.js +2 -2
- package/lib/{compat-B6ZqbQBy.js → compat-DF_yL1bB.js} +160 -2
- package/lib/compat.js +1 -1
- package/lib/contract-DZ4zczDT.js +238 -0
- package/lib/{fs-D_Sw3o5z.js → fs-9JIzGaf3.js} +55 -19
- package/lib/fs.js +2 -1
- package/lib/index.js +10 -8
- package/lib/{instructions-DfzN-wVf.js → instructions-D19jRxN0.js} +3 -2
- package/lib/instructions.js +1 -1
- package/lib/lsp.js +7 -2
- package/lib/method-wrapper-CFY3p3j2.js +26 -0
- package/lib/process-fence.js +280 -0
- package/lib/providers.js +87 -0
- package/lib/{registry-DySMbjgW.js → registry-DCxC5Lay.js} +256 -35
- package/lib/registry.js +2 -2
- package/lib/{roots-DasWEBJk.js → roots-B-0l8mNG.js} +117 -20
- package/lib/sandbox-Kup4_ZIb.js +747 -0
- package/lib/sandbox.js +2 -1
- package/lib/scope.js +229 -20
- package/lib/types/client/RootSwitcher.d.ts +63 -0
- package/lib/types/client/WorkspaceFoldersAction.d.ts +39 -1
- package/lib/types/client/files-tree/FilesBody.module.css.d.ts +24 -0
- package/lib/types/client/files-tree/FilesTree.d.ts +47 -0
- package/lib/types/client/files-tree/adapter.d.ts +94 -0
- package/lib/types/client/files-tree/directory-node.d.ts +62 -0
- package/lib/types/client/files-tree/face.d.ts +107 -0
- package/lib/types/client/files-tree/locales.d.ts +10 -0
- package/lib/types/client/files-tree/primitives.d.ts +33 -0
- package/lib/types/client/files-tree/store.d.ts +81 -0
- package/lib/types/client/locales.d.ts +56 -10
- package/lib/types/client/open-in-app/OpenInAppAction.d.ts +27 -0
- package/lib/types/client/open-in-app/OpenTargetButton.d.ts +52 -0
- package/lib/types/client/open-in-app/OpenTargetButton.module.css.d.ts +29 -0
- package/lib/types/client/open-in-app/adapter.d.ts +46 -0
- package/lib/types/client/open-in-app/applications.d.ts +4 -0
- package/lib/types/client/open-in-app/controller.d.ts +53 -0
- package/lib/types/client/open-in-app/locales.d.ts +10 -0
- package/lib/types/client/open-in-app/open-failure-toast.d.ts +11 -0
- package/lib/types/client/open-in-app/open-path.d.ts +13 -0
- package/lib/types/client/panel-client.d.ts +2 -1
- package/lib/types/client/styles.d.ts +1 -1
- package/lib/types/command.d.ts +16 -3
- package/lib/types/compat/client-icons.d.ts +18 -0
- package/lib/types/compat/dsh-version.d.ts +1 -1
- package/lib/types/compat/open-in-app-routes.d.ts +38 -0
- package/lib/types/compat/process-entrypoints.d.ts +31 -0
- package/lib/types/compat/process-result.d.ts +14 -0
- package/lib/types/compat/pty-marker.d.ts +83 -0
- package/lib/types/compat/sandbox-confine.d.ts +2 -0
- package/lib/types/compat/storage-zod.d.ts +65 -0
- package/lib/types/compat/windows-deny-preflight.d.ts +21 -0
- package/lib/types/compat/windows-native.d.ts +68 -0
- package/lib/types/compat/windows-path-guard.d.ts +56 -0
- package/lib/types/compat/workspace-files.d.ts +2 -0
- package/lib/types/contract.d.ts +31 -19
- package/lib/types/dialects.d.ts +99 -3
- package/lib/types/fs.d.ts +32 -10
- package/lib/types/index.d.ts +7 -2
- package/lib/types/process-fence.d.ts +54 -0
- package/lib/types/profile-isolation.d.ts +178 -0
- package/lib/types/providers.d.ts +70 -0
- package/lib/types/registry.d.ts +132 -31
- package/lib/types/roots.d.ts +100 -13
- package/lib/types/sandbox.d.ts +59 -3
- package/lib/types/scope.d.ts +200 -14
- package/lib/types/windows-broker.d.ts +17 -0
- package/lib/types/windows-deny.d.ts +1 -0
- package/lib/types/windows-profile.d.ts +10 -13
- package/lib/types/windows-runner-contract.d.ts +97 -0
- package/lib/windows-runner-contract-CEfmZzu0.js +141 -0
- package/lib/windows-runner.js +1256 -13
- package/lib/workspace-files.js +138 -3
- package/lib/workspace-routing-YPxaQ4V3.js +26 -0
- package/package.json +84 -62
- package/scripts/windows-broker/install.ps1 +89 -0
- package/scripts/windows-broker/rights.cs +58 -0
- package/scripts/windows-broker/service.cs +323 -0
- package/scripts/windows-broker/verify-install.ps1 +80 -0
- package/lib/command-CTdNvwt1.js +0 -791
- package/lib/method-wrapper-Zc-JlDP3.js +0 -51
- package/lib/sandbox-BRXl_FpF.js +0 -132
- package/lib/types/client/RootFileTree.d.ts +0 -12
- package/lib/types/panel-files.d.ts +0 -3
- package/lib/windows-profile-CiFsFYIV.js +0 -216
- 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}
|
|
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
|
-
|
|
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
|
-
"
|
|
5446
|
-
"
|
|
5447
|
-
"
|
|
5448
|
-
"
|
|
5449
|
-
"
|
|
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": "
|
|
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
|
-
"
|
|
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
|
-
"
|
|
5505
|
-
"
|
|
5506
|
-
"
|
|
5507
|
-
"
|
|
5508
|
-
"
|
|
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
|
|
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
|
-
"
|
|
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) :
|
|
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-
|
|
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
|
|
6040
|
-
/**
|
|
6041
|
-
|
|
6042
|
-
|
|
6043
|
-
|
|
6044
|
-
|
|
6045
|
-
|
|
6046
|
-
|
|
6047
|
-
|
|
6048
|
-
|
|
6049
|
-
|
|
6050
|
-
|
|
6051
|
-
|
|
6052
|
-
|
|
6053
|
-
|
|
6054
|
-
|
|
6055
|
-
|
|
6056
|
-
|
|
6057
|
-
|
|
6058
|
-
|
|
6059
|
-
|
|
6060
|
-
|
|
6061
|
-
|
|
6062
|
-
|
|
6063
|
-
|
|
6064
|
-
|
|
6065
|
-
|
|
6066
|
-
|
|
6067
|
-
|
|
6068
|
-
|
|
6069
|
-
|
|
6070
|
-
|
|
6071
|
-
|
|
6072
|
-
|
|
6073
|
-
|
|
6074
|
-
|
|
6075
|
-
|
|
6076
|
-
|
|
6077
|
-
|
|
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
|
-
|
|
6080
|
-
|
|
6081
|
-
|
|
6082
|
-
|
|
6083
|
-
|
|
6084
|
-
|
|
6085
|
-
|
|
6086
|
-
|
|
6087
|
-
|
|
6088
|
-
|
|
6089
|
-
|
|
6090
|
-
|
|
6091
|
-
|
|
6092
|
-
|
|
6093
|
-
|
|
6094
|
-
|
|
6095
|
-
|
|
6096
|
-
|
|
6097
|
-
|
|
6098
|
-
|
|
6099
|
-
|
|
6100
|
-
|
|
6101
|
-
|
|
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
|
-
|
|
6104
|
-
|
|
6105
|
-
|
|
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.
|
|
6108
|
-
className:
|
|
6109
|
-
|
|
6110
|
-
|
|
6111
|
-
|
|
6112
|
-
|
|
6113
|
-
|
|
6114
|
-
|
|
6115
|
-
|
|
6116
|
-
|
|
6117
|
-
|
|
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
|
-
|
|
6128
|
-
|
|
6129
|
-
const
|
|
6130
|
-
const
|
|
6131
|
-
|
|
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
|
-
|
|
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
|
-
|
|
6136
|
-
|
|
6137
|
-
|
|
6138
|
-
|
|
6139
|
-
|
|
6140
|
-
|
|
6141
|
-
|
|
6142
|
-
|
|
6143
|
-
|
|
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
|
-
|
|
6147
|
-
|
|
6148
|
-
|
|
6149
|
-
|
|
6150
|
-
|
|
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
|
-
|
|
6153
|
-
|
|
6154
|
-
|
|
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
|
-
|
|
6158
|
-
|
|
6159
|
-
|
|
6160
|
-
|
|
6161
|
-
|
|
6162
|
-
|
|
6163
|
-
|
|
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
|
-
|
|
6166
|
-
|
|
6167
|
-
|
|
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)(
|
|
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 =
|
|
6340
|
-
|
|
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", {
|
|
6511
|
-
|
|
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", {
|
|
6521
|
-
|
|
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 (
|
|
8505
|
+
const copyPath = (0, react.useCallback)(async (key, path) => {
|
|
6562
8506
|
try {
|
|
6563
|
-
await navigator.clipboard.writeText(
|
|
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
|
-
|
|
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:
|
|
6656
|
-
|
|
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:
|
|
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
|
-
|
|
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)(
|
|
6675
|
-
|
|
6676
|
-
|
|
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.
|
|
8684
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
6709
8685
|
className: "mrfw-rootName",
|
|
6710
|
-
children:
|
|
6711
|
-
|
|
6712
|
-
|
|
6713
|
-
|
|
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
|
-
|
|
8721
|
+
reRegister(root);
|
|
6724
8722
|
},
|
|
6725
|
-
icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(
|
|
6726
|
-
label:
|
|
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
|
-
|
|
6759
|
-
|
|
6760
|
-
|
|
6761
|
-
|
|
6762
|
-
|
|
6763
|
-
|
|
6764
|
-
|
|
6765
|
-
|
|
6766
|
-
|
|
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: [
|
|
6843
|
-
|
|
6844
|
-
|
|
6845
|
-
|
|
6846
|
-
|
|
6847
|
-
|
|
6848
|
-
|
|
6849
|
-
|
|
6850
|
-
|
|
6851
|
-
|
|
6852
|
-
|
|
6853
|
-
|
|
6854
|
-
|
|
6855
|
-
|
|
6856
|
-
|
|
6857
|
-
|
|
6858
|
-
|
|
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
|
-
|
|
6946
|
-
|
|
6947
|
-
|
|
6948
|
-
|
|
6949
|
-
|
|
6950
|
-
|
|
6951
|
-
|
|
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.
|