@dsh-electron/dsh-plugin-multi-root-workspace 0.1.14 → 0.1.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.en.md +32 -1
- package/CHANGELOG.md +32 -1
- package/README.en.md +25 -8
- package/README.md +25 -8
- package/cordis.patch.yml +5 -1
- package/lib/{llm-message-fYORacvh.js → agent-instructions-BXuKhOzr.js} +1 -84
- package/lib/auto-review.js +2 -2
- package/lib/client.js +969 -100
- package/lib/client.js.map +1 -1
- package/lib/{command-Dcz6nULo.js → command-DaVJ-mK3.js} +76 -11
- package/lib/command.js +1 -1
- package/lib/compat-CTJbpCxu.js +267 -0
- package/lib/compat.js +1 -1
- package/lib/{dialects-Dr4rDR-H.js → dialects-CdlG3RF9.js} +1 -1
- package/lib/{compat-veDqCOoa.js → dsh-version-DCwF7REk.js} +2 -264
- package/lib/index.js +10 -8
- package/lib/{instructions-KsUwUFE0.js → instructions-Ca80SkZq.js} +696 -27
- package/lib/instructions.js +1 -1
- package/lib/llm-message-CepyzEBQ.js +84 -0
- package/lib/lsp.js +4 -4
- package/lib/{metadata-DgqSdVX_.js → metadata-99jgViHQ.js} +1 -1
- package/lib/{method-wrapper-CFY3p3j2.js → method-wrapper-D4oUCsEF.js} +22 -2
- package/lib/{native-win32-CRNXr11U.js → native-win32-C5oMtt7f.js} +1 -1
- package/lib/ops-compatibility.js +58 -0
- package/lib/ops-settings-UN2oOKEp.js +40 -0
- package/lib/ops-tools-k4JYaOrM.js +152 -0
- package/lib/process-fence.js +193 -34
- package/lib/{protocol-CnR1KTXy.js → protocol-CwDnFEyK.js} +8 -4
- package/lib/{registry-DR_sMuvS.js → registry-BibiukWk.js} +236 -80
- package/lib/{registry-lease-win32-DDO-c7ig.js → registry-lease-win32-r4puac9m.js} +1 -1
- package/lib/{registry-operation-C2ZV3Jhw.js → registry-operation-CIlmt6_c.js} +5 -1
- package/lib/registry.js +2 -2
- package/lib/{review-context-NDpKeMCc.js → review-context-OxO-fMWR.js} +24 -7
- package/lib/review-context.js +1 -1
- package/lib/{roots-Ca_W1S9n.js → roots-CmaWxRVy.js} +112 -22
- package/lib/{sandbox-C-vCJCp3.js → sandbox-aobdyQGi.js} +7 -4
- package/lib/sandbox.js +1 -1
- package/lib/scope.js +162 -12
- package/lib/skills-utyuvmwr.js +104 -0
- package/lib/skills.js +5 -106
- package/lib/{tool-context-CpgIFcCB.js → tool-context-CCeY3mrK.js} +1 -1
- package/lib/types/authority/protocol.d.ts +5 -1
- package/lib/types/client/files-tree/FilesTree.d.ts +49 -6
- package/lib/types/client/files-tree/adapter.d.ts +69 -16
- package/lib/types/client/files-tree/face.d.ts +31 -2
- package/lib/types/client/files-tree/locate.d.ts +74 -0
- package/lib/types/client/files-tree/navigator.d.ts +98 -0
- package/lib/types/client/index.d.ts +7 -0
- package/lib/types/client/locales.d.ts +30 -0
- package/lib/types/client/ops-settings.d.ts +6 -0
- package/lib/types/client/skills-settings.d.ts +3 -2
- package/lib/types/client/styles.d.ts +1 -1
- package/lib/types/command.d.ts +3 -3
- package/lib/types/compat/files-tree.d.ts +8 -1
- package/lib/types/compat/forge-entrypoints.d.ts +44 -0
- package/lib/types/compat/instruction-activity.d.ts +32 -1
- package/lib/types/compat/ops-confirmation.d.ts +17 -0
- package/lib/types/compat/ops-settings.d.ts +11 -0
- package/lib/types/compat/ops-tools.d.ts +25 -0
- package/lib/types/compat/session-existence.d.ts +41 -0
- package/lib/types/compat/skills-client.d.ts +1 -0
- package/lib/types/compat/tool-context.d.ts +4 -0
- package/lib/types/compat.d.ts +2 -2
- package/lib/types/instructions.d.ts +6 -1
- package/lib/types/ops-compatibility.d.ts +34 -0
- package/lib/types/registry-operation.d.ts +7 -0
- package/lib/types/registry.d.ts +67 -8
- package/lib/types/review-context.d.ts +2 -0
- package/lib/types/roots.d.ts +116 -20
- package/lib/types/scope.d.ts +95 -3
- package/lib/workspace-files.js +4 -4
- package/lib/{workspace-routing-YPxaQ4V3.js → workspace-routing-D_qwMUh-.js} +4 -4
- package/package.json +5 -1
package/lib/client.js
CHANGED
|
@@ -128,7 +128,7 @@ window.__ModuleLoader__.load({
|
|
|
128
128
|
return typeof value === "function" ? value : void 0;
|
|
129
129
|
}
|
|
130
130
|
/** The factory consumes absolute, slash-separated keys and already-localized errors. */
|
|
131
|
-
function nativeTreeInput(state, failureLine, toggle, open) {
|
|
131
|
+
function nativeTreeInput(state, failureLine, toggle, open, current) {
|
|
132
132
|
const root = state.root.replace(/[/\\]+$/, "");
|
|
133
133
|
const keyOf = (path) => path === state.root ? root : path;
|
|
134
134
|
const entries = [];
|
|
@@ -155,6 +155,10 @@ window.__ModuleLoader__.load({
|
|
|
155
155
|
entries,
|
|
156
156
|
directories,
|
|
157
157
|
expanded: state.expanded.map(keyOf),
|
|
158
|
+
...current === void 0 ? {} : {
|
|
159
|
+
current: current.path,
|
|
160
|
+
currentRef: current.ref
|
|
161
|
+
},
|
|
158
162
|
onToggle: (entry) => {
|
|
159
163
|
const parent = entry.path.slice(0, entry.path.lastIndexOf("/"));
|
|
160
164
|
toggle(parent === root ? state.root : parent, entry.path);
|
|
@@ -5593,6 +5597,16 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
5593
5597
|
"autoReview.loadFailed": "无法读取插件状态。",
|
|
5594
5598
|
"autoReview.restartRequired": "更改将在重启后生效。",
|
|
5595
5599
|
"autoReview.settingsFailed": "设置保存失败,请重试。",
|
|
5600
|
+
"ops.compatibilityEnable": "OPS 兼容模式",
|
|
5601
|
+
"ops.experimental": "实验性",
|
|
5602
|
+
"ops.compatibilityHint": "仅限 DSH 0.2.*。启用 OPS 写保护、实时活动与 Review 增强。受限模式可能拒绝附加根操作(包括读取),后台缓存可能残留;关闭时显式只读附加根可能被 OPS 写入。",
|
|
5603
|
+
"ops.settingsFailed": "设置保存失败,请重试。",
|
|
5604
|
+
"ops.confirmTitle": "开启 OPS 实验性兼容?",
|
|
5605
|
+
"ops.confirmDescription": "这仍是有限的实验性适配。开启后,附加根的合法读写及部分 PTC 操作可能被拒绝;Windows 存在只读边界时,OPS 读取和执行也可能被拒绝。Linux 批处理或结果读取、后台任务及清理可能不可用,并留下缓存。活动记录尚不持久化,恢复会话或审查动态操作时,Review 可能因证据不足而失败。目前尚未实现 OPS 全功能兼容,请仅在能接受这些限制时开启。",
|
|
5606
|
+
"ops.confirmAcknowledge": "我已了解上述限制,仍要开启实验性兼容",
|
|
5607
|
+
"ops.confirmCancel": "暂不开启",
|
|
5608
|
+
"ops.confirmClose": "关闭并保持未开启",
|
|
5609
|
+
"ops.confirmEnable": "确认开启",
|
|
5596
5610
|
"skills.userOnly": "仅用户调用",
|
|
5597
5611
|
"panel.skills": "目录技能",
|
|
5598
5612
|
"panel.skillsEnabled": "此目录技能已启用,点击停用",
|
|
@@ -5637,6 +5651,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
5637
5651
|
"panel.copied": "已复制",
|
|
5638
5652
|
"panel.reveal": "在文件管理器中显示",
|
|
5639
5653
|
"panel.reRegister": "重新登记(保留原访问级别)",
|
|
5654
|
+
"panel.redirectedHint": "目标已改变:请移除该登记后再显式添加新目录。",
|
|
5640
5655
|
"panel.moveUp": "上移",
|
|
5641
5656
|
"panel.moveDown": "下移",
|
|
5642
5657
|
"access.inherit": "跟随会话",
|
|
@@ -5672,7 +5687,11 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
5672
5687
|
"error.unavailable": "无法连接到 dsh 主进程。",
|
|
5673
5688
|
"error.copy-failed": "无法写入剪贴板,请检查浏览器的剪贴板权限。",
|
|
5674
5689
|
"error.session-not-found": "找不到该会话;请先打开一个工作区会话。",
|
|
5690
|
+
"error.session-not-live": "该会话存在但未在本实例激活;它可能正被另一个 DSH 实例持有、仍在恢复,或格式待迁移。若另一实例持有它,请退出那个实例;也可以切换到同一工作区的活动会话;格式待迁移时请在该会话完成迁移。否则请稍后重试。",
|
|
5675
5691
|
"error.unsupported-path": "该路径是网络或设备路径,Windows ACL 隔离无法覆盖它。",
|
|
5692
|
+
"error.protected-path": "该目录与 DSH 主目录或根登记存储目录重叠,不能登记。",
|
|
5693
|
+
"error.protected-boundary-unknown": "暂时无法确定 DSH 主目录等受保护目录的位置;恢复之前不能新增登记。",
|
|
5694
|
+
"error.revive-target-changed": "该登记现在指向另一个目录;请先移除它,再显式添加新目录。",
|
|
5676
5695
|
"error.fallback": "操作失败。"
|
|
5677
5696
|
};
|
|
5678
5697
|
/** English copy, pinned to the Chinese key set. */
|
|
@@ -5686,6 +5705,16 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
5686
5705
|
"autoReview.loadFailed": "Could not read plugin state.",
|
|
5687
5706
|
"autoReview.restartRequired": "The change takes effect after restart.",
|
|
5688
5707
|
"autoReview.settingsFailed": "Could not save settings. Please try again.",
|
|
5708
|
+
"ops.compatibilityEnable": "OPS compatibility mode",
|
|
5709
|
+
"ops.experimental": "Experimental",
|
|
5710
|
+
"ops.compatibilityHint": "Requires DSH 0.2.*. Enables OPS write protection, live activity, and Review enhancements. Restricted modes may reject additional-root operations, including reads, and background caches may remain. When off, OPS may write to an explicitly read-only additional root.",
|
|
5711
|
+
"ops.settingsFailed": "Could not save settings. Please try again.",
|
|
5712
|
+
"ops.confirmTitle": "Enable experimental OPS compatibility?",
|
|
5713
|
+
"ops.confirmDescription": "This is a limited experimental adaptation. Valid additional-root reads/writes and some PTC calls may be rejected; Windows may also block OPS reads and execution when read-only boundaries apply. Linux batch/result operations, background tasks, and cleanup may fail or leave caches behind. Activity is not persisted: after resume, or when reviewing dynamic operations, Review may fail for lack of evidence. Full OPS compatibility is not available. Enable only if you accept these limitations.",
|
|
5714
|
+
"ops.confirmAcknowledge": "I understand these limitations and want to enable experimental compatibility",
|
|
5715
|
+
"ops.confirmCancel": "Keep disabled",
|
|
5716
|
+
"ops.confirmClose": "Close and keep disabled",
|
|
5717
|
+
"ops.confirmEnable": "Confirm and enable",
|
|
5689
5718
|
"skills.userOnly": "User invocation only",
|
|
5690
5719
|
"panel.skills": "Folder skills",
|
|
5691
5720
|
"panel.skillsEnabled": "Skills enabled for this folder. Click to disable.",
|
|
@@ -5730,6 +5759,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
5730
5759
|
"panel.copied": "Copied",
|
|
5731
5760
|
"panel.reveal": "Reveal in file manager",
|
|
5732
5761
|
"panel.reRegister": "Re-register (keeps its access level)",
|
|
5762
|
+
"panel.redirectedHint": "The target changed: remove this registration, then add the new folder explicitly.",
|
|
5733
5763
|
"panel.moveUp": "Move up",
|
|
5734
5764
|
"panel.moveDown": "Move down",
|
|
5735
5765
|
"access.inherit": "Inherit",
|
|
@@ -5765,7 +5795,11 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
5765
5795
|
"error.unavailable": "Could not reach the dsh host process.",
|
|
5766
5796
|
"error.copy-failed": "Could not write to the clipboard; check the browser's clipboard permission.",
|
|
5767
5797
|
"error.session-not-found": "That session is not active; open a workspace session first.",
|
|
5798
|
+
"error.session-not-live": "That session exists but is not active in this instance — it may be held by another DSH instance, still resuming, or awaiting a format migration. If another instance holds it, quit that instance; alternatively, switch to an active session in the same workspace; if a migration is pending, complete it in that session. Otherwise retry shortly.",
|
|
5768
5799
|
"error.unsupported-path": "That is a network or device path, which Windows ACL confinement cannot cover.",
|
|
5800
|
+
"error.protected-path": "That directory overlaps the DSH home or the registry storage directories; it cannot be registered.",
|
|
5801
|
+
"error.protected-boundary-unknown": "The protected DSH directories cannot be resolved right now; no folder can be registered until they can.",
|
|
5802
|
+
"error.revive-target-changed": "That registration now resolves to a different directory; remove it, then add the new folder explicitly.",
|
|
5769
5803
|
"error.fallback": "The operation failed."
|
|
5770
5804
|
};
|
|
5771
5805
|
//#endregion
|
|
@@ -6095,6 +6129,27 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
6095
6129
|
background: var(--dsw-alias-interactive-bg-hover);
|
|
6096
6130
|
}
|
|
6097
6131
|
|
|
6132
|
+
/* The document preview's navigator aside, while the retarget holds it: the
|
|
6133
|
+
native body hides (it lists the session's primary root regardless of the
|
|
6134
|
+
previewed file), and the forked tree — portaled in as the aside's sibling
|
|
6135
|
+
child — takes the same stretch item in the aside's column flex, right
|
|
6136
|
+
down to the body's own padding and 2px scrollbar offset. The aside's
|
|
6137
|
+
chrome is untouched: width, push/overlay/fill modes, and the resize
|
|
6138
|
+
separator all stay native. The marker attribute is set only while a
|
|
6139
|
+
takeover actually holds the aside, so a navigator we never matched — or
|
|
6140
|
+
one whose plugin sheet is gone — renders exactly as upstream drew it. */
|
|
6141
|
+
aside[data-mrfw-navigator] > [data-files-body] {
|
|
6142
|
+
display: none;
|
|
6143
|
+
}
|
|
6144
|
+
|
|
6145
|
+
/* The reveal's current row, the fork's spelling of upstream's .current:
|
|
6146
|
+
the interactive-active fill, kept under hover exactly as the native
|
|
6147
|
+
tree's current row keeps it. */
|
|
6148
|
+
.mrfw-ftRow[data-files-current],
|
|
6149
|
+
.mrfw-ftRow[data-files-current]:hover {
|
|
6150
|
+
background: var(--dsw-alias-interactive-bg-active);
|
|
6151
|
+
}
|
|
6152
|
+
|
|
6098
6153
|
/* ── files-tree fork (verbatim rules, renamed classes) ─────────────────────
|
|
6099
6154
|
Copied from upstream's FilesBody.module.css (ui-sidebar-files @
|
|
6100
6155
|
0.2.2-alpha.2) with local names mapped to mrfw-ft* so the rules cannot
|
|
@@ -6682,10 +6737,13 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
6682
6737
|
`;
|
|
6683
6738
|
//#endregion
|
|
6684
6739
|
//#region src/compat/skills-client.ts
|
|
6685
|
-
function
|
|
6740
|
+
function bindSettingsForm(ctx, entryId) {
|
|
6686
6741
|
const modern = ctx.get("configForms");
|
|
6687
|
-
if (modern !== void 0) return modern.get(
|
|
6688
|
-
return ctx.get("settingsScope")?.bind({ namespace:
|
|
6742
|
+
if (modern !== void 0) return modern.get(entryId);
|
|
6743
|
+
return ctx.get("settingsScope")?.bind({ namespace: entryId });
|
|
6744
|
+
}
|
|
6745
|
+
function bindSkillsForm(ctx) {
|
|
6746
|
+
return bindSettingsForm(ctx, "multi-root-skills");
|
|
6689
6747
|
}
|
|
6690
6748
|
function clientInject(ctx, services, callback) {
|
|
6691
6749
|
if (typeof ctx.inject === "function") ctx.inject(services, callback);
|
|
@@ -7122,6 +7180,123 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
7122
7180
|
});
|
|
7123
7181
|
}
|
|
7124
7182
|
//#endregion
|
|
7183
|
+
//#region src/compat/ops-confirmation.ts
|
|
7184
|
+
function opsRiskConfirmation() {
|
|
7185
|
+
return _deepseek_ai_dsh_client_ui_primitives.RiskConfirmation;
|
|
7186
|
+
}
|
|
7187
|
+
//#endregion
|
|
7188
|
+
//#region src/client/ops-settings.tsx
|
|
7189
|
+
function OpsCompatibilitySettingsCard({ form, t }) {
|
|
7190
|
+
const snapshot = (0, react.useSyncExternalStore)((listener) => form.subscribe(listener), () => form.getSnapshot());
|
|
7191
|
+
const [busy, setBusy] = (0, react.useState)(false), [error, setError] = (0, react.useState)();
|
|
7192
|
+
const [confirmation, setConfirmation] = (0, react.useState)();
|
|
7193
|
+
const [acknowledged, setAcknowledged] = (0, react.useState)(false);
|
|
7194
|
+
const saving = (0, react.useRef)(false);
|
|
7195
|
+
const mounted = (0, react.useRef)(true);
|
|
7196
|
+
const RiskConfirmation = opsRiskConfirmation();
|
|
7197
|
+
const canConfirm = confirmation !== void 0 && confirmation.form === form && confirmation.revision === snapshot.revision && snapshot.status === "ready" && snapshot.writable && snapshot.value?.enabled !== true && RiskConfirmation !== void 0;
|
|
7198
|
+
const dismiss = () => {
|
|
7199
|
+
setConfirmation(void 0);
|
|
7200
|
+
setAcknowledged(false);
|
|
7201
|
+
};
|
|
7202
|
+
(0, react.useEffect)(() => {
|
|
7203
|
+
mounted.current = true;
|
|
7204
|
+
return () => {
|
|
7205
|
+
mounted.current = false;
|
|
7206
|
+
};
|
|
7207
|
+
}, []);
|
|
7208
|
+
(0, react.useEffect)(() => {
|
|
7209
|
+
if (confirmation !== void 0 && !canConfirm) {
|
|
7210
|
+
setConfirmation(void 0);
|
|
7211
|
+
setAcknowledged(false);
|
|
7212
|
+
}
|
|
7213
|
+
}, [confirmation, canConfirm]);
|
|
7214
|
+
const save = async (enabled, revision) => {
|
|
7215
|
+
if (saving.current) return;
|
|
7216
|
+
saving.current = true;
|
|
7217
|
+
setBusy(true);
|
|
7218
|
+
setError(void 0);
|
|
7219
|
+
try {
|
|
7220
|
+
const ok = await form.mutate([{
|
|
7221
|
+
op: "set",
|
|
7222
|
+
path: ["enabled"],
|
|
7223
|
+
value: enabled
|
|
7224
|
+
}], revision);
|
|
7225
|
+
if (mounted.current && !ok) setError(t("ops.settingsFailed"));
|
|
7226
|
+
} catch {
|
|
7227
|
+
if (mounted.current) setError(t("ops.settingsFailed"));
|
|
7228
|
+
} finally {
|
|
7229
|
+
saving.current = false;
|
|
7230
|
+
if (mounted.current) setBusy(false);
|
|
7231
|
+
}
|
|
7232
|
+
};
|
|
7233
|
+
if (snapshot.status === "unavailable") return null;
|
|
7234
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
7235
|
+
className: "mrfw-skillsSettings",
|
|
7236
|
+
children: [
|
|
7237
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
7238
|
+
className: "mrfw-skillsSettingsText",
|
|
7239
|
+
children: [
|
|
7240
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
7241
|
+
className: "mrfw-settingsTitleRow",
|
|
7242
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
7243
|
+
className: "mrfw-skillsSettingsTitle",
|
|
7244
|
+
children: t("ops.compatibilityEnable")
|
|
7245
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Tag, {
|
|
7246
|
+
tone: "info",
|
|
7247
|
+
children: t("ops.experimental")
|
|
7248
|
+
})]
|
|
7249
|
+
}),
|
|
7250
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
7251
|
+
className: "mrfw-skillsSettingsDescription",
|
|
7252
|
+
children: t("ops.compatibilityHint")
|
|
7253
|
+
}),
|
|
7254
|
+
error ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
7255
|
+
role: "alert",
|
|
7256
|
+
children: error
|
|
7257
|
+
}) : null
|
|
7258
|
+
]
|
|
7259
|
+
}),
|
|
7260
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Switch, {
|
|
7261
|
+
label: t("ops.compatibilityEnable"),
|
|
7262
|
+
checked: snapshot.value?.enabled === true,
|
|
7263
|
+
disabled: busy || confirmation !== void 0 || snapshot.status !== "ready" || !snapshot.writable || snapshot.value?.enabled !== true && RiskConfirmation === void 0,
|
|
7264
|
+
onChange: (enabled) => {
|
|
7265
|
+
if (!enabled) {
|
|
7266
|
+
save(false, snapshot.revision);
|
|
7267
|
+
return;
|
|
7268
|
+
}
|
|
7269
|
+
setError(void 0);
|
|
7270
|
+
setAcknowledged(false);
|
|
7271
|
+
setConfirmation({
|
|
7272
|
+
form,
|
|
7273
|
+
revision: snapshot.revision
|
|
7274
|
+
});
|
|
7275
|
+
}
|
|
7276
|
+
}),
|
|
7277
|
+
RiskConfirmation === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(RiskConfirmation, {
|
|
7278
|
+
open: canConfirm,
|
|
7279
|
+
title: t("ops.confirmTitle"),
|
|
7280
|
+
description: t("ops.confirmDescription"),
|
|
7281
|
+
acknowledgeLabel: t("ops.confirmAcknowledge"),
|
|
7282
|
+
cancelLabel: t("ops.confirmCancel"),
|
|
7283
|
+
closeLabel: t("ops.confirmClose"),
|
|
7284
|
+
confirmLabel: t("ops.confirmEnable"),
|
|
7285
|
+
acknowledged,
|
|
7286
|
+
disabled: busy || !canConfirm,
|
|
7287
|
+
onAcknowledgedChange: setAcknowledged,
|
|
7288
|
+
onCancel: dismiss,
|
|
7289
|
+
onConfirm: () => {
|
|
7290
|
+
if (!canConfirm || !acknowledged || saving.current) return;
|
|
7291
|
+
const revision = confirmation.revision;
|
|
7292
|
+
dismiss();
|
|
7293
|
+
save(true, revision);
|
|
7294
|
+
}
|
|
7295
|
+
})
|
|
7296
|
+
]
|
|
7297
|
+
});
|
|
7298
|
+
}
|
|
7299
|
+
//#endregion
|
|
7125
7300
|
//#region src/client/skills-settings.tsx
|
|
7126
7301
|
const BUNDLE_KEY = "@dsh-electron/dsh-plugin-multi-root-workspace";
|
|
7127
7302
|
function SkillsSettingsCard({ form, t }) {
|
|
@@ -7189,42 +7364,55 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
7189
7364
|
/**
|
|
7190
7365
|
* The bundle's one `plugins.bundle.config` cell: every plugin-level switch the
|
|
7191
7366
|
* package offers, in display order. Each row owns its own form and write path —
|
|
7192
|
-
* Skills through
|
|
7367
|
+
* Skills and OPS through their settings namespaces, Auto Review through the Plugin
|
|
7193
7368
|
* Manager's Loader-row enablement — so one cell never serializes their state.
|
|
7194
7369
|
*/
|
|
7195
|
-
function BundleSettingsCard({ form, autoReviewForm, t }) {
|
|
7196
|
-
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
|
|
7197
|
-
form,
|
|
7198
|
-
|
|
7199
|
-
|
|
7200
|
-
|
|
7201
|
-
|
|
7202
|
-
|
|
7370
|
+
function BundleSettingsCard({ form, autoReviewForm, opsForm, t }) {
|
|
7371
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
|
|
7372
|
+
form !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SkillsSettingsCard, {
|
|
7373
|
+
form,
|
|
7374
|
+
t
|
|
7375
|
+
}) : null,
|
|
7376
|
+
autoReviewForm !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AutoReviewSettingsCard, {
|
|
7377
|
+
form: autoReviewForm,
|
|
7378
|
+
t
|
|
7379
|
+
}) : null,
|
|
7380
|
+
opsForm !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(OpsCompatibilitySettingsCard, {
|
|
7381
|
+
form: opsForm,
|
|
7382
|
+
t
|
|
7383
|
+
}) : null
|
|
7384
|
+
] });
|
|
7203
7385
|
}
|
|
7204
7386
|
function installSkillsSettingsCard(ctx, t) {
|
|
7205
7387
|
let currentSkills;
|
|
7388
|
+
let currentOps;
|
|
7206
7389
|
let currentAutoReview = false;
|
|
7207
7390
|
let stop;
|
|
7208
7391
|
const candidates = /* @__PURE__ */ new Map();
|
|
7209
7392
|
const update = () => {
|
|
7210
|
-
const
|
|
7393
|
+
const chosen = [...candidates.entries()].sort(([a], [b]) => Number(b.get("configForms") !== void 0) - Number(a.get("configForms") !== void 0))[0];
|
|
7394
|
+
const skillsForm = chosen?.[1].skills;
|
|
7395
|
+
const opsCandidate = chosen?.[1].ops;
|
|
7396
|
+
const opsForm = opsCandidate?.getSnapshot().status === "unavailable" ? void 0 : opsCandidate;
|
|
7211
7397
|
const autoSnapshot = autoForm.getSnapshot();
|
|
7212
7398
|
const autoAvailable = autoSnapshot.status === "loading" || autoSnapshot.status === "error" || autoSnapshot.status === "ready" && autoSnapshot.row !== void 0;
|
|
7213
|
-
if (skillsForm === void 0 && !autoAvailable) {
|
|
7214
|
-
if (currentSkills !== void 0 || currentAutoReview) {
|
|
7399
|
+
if (skillsForm === void 0 && opsForm === void 0 && !autoAvailable) {
|
|
7400
|
+
if (currentSkills !== void 0 || currentOps !== void 0 || currentAutoReview) {
|
|
7215
7401
|
stop?.();
|
|
7216
7402
|
stop = void 0;
|
|
7217
7403
|
currentSkills = void 0;
|
|
7404
|
+
currentOps = void 0;
|
|
7218
7405
|
currentAutoReview = false;
|
|
7219
7406
|
}
|
|
7220
7407
|
return;
|
|
7221
7408
|
}
|
|
7222
|
-
if (currentSkills === skillsForm && currentAutoReview === autoAvailable) return;
|
|
7409
|
+
if (currentSkills === skillsForm && currentOps === opsForm && currentAutoReview === autoAvailable) return;
|
|
7223
7410
|
const slots = ctx.slots;
|
|
7224
7411
|
if (slots === void 0) return;
|
|
7225
7412
|
stop?.();
|
|
7226
7413
|
stop = void 0;
|
|
7227
7414
|
currentSkills = skillsForm;
|
|
7415
|
+
currentOps = opsForm;
|
|
7228
7416
|
currentAutoReview = autoAvailable;
|
|
7229
7417
|
const disposers = [slots.inject("plugins.bundle.config", () => slots.register({
|
|
7230
7418
|
name: "plugins.bundle.config",
|
|
@@ -7232,6 +7420,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
7232
7420
|
inject: () => ({
|
|
7233
7421
|
form: skillsForm,
|
|
7234
7422
|
autoReviewForm: autoForm,
|
|
7423
|
+
opsForm,
|
|
7235
7424
|
t
|
|
7236
7425
|
})
|
|
7237
7426
|
}, BundleSettingsCard))];
|
|
@@ -7243,6 +7432,14 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
7243
7432
|
t
|
|
7244
7433
|
})
|
|
7245
7434
|
}, SkillsSettingsCard)));
|
|
7435
|
+
if (opsForm !== void 0) disposers.push(slots.inject("settings.plugin.item", () => slots.register({
|
|
7436
|
+
name: "settings.plugin.item",
|
|
7437
|
+
key: "multi-root-ops-compatibility",
|
|
7438
|
+
inject: () => ({
|
|
7439
|
+
form: opsForm,
|
|
7440
|
+
t
|
|
7441
|
+
})
|
|
7442
|
+
}, OpsCompatibilitySettingsCard)));
|
|
7246
7443
|
stop = () => {
|
|
7247
7444
|
for (const dispose of disposers) dispose();
|
|
7248
7445
|
};
|
|
@@ -7251,11 +7448,17 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
7251
7448
|
const stopAutoWatch = autoForm.subscribe(update);
|
|
7252
7449
|
update();
|
|
7253
7450
|
for (const service of ["settingsScope", "configForms"]) clientInject(ctx, [service], (scope) => {
|
|
7254
|
-
const
|
|
7255
|
-
|
|
7256
|
-
|
|
7451
|
+
const skills = bindSkillsForm(scope);
|
|
7452
|
+
const ops = scope.get("configForms")?.get("multi-root-ops-compatibility");
|
|
7453
|
+
if (skills === void 0 && ops === void 0) return;
|
|
7454
|
+
const stopOpsWatch = ops?.subscribe(update);
|
|
7455
|
+
candidates.set(scope, {
|
|
7456
|
+
skills,
|
|
7457
|
+
ops
|
|
7458
|
+
});
|
|
7257
7459
|
update();
|
|
7258
7460
|
return () => {
|
|
7461
|
+
stopOpsWatch?.();
|
|
7259
7462
|
candidates.delete(scope);
|
|
7260
7463
|
update();
|
|
7261
7464
|
};
|
|
@@ -7571,6 +7774,10 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
7571
7774
|
/** Per tab, per absolute path: the listing generation a settlement must match; the latest request wins. */
|
|
7572
7775
|
const generations = /* @__PURE__ */ new Map();
|
|
7573
7776
|
const roots = /* @__PURE__ */ new Map();
|
|
7777
|
+
/** The root each tab's store bucket belongs to; kept while a hold is released. */
|
|
7778
|
+
const buckets = /* @__PURE__ */ new Map();
|
|
7779
|
+
/** The record signal each tab's store bucket answers to. */
|
|
7780
|
+
const bindings = /* @__PURE__ */ new Map();
|
|
7574
7781
|
const nextGeneration = (tabId, path) => {
|
|
7575
7782
|
const byPath = generations.get(tabId) ?? /* @__PURE__ */ new Map();
|
|
7576
7783
|
generations.set(tabId, byPath);
|
|
@@ -7589,6 +7796,39 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
7589
7796
|
return result.ok ? result.value : void 0;
|
|
7590
7797
|
});
|
|
7591
7798
|
};
|
|
7799
|
+
const closeNode = (tabId) => {
|
|
7800
|
+
const root = roots.get(tabId);
|
|
7801
|
+
roots.delete(tabId);
|
|
7802
|
+
generations.delete(tabId);
|
|
7803
|
+
root?.close();
|
|
7804
|
+
};
|
|
7805
|
+
const forget = (tabId) => {
|
|
7806
|
+
closeNode(tabId);
|
|
7807
|
+
buckets.delete(tabId);
|
|
7808
|
+
bindings.delete(tabId);
|
|
7809
|
+
actions.forget(tabId);
|
|
7810
|
+
};
|
|
7811
|
+
const bindRecord = (tabId, signal) => {
|
|
7812
|
+
if (bindings.get(tabId) === signal) return;
|
|
7813
|
+
bindings.set(tabId, signal);
|
|
7814
|
+
signal.addEventListener("abort", () => {
|
|
7815
|
+
if (bindings.get(tabId) !== signal) return;
|
|
7816
|
+
forget(tabId);
|
|
7817
|
+
}, { once: true });
|
|
7818
|
+
};
|
|
7819
|
+
const open = (tabId, root, signal, restore, reset) => {
|
|
7820
|
+
if (signal.aborted) return;
|
|
7821
|
+
closeNode(tabId);
|
|
7822
|
+
bindRecord(tabId, signal);
|
|
7823
|
+
if (reset || buckets.get(tabId) !== root) {
|
|
7824
|
+
actions.start(tabId, root);
|
|
7825
|
+
buckets.set(tabId, root);
|
|
7826
|
+
restore = [];
|
|
7827
|
+
}
|
|
7828
|
+
roots.set(tabId, new DirectoryNode(root, (path, lifetime) => load(tabId, path, lifetime), (path, lifetime) => watch(sessionId, path, lifetime), (path, error) => {
|
|
7829
|
+
if (!signal.aborted) actions.failed(tabId, path, new RemoteError("gateway/internal", error instanceof Error ? error.message : String(error), {}));
|
|
7830
|
+
}, signal, restore).open());
|
|
7831
|
+
};
|
|
7592
7832
|
return {
|
|
7593
7833
|
refresh: (tabId) => {
|
|
7594
7834
|
roots.get(tabId)?.refreshTree();
|
|
@@ -7597,17 +7837,14 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
7597
7837
|
actions.autoRefresh(tabId, enabled);
|
|
7598
7838
|
roots.get(tabId)?.setAutomatic(enabled);
|
|
7599
7839
|
},
|
|
7600
|
-
start(tabId, root, signal) {
|
|
7601
|
-
|
|
7602
|
-
|
|
7603
|
-
|
|
7604
|
-
|
|
7605
|
-
|
|
7606
|
-
|
|
7607
|
-
|
|
7608
|
-
roots.set(tabId, new DirectoryNode(root, (path, lifetime) => load(tabId, path, lifetime), (path, lifetime) => watch(sessionId, path, lifetime), (path, error) => {
|
|
7609
|
-
if (!signal.aborted) actions.failed(tabId, path, new RemoteError("gateway/internal", error instanceof Error ? error.message : String(error), {}));
|
|
7610
|
-
}, signal).open());
|
|
7840
|
+
start: (tabId, root, signal) => {
|
|
7841
|
+
open(tabId, root, signal, [], true);
|
|
7842
|
+
},
|
|
7843
|
+
hold: (tabId, root, signal, restore = []) => {
|
|
7844
|
+
open(tabId, root, signal, restore, false);
|
|
7845
|
+
},
|
|
7846
|
+
release: (tabId) => {
|
|
7847
|
+
closeNode(tabId);
|
|
7611
7848
|
},
|
|
7612
7849
|
load: (tabId, path, signal) => {
|
|
7613
7850
|
load(tabId, path, signal);
|
|
@@ -7856,6 +8093,10 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
7856
8093
|
function encodePath(path) {
|
|
7857
8094
|
return path.split("/").map(encodeSegment).join("/");
|
|
7858
8095
|
}
|
|
8096
|
+
/** Whether a decoded first path segment is a Windows drive (`C:`). */
|
|
8097
|
+
function isDriveSegment(segment) {
|
|
8098
|
+
return segment !== void 0 && /^[A-Za-z]:$/.test(segment);
|
|
8099
|
+
}
|
|
7859
8100
|
/**
|
|
7860
8101
|
* Build the address of a file read through one Session.
|
|
7861
8102
|
* @param sessionId - the Session whose Host workspace resolves the path.
|
|
@@ -7867,6 +8108,44 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
7867
8108
|
return `${FILE_ADDRESS_PREFIX}session/${encodeSegment(sessionId)}/${encodePath(normalized)}`;
|
|
7868
8109
|
}
|
|
7869
8110
|
/**
|
|
8111
|
+
* Read a file address back into its parts without resolving `.` or `..`.
|
|
8112
|
+
* Query and fragment suffixes are ignored; encoded path segments are decoded.
|
|
8113
|
+
* @param address - a candidate address.
|
|
8114
|
+
* @returns the parts, or `undefined` when the string is not a `dsh-resource://file/` URI in a known scope with a path, or a segment is not validly encoded.
|
|
8115
|
+
*/
|
|
8116
|
+
function parseFileAddress(address) {
|
|
8117
|
+
try {
|
|
8118
|
+
if (!address.startsWith(FILE_ADDRESS_PREFIX)) return void 0;
|
|
8119
|
+
const end = address.search(/[?#]/);
|
|
8120
|
+
const [scope, ...rest] = address.slice(20, end === -1 ? void 0 : end).split("/");
|
|
8121
|
+
if (scope === "session") {
|
|
8122
|
+
const [id, ...segments] = rest;
|
|
8123
|
+
if (id === void 0 || id === "" || segments.length === 0) return void 0;
|
|
8124
|
+
return {
|
|
8125
|
+
scope,
|
|
8126
|
+
sessionId: decodeURIComponent(id),
|
|
8127
|
+
path: segments.map(decodeURIComponent).join("/")
|
|
8128
|
+
};
|
|
8129
|
+
}
|
|
8130
|
+
if (scope === "absolute") {
|
|
8131
|
+
const unc = rest[0] === "" && rest.length > 1;
|
|
8132
|
+
const segments = (unc ? rest.slice(1) : rest).map(decodeURIComponent);
|
|
8133
|
+
if (segments.length === 0 || segments[0] === "") return void 0;
|
|
8134
|
+
if (unc) return {
|
|
8135
|
+
scope,
|
|
8136
|
+
path: `//${segments.join("/")}`
|
|
8137
|
+
};
|
|
8138
|
+
return {
|
|
8139
|
+
scope,
|
|
8140
|
+
path: isDriveSegment(segments[0]) ? segments.join("/") : `/${segments.join("/")}`
|
|
8141
|
+
};
|
|
8142
|
+
}
|
|
8143
|
+
return;
|
|
8144
|
+
} catch {
|
|
8145
|
+
return;
|
|
8146
|
+
}
|
|
8147
|
+
}
|
|
8148
|
+
/**
|
|
7870
8149
|
* Browser-safe Workspace path and display helpers.
|
|
7871
8150
|
* @module @deepseek-ai/dsh-util-workspace-path
|
|
7872
8151
|
*/
|
|
@@ -7901,6 +8180,110 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
7901
8180
|
return sessionFileAddress(sessionId, normalized);
|
|
7902
8181
|
}
|
|
7903
8182
|
//#endregion
|
|
8183
|
+
//#region src/client/files-tree/locate.ts
|
|
8184
|
+
/**
|
|
8185
|
+
* Whether a root is a Windows drive or UNC path, in either separator
|
|
8186
|
+
* spelling, where names compare regardless of case.
|
|
8187
|
+
* @param root - the tree's root, or a prefix built from it.
|
|
8188
|
+
* @returns `true` for a drive letter followed by a separator, or a leading
|
|
8189
|
+
* double separator.
|
|
8190
|
+
*/
|
|
8191
|
+
function windowsRoot(root) {
|
|
8192
|
+
const path = root.replace(/\\/g, "/");
|
|
8193
|
+
return /^[A-Za-z]:\//.test(path) || path.startsWith("//");
|
|
8194
|
+
}
|
|
8195
|
+
/**
|
|
8196
|
+
* An absolute `/`-separated path relative to a tree's root.
|
|
8197
|
+
* @param path - the absolute path, in either separator spelling.
|
|
8198
|
+
* @param root - the tree's root, in either separator spelling.
|
|
8199
|
+
* @returns the path under the root, or `undefined` outside it.
|
|
8200
|
+
*/
|
|
8201
|
+
function underRoot(path, root) {
|
|
8202
|
+
const normalized = path.replace(/\\/g, "/");
|
|
8203
|
+
const prefix = `${root.replace(/\\/g, "/").replace(/\/+$/, "")}/`;
|
|
8204
|
+
const head = normalized.slice(0, prefix.length);
|
|
8205
|
+
return (windowsRoot(prefix) ? head.toLowerCase() === prefix.toLowerCase() : head === prefix) ? normalized.slice(prefix.length) : void 0;
|
|
8206
|
+
}
|
|
8207
|
+
/**
|
|
8208
|
+
* The workspace-relative path of a file under a tree's root, validated the
|
|
8209
|
+
* way upstream's `relativeFileOf` tail validates it: nothing absolute, and
|
|
8210
|
+
* no empty or dot segments.
|
|
8211
|
+
* @param path - the file's absolute path, in either separator spelling.
|
|
8212
|
+
* @param root - the root it must sit under.
|
|
8213
|
+
* @returns the `/`-separated relative path, or `undefined` outside the root.
|
|
8214
|
+
*/
|
|
8215
|
+
function relativeUnderRoot(path, root) {
|
|
8216
|
+
const normalized = underRoot(path, root);
|
|
8217
|
+
if (normalized === void 0 || normalized === "" || /^([A-Za-z]:)?\//.test(normalized)) return void 0;
|
|
8218
|
+
return normalized.split("/").some((part) => part === "" || part === "." || part === "..") ? void 0 : normalized;
|
|
8219
|
+
}
|
|
8220
|
+
/** One path's comparison spelling: separators unified, trailing slashes dropped. */
|
|
8221
|
+
function normalized(path) {
|
|
8222
|
+
return path.replace(/\\/g, "/").replace(/\/+$/, "");
|
|
8223
|
+
}
|
|
8224
|
+
/**
|
|
8225
|
+
* The additional root that owns one absolute file path: available roots
|
|
8226
|
+
* only, the longest matching root path winning when registrations nest.
|
|
8227
|
+
* @param view - the session's registry view.
|
|
8228
|
+
* @param path - the file's absolute path, in either separator spelling.
|
|
8229
|
+
* @returns the owning root, or `undefined` when the file is outside every
|
|
8230
|
+
* available additional root (the primary root included).
|
|
8231
|
+
*/
|
|
8232
|
+
function rootForPath(view, path) {
|
|
8233
|
+
let best;
|
|
8234
|
+
const primary = view.primaryRoot === void 0 ? void 0 : normalized(view.primaryRoot);
|
|
8235
|
+
for (const root of view.roots) {
|
|
8236
|
+
if (root.state !== "available" || primary !== void 0 && normalized(root.path) === primary) continue;
|
|
8237
|
+
if (underRoot(path, root.path) === void 0) continue;
|
|
8238
|
+
if (best === void 0 || root.path.length > best.path.length) best = root;
|
|
8239
|
+
}
|
|
8240
|
+
return best;
|
|
8241
|
+
}
|
|
8242
|
+
/**
|
|
8243
|
+
* Walk the listed levels to a workspace-relative file, keeping the listing's
|
|
8244
|
+
* spelling of each name; a Windows root also matches names regardless of
|
|
8245
|
+
* case. A verbatim port of upstream's navigator helper — the fork's
|
|
8246
|
+
* `LevelState` carries no `refreshing` flag, so a stale level reports
|
|
8247
|
+
* `pending: false` like a settled native one.
|
|
8248
|
+
* @param tree - the tab's tree.
|
|
8249
|
+
* @param relativePath - the file's path under the tree's root, `/`-separated.
|
|
8250
|
+
* @returns the ancestors found so far, the file's tree key, and whether a
|
|
8251
|
+
* level is pending.
|
|
8252
|
+
*/
|
|
8253
|
+
function locateFile(tree, relativePath) {
|
|
8254
|
+
const caseless = windowsRoot(tree.root);
|
|
8255
|
+
const parts = relativePath.split("/");
|
|
8256
|
+
const ancestors = [];
|
|
8257
|
+
let parent = tree.root;
|
|
8258
|
+
for (const [index, part] of parts.entries()) {
|
|
8259
|
+
const level = tree.levels[parent];
|
|
8260
|
+
if (level?.kind !== "ready") return {
|
|
8261
|
+
ancestors,
|
|
8262
|
+
current: void 0,
|
|
8263
|
+
pending: level === void 0 || level.kind === "loading"
|
|
8264
|
+
};
|
|
8265
|
+
const type = index === parts.length - 1 ? "file" : "directory";
|
|
8266
|
+
const candidates = level.level.entries.filter((entry) => entry.type === type);
|
|
8267
|
+
const entry = candidates.find((candidate) => candidate.name === part) ?? (caseless ? candidates.find((candidate) => candidate.name.toLowerCase() === part.toLowerCase()) : void 0);
|
|
8268
|
+
if (entry === void 0) return {
|
|
8269
|
+
ancestors,
|
|
8270
|
+
current: void 0,
|
|
8271
|
+
pending: false
|
|
8272
|
+
};
|
|
8273
|
+
const path = childPath(parent, entry.name);
|
|
8274
|
+
if (type === "directory") ancestors.push({
|
|
8275
|
+
parent,
|
|
8276
|
+
path
|
|
8277
|
+
});
|
|
8278
|
+
parent = path;
|
|
8279
|
+
}
|
|
8280
|
+
return {
|
|
8281
|
+
ancestors,
|
|
8282
|
+
current: parent,
|
|
8283
|
+
pending: false
|
|
8284
|
+
};
|
|
8285
|
+
}
|
|
8286
|
+
//#endregion
|
|
7904
8287
|
//#region src/client/files-tree/FilesBody.module.css.ts
|
|
7905
8288
|
/**
|
|
7906
8289
|
* Shim for the CSS-module virtual module `./FilesBody.module.css`.
|
|
@@ -8006,26 +8389,32 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
8006
8389
|
})]
|
|
8007
8390
|
});
|
|
8008
8391
|
}
|
|
8009
|
-
if (entry.type === "file")
|
|
8010
|
-
|
|
8011
|
-
|
|
8012
|
-
|
|
8013
|
-
|
|
8014
|
-
|
|
8015
|
-
|
|
8016
|
-
|
|
8017
|
-
|
|
8018
|
-
|
|
8019
|
-
|
|
8020
|
-
|
|
8021
|
-
|
|
8022
|
-
|
|
8023
|
-
|
|
8024
|
-
|
|
8025
|
-
|
|
8026
|
-
|
|
8027
|
-
|
|
8028
|
-
|
|
8392
|
+
if (entry.type === "file") {
|
|
8393
|
+
const current = tree.current === path;
|
|
8394
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", {
|
|
8395
|
+
className: css$1.item,
|
|
8396
|
+
"data-files-entry": "file",
|
|
8397
|
+
"data-files-path": path,
|
|
8398
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
8399
|
+
type: "button",
|
|
8400
|
+
className: css$1.row,
|
|
8401
|
+
onClick: () => {
|
|
8402
|
+
tree.onOpen(path);
|
|
8403
|
+
},
|
|
8404
|
+
ref: current ? tree.currentRef : void 0,
|
|
8405
|
+
"data-files-current": current ? "" : void 0,
|
|
8406
|
+
"aria-current": current ? true : void 0,
|
|
8407
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(FileTypeIcon, {
|
|
8408
|
+
kind: classifyFileType(entry.name),
|
|
8409
|
+
size: 16,
|
|
8410
|
+
className: css$1.fileIcon
|
|
8411
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
8412
|
+
className: css$1.name,
|
|
8413
|
+
children: entry.name
|
|
8414
|
+
})]
|
|
8415
|
+
})
|
|
8416
|
+
});
|
|
8417
|
+
}
|
|
8029
8418
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", {
|
|
8030
8419
|
className: css$1.item,
|
|
8031
8420
|
"data-files-entry": "other",
|
|
@@ -8085,46 +8474,181 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
8085
8474
|
* has opened under it — the upstream body's scroll, seed, and render logic
|
|
8086
8475
|
* verbatim, with the root arriving as a prop instead of `useSessions(cwd)`.
|
|
8087
8476
|
*/
|
|
8088
|
-
function FilesTree({ tabId, sessionId, root, signal, useStore, actions,
|
|
8477
|
+
function FilesTree({ tabId, sessionId, root, signal, useStore, actions, hold, release, toggle, onOpen, t, nativeTree, reveal, saved, onScrollOffset }) {
|
|
8089
8478
|
const state = useStore((store) => store.byTab[tabId]);
|
|
8090
8479
|
const bodyRef = (0, react.useRef)(null);
|
|
8091
8480
|
const scrollTopRef = (0, react.useRef)(0);
|
|
8481
|
+
const [current, setCurrent] = (0, react.useState)(void 0);
|
|
8482
|
+
const walk = (0, react.useRef)({
|
|
8483
|
+
key: void 0,
|
|
8484
|
+
settled: false,
|
|
8485
|
+
canceled: false
|
|
8486
|
+
});
|
|
8487
|
+
const revealSeed = (0, react.useRef)(saved?.revealed);
|
|
8488
|
+
const seededKey = (0, react.useRef)(void 0);
|
|
8489
|
+
const wantScroll = (0, react.useRef)(false);
|
|
8490
|
+
const currentRef = (0, react.useCallback)((row) => {
|
|
8491
|
+
if (row instanceof HTMLElement && wantScroll.current) {
|
|
8492
|
+
wantScroll.current = false;
|
|
8493
|
+
row.scrollIntoView({
|
|
8494
|
+
block: "nearest",
|
|
8495
|
+
inline: "nearest"
|
|
8496
|
+
});
|
|
8497
|
+
}
|
|
8498
|
+
}, []);
|
|
8499
|
+
const revealPath = reveal?.path;
|
|
8500
|
+
const revealSaved = saved?.revealed;
|
|
8501
|
+
const revealOnSettled = reveal?.onSettled;
|
|
8502
|
+
const revealKey = revealPath === void 0 || state === void 0 ? void 0 : JSON.stringify([state.root, revealPath]);
|
|
8503
|
+
(0, react.useEffect)(() => {
|
|
8504
|
+
if (revealPath === void 0 || revealKey === void 0 || state === void 0 || signal.aborted) {
|
|
8505
|
+
seededKey.current = void 0;
|
|
8506
|
+
setCurrent(void 0);
|
|
8507
|
+
return;
|
|
8508
|
+
}
|
|
8509
|
+
if (walk.current.key !== revealKey || seededKey.current !== revealKey) {
|
|
8510
|
+
walk.current = {
|
|
8511
|
+
key: revealKey,
|
|
8512
|
+
settled: false,
|
|
8513
|
+
canceled: false
|
|
8514
|
+
};
|
|
8515
|
+
revealSeed.current = revealSaved;
|
|
8516
|
+
seededKey.current = revealKey;
|
|
8517
|
+
wantScroll.current = false;
|
|
8518
|
+
}
|
|
8519
|
+
const relative = relativeUnderRoot(revealPath, state.root);
|
|
8520
|
+
if (relative === void 0) {
|
|
8521
|
+
setCurrent(void 0);
|
|
8522
|
+
return;
|
|
8523
|
+
}
|
|
8524
|
+
const location = locateFile(state, relative);
|
|
8525
|
+
if (walk.current.settled || walk.current.canceled || revealSeed.current === revealPath) {
|
|
8526
|
+
setCurrent(location?.current);
|
|
8527
|
+
return;
|
|
8528
|
+
}
|
|
8529
|
+
const next = location?.ancestors.find((ancestor) => !state.expanded.includes(ancestor.path));
|
|
8530
|
+
if (location !== void 0 && next !== void 0) {
|
|
8531
|
+
toggle(tabId, next.parent, next.path, state.expanded, signal);
|
|
8532
|
+
setCurrent(void 0);
|
|
8533
|
+
return;
|
|
8534
|
+
}
|
|
8535
|
+
if (location?.pending === true) return;
|
|
8536
|
+
walk.current.settled = true;
|
|
8537
|
+
revealSeed.current = void 0;
|
|
8538
|
+
revealOnSettled?.(revealPath);
|
|
8539
|
+
if (location?.current !== void 0) wantScroll.current = true;
|
|
8540
|
+
setCurrent(location?.current);
|
|
8541
|
+
}, [
|
|
8542
|
+
revealKey,
|
|
8543
|
+
state,
|
|
8544
|
+
tabId,
|
|
8545
|
+
signal,
|
|
8546
|
+
toggle,
|
|
8547
|
+
revealPath,
|
|
8548
|
+
revealOnSettled,
|
|
8549
|
+
revealSaved
|
|
8550
|
+
]);
|
|
8551
|
+
const pendingScroll = (0, react.useRef)(0);
|
|
8552
|
+
const stateRef = (0, react.useRef)(state);
|
|
8553
|
+
const savedRef = (0, react.useRef)(saved);
|
|
8554
|
+
(0, react.useLayoutEffect)(() => {
|
|
8555
|
+
stateRef.current = state;
|
|
8556
|
+
savedRef.current = saved;
|
|
8557
|
+
});
|
|
8092
8558
|
const seeded = state !== void 0;
|
|
8093
8559
|
(0, react.useLayoutEffect)(() => {
|
|
8094
8560
|
const body = bodyRef.current;
|
|
8095
8561
|
if (seeded && body !== null) {
|
|
8096
8562
|
body.scrollTop = state.scrollTop;
|
|
8097
8563
|
scrollTopRef.current = body.scrollTop;
|
|
8564
|
+
if ((body.parentElement?.querySelector("[data-files-body]"))?.contains(body.ownerDocument.activeElement) === true) body.focus();
|
|
8098
8565
|
}
|
|
8099
8566
|
}, [seeded]);
|
|
8567
|
+
(0, react.useLayoutEffect)(() => {
|
|
8568
|
+
const body = bodyRef.current;
|
|
8569
|
+
if (body === null || pendingScroll.current <= 0) return;
|
|
8570
|
+
const seededIdentity = seededKey.current === revealKey;
|
|
8571
|
+
const seed = seededIdentity ? revealSeed.current : revealSaved;
|
|
8572
|
+
const inFlight = revealPath !== void 0 && !(seededIdentity && (walk.current.settled || walk.current.canceled)) && seed !== revealPath;
|
|
8573
|
+
if (inFlight || body.scrollHeight > body.clientHeight) {
|
|
8574
|
+
if (!inFlight) {
|
|
8575
|
+
body.scrollTop = pendingScroll.current;
|
|
8576
|
+
scrollTopRef.current = body.scrollTop;
|
|
8577
|
+
}
|
|
8578
|
+
pendingScroll.current = 0;
|
|
8579
|
+
}
|
|
8580
|
+
});
|
|
8581
|
+
(0, react.useLayoutEffect)(() => {
|
|
8582
|
+
const body = bodyRef.current;
|
|
8583
|
+
if (body === null) return;
|
|
8584
|
+
const scroll = () => {
|
|
8585
|
+
if (body.scrollTop !== scrollTopRef.current && pendingScroll.current > 0) pendingScroll.current = 0;
|
|
8586
|
+
scrollTopRef.current = body.scrollTop;
|
|
8587
|
+
onScrollOffset?.(scrollTopRef.current);
|
|
8588
|
+
};
|
|
8589
|
+
body.addEventListener("scroll", scroll, { passive: true });
|
|
8590
|
+
return () => {
|
|
8591
|
+
body.removeEventListener("scroll", scroll);
|
|
8592
|
+
};
|
|
8593
|
+
}, [seeded, onScrollOffset]);
|
|
8100
8594
|
(0, react.useEffect)(() => () => {
|
|
8101
8595
|
if (seeded && !signal.aborted) actions.scrolled(tabId, scrollTopRef.current);
|
|
8596
|
+
onScrollOffset?.(scrollTopRef.current);
|
|
8102
8597
|
}, [
|
|
8103
8598
|
seeded,
|
|
8104
8599
|
signal,
|
|
8105
8600
|
tabId,
|
|
8106
|
-
actions
|
|
8601
|
+
actions,
|
|
8602
|
+
onScrollOffset
|
|
8107
8603
|
]);
|
|
8108
8604
|
(0, react.useEffect)(() => {
|
|
8109
|
-
if (
|
|
8110
|
-
|
|
8605
|
+
if (signal.aborted) return;
|
|
8606
|
+
const current = stateRef.current;
|
|
8607
|
+
const resumed = current !== void 0 && current.root === root;
|
|
8608
|
+
hold(tabId, root, signal, resumed ? current.expanded : void 0);
|
|
8609
|
+
if (!resumed) {
|
|
8610
|
+
const snapshot = savedRef.current;
|
|
8611
|
+
if (snapshot?.expanded !== void 0) for (const path of snapshot.expanded) {
|
|
8612
|
+
if (path === root || relativeUnderRoot(path, root) === void 0) continue;
|
|
8613
|
+
toggle(tabId, path.slice(0, path.lastIndexOf("/")), path, snapshot.expanded.filter((value) => value !== path), signal);
|
|
8614
|
+
}
|
|
8615
|
+
if (snapshot?.scrollTop !== void 0) pendingScroll.current = snapshot.scrollTop;
|
|
8616
|
+
}
|
|
8617
|
+
return () => {
|
|
8618
|
+
release(tabId);
|
|
8619
|
+
};
|
|
8111
8620
|
}, [
|
|
8112
|
-
state,
|
|
8113
8621
|
root,
|
|
8114
8622
|
tabId,
|
|
8115
8623
|
signal,
|
|
8116
|
-
|
|
8624
|
+
hold,
|
|
8625
|
+
release,
|
|
8626
|
+
toggle
|
|
8117
8627
|
]);
|
|
8118
8628
|
if (state === void 0) return null;
|
|
8119
8629
|
const tree = {
|
|
8120
8630
|
state,
|
|
8121
8631
|
onToggle: (parent, path) => {
|
|
8632
|
+
const seededIdentity = seededKey.current === revealKey;
|
|
8633
|
+
const seed = seededIdentity ? revealSeed.current : revealSaved;
|
|
8634
|
+
if (revealPath !== void 0 && revealKey !== void 0 && seed !== revealPath && state.expanded.includes(path) && (!seededIdentity || !walk.current.settled && !walk.current.canceled)) {
|
|
8635
|
+
if (!seededIdentity) walk.current = {
|
|
8636
|
+
key: revealKey,
|
|
8637
|
+
settled: false,
|
|
8638
|
+
canceled: true
|
|
8639
|
+
};
|
|
8640
|
+
else walk.current.canceled = true;
|
|
8641
|
+
revealSeed.current = void 0;
|
|
8642
|
+
revealOnSettled?.(revealPath);
|
|
8643
|
+
}
|
|
8122
8644
|
toggle(tabId, parent, path, state.expanded, signal);
|
|
8123
8645
|
},
|
|
8124
8646
|
onOpen: (path) => {
|
|
8125
8647
|
onOpen(fileAddressFor(sessionId, void 0, path));
|
|
8126
8648
|
},
|
|
8127
|
-
t
|
|
8649
|
+
t,
|
|
8650
|
+
current,
|
|
8651
|
+
currentRef
|
|
8128
8652
|
};
|
|
8129
8653
|
const legacy = /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
|
|
8130
8654
|
className: css$1.level,
|
|
@@ -8133,13 +8657,14 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
8133
8657
|
tree
|
|
8134
8658
|
})
|
|
8135
8659
|
});
|
|
8136
|
-
const content = nativeTree === void 0 ? legacy : nativeTree("sidebar.files.tree", nativeTreeInput(state, (failure) => failureLine(t, failure), tree.onToggle, tree.onOpen
|
|
8660
|
+
const content = nativeTree === void 0 ? legacy : nativeTree("sidebar.files.tree", nativeTreeInput(state, (failure) => failureLine(t, failure), tree.onToggle, tree.onOpen, current === void 0 ? void 0 : {
|
|
8661
|
+
path: current,
|
|
8662
|
+
ref: currentRef
|
|
8663
|
+
}), { fallback: legacy });
|
|
8137
8664
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
8138
8665
|
ref: bodyRef,
|
|
8139
8666
|
className: css$1.body,
|
|
8140
|
-
|
|
8141
|
-
scrollTopRef.current = event.currentTarget.scrollTop;
|
|
8142
|
-
},
|
|
8667
|
+
tabIndex: -1,
|
|
8143
8668
|
children: content
|
|
8144
8669
|
});
|
|
8145
8670
|
}
|
|
@@ -8219,9 +8744,10 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
8219
8744
|
/**
|
|
8220
8745
|
* The registration's `store`/`inject` declarations, built once in `apply`.
|
|
8221
8746
|
*
|
|
8222
|
-
* The inject face nests the
|
|
8223
|
-
* session-bound `openFile
|
|
8224
|
-
*
|
|
8747
|
+
* The inject face nests the files face under `face` and adds the
|
|
8748
|
+
* session-bound `openFile`; the fork's call sites stay upstream-shaped, the
|
|
8749
|
+
* one lifecycle delta being the hold/release pair the preview navigator needs
|
|
8750
|
+
* (see FORK.md).
|
|
8225
8751
|
* @param ctx - the client root context (the remote/sidebarRight lookups).
|
|
8226
8752
|
* @returns the store handle and the inject factory for `register`.
|
|
8227
8753
|
*/
|
|
@@ -8232,17 +8758,47 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
8232
8758
|
const inject = (sessionId, actions) => {
|
|
8233
8759
|
const selection = (0, _deepseek_ai_dsh_client_store.createSnapshotStore)({});
|
|
8234
8760
|
const takeovers = /* @__PURE__ */ new Map();
|
|
8235
|
-
const
|
|
8236
|
-
|
|
8237
|
-
|
|
8238
|
-
|
|
8239
|
-
takeovers.
|
|
8761
|
+
const savedTrees = /* @__PURE__ */ new Map();
|
|
8762
|
+
const watchers = /* @__PURE__ */ new Map();
|
|
8763
|
+
let watcherObserver;
|
|
8764
|
+
const endTakeover = (key, dropSaved = false) => {
|
|
8765
|
+
takeovers.get(key)?.controller.abort();
|
|
8766
|
+
takeovers.delete(key);
|
|
8767
|
+
if (dropSaved) savedTrees.delete(key);
|
|
8768
|
+
};
|
|
8769
|
+
const dropDeadOwners = () => {
|
|
8770
|
+
for (const [key, alive] of watchers) if (!alive()) {
|
|
8771
|
+
watchers.delete(key);
|
|
8772
|
+
endTakeover(key, true);
|
|
8773
|
+
}
|
|
8774
|
+
if (watchers.size === 0) {
|
|
8775
|
+
watcherObserver?.disconnect();
|
|
8776
|
+
watcherObserver = void 0;
|
|
8777
|
+
}
|
|
8778
|
+
};
|
|
8779
|
+
const armDeathWatch = (key, alive) => {
|
|
8780
|
+
watchers.set(key, alive);
|
|
8781
|
+
if (watcherObserver === void 0) {
|
|
8782
|
+
watcherObserver = new MutationObserver((mutations) => {
|
|
8783
|
+
if (mutations.some((mutation) => mutation.type === "attributes" || mutation.addedNodes.length !== 0 || mutation.removedNodes.length !== 0)) dropDeadOwners();
|
|
8784
|
+
});
|
|
8785
|
+
watcherObserver.observe(document, {
|
|
8786
|
+
childList: true,
|
|
8787
|
+
subtree: true,
|
|
8788
|
+
attributes: true,
|
|
8789
|
+
attributeFilter: ["data-dockkit-pane", "data-dockkit-float"]
|
|
8790
|
+
});
|
|
8791
|
+
}
|
|
8792
|
+
dropDeadOwners();
|
|
8240
8793
|
};
|
|
8241
8794
|
return {
|
|
8242
8795
|
face: face(sessionId, actions),
|
|
8243
8796
|
openFile: (address) => {
|
|
8244
8797
|
ctx.get("sidebarRight")?.openResourceIn?.(sessionId, address);
|
|
8245
8798
|
},
|
|
8799
|
+
openFileIn: (tabId, address) => {
|
|
8800
|
+
ctx.get("sidebarRight")?.openResourceIn?.(sessionId, address, { replaceTab: tabId });
|
|
8801
|
+
},
|
|
8246
8802
|
hooks: { treeSelection: selection },
|
|
8247
8803
|
selectRoot: (tabId, root) => {
|
|
8248
8804
|
const next = { ...selection.getSnapshot() };
|
|
@@ -8251,34 +8807,37 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
8251
8807
|
selection.set(next);
|
|
8252
8808
|
},
|
|
8253
8809
|
takeover: {
|
|
8254
|
-
signal: (
|
|
8255
|
-
let held = takeovers.get(
|
|
8810
|
+
signal: (key, root, section) => {
|
|
8811
|
+
let held = takeovers.get(key);
|
|
8256
8812
|
if (held === void 0 || held.root !== root || held.controller.signal.aborted) {
|
|
8257
|
-
held?.observer?.disconnect();
|
|
8258
8813
|
held?.controller.abort();
|
|
8259
8814
|
held = {
|
|
8260
8815
|
controller: new AbortController(),
|
|
8261
|
-
root
|
|
8262
|
-
section: void 0,
|
|
8263
|
-
observer: void 0
|
|
8816
|
+
root
|
|
8264
8817
|
};
|
|
8265
|
-
takeovers.set(
|
|
8266
|
-
}
|
|
8267
|
-
if (section !== void 0 && held.section !== section) {
|
|
8268
|
-
held.section = section;
|
|
8269
|
-
held.observer?.disconnect();
|
|
8270
|
-
held.observer = new MutationObserver(() => {
|
|
8271
|
-
if (!section.isConnected) endTakeover(tabId);
|
|
8272
|
-
});
|
|
8273
|
-
held.observer.observe(document, {
|
|
8274
|
-
childList: true,
|
|
8275
|
-
subtree: true
|
|
8276
|
-
});
|
|
8818
|
+
takeovers.set(key, held);
|
|
8277
8819
|
}
|
|
8278
|
-
if (
|
|
8820
|
+
if (section !== void 0) armDeathWatch(key, typeof section === "function" ? section : () => section.isConnected);
|
|
8279
8821
|
return held.controller.signal;
|
|
8280
8822
|
},
|
|
8281
|
-
end:
|
|
8823
|
+
end: (key) => {
|
|
8824
|
+
endTakeover(key);
|
|
8825
|
+
},
|
|
8826
|
+
saved: (key, root) => savedTrees.get(key)?.get(root),
|
|
8827
|
+
save: (key, snapshot) => {
|
|
8828
|
+
if (!takeovers.has(key) && !watchers.has(key)) return;
|
|
8829
|
+
let perRoot = savedTrees.get(key);
|
|
8830
|
+
if (perRoot === void 0) savedTrees.set(key, perRoot = /* @__PURE__ */ new Map());
|
|
8831
|
+
const previous = perRoot.get(snapshot.root);
|
|
8832
|
+
const merged = { root: snapshot.root };
|
|
8833
|
+
const expanded = snapshot.expanded ?? previous?.expanded;
|
|
8834
|
+
const scrollTop = snapshot.scrollTop ?? previous?.scrollTop;
|
|
8835
|
+
const revealed = snapshot.revealed ?? previous?.revealed;
|
|
8836
|
+
if (expanded !== void 0) merged.expanded = expanded;
|
|
8837
|
+
if (scrollTop !== void 0) merged.scrollTop = scrollTop;
|
|
8838
|
+
if (revealed !== void 0) merged.revealed = revealed;
|
|
8839
|
+
perRoot.set(snapshot.root, merged);
|
|
8840
|
+
}
|
|
8282
8841
|
}
|
|
8283
8842
|
};
|
|
8284
8843
|
};
|
|
@@ -8298,14 +8857,15 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
8298
8857
|
*/
|
|
8299
8858
|
function createFilesTreeSurface(props, t) {
|
|
8300
8859
|
if (!filesTreeSurfaceSupported()) return void 0;
|
|
8301
|
-
const { sessionId, useStore, actions, face, openFile, useTreeSelection, selectRoot, takeover } = props;
|
|
8302
|
-
if (face === void 0 || openFile === void 0 || useStore === void 0 || actions === void 0 || useTreeSelection === void 0 || selectRoot === void 0 || takeover === void 0) return void 0;
|
|
8860
|
+
const { sessionId, useStore, actions, face, openFile, openFileIn, useTreeSelection, selectRoot, takeover } = props;
|
|
8861
|
+
if (face === void 0 || openFile === void 0 || openFileIn === void 0 || useStore === void 0 || actions === void 0 || useTreeSelection === void 0 || selectRoot === void 0 || takeover === void 0) return void 0;
|
|
8303
8862
|
return {
|
|
8304
8863
|
sessionId,
|
|
8305
8864
|
useStore,
|
|
8306
8865
|
actions,
|
|
8307
8866
|
face,
|
|
8308
8867
|
openFile,
|
|
8868
|
+
openFileIn,
|
|
8309
8869
|
useTreeSelection,
|
|
8310
8870
|
selectRoot,
|
|
8311
8871
|
takeover,
|
|
@@ -9394,12 +9954,14 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
9394
9954
|
accessChosen
|
|
9395
9955
|
]);
|
|
9396
9956
|
/**
|
|
9397
|
-
* Re-register one
|
|
9398
|
-
*
|
|
9399
|
-
*
|
|
9957
|
+
* Re-register one MISSING root in place. The request carries the stored
|
|
9958
|
+
* path and deliberately NO access: `registry.add` treats the call as a
|
|
9959
|
+
* revive — it keeps the record's access, alias, and identity instead of
|
|
9400
9960
|
* creating a new registration — which is also why the root limit must not
|
|
9401
|
-
* gate this button: a revive consumes no slot
|
|
9402
|
-
*
|
|
9961
|
+
* gate this button: a revive consumes no slot. Only `missing` rows get it:
|
|
9962
|
+
* a `redirected` registration now names a different directory, and revive
|
|
9963
|
+
* never changes the grant's target — replacing it is the explicit
|
|
9964
|
+
* remove-then-add flow the redirected hint describes (ADR-0027).
|
|
9403
9965
|
*/
|
|
9404
9966
|
const reRegister = (0, react.useCallback)(async (root) => {
|
|
9405
9967
|
await mutate("add", { path: root.path });
|
|
@@ -9627,14 +10189,18 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
9627
10189
|
className: "mrfw-stateWarn",
|
|
9628
10190
|
children: t(stateKey(root.state))
|
|
9629
10191
|
}),
|
|
9630
|
-
root.state === "
|
|
10192
|
+
root.state === "missing" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(IconButton, {
|
|
9631
10193
|
pending: state.busy,
|
|
9632
10194
|
onClick: () => {
|
|
9633
10195
|
reRegister(root);
|
|
9634
10196
|
},
|
|
9635
10197
|
icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(IconRefresh, {}),
|
|
9636
10198
|
label: t("panel.reRegister")
|
|
9637
|
-
}),
|
|
10199
|
+
}) : null,
|
|
10200
|
+
root.state === "redirected" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
10201
|
+
className: "mrfw-note",
|
|
10202
|
+
children: t("panel.redirectedHint")
|
|
10203
|
+
}) : null,
|
|
9638
10204
|
root.skillsEnabled === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(IconButton, {
|
|
9639
10205
|
pending: state.busy,
|
|
9640
10206
|
pressed: root.skillsEnabled,
|
|
@@ -10191,7 +10757,8 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
10191
10757
|
signal: takeover.signal,
|
|
10192
10758
|
useStore: filesTree.useStore,
|
|
10193
10759
|
actions: filesTree.actions,
|
|
10194
|
-
|
|
10760
|
+
hold: filesTree.face.hold,
|
|
10761
|
+
release: filesTree.face.release,
|
|
10195
10762
|
toggle: filesTree.face.toggle,
|
|
10196
10763
|
onOpen: filesTree.openFile,
|
|
10197
10764
|
t: filesTree.t,
|
|
@@ -10211,9 +10778,293 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
10211
10778
|
] });
|
|
10212
10779
|
}
|
|
10213
10780
|
//#endregion
|
|
10781
|
+
//#region src/client/files-tree/navigator.tsx
|
|
10782
|
+
/**
|
|
10783
|
+
* The document-preview navigator's additional-root retarget.
|
|
10784
|
+
*
|
|
10785
|
+
* Upstream's `sidebar.files.navigator` Factory (`ui-sidebar-files`, added at
|
|
10786
|
+
* `0.2.2-alpha.3`) is the Folder-toggle tree the document preview draws
|
|
10787
|
+
* beside its body. Its root is hard-wired to the session's `cwd` — the
|
|
10788
|
+
* previewed file's absolute path is located against that root, so a file
|
|
10789
|
+
* that lives under an additional root lands outside every tree it can draw:
|
|
10790
|
+
* the navigator opens on the primary root, unexpanded and unmarked.
|
|
10791
|
+
*
|
|
10792
|
+
* The Factory itself cannot be retargeted — it is a registration upstream
|
|
10793
|
+
* already owns, and its root is not an input. What CAN move is what its
|
|
10794
|
+
* `aside[data-files-navigator]` draws: this occupant mounts in the preview
|
|
10795
|
+
* header's `sidebar.right.tab.document.actions` seat (owner prop
|
|
10796
|
+
* `absolutePath` — the previewed file's host path), watches the preview's
|
|
10797
|
+
* `[data-textpreview-content]` row for the aside, and when the file sits
|
|
10798
|
+
* under an available additional root it lends the aside's body area to the
|
|
10799
|
+
* forked tree, the same takeover the Files page's root switcher performs.
|
|
10800
|
+
* The aside itself — its width, its push/overlay/fill modes, its resize
|
|
10801
|
+
* separator — stays native; only the tree inside is the fork's.
|
|
10802
|
+
*
|
|
10803
|
+
* Everything is keyed by the dockkit PANE id (`data-files-navigator` on the
|
|
10804
|
+
* aside, `data-dockkit-pane` on the content section), never by the preview
|
|
10805
|
+
* tab: upstream's navigator records live in `store.byPane`, and the pane
|
|
10806
|
+
* survives what the tab does not — `replaceTab` mints a fresh tab id, so
|
|
10807
|
+
* opening a file from the retargeted tree destroys the preview tab that
|
|
10808
|
+
* opened it. The inject-side takeover map holds the pane-record memory
|
|
10809
|
+
* (`expanded`/`scrollTop`/`revealed`) across that death; the component's
|
|
10810
|
+
* own refs only ever carry the live copy.
|
|
10811
|
+
*
|
|
10812
|
+
* Three upstream behaviors are reproduced, not approximated:
|
|
10813
|
+
*
|
|
10814
|
+
* - **Hold lifetime.** The native navigator holds its pane tree only while
|
|
10815
|
+
* it is open (`hold(paneId, cwd)` gated on `state.open`): closing the
|
|
10816
|
+
* navigator closes the directory node — watches and listings end — while
|
|
10817
|
+
* the pane record keeps `expanded`/`scrollTop`/`revealed` for the next
|
|
10818
|
+
* open. Here the takeover `signal` is requested when the aside appears
|
|
10819
|
+
* and ended when it leaves. The saved snapshot is written into the
|
|
10820
|
+
* inject-side map continuously — the bucket's expansion set mirrors into
|
|
10821
|
+
* it, the forked body reports its live scroll offset, and a settled
|
|
10822
|
+
* reveal records its file — so no cleanup ordering ever decides what is
|
|
10823
|
+
* remembered. A dockkit pane has no stable element (a non-empty pane's
|
|
10824
|
+
* id rides on its selected tab's host, which `replaceTab` swaps out), so
|
|
10825
|
+
* the owner's death is checked by query: the pane lives while some
|
|
10826
|
+
* connected element still carries its id, and the watch that drops the
|
|
10827
|
+
* memory outlives every `end` because the body's cleanup always wins
|
|
10828
|
+
* the unmount race.
|
|
10829
|
+
* - **Reveal.** The native navigator seeds `pane.revealing` with the drawn
|
|
10830
|
+
* file's address — unless `pane.revealed` already names it — and steps
|
|
10831
|
+
* the walk open (ancestors expanded one listing at a time, then the
|
|
10832
|
+
* file's row marked current and scrolled into view). The forked tree's
|
|
10833
|
+
* `reveal` prop drives the same sequence against the additional root,
|
|
10834
|
+
* with the saved `revealed` suppressing a repeat of a completed walk, so
|
|
10835
|
+
* collapsing a directory and reopening the navigator keeps the reader's
|
|
10836
|
+
* collapse exactly as upstream's pane record does.
|
|
10837
|
+
* - **Replace-tab open.** The native navigator's `onOpen` is
|
|
10838
|
+
* `tab.actions.openResource(address, { replaceTab: true })` — the opened
|
|
10839
|
+
* file takes the preview's own tab. The forked tree's opens route through
|
|
10840
|
+
* `openFileIn`, which spells the same intent to `openResourceIn` as
|
|
10841
|
+
* `{ replaceTab: tabId }` against the preview's dockkit tab id.
|
|
10842
|
+
*
|
|
10843
|
+
* Every step degrades silently: no `[data-textpreview-url]` ancestor, no
|
|
10844
|
+
* dockkit pane id, no `panel`, an address that is not a session file of
|
|
10845
|
+
* THIS session, a file outside every available additional root (the
|
|
10846
|
+
* primary root's own files included), or a fork that cannot render — each
|
|
10847
|
+
* leaves the native navigator completely untouched. Where the upstream
|
|
10848
|
+
* hooks do not exist (a runtime without this navigator), the marker span
|
|
10849
|
+
* is the only trace the occupant leaves, and it draws nothing.
|
|
10850
|
+
*
|
|
10851
|
+
* @module @dsh-electron/dsh-plugin-multi-root-workspace/client/files-tree/navigator
|
|
10852
|
+
*/
|
|
10853
|
+
/**
|
|
10854
|
+
* A dockkit pane has no stable element: a non-empty pane's id rides on its
|
|
10855
|
+
* SELECTED tab's host (`data-dockkit-pane`), an empty pane's on its
|
|
10856
|
+
* `TabPanel`, and a floating pane's host carries `data-dockkit-float` —
|
|
10857
|
+
* `replaceTab` swaps the host while the pane lives on. The owner is
|
|
10858
|
+
* therefore alive while some connected element still carries its id.
|
|
10859
|
+
* @param id - the dockkit pane id the aside announced.
|
|
10860
|
+
* @returns whether the document still contains a carrier of the id.
|
|
10861
|
+
*/
|
|
10862
|
+
function paneAlive(id) {
|
|
10863
|
+
for (const element of document.querySelectorAll("[data-dockkit-pane],[data-dockkit-float]")) if (element.dataset.dockkitPane === id || element.dataset.dockkitFloat === id) return true;
|
|
10864
|
+
return false;
|
|
10865
|
+
}
|
|
10866
|
+
/**
|
|
10867
|
+
* The marker that lives in the preview header and, while the file it names
|
|
10868
|
+
* sits under an additional root, lends the navigator aside to the forked
|
|
10869
|
+
* tree. It draws nothing of its own.
|
|
10870
|
+
*/
|
|
10871
|
+
function NavigatorRetarget(props) {
|
|
10872
|
+
const { absolutePath, sessionId, panel, filesTree } = props;
|
|
10873
|
+
const anchorRef = (0, react.useRef)(null);
|
|
10874
|
+
const sectionRef = (0, react.useRef)(null);
|
|
10875
|
+
const fallbackPaneId = (0, react.useRef)(void 0);
|
|
10876
|
+
const paneIdentified = (0, react.useRef)(false);
|
|
10877
|
+
const mounted = (0, react.useRef)(true);
|
|
10878
|
+
const [preview, setPreview] = (0, react.useState)(null);
|
|
10879
|
+
const [tabId, setTabId] = (0, react.useState)(void 0);
|
|
10880
|
+
const [aside, setAside] = (0, react.useState)(null);
|
|
10881
|
+
const [paneId, setPaneId] = (0, react.useState)(void 0);
|
|
10882
|
+
const [root, setRoot] = (0, react.useState)(void 0);
|
|
10883
|
+
const [resolvedPath, setResolvedPath] = (0, react.useState)(void 0);
|
|
10884
|
+
const [takeover, setTakeover] = (0, react.useState)(null);
|
|
10885
|
+
(0, react.useEffect)(() => {
|
|
10886
|
+
mounted.current = true;
|
|
10887
|
+
return () => {
|
|
10888
|
+
mounted.current = false;
|
|
10889
|
+
};
|
|
10890
|
+
}, []);
|
|
10891
|
+
(0, react.useEffect)(() => {
|
|
10892
|
+
const anchor = anchorRef.current;
|
|
10893
|
+
const previewEl = anchor?.closest("[data-textpreview-url]") ?? null;
|
|
10894
|
+
sectionRef.current = anchor?.closest("[data-dockkit-content]") ?? null;
|
|
10895
|
+
const content = sectionRef.current?.dataset.dockkitContent;
|
|
10896
|
+
setTabId(content !== void 0 && content !== "" ? content : fallbackPaneId.current ??= `mrfw-nav-${Math.random().toString(36).slice(2)}`);
|
|
10897
|
+
setPreview(previewEl);
|
|
10898
|
+
}, []);
|
|
10899
|
+
const asideRef = (0, react.useRef)(null);
|
|
10900
|
+
(0, react.useEffect)(() => {
|
|
10901
|
+
if (preview === null) {
|
|
10902
|
+
setAside(null);
|
|
10903
|
+
return;
|
|
10904
|
+
}
|
|
10905
|
+
const content = preview.querySelector("[data-textpreview-content]") ?? preview;
|
|
10906
|
+
const query = () => {
|
|
10907
|
+
const current = asideRef.current;
|
|
10908
|
+
if (current !== null && current.isConnected) return;
|
|
10909
|
+
const found = content.querySelector("aside[data-files-navigator]");
|
|
10910
|
+
if (found !== current) {
|
|
10911
|
+
asideRef.current = found;
|
|
10912
|
+
setAside(found);
|
|
10913
|
+
}
|
|
10914
|
+
};
|
|
10915
|
+
query();
|
|
10916
|
+
const observer = new MutationObserver(query);
|
|
10917
|
+
observer.observe(content, {
|
|
10918
|
+
childList: true,
|
|
10919
|
+
subtree: true
|
|
10920
|
+
});
|
|
10921
|
+
return () => {
|
|
10922
|
+
observer.disconnect();
|
|
10923
|
+
asideRef.current = null;
|
|
10924
|
+
};
|
|
10925
|
+
}, [preview]);
|
|
10926
|
+
(0, react.useEffect)(() => {
|
|
10927
|
+
if (aside === null) return;
|
|
10928
|
+
const read = () => {
|
|
10929
|
+
const identified = aside.dataset.filesNavigator ?? sectionRef.current?.dataset.dockkitPane;
|
|
10930
|
+
paneIdentified.current = identified !== void 0 && identified !== "";
|
|
10931
|
+
setPaneId(identified !== void 0 && identified !== "" ? identified : fallbackPaneId.current ??= `mrfw-nav-${Math.random().toString(36).slice(2)}`);
|
|
10932
|
+
};
|
|
10933
|
+
read();
|
|
10934
|
+
const observer = new MutationObserver(read);
|
|
10935
|
+
observer.observe(aside, {
|
|
10936
|
+
attributes: true,
|
|
10937
|
+
attributeFilter: ["data-files-navigator"]
|
|
10938
|
+
});
|
|
10939
|
+
const section = sectionRef.current;
|
|
10940
|
+
if (section !== null) observer.observe(section, {
|
|
10941
|
+
attributes: true,
|
|
10942
|
+
attributeFilter: ["data-dockkit-pane"]
|
|
10943
|
+
});
|
|
10944
|
+
return () => {
|
|
10945
|
+
observer.disconnect();
|
|
10946
|
+
};
|
|
10947
|
+
}, [aside]);
|
|
10948
|
+
(0, react.useEffect)(() => {
|
|
10949
|
+
if (preview === null || aside === null || sessionId === void 0 || sessionId === "" || panel === void 0) {
|
|
10950
|
+
setRoot(void 0);
|
|
10951
|
+
setResolvedPath(void 0);
|
|
10952
|
+
return;
|
|
10953
|
+
}
|
|
10954
|
+
const address = preview.dataset.textpreviewUrl === void 0 ? void 0 : parseFileAddress(preview.dataset.textpreviewUrl);
|
|
10955
|
+
if (address?.scope !== "session" || address.sessionId !== sessionId) {
|
|
10956
|
+
setRoot(void 0);
|
|
10957
|
+
setResolvedPath(void 0);
|
|
10958
|
+
return;
|
|
10959
|
+
}
|
|
10960
|
+
const path = absolutePath;
|
|
10961
|
+
const controller = new AbortController();
|
|
10962
|
+
panel.call("list", { sessionId }, controller.signal).then((view) => {
|
|
10963
|
+
if (controller.signal.aborted || !mounted.current) return;
|
|
10964
|
+
setRoot(rootForPath(view, path));
|
|
10965
|
+
setResolvedPath(path);
|
|
10966
|
+
}).catch(() => {
|
|
10967
|
+
if (controller.signal.aborted || !mounted.current) return;
|
|
10968
|
+
setRoot(void 0);
|
|
10969
|
+
setResolvedPath(path);
|
|
10970
|
+
});
|
|
10971
|
+
return () => {
|
|
10972
|
+
controller.abort();
|
|
10973
|
+
};
|
|
10974
|
+
}, [
|
|
10975
|
+
preview,
|
|
10976
|
+
aside,
|
|
10977
|
+
sessionId,
|
|
10978
|
+
panel,
|
|
10979
|
+
absolutePath
|
|
10980
|
+
]);
|
|
10981
|
+
const rootPath = root?.path;
|
|
10982
|
+
const takeoverCtl = filesTree.takeover;
|
|
10983
|
+
const bucket = filesTree.useStore((store) => paneId === void 0 ? void 0 : store.byTab[paneId]);
|
|
10984
|
+
(0, react.useEffect)(() => {
|
|
10985
|
+
if (bucket !== void 0 && takeover !== null && takeover.paneId === paneId && takeover.root === rootPath) takeoverCtl.save(takeover.paneId, {
|
|
10986
|
+
root: takeover.root,
|
|
10987
|
+
expanded: bucket.expanded
|
|
10988
|
+
});
|
|
10989
|
+
}, [
|
|
10990
|
+
bucket,
|
|
10991
|
+
takeover,
|
|
10992
|
+
paneId,
|
|
10993
|
+
rootPath,
|
|
10994
|
+
takeoverCtl
|
|
10995
|
+
]);
|
|
10996
|
+
const onScrollOffset = (0, react.useCallback)((offset) => {
|
|
10997
|
+
if (takeover !== null) takeoverCtl.save(takeover.paneId, {
|
|
10998
|
+
root: takeover.root,
|
|
10999
|
+
scrollTop: offset
|
|
11000
|
+
});
|
|
11001
|
+
}, [takeover, takeoverCtl]);
|
|
11002
|
+
const savedTree = takeover === null ? void 0 : takeoverCtl.saved(takeover.paneId, takeover.root);
|
|
11003
|
+
(0, react.useEffect)(() => {
|
|
11004
|
+
if (aside === null || rootPath === void 0 || paneId === void 0 || takeoverCtl === void 0) {
|
|
11005
|
+
setTakeover(null);
|
|
11006
|
+
return;
|
|
11007
|
+
}
|
|
11008
|
+
setTakeover({
|
|
11009
|
+
signal: takeoverCtl.signal(paneId, rootPath, paneIdentified.current ? () => paneAlive(paneId) : void 0),
|
|
11010
|
+
paneId,
|
|
11011
|
+
root: rootPath
|
|
11012
|
+
});
|
|
11013
|
+
aside.setAttribute("data-mrfw-navigator", "");
|
|
11014
|
+
const capturedPaneId = paneId;
|
|
11015
|
+
const identified = paneIdentified.current;
|
|
11016
|
+
return () => {
|
|
11017
|
+
aside.removeAttribute("data-mrfw-navigator");
|
|
11018
|
+
if (!identified) takeoverCtl.end(capturedPaneId);
|
|
11019
|
+
};
|
|
11020
|
+
}, [
|
|
11021
|
+
aside,
|
|
11022
|
+
rootPath,
|
|
11023
|
+
paneId,
|
|
11024
|
+
takeoverCtl
|
|
11025
|
+
]);
|
|
11026
|
+
const onRevealSettled = (0, react.useCallback)((path) => {
|
|
11027
|
+
if (takeover !== null) takeoverCtl.save(takeover.paneId, {
|
|
11028
|
+
root: takeover.root,
|
|
11029
|
+
revealed: path
|
|
11030
|
+
});
|
|
11031
|
+
}, [takeover, takeoverCtl]);
|
|
11032
|
+
const revealLive = takeover !== null && rootPath !== void 0 && resolvedPath === absolutePath && takeover.root === rootPath;
|
|
11033
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
11034
|
+
ref: anchorRef,
|
|
11035
|
+
"data-mrfw-navigator-anchor": true,
|
|
11036
|
+
style: { display: "none" }
|
|
11037
|
+
}), aside !== null && takeover !== null ? (0, react_dom.createPortal)(/* @__PURE__ */ (0, react_jsx_runtime.jsx)(FilesTree, {
|
|
11038
|
+
tabId: takeover.paneId,
|
|
11039
|
+
sessionId: filesTree.sessionId,
|
|
11040
|
+
root: takeover.root,
|
|
11041
|
+
signal: takeover.signal,
|
|
11042
|
+
useStore: filesTree.useStore,
|
|
11043
|
+
actions: filesTree.actions,
|
|
11044
|
+
hold: filesTree.face.hold,
|
|
11045
|
+
release: filesTree.face.release,
|
|
11046
|
+
toggle: filesTree.face.toggle,
|
|
11047
|
+
onOpen: (address) => {
|
|
11048
|
+
if (tabId === void 0) filesTree.openFile(address);
|
|
11049
|
+
else filesTree.openFileIn(tabId, address);
|
|
11050
|
+
},
|
|
11051
|
+
t: filesTree.t,
|
|
11052
|
+
nativeTree: props.nativeTree,
|
|
11053
|
+
onScrollOffset,
|
|
11054
|
+
reveal: revealLive ? {
|
|
11055
|
+
path: absolutePath,
|
|
11056
|
+
revealed: savedTree?.revealed,
|
|
11057
|
+
onSettled: onRevealSettled
|
|
11058
|
+
} : void 0,
|
|
11059
|
+
saved: savedTree
|
|
11060
|
+
}), aside) : null] });
|
|
11061
|
+
}
|
|
11062
|
+
//#endregion
|
|
10214
11063
|
//#region src/client/index.ts
|
|
10215
11064
|
/** The footer-action cell this plugin owns. */
|
|
10216
11065
|
const SLOT_ID = "multi-root-folders";
|
|
11066
|
+
/** The document-preview navigator retarget's occupant id. */
|
|
11067
|
+
const NAVIGATOR_SLOT_ID = "multi-root-navigator";
|
|
10217
11068
|
/** The id of the `<style>` element carrying this plugin's sheet. */
|
|
10218
11069
|
const STYLE_ELEMENT_ID = "multi-root-workspace-styles";
|
|
10219
11070
|
/** Services the client half needs before it may activate. */
|
|
@@ -10322,6 +11173,24 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
10322
11173
|
nativeTree: nativeTreeRenderer(props.renderFactorySlot)
|
|
10323
11174
|
});
|
|
10324
11175
|
}));
|
|
11176
|
+
ctx.slots.inject("sidebar.right.tab.document.actions", () => ctx.slots.register({
|
|
11177
|
+
name: "sidebar.right.tab.document.actions",
|
|
11178
|
+
id: NAVIGATOR_SLOT_ID,
|
|
11179
|
+
order: 50,
|
|
11180
|
+
store: filesTreeSeats.store,
|
|
11181
|
+
inject: filesTreeSeats.inject
|
|
11182
|
+
}, function DocumentActionsOccupant(props) {
|
|
11183
|
+
const absolutePath = props?.absolutePath;
|
|
11184
|
+
const filesTree = createFilesTreeSurface(props, filesTreeT);
|
|
11185
|
+
if (absolutePath === void 0 || filesTree === void 0) return null;
|
|
11186
|
+
return (0, react.createElement)(NavigatorRetarget, {
|
|
11187
|
+
absolutePath,
|
|
11188
|
+
sessionId: props?.sessionId,
|
|
11189
|
+
panel,
|
|
11190
|
+
filesTree,
|
|
11191
|
+
nativeTree: nativeTreeRenderer(props.renderFactorySlot)
|
|
11192
|
+
});
|
|
11193
|
+
}));
|
|
10325
11194
|
}
|
|
10326
11195
|
/**
|
|
10327
11196
|
* Read one optional service by name.
|