@yiln-dsh/dsh-plugin-file-explorer 0.8.1 → 0.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -4
- package/client.js +157 -3
- package/index.js +1 -1
- package/package.json +3 -1
package/README.md
CHANGED
|
@@ -17,13 +17,16 @@ A DSH `dsh.bundle` that contributes a workspace file explorer page to the
|
|
|
17
17
|
live text, so the render follows unsaved edits); image files open in a
|
|
18
18
|
viewer with fullscreen, 25%-400% zoom, wheel zoom, and drag-to-pan after
|
|
19
19
|
zooming.
|
|
20
|
+
- In a headless `dsh web` session, AI tool/file links route to the Files page
|
|
21
|
+
and open the workspace file in the same preview/editor dialog instead of
|
|
22
|
+
invoking a native desktop opener.
|
|
20
23
|
- **Git Graph** page: a vscode/le-git-graph style commit graph rendered from
|
|
21
24
|
the repository containing the current directory —
|
|
22
25
|
- colored lane graph with commit dots and merge curves (all branches or the
|
|
23
26
|
current branch, refreshable),
|
|
24
27
|
- pill decorations for branches / remotes / tags / HEAD,
|
|
25
28
|
- an "Uncommitted changes" row from `git status` (count + changed files vs
|
|
26
|
-
HEAD),
|
|
29
|
+
HEAD), listing each file inside an untracked directory individually,
|
|
27
30
|
- click a commit to expand its full message and changed-file list with
|
|
28
31
|
A/M/D/R status badges,
|
|
29
32
|
- click a file to open its diff patch (commit shows `git show`, working tree
|
|
@@ -42,7 +45,7 @@ A DSH `dsh.bundle` that contributes a workspace file explorer page to the
|
|
|
42
45
|
|
|
43
46
|
## Install
|
|
44
47
|
|
|
45
|
-
The package version is `@yiln-dsh/dsh-plugin-file-explorer@0.
|
|
48
|
+
The package version is `@yiln-dsh/dsh-plugin-file-explorer@0.9.1`.
|
|
46
49
|
|
|
47
50
|
The right-panel package must be installed in the same `web` profile:
|
|
48
51
|
|
|
@@ -59,7 +62,7 @@ pnpm pack
|
|
|
59
62
|
```
|
|
60
63
|
|
|
61
64
|
```bash
|
|
62
|
-
dsh plugin --profile web add ./yiln-dsh-dsh-plugin-file-explorer-0.
|
|
65
|
+
dsh plugin --profile web add ./yiln-dsh-dsh-plugin-file-explorer-0.9.1.tgz
|
|
63
66
|
```
|
|
64
67
|
|
|
65
68
|
### npm package
|
|
@@ -109,7 +112,8 @@ apply the new profile composition.
|
|
|
109
112
|
workspace root), then run `git log --all --date-order` (default 300 commits,
|
|
110
113
|
`req.all === false` for the current branch only), `git diff-tree -m
|
|
111
114
|
--first-parent` for commit file lists, `git show` / `git diff HEAD` for
|
|
112
|
-
patches, and `git status --porcelain=v1 -b` for the working tree
|
|
115
|
+
patches, and `git status --porcelain=v1 -b -uall` for the working tree, so
|
|
116
|
+
every file inside an untracked directory is returned individually. Commit
|
|
113
117
|
hashes are validated against `^[0-9a-fA-F]{6,40}$` (or the `WORKING`
|
|
114
118
|
sentinel) and file paths against a non-option, non-control-character check.
|
|
115
119
|
- Rows have a fixed 34px height, so the hover action swap never changes the
|
package/client.js
CHANGED
|
@@ -19,7 +19,7 @@ window.__ModuleLoader__.load({
|
|
|
19
19
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
20
20
|
let React = require("react");
|
|
21
21
|
|
|
22
|
-
const inject = ["slots", "rightPanel"];
|
|
22
|
+
const inject = ["slots", "rightPanel", "remote", "remote.session"];
|
|
23
23
|
|
|
24
24
|
const LOCALE_NS = "file-explorer";
|
|
25
25
|
const ZH_DICT = {
|
|
@@ -206,11 +206,140 @@ window.__ModuleLoader__.load({
|
|
|
206
206
|
return template.replace(/\{(\w+)\}/g, (match, name) => name in params ? String(params[name]) : match);
|
|
207
207
|
}
|
|
208
208
|
|
|
209
|
+
function createFileExplorerController(rightPanel) {
|
|
210
|
+
let nextRequestId = 0;
|
|
211
|
+
let request = null;
|
|
212
|
+
const listeners = new Set();
|
|
213
|
+
const notify = () => {
|
|
214
|
+
for (const listener of listeners) listener();
|
|
215
|
+
};
|
|
216
|
+
return {
|
|
217
|
+
open(path) {
|
|
218
|
+
if (typeof path !== "string" || path.trim() === "") return false;
|
|
219
|
+
request = { id: ++nextRequestId, path };
|
|
220
|
+
rightPanel.open("file-explorer.files");
|
|
221
|
+
notify();
|
|
222
|
+
return true;
|
|
223
|
+
},
|
|
224
|
+
getSnapshot: () => request,
|
|
225
|
+
consume(id) {
|
|
226
|
+
if (request?.id !== id) return;
|
|
227
|
+
request = null;
|
|
228
|
+
notify();
|
|
229
|
+
},
|
|
230
|
+
subscribe(listener) {
|
|
231
|
+
listeners.add(listener);
|
|
232
|
+
return () => listeners.delete(listener);
|
|
233
|
+
},
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function parentPathOf(path) {
|
|
238
|
+
const cleaned = String(path).replace(/[/\\]+$/u, "");
|
|
239
|
+
if (cleaned === "" || /^[A-Za-z]:$/u.test(cleaned)) return null;
|
|
240
|
+
const slash = Math.max(cleaned.lastIndexOf("/"), cleaned.lastIndexOf("\\"));
|
|
241
|
+
if (slash < 0) return null;
|
|
242
|
+
if (slash === 0) return cleaned[0] === "/" ? "/" : null;
|
|
243
|
+
return cleaned.slice(0, slash);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function installWorkspaceOpenFallback(remote, controller) {
|
|
247
|
+
const session = remote?.session;
|
|
248
|
+
const originalOpen = session?.openWorkspacePath;
|
|
249
|
+
const originalCan = session?.canOpenWorkspacePath;
|
|
250
|
+
if (typeof originalOpen !== "function" || typeof controller?.open !== "function") return () => {};
|
|
251
|
+
|
|
252
|
+
const originalOpenDescriptor = Object.getOwnPropertyDescriptor(session, "openWorkspacePath");
|
|
253
|
+
const originalCanDescriptor = Object.getOwnPropertyDescriptor(session, "canOpenWorkspacePath");
|
|
254
|
+
const nativeOpen = originalOpen.bind(session);
|
|
255
|
+
const nativeCan = typeof originalCan === "function" ? originalCan.bind(session) : null;
|
|
256
|
+
const opened = () => ({ ok: true, value: { opened: true } });
|
|
257
|
+
const defineMethod = (name, value) => {
|
|
258
|
+
const descriptor = Object.getOwnPropertyDescriptor(session, name);
|
|
259
|
+
Object.defineProperty(session, name, {
|
|
260
|
+
configurable: true,
|
|
261
|
+
enumerable: descriptor?.enumerable ?? true,
|
|
262
|
+
writable: true,
|
|
263
|
+
value,
|
|
264
|
+
});
|
|
265
|
+
if (session[name] !== value) throw new Error(`file explorer could not install ${name} fallback`);
|
|
266
|
+
};
|
|
267
|
+
const restoreMethod = (name, descriptor) => {
|
|
268
|
+
try {
|
|
269
|
+
if (descriptor === undefined) delete session[name];
|
|
270
|
+
else Object.defineProperty(session, name, descriptor);
|
|
271
|
+
} catch (_error) {
|
|
272
|
+
// A newer owner may have replaced the Remote method during teardown.
|
|
273
|
+
}
|
|
274
|
+
};
|
|
275
|
+
const wrappedOpen = async (request, signal) => {
|
|
276
|
+
const path = request?.path;
|
|
277
|
+
if (typeof path !== "string" || path.length === 0) return nativeOpen(request, signal);
|
|
278
|
+
|
|
279
|
+
let nativeAvailable = nativeCan === null;
|
|
280
|
+
let nativeResult;
|
|
281
|
+
let nativeError;
|
|
282
|
+
if (nativeCan !== null) {
|
|
283
|
+
try {
|
|
284
|
+
const capability = await nativeCan();
|
|
285
|
+
nativeAvailable = capability?.ok === true && capability.value === true;
|
|
286
|
+
} catch (error) {
|
|
287
|
+
nativeError = error;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
if (nativeAvailable) {
|
|
291
|
+
try {
|
|
292
|
+
nativeResult = await nativeOpen(request, signal);
|
|
293
|
+
if (nativeResult?.ok === true) return nativeResult;
|
|
294
|
+
} catch (error) {
|
|
295
|
+
nativeError = error;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
if (controller.open(path)) return opened();
|
|
300
|
+
if (nativeResult !== undefined) return nativeResult;
|
|
301
|
+
if (nativeError !== undefined) throw nativeError;
|
|
302
|
+
return nativeOpen(request, signal);
|
|
303
|
+
};
|
|
304
|
+
|
|
305
|
+
try {
|
|
306
|
+
defineMethod("openWorkspacePath", wrappedOpen);
|
|
307
|
+
} catch (_error) {
|
|
308
|
+
return () => {};
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
let wrappedCan = null;
|
|
312
|
+
if (nativeCan !== null) {
|
|
313
|
+
wrappedCan = async (...args) => {
|
|
314
|
+
try {
|
|
315
|
+
const capability = await nativeCan(...args);
|
|
316
|
+
if (capability?.ok === true && capability.value === true) return capability;
|
|
317
|
+
} catch (_error) {
|
|
318
|
+
// The file explorer remains available when the native capability is absent.
|
|
319
|
+
}
|
|
320
|
+
return { ok: true, value: true };
|
|
321
|
+
};
|
|
322
|
+
try {
|
|
323
|
+
defineMethod("canOpenWorkspacePath", wrappedCan);
|
|
324
|
+
} catch (_error) {
|
|
325
|
+
wrappedCan = null;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
return () => {
|
|
330
|
+
if (session.openWorkspacePath === wrappedOpen) restoreMethod("openWorkspacePath", originalOpenDescriptor);
|
|
331
|
+
if (wrappedCan !== null && session.canOpenWorkspacePath === wrappedCan) restoreMethod("canOpenWorkspacePath", originalCanDescriptor);
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
|
|
209
335
|
function apply(ctx) {
|
|
210
336
|
const slots = ctx.get('slots')
|
|
211
337
|
const rightPanel = ctx.get('rightPanel')
|
|
212
338
|
if (slots === undefined || rightPanel === undefined) return
|
|
213
339
|
|
|
340
|
+
const fileExplorer = createFileExplorerController(rightPanel);
|
|
341
|
+
ctx.effect(() => installWorkspaceOpenFallback(ctx.get("remote"), fileExplorer), "file-explorer: workspace opener");
|
|
342
|
+
|
|
214
343
|
const locale = ctx.get("locale");
|
|
215
344
|
if (locale !== undefined) {
|
|
216
345
|
ctx.effect(() => locale.register(LOCALE_NS, { zh: ZH_DICT, en: EN_DICT }), "file-explorer: locale");
|
|
@@ -1773,6 +1902,7 @@ window.__ModuleLoader__.load({
|
|
|
1773
1902
|
const [pendingDelete, setPendingDelete] = React.useState(null)
|
|
1774
1903
|
const [mdView, setMdView] = React.useState('editor') // 'editor' | 'render' for markdown files
|
|
1775
1904
|
const [mdHtml, setMdHtml] = React.useState(null) // {html} | {error} while render view is active
|
|
1905
|
+
const openRequest = React.useSyncExternalStore(props.fileExplorer.subscribe, props.fileExplorer.getSnapshot, props.fileExplorer.getSnapshot)
|
|
1776
1906
|
|
|
1777
1907
|
React.useEffect(() => {
|
|
1778
1908
|
if (sessionId === undefined) {
|
|
@@ -1924,7 +2054,7 @@ window.__ModuleLoader__.load({
|
|
|
1924
2054
|
})
|
|
1925
2055
|
}
|
|
1926
2056
|
|
|
1927
|
-
const openPreview = (entry) => {
|
|
2057
|
+
const openPreview = (entry, fromWorkspaceOpen = false) => {
|
|
1928
2058
|
setPendingDelete(null)
|
|
1929
2059
|
resetImageView()
|
|
1930
2060
|
setPreviewLoading(true)
|
|
@@ -1936,14 +2066,36 @@ window.__ModuleLoader__.load({
|
|
|
1936
2066
|
setMdHtml(null)
|
|
1937
2067
|
api('read', { path: entry.path })
|
|
1938
2068
|
.then((raw) => {
|
|
1939
|
-
|
|
2069
|
+
if (raw && raw.ok === true) {
|
|
2070
|
+
setPreview(raw)
|
|
2071
|
+
return
|
|
2072
|
+
}
|
|
2073
|
+
if (fromWorkspaceOpen) {
|
|
2074
|
+
setPreview(null)
|
|
2075
|
+
setRequestPath(entry.path)
|
|
2076
|
+
return
|
|
2077
|
+
}
|
|
2078
|
+
setPreview({ ok: false, error: raw && typeof raw.error === 'string' ? raw.error : t('files.readFailed') })
|
|
1940
2079
|
})
|
|
1941
2080
|
.catch((err) => {
|
|
2081
|
+
if (fromWorkspaceOpen) {
|
|
2082
|
+
setPreview(null)
|
|
2083
|
+
setRequestPath(entry.path)
|
|
2084
|
+
return
|
|
2085
|
+
}
|
|
1942
2086
|
setPreview({ ok: false, error: err && typeof err.message === 'string' ? err.message : String(err) })
|
|
1943
2087
|
})
|
|
1944
2088
|
.finally(() => setPreviewLoading(false))
|
|
1945
2089
|
}
|
|
1946
2090
|
|
|
2091
|
+
React.useEffect(() => {
|
|
2092
|
+
if (openRequest === null) return
|
|
2093
|
+
props.fileExplorer.consume(openRequest.id)
|
|
2094
|
+
const parent = parentPathOf(openRequest.path)
|
|
2095
|
+
setRequestPath(parent === null ? openRequest.path : parent)
|
|
2096
|
+
openPreview({ path: openRequest.path }, true)
|
|
2097
|
+
}, [openRequest])
|
|
2098
|
+
|
|
1947
2099
|
// Render the markdown source to HTML only while the render view is
|
|
1948
2100
|
// active; re-renders when the (edited) text changes so the two
|
|
1949
2101
|
// views never drift apart.
|
|
@@ -2311,6 +2463,7 @@ window.__ModuleLoader__.load({
|
|
|
2311
2463
|
const disposeFilesSlot = slots.inject('right-panel.page', () => slots.register({
|
|
2312
2464
|
name: 'right-panel.page',
|
|
2313
2465
|
key: 'file-explorer.files',
|
|
2466
|
+
inject: () => ({ fileExplorer }),
|
|
2314
2467
|
}, FileExplorerPage))
|
|
2315
2468
|
const disposeGitSlot = slots.inject('right-panel.page', () => slots.register({
|
|
2316
2469
|
name: 'right-panel.page',
|
|
@@ -2327,6 +2480,7 @@ window.__ModuleLoader__.load({
|
|
|
2327
2480
|
|
|
2328
2481
|
exports.apply = apply;
|
|
2329
2482
|
exports.inject = inject;
|
|
2483
|
+
exports.installWorkspaceOpenFallback = installWorkspaceOpenFallback;
|
|
2330
2484
|
return module.exports;
|
|
2331
2485
|
}
|
|
2332
2486
|
});
|
package/index.js
CHANGED
|
@@ -653,7 +653,7 @@ export function apply(ctx) {
|
|
|
653
653
|
if (req.method === 'POST') req = await readJson(req)
|
|
654
654
|
try {
|
|
655
655
|
const { git, root } = await resolveRepo(req.path)
|
|
656
|
-
const status = await runGit(git, root, ['status', '--porcelain=v1', '-b'], 1024 * 1024)
|
|
656
|
+
const status = await runGit(git, root, ['status', '--porcelain=v1', '-b', '-uall'], 1024 * 1024)
|
|
657
657
|
if (status.code !== 0) {
|
|
658
658
|
sendJson(res, 200, { ok: false, error: cleanGitError(status.stderr, 'git status failed') })
|
|
659
659
|
return
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yiln-dsh/dsh-plugin-file-explorer",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.1",
|
|
4
4
|
"publishConfig": {
|
|
5
5
|
"access": "public"
|
|
6
6
|
},
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
"client": {
|
|
20
20
|
"inject": [
|
|
21
21
|
"@yiln-dsh/dsh-plugin-right-panel",
|
|
22
|
+
"@deepseek-ai/dsh-api-remotes",
|
|
22
23
|
"@deepseek-ai/dsh-client-ui-layout",
|
|
23
24
|
"@deepseek-ai/dsh-client-ui-renderer"
|
|
24
25
|
],
|
|
@@ -33,6 +34,7 @@
|
|
|
33
34
|
],
|
|
34
35
|
"peerDependencies": {
|
|
35
36
|
"@deepseek-ai/cordis": ">=4.0.0",
|
|
37
|
+
"@deepseek-ai/dsh-api-remotes": ">=0.1.2-rc.1",
|
|
36
38
|
"@deepseek-ai/dsh-client-ui-layout": ">=0.1.2-rc.1",
|
|
37
39
|
"@deepseek-ai/dsh-client-ui-renderer": ">=0.1.2-rc.1",
|
|
38
40
|
"@yiln-dsh/dsh-plugin-right-panel": ">=0.1.0"
|