dsh-plugin-workbench 0.0.9 → 0.0.10
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 +28 -0
- package/README.md +8 -2
- package/lib/client.js +444 -18
- package/lib/client.js.map +1 -1
- package/lib/index.js +17 -5
- package/package.json +1 -1
- package/src/client/FileExplorer.tsx +106 -25
- package/src/client/composer.ts +139 -0
- package/src/client/index.ts +7 -0
- package/src/client/locales.ts +6 -0
- package/src/client/mentions.ts +207 -0
- package/src/index.ts +19 -6
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,34 @@
|
|
|
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.10] - 2026-08-21
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
|
|
11
|
+
- **「在资源管理器打开」不生效(窗口打开但不可见)**:Windows 下 `spawn` 传
|
|
12
|
+
`windowsHide: true` 会让新开的资源管理器文件夹窗口以**隐藏状态**创建
|
|
13
|
+
(CREATE_NO_WINDOW 沿请求一路传导,窗口存在但 `visible=False`,用户什么都
|
|
14
|
+
看不到)。`explorer.exe` 是 GUI 子系统程序,不带 `windowsHide` 也不会闪
|
|
15
|
+
控制台;`reveal` 端点现在对 explorer.exe 全部不传该标志(含 WSL 转译分支),
|
|
16
|
+
资源管理器窗口正常弹出并选中目标文件
|
|
17
|
+
|
|
18
|
+
### Added
|
|
19
|
+
|
|
20
|
+
- **整列右键菜单**:不只文件行——在左栏任何空白处、头部、剪贴板条上右键
|
|
21
|
+
都弹出当前工作区目录菜单(新建文件 / 新建文件夹 / 粘贴 / 全选 / 撤销 /
|
|
22
|
+
复制路径 / 在资源管理器打开 / 在系统中打开 / 刷新);右键菜单自身不再触发
|
|
23
|
+
浏览器原生菜单
|
|
24
|
+
- **@ 在消息中引用(Claude Code 式文件引用)**:文件右键菜单新增
|
|
25
|
+
「@ 在消息中引用」,把 `@相对工作区路径`(自动带尾随空格)插入聊天输入框
|
|
26
|
+
光标处;发送后消息里的 `@相对路径` 渲染为超链接(点击在工作台预览中打开
|
|
27
|
+
该文件),语法为 `@` + 相对工作区路径——`@src/a.ts`、`@README.md` 匹配,
|
|
28
|
+
绝对路径 / 邮箱 / 无路径形状的普通 `@文本` 一律按原样显示、无特殊含义;
|
|
29
|
+
中文句子标点跟尾视为普通文本
|
|
30
|
+
- **拖文件进会话**:把左栏文件(含多选)拖到聊天区任意位置(不在左栏内)
|
|
31
|
+
即把路径文本插入聊天输入框光标处;拖到左栏目录行仍是原有的移动操作
|
|
32
|
+
- **任意位置点击取消选中**:选中文件/文件夹后,点击树外任何地方(会话、
|
|
33
|
+
其他栏、左栏头部)都会清除选中;树内空白/提示行点击同样取消
|
|
34
|
+
|
|
7
35
|
## [0.0.9] - 2026-08-21
|
|
8
36
|
|
|
9
37
|
### 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
|
剪切(移动)成功后自动清空剪贴板
|
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",
|
|
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 = "
|
|
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
|
-
|
|
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
|
-
|
|
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 ===
|
|
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", {
|