@yiln-dsh/dsh-plugin-file-explorer 0.8.1 → 0.9.0

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.
Files changed (3) hide show
  1. package/README.md +5 -2
  2. package/client.js +157 -3
  3. package/package.json +3 -1
package/README.md CHANGED
@@ -17,6 +17,9 @@ 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
@@ -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.0`.
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.0.tgz
63
66
  ```
64
67
 
65
68
  ### npm package
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/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.0",
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"