@lijian-ui/dsh-file-manager 0.2.3 → 0.2.4

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 (96) hide show
  1. package/LICENSE +38 -38
  2. package/README.i18n.yaml +6 -6
  3. package/README.md +66 -66
  4. package/README.zh.md +66 -66
  5. package/cordis.patch.yml +13 -13
  6. package/lib/client.js +27305 -270
  7. package/lib/client.js.map +1 -1
  8. package/lib/index.js +474 -42
  9. package/lib/tsconfig.client.tsbuildinfo +1 -1
  10. package/lib/tsconfig.host.tsbuildinfo +1 -1
  11. package/lib/types/client/file-source.d.ts +10 -0
  12. package/lib/types/client/file-source.d.ts.map +1 -1
  13. package/lib/types/client/fileType.d.ts +19 -4
  14. package/lib/types/client/fileType.d.ts.map +1 -1
  15. package/lib/types/client/index.d.ts.map +1 -1
  16. package/lib/types/client/locales.d.ts +3 -0
  17. package/lib/types/client/locales.d.ts.map +1 -1
  18. package/lib/types/client/mount.d.ts +1 -1
  19. package/lib/types/client/mount.d.ts.map +1 -1
  20. package/lib/types/client/preview/PreviewPanel.d.ts +2 -1
  21. package/lib/types/client/preview/PreviewPanel.d.ts.map +1 -1
  22. package/lib/types/client/preview/PreviewToolbar.d.ts +3 -1
  23. package/lib/types/client/preview/PreviewToolbar.d.ts.map +1 -1
  24. package/lib/types/client/preview/content.d.ts +3 -1
  25. package/lib/types/client/preview/content.d.ts.map +1 -1
  26. package/lib/types/client/preview/office.d.ts +23 -0
  27. package/lib/types/client/preview/office.d.ts.map +1 -0
  28. package/lib/types/client/reference.d.ts +17 -0
  29. package/lib/types/client/reference.d.ts.map +1 -1
  30. package/lib/types/client/store.d.ts.map +1 -1
  31. package/lib/types/host/fs-service.d.ts.map +1 -1
  32. package/lib/types/host/office-preview.d.ts +13 -0
  33. package/lib/types/host/office-preview.d.ts.map +1 -0
  34. package/lib/types/host/routes.d.ts.map +1 -1
  35. package/lib/types/index.d.ts.map +1 -1
  36. package/package.json +19 -4
  37. package/src/client/FileManagerSettingsCard.tsx +117 -117
  38. package/src/client/PluginSettingsCard.tsx +337 -337
  39. package/src/client/chat/file-ref.ts +144 -144
  40. package/src/client/chat/mermaid-chat.tsx +102 -102
  41. package/src/client/chat/placeholder-hint.tsx +46 -46
  42. package/src/client/components/ExplorerPanel.tsx +534 -534
  43. package/src/client/components/FileIcon.tsx +46 -46
  44. package/src/client/components/ScmPanel.tsx +480 -480
  45. package/src/client/components/a11y.ts +20 -20
  46. package/src/client/components/icons.tsx +274 -274
  47. package/src/client/components/overlay.tsx +225 -225
  48. package/src/client/drag/DragFileInlay.tsx +89 -89
  49. package/src/client/drag/file-drag.ts +67 -67
  50. package/src/client/drag.ts +154 -154
  51. package/src/client/file-source.ts +34 -0
  52. package/src/client/fileType.ts +32 -5
  53. package/src/client/hooks/useResizableSplit.ts +91 -91
  54. package/src/client/hooks/useStore.ts +15 -15
  55. package/src/client/index.ts +42 -3
  56. package/src/client/layout.ts +523 -523
  57. package/src/client/locales.ts +6 -0
  58. package/src/client/maximize.ts +34 -34
  59. package/src/client/mount.tsx +2 -1
  60. package/src/client/persist.ts +194 -194
  61. package/src/client/picker/FilePickerModal.tsx +526 -526
  62. package/src/client/picker/file-picker.ts +86 -86
  63. package/src/client/preview/PreviewPanel.tsx +7 -1
  64. package/src/client/preview/PreviewTabs.tsx +159 -159
  65. package/src/client/preview/PreviewToolbar.tsx +3 -2
  66. package/src/client/preview/content.tsx +7 -2
  67. package/src/client/preview/markdown.ts +351 -351
  68. package/src/client/preview/mermaid.ts +274 -274
  69. package/src/client/preview/office.tsx +321 -0
  70. package/src/client/reference.ts +49 -0
  71. package/src/client/settings-card.module.css +316 -316
  72. package/src/client/settings-form.ts +451 -451
  73. package/src/client/store.ts +17 -12
  74. package/src/client/styles/drag.module.css +30 -30
  75. package/src/client/styles/explorer.module.css +371 -371
  76. package/src/client/styles/picker.module.css +344 -344
  77. package/src/client/styles/preview.module.css +3 -1
  78. package/src/client/styles/scm.module.css +380 -380
  79. package/src/client/styles/tokens.module.css +376 -371
  80. package/src/core/types.ts +162 -162
  81. package/src/host/fs-service.ts +7 -1
  82. package/src/host/gate.ts +73 -73
  83. package/src/host/git-runner.ts +116 -116
  84. package/src/host/git-service.ts +395 -395
  85. package/src/host/loopback.ts +63 -63
  86. package/src/host/office-preview.ts +399 -0
  87. package/src/host/poll-guard.ts +109 -109
  88. package/src/host/routes.ts +72 -1
  89. package/src/index.ts +3 -1
  90. package/src/mount-once.ts +48 -48
  91. package/lib/types/client/chat/AttachedFilesDock.d.ts +0 -51
  92. package/lib/types/client/chat/AttachedFilesDock.d.ts.map +0 -1
  93. package/lib/types/client/floating.d.ts +0 -27
  94. package/lib/types/client/floating.d.ts.map +0 -1
  95. package/lib/types/client/mention.d.ts +0 -44
  96. package/lib/types/client/mention.d.ts.map +0 -1
package/lib/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { Service } from "@deepseek-ai/cordis";
2
2
  import { mkdir, open, readFile, readdir, realpath, rename, rm, stat, writeFile } from "node:fs/promises";
3
3
  import { dirname, join, relative } from "node:path";
4
- import { createReadStream } from "node:fs";
4
+ import { createReadStream, existsSync, watch } from "node:fs";
5
5
  import { pipeline } from "node:stream/promises";
6
6
  import { fileURLToPath } from "node:url";
7
7
  //#region \0rolldown/runtime.js
@@ -224,7 +224,7 @@ function installSettingsSection(ctx, ns, schema, entry, hooks) {
224
224
  });
225
225
  }
226
226
  //#endregion
227
- //#region node_modules/cosmokit/lib/index.cjs
227
+ //#region ../../node_modules/cosmokit/lib/index.cjs
228
228
  var require_lib$1 = /* @__PURE__ */ __commonJSMin(((exports, module) => {
229
229
  var __defProp = Object.defineProperty;
230
230
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
@@ -1458,7 +1458,13 @@ function mimeByExtension(rel) {
1458
1458
  ico: "image/x-icon",
1459
1459
  avif: "image/avif",
1460
1460
  bmp: "image/bmp",
1461
- pdf: "application/pdf"
1461
+ pdf: "application/pdf",
1462
+ doc: "application/msword",
1463
+ docx: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
1464
+ xls: "application/vnd.ms-excel",
1465
+ xlsx: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
1466
+ ppt: "application/vnd.ms-powerpoint",
1467
+ pptx: "application/vnd.openxmlformats-officedocument.presentationml.presentation"
1462
1468
  }[rel.split(".").pop()?.toLowerCase() ?? ""];
1463
1469
  }
1464
1470
  /** Derive the mime type for a raw read from the extension, then the content. */
@@ -2488,11 +2494,11 @@ function isLoopbackRequest(request) {
2488
2494
  }
2489
2495
  //#endregion
2490
2496
  //#region src/host/routes.ts
2491
- const OK = (value) => ({
2497
+ const OK$1 = (value) => ({
2492
2498
  ok: true,
2493
2499
  value
2494
2500
  });
2495
- const FAIL = (error) => ({
2501
+ const FAIL$1 = (error) => ({
2496
2502
  ok: false,
2497
2503
  error
2498
2504
  });
@@ -2565,6 +2571,14 @@ const GIT_POLL_MS = 3e4;
2565
2571
  /** SSE keep-alive comment interval (proxies drop idle connections). */
2566
2572
  const HEARTBEAT_MS = 15e3;
2567
2573
  /**
2574
+ * Debounce window for coalescing fs.watch events into one `{kind:'fs'}`
2575
+ * push. LLM/ad-hoc file writes (e.g. an agent creating work products with a
2576
+ * subprocess) often land several events in quick succession; a single trailing
2577
+ * push per burst keeps the client's refresh amortized (client also coalesces
2578
+ * with its own FS_COALESCE_MS).
2579
+ */
2580
+ const FS_WATCH_DEBOUNCE_MS = 200;
2581
+ /**
2568
2582
  * Parse a `Range: bytes=start-end` header against the file size. RFC 7233
2569
2583
  * lets a server ignore any Range it does not support, so unknown units,
2570
2584
  * malformed headers and multi-range requests all return null (the caller
@@ -2675,7 +2689,7 @@ function strArray(payload, key) {
2675
2689
  return value;
2676
2690
  }
2677
2691
  /** Write one JSON envelope response. */
2678
- function json(res, envelope, status = 200) {
2692
+ function json$1(res, envelope, status = 200) {
2679
2693
  res.writeHead(status, { "content-type": "application/json; charset=utf-8" });
2680
2694
  res.end(JSON.stringify(envelope));
2681
2695
  }
@@ -2743,6 +2757,49 @@ function registerPanelRoutes(ctx, fs, git) {
2743
2757
  gitPoll.stop();
2744
2758
  gitPoll = void 0;
2745
2759
  };
2760
+ let fsWatcher;
2761
+ let fsWatchTimer;
2762
+ const pushFsChange = () => {
2763
+ if (fsWatchTimer !== void 0) return;
2764
+ fsWatchTimer = setTimeout(() => {
2765
+ fsWatchTimer = void 0;
2766
+ for (const subscriber of subscribers) push(subscriber, { kind: "fs" });
2767
+ }, FS_WATCH_DEBOUNCE_MS);
2768
+ };
2769
+ /** Start watching one canonical root; recursive watch falls back to non-recursive when unsupported. */
2770
+ const startFsWatch = () => {
2771
+ if (fsWatcher !== void 0) return;
2772
+ let watcher;
2773
+ try {
2774
+ watcher = watch(subscribersRoot(), { recursive: true });
2775
+ } catch (error) {
2776
+ ctx.logger.warn(`dsh-filemgr: recursive fs.watch unavailable (${String(error)}), falling back to shallow watch`);
2777
+ try {
2778
+ watcher = watch(subscribersRoot());
2779
+ } catch (fallbackError) {
2780
+ ctx.logger.warn(`dsh-filemgr: fs.watch failed, live directory changes will not auto-refresh (${String(fallbackError)})`);
2781
+ return;
2782
+ }
2783
+ }
2784
+ fsWatcher = watcher;
2785
+ const onEvent = () => pushFsChange();
2786
+ watcher.on("change", (_eventType, _filename) => onEvent());
2787
+ watcher.on("error", (error) => {
2788
+ ctx.logger.warn(`dsh-filemgr: fs.watch error (${String(error)}), live refresh degraded`);
2789
+ });
2790
+ ctx.logger.debug(`dsh-filemgr: watching ${subscribersRoot()} for live directory changes`);
2791
+ };
2792
+ const stopFsWatch = () => {
2793
+ if (fsWatchTimer !== void 0) {
2794
+ clearTimeout(fsWatchTimer);
2795
+ fsWatchTimer = void 0;
2796
+ }
2797
+ if (fsWatcher === void 0) return;
2798
+ fsWatcher.close();
2799
+ fsWatcher = void 0;
2800
+ };
2801
+ /** The canonical root of the single connected subscriber; '' when none. */
2802
+ const subscribersRoot = () => subscribers.size > 0 ? [...subscribers][0].root : "";
2746
2803
  /**
2747
2804
  * GET /filemgr/raw: stream one workspace file (markdown image srcs,
2748
2805
  * pdf preview). Gated like every other operation; FsService.readRaw only
@@ -2757,13 +2814,13 @@ function registerPanelRoutes(ctx, fs, git) {
2757
2814
  const root = url.searchParams.get("root");
2758
2815
  const path = url.searchParams.get("path");
2759
2816
  if (root === null || root === "" || path === null || path === "") {
2760
- json(res, FAIL(BAD_REQUEST), 400);
2817
+ json$1(res, FAIL$1(BAD_REQUEST), 400);
2761
2818
  return;
2762
2819
  }
2763
2820
  const result = await fs.readRaw(root, path);
2764
2821
  if (!("abs" in result)) {
2765
2822
  const status = result.code === "path-outside-root" || result.code === "is-directory" ? 403 : 404;
2766
- json(res, FAIL(result), status);
2823
+ json$1(res, FAIL$1(result), status);
2767
2824
  return;
2768
2825
  }
2769
2826
  const etag = `W/"${result.size}-${Math.floor(result.mtime)}"`;
@@ -2875,172 +2932,172 @@ function registerPanelRoutes(ctx, fs, git) {
2875
2932
  return;
2876
2933
  }
2877
2934
  if (!(req.headers["content-type"] ?? "").toLowerCase().startsWith("application/json")) {
2878
- json(res, FAIL(BAD_REQUEST), 415);
2935
+ json$1(res, FAIL$1(BAD_REQUEST), 415);
2879
2936
  return;
2880
2937
  }
2881
2938
  const pathname = new URL(req.url ?? "/", "http://x").pathname;
2882
2939
  const payload = await readJsonBody(req);
2883
2940
  if (payload === null) {
2884
- json(res, FAIL(BAD_REQUEST));
2941
+ json$1(res, FAIL$1(BAD_REQUEST));
2885
2942
  return;
2886
2943
  }
2887
2944
  const root = strField(payload, "root");
2888
2945
  if (root === null) {
2889
- json(res, FAIL(BAD_REQUEST));
2946
+ json$1(res, FAIL$1(BAD_REQUEST));
2890
2947
  return;
2891
2948
  }
2892
2949
  switch (pathname) {
2893
2950
  case "/filemgr/list": {
2894
2951
  const path = strField(payload, "path") ?? "";
2895
2952
  const result = await fs.list(root, path);
2896
- json(res, "entries" in result ? OK(result) : FAIL(result));
2953
+ json$1(res, "entries" in result ? OK$1(result) : FAIL$1(result));
2897
2954
  return;
2898
2955
  }
2899
2956
  case "/filemgr/read": {
2900
2957
  const path = strField(payload, "path");
2901
2958
  if (path === null) {
2902
- json(res, FAIL(BAD_REQUEST));
2959
+ json$1(res, FAIL$1(BAD_REQUEST));
2903
2960
  return;
2904
2961
  }
2905
2962
  const asImage = typeof payload === "object" && payload !== null ? payload.asImage === true : false;
2906
2963
  const result = await fs.read(root, path, asImage);
2907
- json(res, "content" in result ? OK(result) : FAIL(result));
2964
+ json$1(res, "content" in result ? OK$1(result) : FAIL$1(result));
2908
2965
  return;
2909
2966
  }
2910
2967
  case "/filemgr/write": {
2911
2968
  const path = strField(payload, "path");
2912
2969
  const content = strOrEmpty(payload, "content");
2913
2970
  if (path === null || content === null) {
2914
- json(res, FAIL(BAD_REQUEST));
2971
+ json$1(res, FAIL$1(BAD_REQUEST));
2915
2972
  return;
2916
2973
  }
2917
2974
  const rawBase = typeof payload === "object" && payload !== null ? payload.baseMtime : void 0;
2918
2975
  const baseMtime = typeof rawBase === "number" && Number.isFinite(rawBase) ? rawBase : void 0;
2919
2976
  const result = await fs.write(root, path, content, baseMtime);
2920
- json(res, "mtime" in result ? OK(result) : FAIL(result));
2977
+ json$1(res, "mtime" in result ? OK$1(result) : FAIL$1(result));
2921
2978
  return;
2922
2979
  }
2923
2980
  case "/filemgr/search": {
2924
2981
  const query = strField(payload, "query") ?? "";
2925
2982
  const result = await fs.search(root, query);
2926
- json(res, "hits" in result ? OK(result) : FAIL(result));
2983
+ json$1(res, "hits" in result ? OK$1(result) : FAIL$1(result));
2927
2984
  return;
2928
2985
  }
2929
2986
  case "/filemgr/delete": {
2930
2987
  const path = strField(payload, "path");
2931
2988
  if (path === null) {
2932
- json(res, FAIL(BAD_REQUEST));
2989
+ json$1(res, FAIL$1(BAD_REQUEST));
2933
2990
  return;
2934
2991
  }
2935
2992
  const result = await fs.delete(root, path);
2936
- json(res, "ok" in result ? OK(result) : FAIL(result));
2993
+ json$1(res, "ok" in result ? OK$1(result) : FAIL$1(result));
2937
2994
  return;
2938
2995
  }
2939
2996
  case "/filemgr/reveal": {
2940
2997
  const path = strField(payload, "path");
2941
2998
  if (path === null) {
2942
- json(res, FAIL(BAD_REQUEST));
2999
+ json$1(res, FAIL$1(BAD_REQUEST));
2943
3000
  return;
2944
3001
  }
2945
3002
  const resolved = await fs.resolveAbsolute(root, path);
2946
3003
  if (!("ok" in resolved)) {
2947
- json(res, FAIL(resolved));
3004
+ json$1(res, FAIL$1(resolved));
2948
3005
  return;
2949
3006
  }
2950
3007
  const error = spawnOsCommand(ctx, revealArgv(process.platform, resolved.abs));
2951
- json(res, error === null ? OK({ ok: true }) : FAIL(error));
3008
+ json$1(res, error === null ? OK$1({ ok: true }) : FAIL$1(error));
2952
3009
  return;
2953
3010
  }
2954
3011
  case "/filemgr/open-with-default": {
2955
3012
  const path = strField(payload, "path");
2956
3013
  if (path === null) {
2957
- json(res, FAIL(BAD_REQUEST));
3014
+ json$1(res, FAIL$1(BAD_REQUEST));
2958
3015
  return;
2959
3016
  }
2960
3017
  const resolved = await fs.resolveAbsolute(root, path);
2961
3018
  if (!("ok" in resolved)) {
2962
- json(res, FAIL(resolved));
3019
+ json$1(res, FAIL$1(resolved));
2963
3020
  return;
2964
3021
  }
2965
3022
  const error = spawnOsCommand(ctx, openArgv(process.platform, resolved.abs));
2966
- json(res, error === null ? OK({ ok: true }) : FAIL(error));
3023
+ json$1(res, error === null ? OK$1({ ok: true }) : FAIL$1(error));
2967
3024
  return;
2968
3025
  }
2969
3026
  case "/filemgr/rename": {
2970
3027
  const path = strField(payload, "path");
2971
3028
  const newName = strField(payload, "newName");
2972
3029
  if (path === null || newName === null) {
2973
- json(res, FAIL(BAD_REQUEST));
3030
+ json$1(res, FAIL$1(BAD_REQUEST));
2974
3031
  return;
2975
3032
  }
2976
3033
  const result = await fs.rename(root, path, newName);
2977
- json(res, "ok" in result ? OK(result) : FAIL(result));
3034
+ json$1(res, "ok" in result ? OK$1(result) : FAIL$1(result));
2978
3035
  return;
2979
3036
  }
2980
3037
  case "/filemgr/mkdir": {
2981
3038
  const path = strField(payload, "path");
2982
3039
  if (path === null) {
2983
- json(res, FAIL(BAD_REQUEST));
3040
+ json$1(res, FAIL$1(BAD_REQUEST));
2984
3041
  return;
2985
3042
  }
2986
3043
  const result = await fs.mkdir(root, path);
2987
- json(res, "ok" in result ? OK(result) : FAIL(result));
3044
+ json$1(res, "ok" in result ? OK$1(result) : FAIL$1(result));
2988
3045
  return;
2989
3046
  }
2990
3047
  case "/filemgr/new-file": {
2991
3048
  const path = strField(payload, "path");
2992
3049
  if (path === null) {
2993
- json(res, FAIL(BAD_REQUEST));
3050
+ json$1(res, FAIL$1(BAD_REQUEST));
2994
3051
  return;
2995
3052
  }
2996
3053
  const result = await fs.newFile(root, path);
2997
- json(res, "ok" in result ? OK(result) : FAIL(result));
3054
+ json$1(res, "ok" in result ? OK$1(result) : FAIL$1(result));
2998
3055
  return;
2999
3056
  }
3000
3057
  case "/filemgr/git-status": {
3001
3058
  const result = await git.status(root);
3002
- json(res, result === null ? OK(null) : "root" in result ? OK(result) : FAIL(result));
3059
+ json$1(res, result === null ? OK$1(null) : "root" in result ? OK$1(result) : FAIL$1(result));
3003
3060
  return;
3004
3061
  }
3005
3062
  case "/filemgr/git-diff": {
3006
3063
  const path = strField(payload, "path");
3007
3064
  if (path === null) {
3008
- json(res, FAIL(BAD_REQUEST));
3065
+ json$1(res, FAIL$1(BAD_REQUEST));
3009
3066
  return;
3010
3067
  }
3011
3068
  const staged = typeof payload === "object" && payload !== null ? payload.staged === true : false;
3012
3069
  const result = await git.diff(root, path, staged);
3013
- json(res, "content" in result ? OK(result) : FAIL(result));
3070
+ json$1(res, "content" in result ? OK$1(result) : FAIL$1(result));
3014
3071
  return;
3015
3072
  }
3016
3073
  case "/filemgr/git-stage": {
3017
3074
  const paths = strArray(payload, "paths");
3018
3075
  if (paths === null) {
3019
- json(res, FAIL(BAD_REQUEST));
3076
+ json$1(res, FAIL$1(BAD_REQUEST));
3020
3077
  return;
3021
3078
  }
3022
3079
  const result = await git.stage(root, paths);
3023
- json(res, "applied" in result ? OK(result) : FAIL(result));
3080
+ json$1(res, "applied" in result ? OK$1(result) : FAIL$1(result));
3024
3081
  return;
3025
3082
  }
3026
3083
  case "/filemgr/git-unstage": {
3027
3084
  const paths = strArray(payload, "paths");
3028
3085
  if (paths === null) {
3029
- json(res, FAIL(BAD_REQUEST));
3086
+ json$1(res, FAIL$1(BAD_REQUEST));
3030
3087
  return;
3031
3088
  }
3032
3089
  const result = await git.unstage(root, paths);
3033
- json(res, "applied" in result ? OK(result) : FAIL(result));
3090
+ json$1(res, "applied" in result ? OK$1(result) : FAIL$1(result));
3034
3091
  return;
3035
3092
  }
3036
3093
  case "/filemgr/git-discard": {
3037
3094
  const paths = strArray(payload, "paths");
3038
3095
  if (paths === null) {
3039
- json(res, FAIL(BAD_REQUEST));
3096
+ json$1(res, FAIL$1(BAD_REQUEST));
3040
3097
  return;
3041
3098
  }
3042
3099
  const result = await git.discard(root, paths);
3043
- json(res, "applied" in result ? OK(result) : FAIL(result));
3100
+ json$1(res, "applied" in result ? OK$1(result) : FAIL$1(result));
3044
3101
  return;
3045
3102
  }
3046
3103
  default:
@@ -3061,7 +3118,7 @@ function registerPanelRoutes(ctx, fs, git) {
3061
3118
  }
3062
3119
  const gated = await fs.verify(root);
3063
3120
  if (!gated.ok) {
3064
- json(res, FAIL(gated.error), 400);
3121
+ json$1(res, FAIL$1(gated.error), 400);
3065
3122
  return;
3066
3123
  }
3067
3124
  res.writeHead(200, {
@@ -3078,6 +3135,7 @@ function registerPanelRoutes(ctx, fs, git) {
3078
3135
  subscribers.add(subscriber);
3079
3136
  if (gitUnavailable) push(subscriber, { kind: "gitUnavailable" });
3080
3137
  startGitPoll();
3138
+ startFsWatch();
3081
3139
  if (heartbeatTimer === void 0) heartbeatTimer = setInterval(() => {
3082
3140
  for (const current of subscribers) current.res.write(": ping\n\n");
3083
3141
  }, HEARTBEAT_MS);
@@ -3085,6 +3143,7 @@ function registerPanelRoutes(ctx, fs, git) {
3085
3143
  subscribers.delete(subscriber);
3086
3144
  if (subscribers.size === 0) {
3087
3145
  stopGitPoll();
3146
+ stopFsWatch();
3088
3147
  if (heartbeatTimer !== void 0) clearInterval(heartbeatTimer);
3089
3148
  heartbeatTimer = void 0;
3090
3149
  }
@@ -3102,12 +3161,381 @@ function registerPanelRoutes(ctx, fs, git) {
3102
3161
  return () => {
3103
3162
  for (const dispose of disposers) dispose();
3104
3163
  stopGitPoll();
3164
+ stopFsWatch();
3105
3165
  if (heartbeatTimer !== void 0) clearInterval(heartbeatTimer);
3106
3166
  for (const subscriber of subscribers) subscriber.res.end();
3107
3167
  subscribers.clear();
3108
3168
  };
3109
3169
  }
3110
3170
  //#endregion
3171
+ //#region src/host/office-preview.ts
3172
+ const OK = (value) => JSON.stringify({
3173
+ ok: true,
3174
+ value
3175
+ });
3176
+ const FAIL = (error) => JSON.stringify({
3177
+ ok: false,
3178
+ error
3179
+ });
3180
+ /**
3181
+ * Check if officecli is available.
3182
+ * In production (packaged) the binary is bundled via electron-builder extraResources
3183
+ * and injected into PATH by dsh-process.ts addOfficecliPath().
3184
+ * In development we check the vendor/officecli/ directory first, then fall back to PATH.
3185
+ * Cached to avoid repeated probing.
3186
+ */
3187
+ let officecliAvailable = null;
3188
+ let officecliPath = null;
3189
+ function resolveBundledOfficecli() {
3190
+ const binName = process.platform === "win32" ? "officecli.exe" : "officecli";
3191
+ const os = process.platform === "win32" ? "win" : process.platform === "darwin" ? "mac" : "linux";
3192
+ const arch = process.arch === "arm64" ? "arm64" : "x64";
3193
+ try {
3194
+ const vendorPath = join(fileURLToPath(new URL(".", import.meta.url)), "..", "..", "..", "vendor", "officecli", `${os}-${arch}`, binName);
3195
+ if (existsSync(vendorPath)) return vendorPath;
3196
+ } catch {}
3197
+ return null;
3198
+ }
3199
+ async function probeOfficecli(ctx) {
3200
+ if (officecliAvailable !== null) return officecliAvailable;
3201
+ const bundled = resolveBundledOfficecli();
3202
+ if (bundled !== null) {
3203
+ officecliPath = bundled;
3204
+ officecliAvailable = true;
3205
+ ctx.logger.info(`[dsh-filemgr] officecli found at ${bundled}, OfficeCLI preview enabled`);
3206
+ return true;
3207
+ }
3208
+ const subprocess = ctx.get("subprocess");
3209
+ try {
3210
+ officecliAvailable = (await subprocess.spawn({
3211
+ argv: ["officecli", "--version"],
3212
+ cwd: process.cwd(),
3213
+ stdio: {
3214
+ stdin: "ignore",
3215
+ stdout: { maxBytes: 1024 },
3216
+ stderr: { maxBytes: 1024 }
3217
+ },
3218
+ graceMs: 5e3
3219
+ }).done).exitCode === 0;
3220
+ if (officecliAvailable) {
3221
+ officecliPath = "officecli";
3222
+ ctx.logger.info("[dsh-filemgr] officecli found on PATH, OfficeCLI preview enabled");
3223
+ } else ctx.logger.warn("[dsh-filemgr] officecli not available, OfficeCLI preview disabled");
3224
+ } catch {
3225
+ officecliAvailable = false;
3226
+ ctx.logger.warn("[dsh-filemgr] officecli probe failed, OfficeCLI preview disabled");
3227
+ }
3228
+ return officecliAvailable;
3229
+ }
3230
+ /** Write JSON response. */
3231
+ function json(res, status, body) {
3232
+ res.writeHead(status, {
3233
+ "content-type": "application/json; charset=utf-8",
3234
+ "cache-control": "no-cache"
3235
+ });
3236
+ res.end(body);
3237
+ }
3238
+ /** Write HTML response. */
3239
+ function html(res, body) {
3240
+ res.writeHead(200, {
3241
+ "content-type": "text/html; charset=utf-8",
3242
+ "cache-control": "no-cache"
3243
+ });
3244
+ res.end(body);
3245
+ }
3246
+ /**
3247
+ * Inject a bootstrap that turns OfficeCLI's full-document HTML into a single-
3248
+ * slide browse view that KEEPS the thumbnail sidebar. OfficeCLI ships a
3249
+ * fullscreen engine (`enterFullscreen()` + ←/→/space + thumbnail jumps) but its
3250
+ * `.fullscreen` CSS hides the sidebar (pure presentation mode) — so we:
3251
+ * 1. auto-enter fullscreen after first paint (single page at a time), and
3252
+ * 2. override the fullscreen CSS to restore the sidebar and shrink `.main`
3253
+ * so the thumbnail column stays clickable for page jumps.
3254
+ */
3255
+ /**
3256
+ * Injected frame/patch add-ons on top of the raw OfficeCLI HTML.
3257
+ *
3258
+ * OfficeCLI colours its chrome (body, sidebar, borders, counter) with hard-coded
3259
+ * dark values and does not follow the dsh light/dark theme. The preview is
3260
+ * rendered inside an isolated blob iframe, so the outer document's
3261
+ * `body[data-ds-dark-theme]` token is NOT visible here — the host sends the
3262
+ * current theme via postMessage and we react by toggling a data attribute that
3263
+ * this stylesheet consumes.
3264
+ *
3265
+ * The slide artwork itself (`.slide`, `.thumb-slide`, `--slide-*`) is authored
3266
+ * by the deck and intentionally left untouched.
3267
+ */
3268
+ function injectSlideShowMode(htmlContent) {
3269
+ return htmlContent.replace("</body>", `
3270
+ <style data-dsh-officecli-fullscreen>
3271
+ /* Restore the thumbnail sidebar in single-slide (fullscreen) mode and give the
3272
+ slide stage a wider initial viewport by narrowing the sidebar. */
3273
+ body.fullscreen .sidebar { display: flex; width: 140px; min-width: 140px; }
3274
+ body.fullscreen .main { width: calc(100vw - 140px); }
3275
+ body.fullscreen .slide-container { width: 100%; }
3276
+ /* Scale the single slide down so it fits within the stage without being clipped.
3277
+ Using zoom (not transform) because zoom changes both rendering AND layout size,
3278
+ so the parent container adapts naturally. transform:scale() only changes visual
3279
+ size but keeps the original layout footprint, causing overflow clipping. */
3280
+ body.fullscreen .slide {
3281
+ zoom: 0.85;
3282
+ }
3283
+
3284
+ /* Light-theme chrome (host sends theme via postMessage; default stays dark). */
3285
+ body[data-dsh-theme="light"] { background: #f7f7f8; }
3286
+ body[data-dsh-theme="light"] .sidebar {
3287
+ background: #eef0f3;
3288
+ border-right: 1px solid rgba(0,0,0,0.09);
3289
+ }
3290
+ body[data-dsh-theme="light"] .sidebar-title,
3291
+ body[data-dsh-theme="light"] .thumb-num,
3292
+ body[data-dsh-theme="light"] .sidebar-toggle { color: #5f6368; }
3293
+ body[data-dsh-theme="light"] .thumb:hover { border-color: #b9bdc6; }
3294
+ body[data-dsh-theme="light"] .page-counter { background: rgba(0,0,0,0.55); color: #f1f3f4; }
3295
+ </style>
3296
+ <script data-dsh-officecli-fullscreen>
3297
+ (function () {
3298
+ // --- single-slide browsing mode (F) ---
3299
+ function boot() {
3300
+ // Only decks carry the fullscreen machinery; skip docs/sheets.
3301
+ if (!document.querySelector('.main .slide-container')) return;
3302
+ if (document.body.classList.contains('fullscreen')) return;
3303
+ document.dispatchEvent(new KeyboardEvent('keydown', { key: 'F', code: 'KeyF', bubbles: true }));
3304
+ }
3305
+ function onReady(fn) {
3306
+ if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', fn, { once: true });
3307
+ else fn();
3308
+ }
3309
+ onReady(boot);
3310
+
3311
+ // --- follow the host light/dark theme via postMessage (blob iframe isolation) ---
3312
+ window.addEventListener('message', function (event) {
3313
+ if (!event.data || event.data.source !== 'dsh-filemgr' || event.data.type !== 'dsh-theme') return;
3314
+ document.body.setAttribute('data-dsh-theme', event.data.dark ? 'dark' : 'light');
3315
+ });
3316
+ })();
3317
+ <\/script>
3318
+ <style data-dsh-officecli-sheet>
3319
+ .dsh-sheet-selected { outline: 1px solid #2e7df6 !important; outline-offset: -1px; background: rgba(46, 125, 246, 0.12) !important; }
3320
+ </style>
3321
+ <script data-dsh-officecli-sheet-select>
3322
+ (function () {
3323
+ // Excel selection -> host reference. OfficeCLI annotates every cell with
3324
+ // data-path="/<sheet>/<A1>" (column headers use col[A], row headers row[1]).
3325
+ // Some renderers set user-select:none, so a native text selection cannot span
3326
+ // cells; instead we track a pointer drag ourselves (cell under pointerdown ->
3327
+ // cell under the pointer), highlight the bounding rectangle, and postMessage
3328
+ // the sheet + A1 range up. The host turns that into a "引用到对话" chip
3329
+ // (相对路径!Sheet!Range). Docs/slides have no .sheet-content, so this block
3330
+ // is inert for Word/PPT.
3331
+ var MAX_HIGHLIGHT_CELLS = 4000;
3332
+ function isSheetDoc() { return !!document.querySelector('.sheet-content'); }
3333
+ function colToNum(letters) {
3334
+ var n = 0;
3335
+ for (var i = 0; i < letters.length; i++) n = n * 26 + (letters.charCodeAt(i) - 64);
3336
+ return n;
3337
+ }
3338
+ function numToCol(n) {
3339
+ var s = '';
3340
+ while (n > 0) { var r = (n - 1) % 26; s = String.fromCharCode(65 + r) + s; n = (n - r - 1) / 26 | 0; }
3341
+ return s;
3342
+ }
3343
+ function parsePath(el) {
3344
+ var p = el && el.getAttribute && el.getAttribute('data-path'); if (!p) return null;
3345
+ var parts = p.split('/'); // ['', sheet, ref]
3346
+ if (parts.length < 3) return null;
3347
+ var m = /^([A-Z]+)([0-9]+)$/.exec(parts[2]); if (!m) return null;
3348
+ return { sheet: parts[1], col: colToNum(m[1]), row: +m[2] };
3349
+ }
3350
+ // Walk from a DOM node up to the nearest data cell (or null).
3351
+ function pickCell(node) {
3352
+ var el = node;
3353
+ while (el && el !== document) {
3354
+ if (el.nodeType === 1 && el.hasAttribute('data-path')) {
3355
+ var p = parsePath(el); if (p) return p;
3356
+ }
3357
+ el = el.parentElement;
3358
+ }
3359
+ return null;
3360
+ }
3361
+ function clearHighlight() {
3362
+ var els = document.querySelectorAll('.dsh-sheet-selected');
3363
+ for (var i = 0; i < els.length; i++) els[i].classList.remove('dsh-sheet-selected');
3364
+ }
3365
+ function applyHighlight(r) {
3366
+ clearHighlight();
3367
+ if (!r) return;
3368
+ if ((r.c2 - r.c1 + 1) * (r.r2 - r.r1 + 1) > MAX_HIGHLIGHT_CELLS) return; // skip huge ranges
3369
+ var els = document.querySelectorAll('td[data-path], th[data-path]');
3370
+ for (var i = 0; i < els.length; i++) {
3371
+ var p = parsePath(els[i]);
3372
+ if (p && p.sheet === r.sheet && p.row >= r.r1 && p.row <= r.r2 && p.col >= r.c1 && p.col <= r.c2) {
3373
+ els[i].classList.add('dsh-sheet-selected');
3374
+ }
3375
+ }
3376
+ }
3377
+ function send(payload) {
3378
+ parent.postMessage(Object.assign({ source: 'dsh-filemgr', type: 'dsh-sheet-select' }, payload), '*');
3379
+ }
3380
+ function update(from, to) {
3381
+ var refs = [from, to].filter(Boolean);
3382
+ if (!refs.length) { send({ sheet: null, range: null, x: 0, y: 0 }); applyHighlight(null); return; }
3383
+ if (refs[0].sheet !== (refs[1] ? refs[1].sheet : refs[0].sheet)) { send({ sheet: null, range: null, x: 0, y: 0 }); applyHighlight(null); return; }
3384
+ var sheet = refs[0].sheet;
3385
+ var c1 = 1e9, c2 = 0, r1 = 1e9, r2 = 0;
3386
+ refs.forEach(function (p) { c1 = Math.min(c1, p.col); c2 = Math.max(c2, p.col); r1 = Math.min(r1, p.row); r2 = Math.max(r2, p.row); });
3387
+ var corner = numToCol(c1) + r1, corner2 = numToCol(c2) + r2;
3388
+ var range = corner === corner2 ? corner : corner + ':' + corner2;
3389
+ var anchorEl = to ? findCellEl(sheet, refs[1].col, refs[1].row) : findCellEl(sheet, from.col, from.row);
3390
+ var rect = anchorEl ? anchorEl.getBoundingClientRect() : null;
3391
+ applyHighlight({ sheet: sheet, c1: c1, c2: c2, r1: r1, r2: r2 });
3392
+ send({ sheet: sheet, range: range, x: rect ? rect.left : 0, y: rect ? rect.bottom + 6 : 0 });
3393
+ }
3394
+ function findCellEl(sheet, col, row) {
3395
+ var els = document.querySelectorAll('td[data-path], th[data-path]');
3396
+ for (var i = 0; i < els.length; i++) {
3397
+ var p = parsePath(els[i]);
3398
+ if (p && p.sheet === sheet && p.col === col && p.row === row) return els[i];
3399
+ }
3400
+ return null;
3401
+ }
3402
+ var origin = null, dragging = false;
3403
+ document.addEventListener('pointerdown', function (e) {
3404
+ if (!isSheetDoc()) return;
3405
+ var p = pickCell(e.target);
3406
+ if (!p) return; // corner / header / outside — leave selection as-is
3407
+ dragging = true; origin = p; update(p, p);
3408
+ }, true);
3409
+ document.addEventListener('pointermove', function (e) {
3410
+ if (!dragging || !origin) return;
3411
+ var cur = pickCell(document.elementFromPoint(e.clientX, e.clientY));
3412
+ if (cur) update(origin, cur);
3413
+ }, true);
3414
+ window.addEventListener('pointerup', function () {
3415
+ if (!dragging) return;
3416
+ dragging = false; origin = null;
3417
+ }, true);
3418
+ })();
3419
+ <\/script>
3420
+ </body>`);
3421
+ }
3422
+ /** Forbidden (non-loopback request). */
3423
+ function writeForbidden(res) {
3424
+ json(res, 403, FAIL({
3425
+ code: "internal",
3426
+ message: "forbidden: loopback-only"
3427
+ }));
3428
+ }
3429
+ /**
3430
+ * Handle one GET /filemgr/office-preview request.
3431
+ * Query params: root=workspace/root&path=relative/to/root/file.pptx, probe=1 for mtime check.
3432
+ * Uses the same root+path convention as /filemgr/raw.
3433
+ */
3434
+ async function handleOfficePreview(ctx, req, res) {
3435
+ if (!isLoopbackRequest(req)) {
3436
+ writeForbidden(res);
3437
+ return;
3438
+ }
3439
+ const url = new URL(req.url ?? "/", "http://x");
3440
+ const root = url.searchParams.get("root");
3441
+ const path = url.searchParams.get("path");
3442
+ if (root === null || root === "" || path === null || path === "") {
3443
+ json(res, 400, FAIL({
3444
+ code: "internal",
3445
+ message: "missing root or path parameter"
3446
+ }));
3447
+ return;
3448
+ }
3449
+ const absPath = join(root, path);
3450
+ if (url.searchParams.get("probe") === "1") {
3451
+ try {
3452
+ const info = await stat(absPath);
3453
+ json(res, 200, OK({ mtime: info.mtimeMs }));
3454
+ } catch {
3455
+ json(res, 404, FAIL({
3456
+ code: "not-found",
3457
+ message: "file not found"
3458
+ }));
3459
+ }
3460
+ return;
3461
+ }
3462
+ if (!await probeOfficecli(ctx) || officecliPath === null) {
3463
+ json(res, 501, FAIL({
3464
+ code: "internal",
3465
+ message: "officecli not installed or not on PATH"
3466
+ }));
3467
+ return;
3468
+ }
3469
+ const subprocess = ctx.get("subprocess");
3470
+ let timeoutMs = 6e4;
3471
+ const controller = new AbortController();
3472
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
3473
+ try {
3474
+ const handle = subprocess.spawn({
3475
+ argv: [
3476
+ officecliPath,
3477
+ "view",
3478
+ absPath,
3479
+ "html"
3480
+ ],
3481
+ cwd: process.cwd(),
3482
+ stdio: {
3483
+ stdin: "ignore",
3484
+ stdout: { maxBytes: 10 * 1024 * 1024 },
3485
+ stderr: { maxBytes: 1 << 20 }
3486
+ },
3487
+ graceMs: 5e3,
3488
+ signal: controller.signal
3489
+ });
3490
+ const outcome = await handle.done;
3491
+ clearTimeout(timer);
3492
+ if (controller.signal.aborted) {
3493
+ json(res, 504, FAIL({
3494
+ code: "internal",
3495
+ message: "officecli rendering timed out after 60s"
3496
+ }));
3497
+ return;
3498
+ }
3499
+ const readStdout = () => handle.collected?.stdout?.readFrom(0).text ?? "";
3500
+ const readStderr = () => handle.collected?.stderr?.readFrom(0).text ?? "";
3501
+ if (outcome.exitCode !== 0) {
3502
+ const stderr = readStderr();
3503
+ const stdout = readStdout();
3504
+ const error = `officecli exited with code ${String(outcome.exitCode)}\n${stderr}\n${stdout}`.slice(0, 1e3);
3505
+ json(res, 422, FAIL({
3506
+ code: "read-failed",
3507
+ message: error.trim()
3508
+ }));
3509
+ return;
3510
+ }
3511
+ const htmlContent = readStdout();
3512
+ if (htmlContent === "") {
3513
+ json(res, 422, FAIL({
3514
+ code: "read-failed",
3515
+ message: "officecli returned empty output"
3516
+ }));
3517
+ return;
3518
+ }
3519
+ html(res, injectSlideShowMode(htmlContent));
3520
+ } catch (error) {
3521
+ clearTimeout(timer);
3522
+ const msg = error instanceof Error ? error.message : String(error);
3523
+ json(res, 500, FAIL({
3524
+ code: "internal",
3525
+ message: `officecli invocation failed: ${msg}`
3526
+ }));
3527
+ }
3528
+ }
3529
+ /** Register the /filemgr/office-preview route on the web server. */
3530
+ function registerOfficePreviewRoute(ctx) {
3531
+ probeOfficecli(ctx);
3532
+ return ctx.webServer.register({
3533
+ kind: "prefix",
3534
+ path: "/filemgr/office-preview",
3535
+ handler: (req, res) => handleOfficePreview(ctx, req, res)
3536
+ });
3537
+ }
3538
+ //#endregion
3111
3539
  //#region src/mount-once.ts
3112
3540
  /**
3113
3541
  * Host single-instance guard shared by the plugin family. The family bundle
@@ -3181,7 +3609,11 @@ function applyImpl(ctx, config = {}) {
3181
3609
  if (disposePanel === void 0 && enabled) {
3182
3610
  disposePanel = ctx.effect(() => {
3183
3611
  const disposeRoutes = registerPanelRoutes(ctx, fs, git);
3184
- return () => disposeRoutes();
3612
+ const disposeOffice = registerOfficePreviewRoute(ctx);
3613
+ return () => {
3614
+ disposeRoutes();
3615
+ disposeOffice();
3616
+ };
3185
3617
  }, "dsh-filemgr: /filemgr routes");
3186
3618
  disposePrompt = ctx.effect(() => ctx.systemPrompt.section({
3187
3619
  name: "plugin:filemgr",