@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 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.8.1`.
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.8.1.tgz
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. Commit
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
- setPreview(raw && raw.ok === true ? raw : { ok: false, error: raw && typeof raw.error === 'string' ? raw.error : t('files.readFailed') })
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.8.1",
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"