@weniatall/dsh-file-viewer 0.2.8 → 0.2.9

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 +2 -0
  2. package/lib/client.js +116 -10
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -46,6 +46,8 @@ dsh plugin --profile web add <release-tgz-url>
46
46
  - **会话中点击文件**:在会话回复(变更文件卡片、产物交付卡片、行内代码引用等)中点击任何文件路径,自动唤起文件查看抽屉并直接选中预览该文件。
47
47
  - `Esc` 或点击抽屉外部遮罩关闭。
48
48
 
49
+ **桌面端接管说明**:桌面版宿主判定自己能在系统里打开路径,因此会在文件路径与更改卡片旁额外渲染原生「在应用中打开」按钮,更改卡片还会走 `/api/changes.open` 这条直接交给操作系统的路由。插件在这两处都会先接管,改为唤起文件查看抽屉:原生打开按钮按 `data-open-path-open` 识别,更改卡片打开路由则从 URL 里的 `sessionId / seq / index` 回查文件路径。取不到路径时一律放行,仍按宿主原方式打开,不会出现点了没反应。
50
+
49
51
  顶部可切换工作区、刷新、显示或隐藏点文件。目录在树中原地展开,子级首次展开时才加载。
50
52
 
51
53
  文件树支持**右键上下文菜单**与单项 `@` 快捷按钮:
package/lib/client.js CHANGED
@@ -870,10 +870,27 @@ window.__ModuleLoader__.load({
870
870
  // Guard so a hot reload or re-apply never double-wraps shared open
871
871
  // handlers; older hosts used workspaces.openPath.
872
872
  const wiredOpenPath = typeof WeakSet !== "undefined" ? new WeakSet() : null;
873
+ // The wrappers below outlive a single apply. They were closing over the
874
+ // store of whichever apply installed them, so a second apply left the Host
875
+ // calling the previous store: the click was swallowed, the drawer never
876
+ // opened, and the file viewer looked like it had stopped listening. These
877
+ // pointers are refreshed on every apply so the wrappers always write the
878
+ // store the mounted overlay is actually subscribed to.
879
+ let liveOpenStore = null;
880
+ let liveSessionStore = null;
873
881
 
874
- function openViewerForPath(openStore, sessionStore, path) {
875
- const sid = sessionStore.get();
876
- openStore.set({ filePath: path, sessionId: sid, _t: Date.now() });
882
+ /**
883
+ * Open the drawer on one path.
884
+ *
885
+ * `sessionId` is optional: a `dsh-resource://file/session/<id>/…` address
886
+ * names the session it belongs to, and honouring it keeps the drawer on that
887
+ * session's project instead of whatever session happens to be current.
888
+ */
889
+ function openViewerForPath(openStore, sessionStore, path, sessionId) {
890
+ const target = liveOpenStore || openStore;
891
+ const session = liveSessionStore || sessionStore;
892
+ const sid = sessionId !== undefined && sessionId !== "" ? sessionId : (session ? session.get() : undefined);
893
+ target.set({ filePath: path, sessionId: sid, _t: Date.now() });
877
894
  }
878
895
 
879
896
  function parseFileResource(address) {
@@ -918,8 +935,7 @@ window.__ModuleLoader__.load({
918
935
  sr.openResource = function(address, options) {
919
936
  const parsed = parseFileResource(address);
920
937
  if (parsed && parsed.path) {
921
- const sid = parsed.sessionId || sessionStore.get();
922
- openStore.set({ filePath: parsed.path, sessionId: sid, _t: Date.now() });
938
+ openViewerForPath(openStore, sessionStore, parsed.path, parsed.sessionId);
923
939
  return;
924
940
  }
925
941
  const review = parseChangesReviewResource(address);
@@ -933,7 +949,7 @@ window.__ModuleLoader__.load({
933
949
  .then((data) => {
934
950
  const file = data?.files?.[idx]?.path || data?.files?.[0]?.path;
935
951
  if (file) {
936
- openStore.set({ filePath: file, sessionId: sid, _t: Date.now() });
952
+ openViewerForPath(openStore, sessionStore, file);
937
953
  }
938
954
  })
939
955
  .catch(() => {});
@@ -946,8 +962,7 @@ window.__ModuleLoader__.load({
946
962
  sr.openResourceIn = function(sessionId, address, options) {
947
963
  const parsed = parseFileResource(address);
948
964
  if (parsed && parsed.path) {
949
- const sid = sessionId || parsed.sessionId || sessionStore.get();
950
- openStore.set({ filePath: parsed.path, sessionId: sid, _t: Date.now() });
965
+ openViewerForPath(openStore, sessionStore, parsed.path, sessionId || parsed.sessionId);
951
966
  return;
952
967
  }
953
968
  const review = parseChangesReviewResource(address);
@@ -960,7 +975,7 @@ window.__ModuleLoader__.load({
960
975
  .then((data) => {
961
976
  const file = data?.files?.[idx]?.path || data?.files?.[0]?.path;
962
977
  if (file) {
963
- openStore.set({ filePath: file, sessionId: sid, _t: Date.now() });
978
+ openViewerForPath(openStore, sessionStore, file, sid);
964
979
  }
965
980
  })
966
981
  .catch(() => {});
@@ -1000,6 +1015,63 @@ window.__ModuleLoader__.load({
1000
1015
  };
1001
1016
  }
1002
1017
 
1018
+ /**
1019
+ * Take over the Desktop Host's changed-file open route.
1020
+ *
1021
+ * The changes card's native open control does not go through the RPC
1022
+ * carrier: it POSTs `/api/changes.open?sessionId=…&seq=…&index=…`, which
1023
+ * hands the file to the operating system. Nothing in the RPC path can see
1024
+ * that call, so on Desktop the changes card opened the system editor while
1025
+ * the Web build (where the control is not mounted at all) opened the
1026
+ * viewer. Resolving the same coordinates through the summary route gives the
1027
+ * path, and a synthetic OK response keeps the Host's card state settled.
1028
+ */
1029
+ function wrapChangesOpenFetch(openStore, sessionStore) {
1030
+ if (typeof window === "undefined" || typeof window.fetch !== "function") return;
1031
+ const originalFetch = window.fetch;
1032
+ if (wiredOpenPath === null || wiredOpenPath.has(originalFetch)) return;
1033
+ wiredOpenPath.add(originalFetch);
1034
+
1035
+ const summaryPath = (sessionId, seq) =>
1036
+ `/api/changes.summary?${new URLSearchParams({ sessionId, seq: String(seq) })}`;
1037
+
1038
+ window.fetch = function(input, init) {
1039
+ const requestUrl = typeof input === "string" ? input : (input && typeof input.url === "string" ? input.url : "");
1040
+ const method = String((init && init.method) || (input && input.method) || "GET").toUpperCase();
1041
+ if (method !== "POST" || !/\/api\/changes\.open\b/.test(requestUrl)) {
1042
+ return originalFetch.apply(this, arguments);
1043
+ }
1044
+ let coordinates;
1045
+ try {
1046
+ const query = new URLSearchParams(requestUrl.split("?")[1] || "");
1047
+ coordinates = {
1048
+ sessionId: query.get("sessionId") || "",
1049
+ seq: Number(query.get("seq")),
1050
+ index: Number(query.get("index") || 0),
1051
+ };
1052
+ } catch (_) {
1053
+ coordinates = null;
1054
+ }
1055
+ if (!coordinates || !coordinates.sessionId || !Number.isFinite(coordinates.seq)) {
1056
+ return originalFetch.apply(this, arguments);
1057
+ }
1058
+ const { sessionId, seq, index } = coordinates;
1059
+ return originalFetch(summaryPath(sessionId, seq), { signal: init && init.signal })
1060
+ .then((response) => (response && response.ok ? response.json() : null))
1061
+ .then((data) => {
1062
+ const files = Array.isArray(data && data.files) ? data.files : [];
1063
+ const entry = files[Number.isFinite(index) ? index : 0] || files[0];
1064
+ const file = entry && entry.path;
1065
+ if (!file) return originalFetch.apply(this, arguments);
1066
+ openViewerForPath(openStore, sessionStore, file, sessionId);
1067
+ // The Host only reads `response.ok`; a bodiless 204 settles its card
1068
+ // without claiming the OS actually opened anything.
1069
+ return new Response(null, { status: 204 });
1070
+ })
1071
+ .catch(() => originalFetch.apply(this, arguments));
1072
+ };
1073
+ }
1074
+
1003
1075
  const COMMON_FILE_EXTENSIONS = new Set([
1004
1076
  "go", "ts", "js", "tsx", "jsx", "mjs", "cjs", "py", "java", "c", "cpp", "h", "hpp", "rs", "php", "rb", "swift", "kt", "scala",
1005
1077
  "md", "markdown", "txt", "json", "jsonc", "yaml", "yml", "toml", "xml", "html", "css", "scss", "less", "vue", "svelte",
@@ -1113,6 +1185,25 @@ window.__ModuleLoader__.load({
1113
1185
  }
1114
1186
  }
1115
1187
 
1188
+ // 3. The Host's native open control carries no path of its own: its label
1189
+ // is the action ("Open", "Open in …"), never the file. The path is
1190
+ // published by the surrounding row in a hidden description span, so
1191
+ // walk up a few levels and take the first span that holds a path. The
1192
+ // walk is bounded and every candidate still has to pass the path
1193
+ // shape check, so a miss falls through to the Host rather than opening
1194
+ // the wrong file.
1195
+ if (element.closest && element.closest("[data-open-path-open]")) {
1196
+ let scope = element.parentElement;
1197
+ for (let depth = 0; scope && depth < 4; depth += 1) {
1198
+ const hiddenPath = scope.querySelector ? scope.querySelector("span[hidden]") : null;
1199
+ if (hiddenPath && hiddenPath.textContent) {
1200
+ const p = normalizeClickedPath(hiddenPath.textContent);
1201
+ if (p) return p;
1202
+ }
1203
+ scope = scope.parentElement;
1204
+ }
1205
+ }
1206
+
1116
1207
  const sources = [
1117
1208
  element.getAttribute("data-file-path"),
1118
1209
  element.getAttribute("data-path"),
@@ -1130,6 +1221,13 @@ window.__ModuleLoader__.load({
1130
1221
  function isFileOpenControl(element) {
1131
1222
  if (!element) return false;
1132
1223
  if (element.hasAttribute("data-file-path") || element.hasAttribute("data-path")) return true;
1224
+ // The Desktop Host answers `canOpenWorkspacePath` with true, so the shell
1225
+ // mounts its native "open in application" button beside every file path
1226
+ // (the changes card included). That button sits above the path control and
1227
+ // takes the click, so it has to count as a file control or the viewer
1228
+ // never sees the gesture. The `-more` chevron is a menu of applications,
1229
+ // not a plain open, so it is deliberately left to the Host.
1230
+ if (element.closest("[data-open-path-open]")) return true;
1133
1231
  if (element.closest("[data-produced-files-row]")) return true;
1134
1232
  // Support deliverables: changed files card (ChangedFiles) and presented file cards (PresentedFileCard)
1135
1233
  if (element.closest("[data-changed-files], [data-presented-file], [data-presented-files-row]")) return true;
@@ -1493,6 +1591,11 @@ window.__ModuleLoader__.load({
1493
1591
  // Which session the conversation header currently belongs to. The phone
1494
1592
  // entry is drawn outside that header and cannot receive its props.
1495
1593
  const sessionStore = createStore(undefined);
1594
+ // Publish this apply's stores to the Host wrappers. A re-apply installs new
1595
+ // stores but the wrappers are already in place, so they must follow the
1596
+ // ones the freshly mounted overlay subscribes to.
1597
+ liveOpenStore = openStore;
1598
+ liveSessionStore = sessionStore;
1496
1599
  // The composer's draft accessors, captured from the slot that receives
1497
1600
  // them. The drawer renders in a different subtree and cannot reach the
1498
1601
  // input state through props.
@@ -3394,6 +3497,9 @@ window.__ModuleLoader__.load({
3394
3497
  // Intercept any openWorkspacePath RPC calls (connection is declared in inject)
3395
3498
  wrapConnectionRpc(ctx, openStore, sessionStore);
3396
3499
 
3500
+ // The changes card's native open route bypasses the RPC carrier entirely.
3501
+ wrapChangesOpenFetch(openStore, sessionStore);
3502
+
3397
3503
  // Wrap sidebarRight if already registered or whenever injected. Direct
3398
3504
  // property access is not safe here: sidebarRight is not part of this
3399
3505
  // plugin's inject, so cordis would reject the read outright.
@@ -3441,7 +3547,7 @@ window.__ModuleLoader__.load({
3441
3547
  LANG_OVERRIDES_KEY, LANG_OVERRIDES_MAX, readLangOverrides, writeLangOverrides, readLangOverride, writeLangOverride,
3442
3548
  langOverrideKey, isKnownLanguageId, setLanguageCatalog, getLanguageCatalog: () => languageCatalog, LanguagePicker,
3443
3549
  MARKDOWN_LABELS, MARKDOWN_CODE_LABELS, READ_BLOCK_LABELS, JSON_TREE_LABELS, DIFF_BLOCK_LABELS, ErrorBoundary,
3444
- openViewerForPath, wrapWorkspaceOpenPath, wrapConnectionRpc, wrapSidebarRight, parseFileResource, setupGlobalFileClickInterceptor, extractClickedPath, normalizeClickedPath, looksLikeFilePath, isFileOpenControl, isFileMentionCode, renderRootOptions,
3550
+ openViewerForPath, wrapWorkspaceOpenPath, wrapConnectionRpc, wrapChangesOpenFetch, wrapSidebarRight, parseFileResource, setupGlobalFileClickInterceptor, extractClickedPath, normalizeClickedPath, looksLikeFilePath, isFileOpenControl, isFileMentionCode, renderRootOptions,
3445
3551
  triggerDownload, performDownload, SvgIconDownload,
3446
3552
  };
3447
3553
  return module.exports;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@weniatall/dsh-file-viewer",
3
- "version": "0.2.8",
3
+ "version": "0.2.9",
4
4
  "description": "Workspace file viewer for DeepSeek Harness: file tree, syntax-highlighted code with language detection and manual override, images, PDF, Excel and Word",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",