dsh-plugin-workbench 0.0.9 → 0.0.11

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/CHANGELOG.md CHANGED
@@ -4,6 +4,40 @@
4
4
  格式基于 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.1.0/),
5
5
  版本号遵循 [Semantic Versioning](https://semver.org/lang/zh-CN/)。
6
6
 
7
+ ## [0.0.11] - 2026-08-21
8
+
9
+ ### Added
10
+
11
+ - **README 互相引用**:新增「相关插件」段,列出同系列已发布插件(npm / GitHub 链接 + 一句话说明),互相引流。
12
+
13
+ ## [0.0.10] - 2026-08-21
14
+
15
+ ### Fixed
16
+
17
+ - **「在资源管理器打开」不生效(窗口打开但不可见)**:Windows 下 `spawn` 传
18
+ `windowsHide: true` 会让新开的资源管理器文件夹窗口以**隐藏状态**创建
19
+ (CREATE_NO_WINDOW 沿请求一路传导,窗口存在但 `visible=False`,用户什么都
20
+ 看不到)。`explorer.exe` 是 GUI 子系统程序,不带 `windowsHide` 也不会闪
21
+ 控制台;`reveal` 端点现在对 explorer.exe 全部不传该标志(含 WSL 转译分支),
22
+ 资源管理器窗口正常弹出并选中目标文件
23
+
24
+ ### Added
25
+
26
+ - **整列右键菜单**:不只文件行——在左栏任何空白处、头部、剪贴板条上右键
27
+ 都弹出当前工作区目录菜单(新建文件 / 新建文件夹 / 粘贴 / 全选 / 撤销 /
28
+ 复制路径 / 在资源管理器打开 / 在系统中打开 / 刷新);右键菜单自身不再触发
29
+ 浏览器原生菜单
30
+ - **@ 在消息中引用(Claude Code 式文件引用)**:文件右键菜单新增
31
+ 「@ 在消息中引用」,把 `@相对工作区路径`(自动带尾随空格)插入聊天输入框
32
+ 光标处;发送后消息里的 `@相对路径` 渲染为超链接(点击在工作台预览中打开
33
+ 该文件),语法为 `@` + 相对工作区路径——`@src/a.ts`、`@README.md` 匹配,
34
+ 绝对路径 / 邮箱 / 无路径形状的普通 `@文本` 一律按原样显示、无特殊含义;
35
+ 中文句子标点跟尾视为普通文本
36
+ - **拖文件进会话**:把左栏文件(含多选)拖到聊天区任意位置(不在左栏内)
37
+ 即把路径文本插入聊天输入框光标处;拖到左栏目录行仍是原有的移动操作
38
+ - **任意位置点击取消选中**:选中文件/文件夹后,点击树外任何地方(会话、
39
+ 其他栏、左栏头部)都会清除选中;树内空白/提示行点击同样取消
40
+
7
41
  ## [0.0.9] - 2026-08-21
8
42
 
9
43
  ### Added
package/README.md CHANGED
@@ -25,11 +25,17 @@
25
25
  干净标签页自动重读,带未保存编辑的标签页显示「⟳」徽标(点击重新加载)
26
26
  - **行号栏**:编辑器左侧逻辑行号,与文本滚动锁定对齐(纯文本/高亮模式均生效)
27
27
  - **图片预览**:png/jpg/gif/webp/avif/svg 等直接内联渲染(同源字节路由,20MB 上限)
28
- - **右键菜单**(VS Code 风格):文件/文件夹/空白区域均可右键——
28
+ - **右键菜单**(VS Code 风格):文件/文件夹/**整列任意空白、头部**均可右键——
29
29
  新建文件、新建文件夹、重命名、删除(递归带确认)、复制路径、复制 / 剪切 / 粘贴、
30
- 刷新、在系统中打开、
30
+ 刷新、在系统中打开、全选、撤销、
31
31
  在资源管理器打开(文件在所在文件夹中被选中,文件夹直接打开;
32
32
  Windows `explorer` / macOS `open`,WSL 自动转译)
33
+ - **@ 在消息中引用**:文件右键「@ 在消息中引用」把 `@相对工作区路径` 插入聊天输入框;
34
+ 发送后消息中的 `@相对路径` 渲染为超链接(点击在工作台预览打开),
35
+ 语法为 `@` + 相对工作区路径,其它 `@文本` 一律按原样显示、无特殊含义
36
+ - **拖文件进会话**:把文件/多选从左栏拖到聊天区任意位置,路径文本插入输入框光标处;
37
+ 拖到左栏目录行仍是移动操作
38
+ - **任意位置点击取消选中**:点击树外的任何地方(会话/其它栏/左栏头部)清除选中
33
39
  - **复制 / 剪切 / 粘贴**:Ctrl/Cmd+C/X/V(或右键菜单),支持跨工作区粘贴;
34
40
  复制到同名已存在的目标时询问「是否覆盖」,与系统文件管理器一致;
35
41
  剪切(移动)成功后自动清空剪贴板
@@ -123,6 +129,20 @@ dsh plugin --profile web remove dsh-plugin-workbench
123
129
  见 [CONTRIBUTING](./CONTRIBUTING.md) 与 [CODE_OF_CONDUCT](./CODE_OF_CONDUCT.md);
124
130
  变更记录见 [CHANGELOG](./CHANGELOG.md)。
125
131
 
132
+ ## 相关插件
133
+
134
+ 本插件属于 **Pasumao 的 dsh 插件生态**,同系列已发布插件可搭配使用:
135
+
136
+ | 插件(npm) | GitHub | 说明 |
137
+ |---|---|---|
138
+ | [dsh-notify](https://www.npmjs.com/package/dsh-notify) | [GitHub 仓库](https://github.com/Pasumao/dsh-plugin-notify) | Windows 原生通知 + 系统托盘 |
139
+ | [dsh-plugin-choice-refresh](https://www.npmjs.com/package/dsh-plugin-choice-refresh) | [GitHub 仓库](https://github.com/Pasumao/dsh-plugin-choice-refresh) | 选择增强:重新生成选项 / 更多选项 |
140
+ | [dsh-plugin-dev-kb](https://www.npmjs.com/package/dsh-plugin-dev-kb) | [GitHub 仓库](https://github.com/Pasumao/dsh-plugin-dev-kb) | 插件开发知识库(官方文档完整镜像 + 技能) |
141
+ | [dsh-plugin-image-tools](https://www.npmjs.com/package/dsh-plugin-image-tools) | [GitHub 仓库](https://github.com/Pasumao/dsh-plugin-image-tools) | 图片选择卡 + 回复内嵌图片 + 盲模型收图 |
142
+ | [dsh-plugin-table-zoom](https://www.npmjs.com/package/dsh-plugin-table-zoom) | [GitHub 仓库](https://github.com/Pasumao/dsh-plugin-table-zoom) | 聊天长表格浮窗查看 + 一键复制 Markdown |
143
+
144
+ > 本系列其余插件见 [Pasumao · dsh 插件](https://github.com/Pasumao);觉得好用欢迎到 GitHub 点 ⭐。
145
+
126
146
  ## AI 生成声明
127
147
 
128
148
  部分源码与文档由 AI 辅助生成(DeepSeek Harness),均经人工审查与实机验证;
package/lib/client.js CHANGED
@@ -855,6 +855,126 @@ window.__ModuleLoader__.load({
855
855
  return entry;
856
856
  }
857
857
  //#endregion
858
+ //#region src/client/composer.ts
859
+ /**
860
+ * Composer integration for the workbench file column.
861
+ *
862
+ * Two gestures land text in the chat composer without touching the core:
863
+ *
864
+ * 1. Drag & drop — dragging one or more tree rows and dropping ANYWHERE
865
+ * outside the file column (the chat, the composer, the message list)
866
+ * inserts the dragged paths into the composer. Dropping INSIDE the file
867
+ * column still performs the tree's own move operation — the tree's drop
868
+ * handler runs first and stops propagation, so this document-level
869
+ * listener never sees those drops.
870
+ *
871
+ * 2. Context-menu "@引用" — inserts `@<relative-workspace-path>` at the
872
+ * composer caret.
873
+ *
874
+ * The composer is a controlled React textarea, so the value is updated
875
+ * through the native `value` setter + a bubbling `input` event (the standard
876
+ * trick that makes React's onChange see the change), and the caret is
877
+ * restored on the next frame because the React re-render may reset it.
878
+ */
879
+ /** Custom dataTransfer type carrying the dragged workspace paths (JSON array). */
880
+ const DRAG_TYPE = "application/x-dsh-workbench-files";
881
+ /** One-time install guard (the client bundle re-applies on HMR). */
882
+ let dropsInstalled = false;
883
+ /** Start the document-level drop listener (idempotent). */
884
+ function installComposerDrops() {
885
+ if (dropsInstalled || typeof document === "undefined") return;
886
+ dropsInstalled = true;
887
+ document.addEventListener("drop", onDocumentDrop);
888
+ }
889
+ function onDocumentDrop(e) {
890
+ const dt = e.dataTransfer;
891
+ if (dt === null || !dt.types.includes("application/x-dsh-workbench-files")) return;
892
+ const paths = readDraggedPaths(dt);
893
+ if (paths.length === 0) return;
894
+ if (!insertIntoComposer(paths.join("\n"))) return;
895
+ e.preventDefault();
896
+ e.stopPropagation();
897
+ }
898
+ /** Read the JSON paths from the custom type; falls back to raw text. */
899
+ function readDraggedPaths(dt) {
900
+ if (dt === null) return [];
901
+ let raw = "";
902
+ try {
903
+ raw = dt.getData(DRAG_TYPE);
904
+ } catch {
905
+ return [];
906
+ }
907
+ try {
908
+ const parsed = JSON.parse(raw);
909
+ if (Array.isArray(parsed) && parsed.every((p) => typeof p === "string")) return parsed;
910
+ } catch {}
911
+ return raw.trim().length > 0 ? [raw] : [];
912
+ }
913
+ /** The visible composer textarea, or null when none is usable. */
914
+ function composerTextarea() {
915
+ if (typeof document === "undefined") return null;
916
+ const seat = document.querySelector("[data-composer-seat]");
917
+ if (seat === null) return null;
918
+ const input = seat.querySelector("textarea");
919
+ if (!(input instanceof HTMLTextAreaElement)) return null;
920
+ if (input.disabled || input.readOnly) return null;
921
+ return input;
922
+ }
923
+ /**
924
+ * Insert `text` into the composer. If the composer textarea has focus, the
925
+ * text goes at the caret; otherwise it is appended to the draft. Returns
926
+ * false when no usable composer is present.
927
+ */
928
+ function insertIntoComposer(text) {
929
+ const textarea = composerTextarea();
930
+ if (textarea === null) return false;
931
+ const value = textarea.value;
932
+ const atCaret = document.activeElement === textarea;
933
+ const start = atCaret ? textarea.selectionStart : value.length;
934
+ const end = atCaret ? textarea.selectionEnd : value.length;
935
+ const next = value.slice(0, start) + text + value.slice(end);
936
+ const setter = Object.getOwnPropertyDescriptor(HTMLTextAreaElement.prototype, "value")?.set;
937
+ if (setter !== void 0) setter.call(textarea, next);
938
+ else textarea.value = next;
939
+ textarea.dispatchEvent(new Event("input", { bubbles: true }));
940
+ const caret = start + text.length;
941
+ requestAnimationFrame(() => {
942
+ try {
943
+ textarea.setSelectionRange(caret, caret);
944
+ } catch {}
945
+ });
946
+ return true;
947
+ }
948
+ /** Relative workspace path of `path` under `cwd` ('' when it IS the cwd). */
949
+ function relPathOf(path, cwd) {
950
+ if (cwd === void 0 || cwd.length === 0) return path;
951
+ const sep = cwd.includes("\\") ? "\\" : "/";
952
+ const prefix = cwd.endsWith("\\") || cwd.endsWith("/") ? cwd : cwd + sep;
953
+ const lowerPath = path.toLowerCase();
954
+ if (lowerPath.startsWith(prefix.toLowerCase())) return path.slice(prefix.length);
955
+ if (lowerPath === cwd.toLowerCase()) return "";
956
+ return path;
957
+ }
958
+ /**
959
+ * Resolve an @-mention path (workspace-relative or absolute) against the
960
+ * current workspace cwd; returns the absolute OS path, or undefined when the
961
+ * mention cannot be resolved (no cwd and the path is not absolute).
962
+ */
963
+ function resolveMentionPath(mention) {
964
+ const absolute = /^[A-Za-z]:[\\/]/.test(mention) || mention.startsWith("/") || mention.startsWith("\\");
965
+ const { cwd } = getTabsState();
966
+ if (absolute) return mention;
967
+ if (cwd === void 0) return void 0;
968
+ const sep = cwd.includes("\\") ? "\\" : "/";
969
+ return cwd.endsWith("\\") || cwd.endsWith("/") ? cwd + mention : cwd + sep + mention;
970
+ }
971
+ /** Open an @-mention's file in the workbench preview (used by the linkifier). */
972
+ function openMention(mention) {
973
+ const abs = resolveMentionPath(mention);
974
+ if (abs === void 0) return;
975
+ openFile(abs);
976
+ }
977
+ //#endregion
858
978
  //#region src/client/FileExplorer.tsx
859
979
  /**
860
980
  * File tree column, registered into the `explorer` slot declared by the
@@ -1039,6 +1159,19 @@ window.__ModuleLoader__.load({
1039
1159
  window.removeEventListener("blur", onBlur);
1040
1160
  };
1041
1161
  }, [menu]);
1162
+ (0, react.useEffect)(() => {
1163
+ const onDocMouseDown = (e) => {
1164
+ const target = e.target;
1165
+ if (!(target instanceof Node)) return;
1166
+ const tree = treeAreaRef.current;
1167
+ if (tree !== null && tree.contains(target)) return;
1168
+ const m = menuRef.current;
1169
+ if (m !== null && m.contains(target)) return;
1170
+ setSelected({});
1171
+ };
1172
+ document.addEventListener("mousedown", onDocMouseDown);
1173
+ return () => document.removeEventListener("mousedown", onDocMouseDown);
1174
+ }, []);
1042
1175
  const treeRef = (0, react.useRef)({
1043
1176
  root,
1044
1177
  children,
@@ -1217,6 +1350,19 @@ window.__ModuleLoader__.load({
1217
1350
  y: e.clientY
1218
1351
  });
1219
1352
  }, []);
1353
+ /** Right-click anywhere else in the column (header, clipboard bar, blank tree space): folder menu for the cwd. */
1354
+ const onColumnContextMenu = (0, react.useCallback)((e) => {
1355
+ e.preventDefault();
1356
+ const el = menuRef.current;
1357
+ if (el !== null && e.target instanceof Node && el.contains(e.target)) return;
1358
+ if (cwd === void 0) return;
1359
+ setMenu({
1360
+ kind: "blank",
1361
+ path: cwd,
1362
+ x: e.clientX,
1363
+ y: e.clientY
1364
+ });
1365
+ }, [cwd]);
1220
1366
  const runAction = (0, react.useCallback)((action) => {
1221
1367
  setMenu(void 0);
1222
1368
  action();
@@ -1565,9 +1711,10 @@ window.__ModuleLoader__.load({
1565
1711
  const paths = selected[entry.path] !== void 0 ? Object.keys(selected) : [entry.path];
1566
1712
  dragPathsRef.current = paths;
1567
1713
  try {
1568
- e.dataTransfer.setData("text/plain", entry.path);
1714
+ e.dataTransfer.setData("text/plain", paths.join("\n"));
1715
+ e.dataTransfer.setData(DRAG_TYPE, JSON.stringify(paths));
1569
1716
  } catch {}
1570
- e.dataTransfer.effectAllowed = "move";
1717
+ e.dataTransfer.effectAllowed = "copyMove";
1571
1718
  }, [selected]);
1572
1719
  const onRowDragEnd = (0, react.useCallback)(() => {
1573
1720
  dragPathsRef.current = null;
@@ -1646,6 +1793,30 @@ window.__ModuleLoader__.load({
1646
1793
  if (entry.kind !== "file") return;
1647
1794
  openPath(entry.path);
1648
1795
  }, [openPath]);
1796
+ /** Explorer-style "Select All": every visible (non-trash) entry at any depth. */
1797
+ const selectAllEntries = (0, react.useCallback)(() => {
1798
+ const all = {};
1799
+ const collect = (entries) => {
1800
+ for (const entry of entries) {
1801
+ if (entry.name === TRASH_NAME) continue;
1802
+ all[entry.path] = entry.kind;
1803
+ if (entry.kind === "dir" && expanded.has(entry.path) && children[entry.path] !== void 0) collect(children[entry.path]);
1804
+ }
1805
+ };
1806
+ const rootEntries = root !== void 0 ? children[root] : void 0;
1807
+ if (rootEntries !== void 0) collect(rootEntries);
1808
+ setSelected(all);
1809
+ }, [
1810
+ children,
1811
+ expanded,
1812
+ root
1813
+ ]);
1814
+ /** Insert `@<relative-workspace-path>` into the composer (Claude Code-style mention). */
1815
+ const onMention = (0, react.useCallback)((path) => {
1816
+ const mention = relPathOf(path, cwd);
1817
+ if (mention.length === 0) return;
1818
+ insertIntoComposer(`@${mention} `);
1819
+ }, [cwd]);
1649
1820
  const onTreeKeyDown = (0, react.useCallback)((e) => {
1650
1821
  const mod = e.ctrlKey || e.metaKey;
1651
1822
  const key = e.key.toLowerCase();
@@ -1677,17 +1848,7 @@ window.__ModuleLoader__.load({
1677
1848
  }
1678
1849
  if (mod && key === "a") {
1679
1850
  e.preventDefault();
1680
- const all = {};
1681
- const collect = (entries) => {
1682
- for (const entry of entries) {
1683
- if (entry.name === TRASH_NAME) continue;
1684
- all[entry.path] = entry.kind;
1685
- if (entry.kind === "dir" && expanded.has(entry.path) && children[entry.path] !== void 0) collect(children[entry.path]);
1686
- }
1687
- };
1688
- const rootEntries = root !== void 0 ? children[root] : void 0;
1689
- if (rootEntries !== void 0) collect(rootEntries);
1690
- setSelected(all);
1851
+ selectAllEntries();
1691
1852
  return;
1692
1853
  }
1693
1854
  if (mod && key === "z" && !e.shiftKey) {
@@ -1708,13 +1869,11 @@ window.__ModuleLoader__.load({
1708
1869
  }
1709
1870
  }, [
1710
1871
  cancelCut,
1711
- children,
1712
1872
  clipboard.items.length,
1713
1873
  deleteSelection,
1714
- expanded,
1715
1874
  onPaste,
1716
1875
  performUndo,
1717
- root,
1876
+ selectAllEntries,
1718
1877
  selectedItems,
1719
1878
  undoEntries.length
1720
1879
  ]);
@@ -1785,9 +1944,13 @@ window.__ModuleLoader__.load({
1785
1944
  } : {
1786
1945
  label: t("menu.delete"),
1787
1946
  danger: true,
1788
- onClick: () => onDelete(m.path, m.kind)
1947
+ onClick: () => onDelete(m.path, m.kind === "blank" ? "dir" : m.kind)
1789
1948
  };
1790
1949
  if (m.kind === "file") return [
1950
+ {
1951
+ label: t("menu.atFile"),
1952
+ onClick: () => onMention(m.path)
1953
+ },
1791
1954
  {
1792
1955
  label: t("menu.open"),
1793
1956
  onClick: () => openFile(m.path)
@@ -1868,6 +2031,51 @@ window.__ModuleLoader__.load({
1868
2031
  },
1869
2032
  deleteItem
1870
2033
  ];
2034
+ if (m.kind === "blank") {
2035
+ const dirPath = m.path;
2036
+ return [
2037
+ {
2038
+ label: t("menu.newFile"),
2039
+ onClick: () => onNewFile(dirPath)
2040
+ },
2041
+ {
2042
+ label: t("menu.newFolder"),
2043
+ onClick: () => onNewFolder(dirPath)
2044
+ },
2045
+ {
2046
+ label: t("menu.paste"),
2047
+ disabled: clipboard.items.length === 0,
2048
+ onClick: () => void onPaste()
2049
+ },
2050
+ "divider",
2051
+ {
2052
+ label: t("menu.selectAll"),
2053
+ onClick: selectAllEntries
2054
+ },
2055
+ {
2056
+ label: t("menu.undo"),
2057
+ disabled: undoEntries.length === 0,
2058
+ onClick: () => void performUndo()
2059
+ },
2060
+ "divider",
2061
+ {
2062
+ label: t("menu.copyPath"),
2063
+ onClick: () => onCopyPath(dirPath)
2064
+ },
2065
+ {
2066
+ label: t("menu.revealInExplorer"),
2067
+ onClick: () => onReveal(dirPath, "dir")
2068
+ },
2069
+ {
2070
+ label: t("menu.openSystem"),
2071
+ onClick: () => void openPath(dirPath)
2072
+ },
2073
+ {
2074
+ label: t("menu.refresh"),
2075
+ onClick: () => refreshDir(dirPath)
2076
+ }
2077
+ ];
2078
+ }
1871
2079
  return [{
1872
2080
  label: t("menu.paste"),
1873
2081
  disabled: clipboard.items.length === 0,
@@ -1879,6 +2087,7 @@ window.__ModuleLoader__.load({
1879
2087
  style: { width: width > 0 ? width : void 0 },
1880
2088
  "data-pane": "explorer",
1881
2089
  "data-fe-theme": theme,
2090
+ onContextMenu: onColumnContextMenu,
1882
2091
  children: [
1883
2092
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1884
2093
  className: files_module_css_default.header,
@@ -1947,7 +2156,7 @@ window.__ModuleLoader__.load({
1947
2156
  className: files_module_css_default.treeArea,
1948
2157
  onKeyDown: onTreeKeyDown,
1949
2158
  onClick: (e) => {
1950
- if (e.target === e.currentTarget) setSelected({});
2159
+ if (e.target.closest("[role=\"treeitem\"]") === null) setSelected({});
1951
2160
  },
1952
2161
  onDragOver: (e) => {
1953
2162
  if (dragPathsRef.current === null) return;
@@ -1977,6 +2186,10 @@ window.__ModuleLoader__.load({
1977
2186
  ref: menuRef,
1978
2187
  className: files_module_css_default.contextMenu,
1979
2188
  role: "menu",
2189
+ onContextMenu: (e) => {
2190
+ e.preventDefault();
2191
+ e.stopPropagation();
2192
+ },
1980
2193
  style: menuPos !== void 0 ? {
1981
2194
  left: menuPos.x,
1982
2195
  top: menuPos.y
@@ -15677,6 +15890,7 @@ window.__ModuleLoader__.load({
15677
15890
  "tab.expand": "弹出文件详情",
15678
15891
  "tab.diskChanged": "文件已在磁盘上被修改,点击重新加载(会放弃未保存的编辑)",
15679
15892
  "menu.open": "打开预览",
15893
+ "menu.atFile": "@ 在消息中引用",
15680
15894
  "menu.newFile": "新建文件",
15681
15895
  "menu.newFolder": "新建文件夹",
15682
15896
  "menu.rename": "重命名",
@@ -15689,6 +15903,8 @@ window.__ModuleLoader__.load({
15689
15903
  "menu.openSystem": "在系统中打开",
15690
15904
  "menu.revealInExplorer": "在资源管理器打开",
15691
15905
  "menu.deleteSelected": "删除选中的 {count} 项",
15906
+ "menu.selectAll": "全选",
15907
+ "menu.undo": "撤销",
15692
15908
  "action.undo": "撤销(Ctrl+Z)",
15693
15909
  "undo.copy": "复制「{name}」",
15694
15910
  "undo.move": "移动「{name}」",
@@ -15732,6 +15948,7 @@ window.__ModuleLoader__.load({
15732
15948
  "tab.expand": "Expand file details",
15733
15949
  "tab.diskChanged": "File changed on disk — click to reload (discards unsaved edits)",
15734
15950
  "menu.open": "Open preview",
15951
+ "menu.atFile": "@ Mention in message",
15735
15952
  "menu.newFile": "New File",
15736
15953
  "menu.newFolder": "New Folder",
15737
15954
  "menu.rename": "Rename",
@@ -15744,6 +15961,8 @@ window.__ModuleLoader__.load({
15744
15961
  "menu.openSystem": "Open in System",
15745
15962
  "menu.revealInExplorer": "Reveal in Explorer",
15746
15963
  "menu.deleteSelected": "Delete {count} selected item(s)",
15964
+ "menu.selectAll": "Select All",
15965
+ "menu.undo": "Undo",
15747
15966
  "action.undo": "Undo (Ctrl+Z)",
15748
15967
  "undo.copy": "Copy \"{name}\"",
15749
15968
  "undo.move": "Move \"{name}\"",
@@ -15772,6 +15991,211 @@ window.__ModuleLoader__.load({
15772
15991
  "preview.mdRenderOff": "Large file: source view (still editable)"
15773
15992
  };
15774
15993
  //#endregion
15994
+ //#region src/client/mentions.ts
15995
+ /**
15996
+ * @-mention linkifier for the conversation.
15997
+ *
15998
+ * The workbench inserts `@<relative-workspace-path>` into the composer (menu
15999
+ * gesture "在消息中引用"), and this module makes the mention VISIBLE as a
16000
+ * hyperlink once the message is rendered: any `@` followed by a token that
16001
+ * matches the mention grammar is wrapped in an anchor, and clicking it opens
16002
+ * the file in the workbench preview.
16003
+ *
16004
+ * Grammar (anything else stays plain text with no special meaning):
16005
+ * - `@` must sit at a token boundary (start of text, whitespace, or
16006
+ * punctuation like `((` `,,` `。.` quotes and brackets) — so `user@x`,
16007
+ * `a.b@c` and URLs never match;
16008
+ * - the token is the longest run of non-whitespace, non-`@` characters,
16009
+ * with trailing sentence punctuation trimmed (`.。 ,, ;; :: !! ?? ))`…);
16010
+ * - the remaining token must be a RELATIVE path (never drive-absolute or
16011
+ * leading-slash), and path-shaped: either contains a `/` or `\` directory
16012
+ * separator, or is a single segment ending in a file extension.
16013
+ *
16014
+ * Scanning mirrors the table-zoom enhancer: a MutationObserver on
16015
+ * `document.body`, rAF-coalesced, walks the text nodes of every
16016
+ * `[data-conversation-scroll]` subtree and skips protected containers
16017
+ * (code blocks, existing links, aria-hidden overlays like the composer
16018
+ * mirror/backdrop, the composer seat, popups, and the workbench column).
16019
+ */
16020
+ /** Mention pattern: `@` + token (no whitespace, no embedded `@`). */
16021
+ const MENTION_RE = /@([^\s@]+)/g;
16022
+ /** Trailing characters trimmed from a mention token before validation. */
16023
+ const TRAILING = /* @__PURE__ */ new Set([
16024
+ ".",
16025
+ ",",
16026
+ ";",
16027
+ ":",
16028
+ "!",
16029
+ "?",
16030
+ "。",
16031
+ ",",
16032
+ ";",
16033
+ ":",
16034
+ "!",
16035
+ "?",
16036
+ ")",
16037
+ ")",
16038
+ "]",
16039
+ "】",
16040
+ "}",
16041
+ "》",
16042
+ "」",
16043
+ "』",
16044
+ "\"",
16045
+ "'"
16046
+ ]);
16047
+ /** Characters that may legally precede `@` in a mention (start/whitespace/punctuation). */
16048
+ function isBoundaryBefore(ch) {
16049
+ if (ch === void 0) return true;
16050
+ return /[\s(([【「『"'`、,。;:!?,.!?:;>\])})]/.test(ch);
16051
+ }
16052
+ /** Whether `token` (already trailing-trimmed) matches the relative-path grammar. */
16053
+ function isMentionToken(token) {
16054
+ if (token.length === 0) return false;
16055
+ if (/[。,、;:!?()、【】「」『』《》]/.test(token)) return false;
16056
+ if (/^[A-Za-z]:[\\/]/.test(token)) return false;
16057
+ if (token.startsWith("/") || token.startsWith("\\")) return false;
16058
+ if (token.startsWith("..")) return false;
16059
+ if (token.includes("/") || token.includes("\\")) return true;
16060
+ return /^[^\\/]+\.[A-Za-z0-9_][A-Za-z0-9._~+-]*$/.test(token);
16061
+ }
16062
+ /** Trim trailing sentence punctuation from a raw mention token. */
16063
+ function trimMentionToken(raw) {
16064
+ let token = raw;
16065
+ while (token.length > 0 && TRAILING.has(token[token.length - 1])) token = token.slice(0, -1);
16066
+ return token;
16067
+ }
16068
+ /**
16069
+ * Extract every valid mention from `text` as [start, end, mention] ranges.
16070
+ * `end` covers `@` + the TRIMMED token (trailing punctuation stays outside the
16071
+ * link). Pure and testable: the DOM walk uses it and then splits the text node.
16072
+ */
16073
+ function findMentions(text) {
16074
+ const hits = [];
16075
+ MENTION_RE.lastIndex = 0;
16076
+ let match;
16077
+ while ((match = MENTION_RE.exec(text)) !== null) {
16078
+ const at = match.index;
16079
+ const raw = match[1];
16080
+ if (!isBoundaryBefore(at > 0 ? text[at - 1] : void 0)) continue;
16081
+ const token = trimMentionToken(raw);
16082
+ if (!isMentionToken(token)) continue;
16083
+ hits.push({
16084
+ start: at,
16085
+ end: at + 1 + token.length,
16086
+ mention: token
16087
+ });
16088
+ }
16089
+ return hits;
16090
+ }
16091
+ /** Containers whose text is never linkified (code, existing links, overlays). */
16092
+ const SKIP_SELECTOR = [
16093
+ "code",
16094
+ "pre",
16095
+ "a",
16096
+ "script",
16097
+ "style",
16098
+ "textarea",
16099
+ "[data-composer-seat]",
16100
+ "[data-input-mirror]",
16101
+ "[data-input-backdrop]",
16102
+ "[data-pane=\"explorer\"]",
16103
+ "[aria-hidden=\"true\"]",
16104
+ ".dstz-popup",
16105
+ ".dshpick-lightbox",
16106
+ "[data-wb-mention]"
16107
+ ].join(",");
16108
+ /** Walk the text nodes of one conversation scroll container and linkify. */
16109
+ function linkifyRoot(root) {
16110
+ const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT);
16111
+ const toCheck = [];
16112
+ let node = walker.nextNode();
16113
+ while (node !== null) {
16114
+ if (node.parentElement !== null && node.parentElement.closest(SKIP_SELECTOR) === null) toCheck.push(node);
16115
+ node = walker.nextNode();
16116
+ }
16117
+ for (const textNode of toCheck) linkifyTextNode(textNode);
16118
+ }
16119
+ /** Wrap every mention in one text node; returns true when anything changed. */
16120
+ function linkifyTextNode(node) {
16121
+ const text = node.data;
16122
+ const hits = findMentions(text);
16123
+ if (hits.length === 0) return false;
16124
+ const frag = document.createDocumentFragment();
16125
+ let cursor = 0;
16126
+ for (const hit of hits) {
16127
+ if (hit.start > cursor) frag.appendChild(document.createTextNode(text.slice(cursor, hit.start)));
16128
+ const anchor = document.createElement("a");
16129
+ anchor.className = "dswb-mention";
16130
+ anchor.dataset.wbMention = hit.mention;
16131
+ anchor.textContent = text.slice(hit.start, hit.end);
16132
+ anchor.title = `@${hit.mention}`;
16133
+ frag.appendChild(anchor);
16134
+ cursor = hit.end;
16135
+ }
16136
+ if (cursor < text.length) frag.appendChild(document.createTextNode(text.slice(cursor)));
16137
+ node.parentNode?.replaceChild(frag, node);
16138
+ return true;
16139
+ }
16140
+ /** Style tag guard (the bundle may re-apply on HMR). */
16141
+ let styleInstalled = false;
16142
+ const MENTION_CSS = [".dswb-mention{color:var(--dsw-alias-state-business-primary);text-decoration:underline;text-underline-offset:2px;cursor:pointer;border-radius:4px;padding:0 1px}", ".dswb-mention:hover{background:var(--dsw-alias-interactive-bg-hover)}"].join("");
16143
+ function installStyle() {
16144
+ if (styleInstalled || typeof document === "undefined") return;
16145
+ styleInstalled = true;
16146
+ const tagId = "dsh-plugin-workbench/mention.module.css";
16147
+ if (document.querySelector(`style[data-plugin-css=${JSON.stringify(tagId)}]`) === null) {
16148
+ const tag = document.createElement("style");
16149
+ tag.dataset.plugin = "dsh-plugin-workbench";
16150
+ tag.dataset.pluginCss = tagId;
16151
+ tag.textContent = MENTION_CSS;
16152
+ document.head.appendChild(tag);
16153
+ }
16154
+ }
16155
+ /** Click handler: open the mentioned file in the workbench preview. */
16156
+ function onDocumentClick(e) {
16157
+ const target = e.target;
16158
+ if (!(target instanceof Element)) return;
16159
+ const anchor = target.closest("a.dswb-mention");
16160
+ if (anchor === null) return;
16161
+ const mention = anchor.getAttribute("data-wb-mention");
16162
+ if (mention === null) return;
16163
+ e.preventDefault();
16164
+ e.stopPropagation();
16165
+ openMention(mention);
16166
+ }
16167
+ /** One-time install guard (HMR re-applies must not double-observe). */
16168
+ let linkifierInstalled = false;
16169
+ /** Start the linkifier: initial scan + MutationObserver with rAF coalescing. */
16170
+ function installMentionLinkifier() {
16171
+ if (typeof document === "undefined" || typeof MutationObserver === "undefined") return () => void 0;
16172
+ if (linkifierInstalled) return () => void 0;
16173
+ linkifierInstalled = true;
16174
+ installStyle();
16175
+ document.addEventListener("click", onDocumentClick);
16176
+ let pending = false;
16177
+ const scan = () => {
16178
+ pending = false;
16179
+ for (const root of document.querySelectorAll("[data-conversation-scroll]")) linkifyRoot(root);
16180
+ };
16181
+ const observer = new MutationObserver(() => {
16182
+ if (pending) return;
16183
+ pending = true;
16184
+ requestAnimationFrame(scan);
16185
+ });
16186
+ observer.observe(document.body, {
16187
+ childList: true,
16188
+ subtree: true,
16189
+ characterData: true
16190
+ });
16191
+ scan();
16192
+ return () => {
16193
+ observer.disconnect();
16194
+ document.removeEventListener("click", onDocumentClick);
16195
+ linkifierInstalled = false;
16196
+ };
16197
+ }
16198
+ //#endregion
15775
16199
  //#region src/client/index.ts
15776
16200
  const CHANNEL = "/dsh-plugin-files";
15777
16201
  /** Required client services (sequencing only — data reads use global hooks). */
@@ -15787,6 +16211,8 @@ window.__ModuleLoader__.load({
15787
16211
  zh,
15788
16212
  en
15789
16213
  }), "files-explorer: dictionaries");
16214
+ installComposerDrops();
16215
+ ctx.effect(() => installMentionLinkifier(), "files-explorer: @mention linkifier");
15790
16216
  const listDir = (path, signal) => unwrap(ctx.connection.rpc.call(CHANNEL, "list", { path }, signal));
15791
16217
  const readFile = (path, signal) => unwrap(ctx.connection.rpc.call(CHANNEL, "read", { path }, signal));
15792
16218
  const writeFile = (path, content, signal) => unwrap(ctx.connection.rpc.call(CHANNEL, "write", {