@dsh-electron/dsh-plugin-multi-root-workspace 0.1.15 → 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 +13 -1
- package/CHANGELOG.md +13 -1
- package/README.en.md +6 -6
- package/README.md +6 -6
- package/lib/auto-review.js +1 -1
- package/lib/client.js +761 -74
- package/lib/client.js.map +1 -1
- package/lib/{command-y5tfcAeC.js → command-DaVJ-mK3.js} +55 -1
- package/lib/command.js +1 -1
- package/lib/index.js +4 -4
- package/lib/{instructions-Ck9wp4J4.js → instructions-Ca80SkZq.js} +1 -1
- package/lib/instructions.js +1 -1
- package/lib/lsp.js +1 -1
- package/lib/process-fence.js +95 -12
- package/lib/{registry-DyDNDSJZ.js → registry-BibiukWk.js} +1 -1
- package/lib/{registry-operation-C2ZV3Jhw.js → registry-operation-CIlmt6_c.js} +5 -1
- package/lib/registry.js +1 -1
- package/lib/{review-context-B-oHkU6a.js → review-context-OxO-fMWR.js} +1 -1
- package/lib/review-context.js +1 -1
- package/lib/{sandbox-CZElTNPz.js → sandbox-aobdyQGi.js} +6 -3
- package/lib/sandbox.js +1 -1
- package/lib/scope.js +91 -4
- package/lib/skills.js +1 -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 +2 -0
- package/lib/types/client/styles.d.ts +1 -1
- package/lib/types/compat/files-tree.d.ts +8 -1
- package/lib/types/compat/forge-entrypoints.d.ts +44 -0
- package/lib/types/compat/session-existence.d.ts +41 -0
- package/lib/types/registry-operation.d.ts +7 -0
- package/lib/types/roots.d.ts +3 -1
- package/lib/types/scope.d.ts +67 -1
- package/lib/workspace-files.js +1 -1
- package/package.json +1 -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);
|
|
@@ -5683,6 +5687,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
5683
5687
|
"error.unavailable": "无法连接到 dsh 主进程。",
|
|
5684
5688
|
"error.copy-failed": "无法写入剪贴板,请检查浏览器的剪贴板权限。",
|
|
5685
5689
|
"error.session-not-found": "找不到该会话;请先打开一个工作区会话。",
|
|
5690
|
+
"error.session-not-live": "该会话存在但未在本实例激活;它可能正被另一个 DSH 实例持有、仍在恢复,或格式待迁移。若另一实例持有它,请退出那个实例;也可以切换到同一工作区的活动会话;格式待迁移时请在该会话完成迁移。否则请稍后重试。",
|
|
5686
5691
|
"error.unsupported-path": "该路径是网络或设备路径,Windows ACL 隔离无法覆盖它。",
|
|
5687
5692
|
"error.protected-path": "该目录与 DSH 主目录或根登记存储目录重叠,不能登记。",
|
|
5688
5693
|
"error.protected-boundary-unknown": "暂时无法确定 DSH 主目录等受保护目录的位置;恢复之前不能新增登记。",
|
|
@@ -5790,6 +5795,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
5790
5795
|
"error.unavailable": "Could not reach the dsh host process.",
|
|
5791
5796
|
"error.copy-failed": "Could not write to the clipboard; check the browser's clipboard permission.",
|
|
5792
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.",
|
|
5793
5799
|
"error.unsupported-path": "That is a network or device path, which Windows ACL confinement cannot cover.",
|
|
5794
5800
|
"error.protected-path": "That directory overlaps the DSH home or the registry storage directories; it cannot be registered.",
|
|
5795
5801
|
"error.protected-boundary-unknown": "The protected DSH directories cannot be resolved right now; no folder can be registered until they can.",
|
|
@@ -6123,6 +6129,27 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
6123
6129
|
background: var(--dsw-alias-interactive-bg-hover);
|
|
6124
6130
|
}
|
|
6125
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
|
+
|
|
6126
6153
|
/* ── files-tree fork (verbatim rules, renamed classes) ─────────────────────
|
|
6127
6154
|
Copied from upstream's FilesBody.module.css (ui-sidebar-files @
|
|
6128
6155
|
0.2.2-alpha.2) with local names mapped to mrfw-ft* so the rules cannot
|
|
@@ -7747,6 +7774,10 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
7747
7774
|
/** Per tab, per absolute path: the listing generation a settlement must match; the latest request wins. */
|
|
7748
7775
|
const generations = /* @__PURE__ */ new Map();
|
|
7749
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();
|
|
7750
7781
|
const nextGeneration = (tabId, path) => {
|
|
7751
7782
|
const byPath = generations.get(tabId) ?? /* @__PURE__ */ new Map();
|
|
7752
7783
|
generations.set(tabId, byPath);
|
|
@@ -7765,6 +7796,39 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
7765
7796
|
return result.ok ? result.value : void 0;
|
|
7766
7797
|
});
|
|
7767
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
|
+
};
|
|
7768
7832
|
return {
|
|
7769
7833
|
refresh: (tabId) => {
|
|
7770
7834
|
roots.get(tabId)?.refreshTree();
|
|
@@ -7773,17 +7837,14 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
7773
7837
|
actions.autoRefresh(tabId, enabled);
|
|
7774
7838
|
roots.get(tabId)?.setAutomatic(enabled);
|
|
7775
7839
|
},
|
|
7776
|
-
start(tabId, root, signal) {
|
|
7777
|
-
|
|
7778
|
-
|
|
7779
|
-
|
|
7780
|
-
|
|
7781
|
-
|
|
7782
|
-
|
|
7783
|
-
|
|
7784
|
-
roots.set(tabId, new DirectoryNode(root, (path, lifetime) => load(tabId, path, lifetime), (path, lifetime) => watch(sessionId, path, lifetime), (path, error) => {
|
|
7785
|
-
if (!signal.aborted) actions.failed(tabId, path, new RemoteError("gateway/internal", error instanceof Error ? error.message : String(error), {}));
|
|
7786
|
-
}, 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);
|
|
7787
7848
|
},
|
|
7788
7849
|
load: (tabId, path, signal) => {
|
|
7789
7850
|
load(tabId, path, signal);
|
|
@@ -8032,6 +8093,10 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
8032
8093
|
function encodePath(path) {
|
|
8033
8094
|
return path.split("/").map(encodeSegment).join("/");
|
|
8034
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
|
+
}
|
|
8035
8100
|
/**
|
|
8036
8101
|
* Build the address of a file read through one Session.
|
|
8037
8102
|
* @param sessionId - the Session whose Host workspace resolves the path.
|
|
@@ -8043,6 +8108,44 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
8043
8108
|
return `${FILE_ADDRESS_PREFIX}session/${encodeSegment(sessionId)}/${encodePath(normalized)}`;
|
|
8044
8109
|
}
|
|
8045
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
|
+
/**
|
|
8046
8149
|
* Browser-safe Workspace path and display helpers.
|
|
8047
8150
|
* @module @deepseek-ai/dsh-util-workspace-path
|
|
8048
8151
|
*/
|
|
@@ -8077,6 +8180,110 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
8077
8180
|
return sessionFileAddress(sessionId, normalized);
|
|
8078
8181
|
}
|
|
8079
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
|
|
8080
8287
|
//#region src/client/files-tree/FilesBody.module.css.ts
|
|
8081
8288
|
/**
|
|
8082
8289
|
* Shim for the CSS-module virtual module `./FilesBody.module.css`.
|
|
@@ -8182,26 +8389,32 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
8182
8389
|
})]
|
|
8183
8390
|
});
|
|
8184
8391
|
}
|
|
8185
|
-
if (entry.type === "file")
|
|
8186
|
-
|
|
8187
|
-
|
|
8188
|
-
|
|
8189
|
-
|
|
8190
|
-
|
|
8191
|
-
|
|
8192
|
-
|
|
8193
|
-
|
|
8194
|
-
|
|
8195
|
-
|
|
8196
|
-
|
|
8197
|
-
|
|
8198
|
-
|
|
8199
|
-
|
|
8200
|
-
|
|
8201
|
-
|
|
8202
|
-
|
|
8203
|
-
|
|
8204
|
-
|
|
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
|
+
}
|
|
8205
8418
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", {
|
|
8206
8419
|
className: css$1.item,
|
|
8207
8420
|
"data-files-entry": "other",
|
|
@@ -8261,46 +8474,181 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
8261
8474
|
* has opened under it — the upstream body's scroll, seed, and render logic
|
|
8262
8475
|
* verbatim, with the root arriving as a prop instead of `useSessions(cwd)`.
|
|
8263
8476
|
*/
|
|
8264
|
-
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 }) {
|
|
8265
8478
|
const state = useStore((store) => store.byTab[tabId]);
|
|
8266
8479
|
const bodyRef = (0, react.useRef)(null);
|
|
8267
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
|
+
});
|
|
8268
8558
|
const seeded = state !== void 0;
|
|
8269
8559
|
(0, react.useLayoutEffect)(() => {
|
|
8270
8560
|
const body = bodyRef.current;
|
|
8271
8561
|
if (seeded && body !== null) {
|
|
8272
8562
|
body.scrollTop = state.scrollTop;
|
|
8273
8563
|
scrollTopRef.current = body.scrollTop;
|
|
8564
|
+
if ((body.parentElement?.querySelector("[data-files-body]"))?.contains(body.ownerDocument.activeElement) === true) body.focus();
|
|
8274
8565
|
}
|
|
8275
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]);
|
|
8276
8594
|
(0, react.useEffect)(() => () => {
|
|
8277
8595
|
if (seeded && !signal.aborted) actions.scrolled(tabId, scrollTopRef.current);
|
|
8596
|
+
onScrollOffset?.(scrollTopRef.current);
|
|
8278
8597
|
}, [
|
|
8279
8598
|
seeded,
|
|
8280
8599
|
signal,
|
|
8281
8600
|
tabId,
|
|
8282
|
-
actions
|
|
8601
|
+
actions,
|
|
8602
|
+
onScrollOffset
|
|
8283
8603
|
]);
|
|
8284
8604
|
(0, react.useEffect)(() => {
|
|
8285
|
-
if (
|
|
8286
|
-
|
|
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
|
+
};
|
|
8287
8620
|
}, [
|
|
8288
|
-
state,
|
|
8289
8621
|
root,
|
|
8290
8622
|
tabId,
|
|
8291
8623
|
signal,
|
|
8292
|
-
|
|
8624
|
+
hold,
|
|
8625
|
+
release,
|
|
8626
|
+
toggle
|
|
8293
8627
|
]);
|
|
8294
8628
|
if (state === void 0) return null;
|
|
8295
8629
|
const tree = {
|
|
8296
8630
|
state,
|
|
8297
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
|
+
}
|
|
8298
8644
|
toggle(tabId, parent, path, state.expanded, signal);
|
|
8299
8645
|
},
|
|
8300
8646
|
onOpen: (path) => {
|
|
8301
8647
|
onOpen(fileAddressFor(sessionId, void 0, path));
|
|
8302
8648
|
},
|
|
8303
|
-
t
|
|
8649
|
+
t,
|
|
8650
|
+
current,
|
|
8651
|
+
currentRef
|
|
8304
8652
|
};
|
|
8305
8653
|
const legacy = /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
|
|
8306
8654
|
className: css$1.level,
|
|
@@ -8309,13 +8657,14 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
8309
8657
|
tree
|
|
8310
8658
|
})
|
|
8311
8659
|
});
|
|
8312
|
-
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 });
|
|
8313
8664
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
8314
8665
|
ref: bodyRef,
|
|
8315
8666
|
className: css$1.body,
|
|
8316
|
-
|
|
8317
|
-
scrollTopRef.current = event.currentTarget.scrollTop;
|
|
8318
|
-
},
|
|
8667
|
+
tabIndex: -1,
|
|
8319
8668
|
children: content
|
|
8320
8669
|
});
|
|
8321
8670
|
}
|
|
@@ -8395,9 +8744,10 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
8395
8744
|
/**
|
|
8396
8745
|
* The registration's `store`/`inject` declarations, built once in `apply`.
|
|
8397
8746
|
*
|
|
8398
|
-
* The inject face nests the
|
|
8399
|
-
* session-bound `openFile
|
|
8400
|
-
*
|
|
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).
|
|
8401
8751
|
* @param ctx - the client root context (the remote/sidebarRight lookups).
|
|
8402
8752
|
* @returns the store handle and the inject factory for `register`.
|
|
8403
8753
|
*/
|
|
@@ -8408,17 +8758,47 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
8408
8758
|
const inject = (sessionId, actions) => {
|
|
8409
8759
|
const selection = (0, _deepseek_ai_dsh_client_store.createSnapshotStore)({});
|
|
8410
8760
|
const takeovers = /* @__PURE__ */ new Map();
|
|
8411
|
-
const
|
|
8412
|
-
|
|
8413
|
-
|
|
8414
|
-
|
|
8415
|
-
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();
|
|
8416
8793
|
};
|
|
8417
8794
|
return {
|
|
8418
8795
|
face: face(sessionId, actions),
|
|
8419
8796
|
openFile: (address) => {
|
|
8420
8797
|
ctx.get("sidebarRight")?.openResourceIn?.(sessionId, address);
|
|
8421
8798
|
},
|
|
8799
|
+
openFileIn: (tabId, address) => {
|
|
8800
|
+
ctx.get("sidebarRight")?.openResourceIn?.(sessionId, address, { replaceTab: tabId });
|
|
8801
|
+
},
|
|
8422
8802
|
hooks: { treeSelection: selection },
|
|
8423
8803
|
selectRoot: (tabId, root) => {
|
|
8424
8804
|
const next = { ...selection.getSnapshot() };
|
|
@@ -8427,34 +8807,37 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
8427
8807
|
selection.set(next);
|
|
8428
8808
|
},
|
|
8429
8809
|
takeover: {
|
|
8430
|
-
signal: (
|
|
8431
|
-
let held = takeovers.get(
|
|
8810
|
+
signal: (key, root, section) => {
|
|
8811
|
+
let held = takeovers.get(key);
|
|
8432
8812
|
if (held === void 0 || held.root !== root || held.controller.signal.aborted) {
|
|
8433
|
-
held?.observer?.disconnect();
|
|
8434
8813
|
held?.controller.abort();
|
|
8435
8814
|
held = {
|
|
8436
8815
|
controller: new AbortController(),
|
|
8437
|
-
root
|
|
8438
|
-
section: void 0,
|
|
8439
|
-
observer: void 0
|
|
8816
|
+
root
|
|
8440
8817
|
};
|
|
8441
|
-
takeovers.set(
|
|
8818
|
+
takeovers.set(key, held);
|
|
8442
8819
|
}
|
|
8443
|
-
if (section !== void 0
|
|
8444
|
-
held.section = section;
|
|
8445
|
-
held.observer?.disconnect();
|
|
8446
|
-
held.observer = new MutationObserver(() => {
|
|
8447
|
-
if (!section.isConnected) endTakeover(tabId);
|
|
8448
|
-
});
|
|
8449
|
-
held.observer.observe(document, {
|
|
8450
|
-
childList: true,
|
|
8451
|
-
subtree: true
|
|
8452
|
-
});
|
|
8453
|
-
}
|
|
8454
|
-
if (held.section !== void 0 && !held.section.isConnected) endTakeover(tabId);
|
|
8820
|
+
if (section !== void 0) armDeathWatch(key, typeof section === "function" ? section : () => section.isConnected);
|
|
8455
8821
|
return held.controller.signal;
|
|
8456
8822
|
},
|
|
8457
|
-
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
|
+
}
|
|
8458
8841
|
}
|
|
8459
8842
|
};
|
|
8460
8843
|
};
|
|
@@ -8474,14 +8857,15 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
8474
8857
|
*/
|
|
8475
8858
|
function createFilesTreeSurface(props, t) {
|
|
8476
8859
|
if (!filesTreeSurfaceSupported()) return void 0;
|
|
8477
|
-
const { sessionId, useStore, actions, face, openFile, useTreeSelection, selectRoot, takeover } = props;
|
|
8478
|
-
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;
|
|
8479
8862
|
return {
|
|
8480
8863
|
sessionId,
|
|
8481
8864
|
useStore,
|
|
8482
8865
|
actions,
|
|
8483
8866
|
face,
|
|
8484
8867
|
openFile,
|
|
8868
|
+
openFileIn,
|
|
8485
8869
|
useTreeSelection,
|
|
8486
8870
|
selectRoot,
|
|
8487
8871
|
takeover,
|
|
@@ -10373,7 +10757,8 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
10373
10757
|
signal: takeover.signal,
|
|
10374
10758
|
useStore: filesTree.useStore,
|
|
10375
10759
|
actions: filesTree.actions,
|
|
10376
|
-
|
|
10760
|
+
hold: filesTree.face.hold,
|
|
10761
|
+
release: filesTree.face.release,
|
|
10377
10762
|
toggle: filesTree.face.toggle,
|
|
10378
10763
|
onOpen: filesTree.openFile,
|
|
10379
10764
|
t: filesTree.t,
|
|
@@ -10393,9 +10778,293 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
10393
10778
|
] });
|
|
10394
10779
|
}
|
|
10395
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
|
|
10396
11063
|
//#region src/client/index.ts
|
|
10397
11064
|
/** The footer-action cell this plugin owns. */
|
|
10398
11065
|
const SLOT_ID = "multi-root-folders";
|
|
11066
|
+
/** The document-preview navigator retarget's occupant id. */
|
|
11067
|
+
const NAVIGATOR_SLOT_ID = "multi-root-navigator";
|
|
10399
11068
|
/** The id of the `<style>` element carrying this plugin's sheet. */
|
|
10400
11069
|
const STYLE_ELEMENT_ID = "multi-root-workspace-styles";
|
|
10401
11070
|
/** Services the client half needs before it may activate. */
|
|
@@ -10504,6 +11173,24 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
10504
11173
|
nativeTree: nativeTreeRenderer(props.renderFactorySlot)
|
|
10505
11174
|
});
|
|
10506
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
|
+
}));
|
|
10507
11194
|
}
|
|
10508
11195
|
/**
|
|
10509
11196
|
* Read one optional service by name.
|