dsh-plugin-workbench 0.0.11 → 0.0.13
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 +27 -0
- package/README.md +1 -0
- package/lib/client.js +346 -41
- package/lib/client.js.map +1 -1
- package/lib/index.js +35 -1
- package/package.json +1 -1
- package/src/client/FileExplorer.tsx +6 -3
- package/src/client/FilePreview.tsx +106 -6
- package/src/client/composer.ts +57 -14
- package/src/client/composerMentions.ts +166 -0
- package/src/client/index.ts +5 -2
- package/src/client/locales.ts +2 -2
- package/src/client/mentions.ts +44 -11
- package/src/dsh.d.ts +12 -0
- package/src/index.ts +37 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,32 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.0.13] - 2026-08-26
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
|
|
7
|
+
- **预览分栏宽度拖拽:一拖就骤然缩小到最窄,之后无法拖动、无法复原**。根因:
|
|
8
|
+
拖拽计算"预览最大宽度"时用 `handle.parentElement` 测量中间列宽度,但 slot 系统把
|
|
9
|
+
slot 内容包在一层 `display: contents` 的包装元素里——该包装层不参与布局,
|
|
10
|
+
`getBoundingClientRect().width` 恒为 **0**,于是
|
|
11
|
+
`max = max(240, 0 - 240) = 240`,第一次 `pointermove` 就把宽度钳到最小宽度 240,
|
|
12
|
+
之后每次移动都被钳在 240,表现为骤然缩小且拖不回来(100% 复现,与窗口/鼠标操作
|
|
13
|
+
无关)。修复:`laidOutParent()` 跳过所有 `display: contents` 层,取真正参与布局的
|
|
14
|
+
flex 容器(中间列)再测量;持久化宽度恢复时同样用它校准。
|
|
15
|
+
- 拖拽健壮性(防同类"卡死"):`setPointerCapture` 保证 `pointerup` 在指针移出窗口后
|
|
16
|
+
也能派发并清理监听器;监听 `pointercancel`;每次 `pointerdown` 防御性移除上一次
|
|
17
|
+
残留的监听器;组件卸载时清理窗口监听器;拖拽中每步实时重测中间列宽度。
|
|
18
|
+
|
|
19
|
+
### Added
|
|
20
|
+
|
|
21
|
+
- **预览宽度持久化**:拖拽后的分栏宽度写入 localStorage,刷新/重开会话后恢复
|
|
22
|
+
(自动按当前窗宽 clamp 在有效区间内)。
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
## [0.0.12] - 2026-08-25
|
|
26
|
+
|
|
27
|
+
- 相关插件段新增 dsh-plugin-windows-guard(Windows 环境防坑守则 skill 插件,互相引流)。
|
|
28
|
+
|
|
29
|
+
|
|
3
30
|
本项目所有重要变更都会记录在此文件。
|
|
4
31
|
格式基于 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.1.0/),
|
|
5
32
|
版本号遵循 [Semantic Versioning](https://semver.org/lang/zh-CN/)。
|
package/README.md
CHANGED
|
@@ -140,6 +140,7 @@ dsh plugin --profile web remove dsh-plugin-workbench
|
|
|
140
140
|
| [dsh-plugin-dev-kb](https://www.npmjs.com/package/dsh-plugin-dev-kb) | [GitHub 仓库](https://github.com/Pasumao/dsh-plugin-dev-kb) | 插件开发知识库(官方文档完整镜像 + 技能) |
|
|
141
141
|
| [dsh-plugin-image-tools](https://www.npmjs.com/package/dsh-plugin-image-tools) | [GitHub 仓库](https://github.com/Pasumao/dsh-plugin-image-tools) | 图片选择卡 + 回复内嵌图片 + 盲模型收图 |
|
|
142
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
|
+
| [dsh-plugin-windows-guard](https://www.npmjs.com/package/dsh-plugin-windows-guard) | [GitHub 仓库](https://github.com/Pasumao/dsh-plugin-windows-guard) | Windows 环境防坑守则 skill(编码/转义/路径/进程/乱码预防) |
|
|
143
144
|
|
|
144
145
|
> 本系列其余插件见 [Pasumao · dsh 插件](https://github.com/Pasumao);觉得好用欢迎到 GitHub 点 ⭐。
|
|
145
146
|
|
package/lib/client.js
CHANGED
|
@@ -859,17 +859,24 @@ window.__ModuleLoader__.load({
|
|
|
859
859
|
/**
|
|
860
860
|
* Composer integration for the workbench file column.
|
|
861
861
|
*
|
|
862
|
-
*
|
|
862
|
+
* Three gestures land text in the chat composer without touching the core:
|
|
863
863
|
*
|
|
864
864
|
* 1. Drag & drop — dragging one or more tree rows and dropping ANYWHERE
|
|
865
865
|
* outside the file column (the chat, the composer, the message list)
|
|
866
|
-
* inserts the dragged paths into the composer
|
|
867
|
-
*
|
|
868
|
-
*
|
|
869
|
-
*
|
|
866
|
+
* inserts the dragged paths into the composer as `@.\` mentions (falling
|
|
867
|
+
* back to the absolute path when the file sits outside the workspace).
|
|
868
|
+
* Dropping INSIDE the file column still performs the tree's own move
|
|
869
|
+
* operation — the tree's drop handler runs first and stops propagation, so
|
|
870
|
+
* this document-level listener never sees those drops.
|
|
870
871
|
*
|
|
871
|
-
* 2. Context-menu "@引用" — inserts
|
|
872
|
-
* composer caret
|
|
872
|
+
* 2. Context-menu "@引用" — inserts `@.\<relative-workspace-path>` at the
|
|
873
|
+
* composer caret (the `.\` prefix marks the path as workspace-relative; a
|
|
874
|
+
* path containing whitespace uses the quoted `@"\.\path with space"` form).
|
|
875
|
+
*
|
|
876
|
+
* 3. @-mention resolution — turns a mention token (with or without the `.\`
|
|
877
|
+
* prefix, quoted or plain) into an absolute path against the session cwd;
|
|
878
|
+
* used by the message linkifier and the composer overlay to open the file
|
|
879
|
+
* in the workbench preview.
|
|
873
880
|
*
|
|
874
881
|
* The composer is a controlled React textarea, so the value is updated
|
|
875
882
|
* through the native `value` setter + a bubbling `input` event (the standard
|
|
@@ -891,10 +898,21 @@ window.__ModuleLoader__.load({
|
|
|
891
898
|
if (dt === null || !dt.types.includes("application/x-dsh-workbench-files")) return;
|
|
892
899
|
const paths = readDraggedPaths(dt);
|
|
893
900
|
if (paths.length === 0) return;
|
|
894
|
-
if (!insertIntoComposer(paths.join("\n"))) return;
|
|
901
|
+
if (!insertIntoComposer(paths.map(dragMentionText).join("\n"))) return;
|
|
895
902
|
e.preventDefault();
|
|
896
903
|
e.stopPropagation();
|
|
897
904
|
}
|
|
905
|
+
/**
|
|
906
|
+
* One dropped path as chat text: an `@.\` mention when it lives under the
|
|
907
|
+
* workspace, the absolute path otherwise (or when no cwd is known yet).
|
|
908
|
+
*/
|
|
909
|
+
function dragMentionText(path) {
|
|
910
|
+
const { cwd } = getTabsState();
|
|
911
|
+
if (cwd === void 0) return path;
|
|
912
|
+
const rel = relPathOf(path, cwd);
|
|
913
|
+
if (rel.length === 0 || rel === path) return path;
|
|
914
|
+
return composerMention(rel);
|
|
915
|
+
}
|
|
898
916
|
/** Read the JSON paths from the custom type; falls back to raw text. */
|
|
899
917
|
function readDraggedPaths(dt) {
|
|
900
918
|
if (dt === null) return [];
|
|
@@ -956,17 +974,38 @@ window.__ModuleLoader__.load({
|
|
|
956
974
|
return path;
|
|
957
975
|
}
|
|
958
976
|
/**
|
|
959
|
-
*
|
|
960
|
-
*
|
|
961
|
-
*
|
|
977
|
+
* Format a workspace-relative path as an `@` mention for the composer: the
|
|
978
|
+
* `.\` (or `./`) prefix marks the path as relative to the workspace root, and
|
|
979
|
+
* a path containing whitespace uses the quoted `@"..."` form so it stays one
|
|
980
|
+
* token in the draft (and one link in the rendered message).
|
|
981
|
+
*/
|
|
982
|
+
function composerMention(rel) {
|
|
983
|
+
const raw = `@.${rel.includes("\\") ? "\\" : "/"}${rel}`;
|
|
984
|
+
if (/[\s"]/.test(raw)) return `@"${raw}"`;
|
|
985
|
+
return raw;
|
|
986
|
+
}
|
|
987
|
+
/** Strip quote wrapping and a leading `.\` / `./` workspace marker. */
|
|
988
|
+
function normalizeMention(mention) {
|
|
989
|
+
let inner = mention;
|
|
990
|
+
if (inner.startsWith("\"") && inner.endsWith("\"") && inner.length >= 2) inner = inner.slice(1, -1);
|
|
991
|
+
if (inner.startsWith(".\\") || inner.startsWith("./")) inner = inner.slice(2);
|
|
992
|
+
return inner;
|
|
993
|
+
}
|
|
994
|
+
/**
|
|
995
|
+
* Resolve an @-mention path (workspace-relative with or without the `.\`
|
|
996
|
+
* marker, or absolute) against the current workspace cwd; returns the absolute
|
|
997
|
+
* OS path, or undefined when the mention cannot be resolved (no cwd and the
|
|
998
|
+
* path is not absolute).
|
|
962
999
|
*/
|
|
963
1000
|
function resolveMentionPath(mention) {
|
|
964
|
-
const
|
|
1001
|
+
const inner = normalizeMention(mention);
|
|
1002
|
+
if (inner.length === 0) return void 0;
|
|
1003
|
+
const absolute = /^[A-Za-z]:[\\/]/.test(inner) || inner.startsWith("/") || inner.startsWith("\\");
|
|
965
1004
|
const { cwd } = getTabsState();
|
|
966
|
-
if (absolute) return
|
|
1005
|
+
if (absolute) return inner;
|
|
967
1006
|
if (cwd === void 0) return void 0;
|
|
968
1007
|
const sep = cwd.includes("\\") ? "\\" : "/";
|
|
969
|
-
return cwd.endsWith("\\") || cwd.endsWith("/") ? cwd +
|
|
1008
|
+
return cwd.endsWith("\\") || cwd.endsWith("/") ? cwd + inner : cwd + sep + inner;
|
|
970
1009
|
}
|
|
971
1010
|
/** Open an @-mention's file in the workbench preview (used by the linkifier). */
|
|
972
1011
|
function openMention(mention) {
|
|
@@ -1811,11 +1850,14 @@ window.__ModuleLoader__.load({
|
|
|
1811
1850
|
expanded,
|
|
1812
1851
|
root
|
|
1813
1852
|
]);
|
|
1814
|
-
/**
|
|
1853
|
+
/**
|
|
1854
|
+
* Insert an `@.\<relative-workspace-path>` mention into the composer (the
|
|
1855
|
+
* `.\` prefix marks the path as workspace-relative; see composerMention).
|
|
1856
|
+
*/
|
|
1815
1857
|
const onMention = (0, react.useCallback)((path) => {
|
|
1816
1858
|
const mention = relPathOf(path, cwd);
|
|
1817
1859
|
if (mention.length === 0) return;
|
|
1818
|
-
insertIntoComposer(
|
|
1860
|
+
insertIntoComposer(`${composerMention(mention)} `);
|
|
1819
1861
|
}, [cwd]);
|
|
1820
1862
|
const onTreeKeyDown = (0, react.useCallback)((e) => {
|
|
1821
1863
|
const mod = e.ctrlKey || e.metaKey;
|
|
@@ -15282,6 +15324,20 @@ window.__ModuleLoader__.load({
|
|
|
15282
15324
|
const PREVIEW_MIN = 240;
|
|
15283
15325
|
const CHAT_MIN = 240;
|
|
15284
15326
|
const PREVIEW_TOO_LARGE_LABEL = "512KB";
|
|
15327
|
+
/** Persisted split width (px) — survives reloads so a drag is never lost. */
|
|
15328
|
+
const PREVIEW_WIDTH_KEY = "dsh-plugin-workbench:preview-width";
|
|
15329
|
+
/** Read the persisted preview width; `null` means "use the default 55%". */
|
|
15330
|
+
function storedPreviewWidth() {
|
|
15331
|
+
try {
|
|
15332
|
+
if (typeof window === "undefined") return null;
|
|
15333
|
+
const raw = window.localStorage.getItem(PREVIEW_WIDTH_KEY);
|
|
15334
|
+
if (raw === null) return null;
|
|
15335
|
+
const px = Number(raw);
|
|
15336
|
+
return Number.isFinite(px) && px > 0 ? px : null;
|
|
15337
|
+
} catch {
|
|
15338
|
+
return null;
|
|
15339
|
+
}
|
|
15340
|
+
}
|
|
15285
15341
|
/** Same-origin raw-bytes route registered by the host half (see src/index.ts). */
|
|
15286
15342
|
const RAW_PREFIX = "/dsh-plugin-files/raw";
|
|
15287
15343
|
/** Same-origin SSE endpoint pushed by the host half (see src/index.ts). */
|
|
@@ -15343,6 +15399,22 @@ window.__ModuleLoader__.load({
|
|
|
15343
15399
|
function clamp(value, min, max) {
|
|
15344
15400
|
return Math.min(max, Math.max(min, value));
|
|
15345
15401
|
}
|
|
15402
|
+
/**
|
|
15403
|
+
* Nearest ancestor that actually takes part in layout. The slot system wraps
|
|
15404
|
+
* each slot's content in a `display: contents` element: children still join
|
|
15405
|
+
* the OUTER flex row, but the wrapper itself reports a 0×0 bounding rect.
|
|
15406
|
+
* Measuring that (the old `handle.parentElement`) made `max` collapse to
|
|
15407
|
+
* `PREVIEW_MIN` on the first move — the pane jumped to its minimum width and
|
|
15408
|
+
* could never be dragged back out. Skip every `display: contents` layer.
|
|
15409
|
+
*/
|
|
15410
|
+
function laidOutParent(el) {
|
|
15411
|
+
let node = el?.parentElement ?? null;
|
|
15412
|
+
while (node !== null) {
|
|
15413
|
+
if (getComputedStyle(node).display !== "contents") return node;
|
|
15414
|
+
node = node.parentElement;
|
|
15415
|
+
}
|
|
15416
|
+
return null;
|
|
15417
|
+
}
|
|
15346
15418
|
/** Parent directory of a path ('C:/a/b.md' → 'C:/a'; '' when there is none). */
|
|
15347
15419
|
function dirnameOf(path) {
|
|
15348
15420
|
const idx = Math.max(path.lastIndexOf("/"), path.lastIndexOf("\\"));
|
|
@@ -15394,6 +15466,8 @@ window.__ModuleLoader__.load({
|
|
|
15394
15466
|
const highlightRef = (0, react.useRef)(null);
|
|
15395
15467
|
const textareaRef = (0, react.useRef)(null);
|
|
15396
15468
|
const gutterRef = (0, react.useRef)(null);
|
|
15469
|
+
const dragMoveRef = (0, react.useRef)(() => void 0);
|
|
15470
|
+
const dragUpRef = (0, react.useRef)(() => void 0);
|
|
15397
15471
|
const refresh = (0, react.useCallback)(() => bump((v) => v + 1), []);
|
|
15398
15472
|
const tabsRef = (0, react.useRef)(tabs);
|
|
15399
15473
|
(0, react.useEffect)(() => {
|
|
@@ -15583,26 +15657,72 @@ window.__ModuleLoader__.load({
|
|
|
15583
15657
|
document.body.style.removeProperty("--dsh-preview-width");
|
|
15584
15658
|
};
|
|
15585
15659
|
}, [isOpen]);
|
|
15660
|
+
(0, react.useEffect)(() => {
|
|
15661
|
+
const saved = storedPreviewWidth();
|
|
15662
|
+
if (saved === null) return;
|
|
15663
|
+
const preview = previewRef.current;
|
|
15664
|
+
const center = laidOutParent(preview);
|
|
15665
|
+
if (preview === null || preview === void 0 || center === null || center === void 0) return;
|
|
15666
|
+
const centerWidth = center.getBoundingClientRect().width;
|
|
15667
|
+
const max = Math.max(PREVIEW_MIN, centerWidth - CHAT_MIN);
|
|
15668
|
+
setPreviewWidth(clamp(saved, PREVIEW_MIN, max));
|
|
15669
|
+
}, [isOpen]);
|
|
15670
|
+
(0, react.useEffect)(() => () => {
|
|
15671
|
+
window.removeEventListener("pointermove", dragMoveRef.current);
|
|
15672
|
+
window.removeEventListener("pointerup", dragUpRef.current);
|
|
15673
|
+
window.removeEventListener("pointercancel", dragUpRef.current);
|
|
15674
|
+
}, []);
|
|
15675
|
+
/**
|
|
15676
|
+
* Start a split-width drag. Robustness notes (the "pane suddenly shrinks
|
|
15677
|
+
* and freezes" bug class):
|
|
15678
|
+
*
|
|
15679
|
+
* - Pointer capture reroutes every later pointer event to the handle, so
|
|
15680
|
+
* `pointerup` fires EVEN when the mouse is released outside the window.
|
|
15681
|
+
* Without it the up event is lost, `onUp` never runs, and the leftover
|
|
15682
|
+
* `onMove` keeps rewriting the width from its stale baseline on every
|
|
15683
|
+
* mouse move anywhere on the page — that is the "cannot drag / cannot
|
|
15684
|
+
* restore" state.
|
|
15685
|
+
* - `pointercancel` is cleaned up too (browser steals the pointer, e.g. a
|
|
15686
|
+
* tablet palm or an OS gesture).
|
|
15687
|
+
* - Pointerdown defensively removes any previous listeners first, so even a
|
|
15688
|
+
* capture-less leftover cannot survive into a second drag.
|
|
15689
|
+
* - The max is re-measured every move: the chat minimum is relative to the
|
|
15690
|
+
* CURRENT center column, which can change while the drag is in flight.
|
|
15691
|
+
*/
|
|
15586
15692
|
const onHandleDown = (0, react.useCallback)((e) => {
|
|
15587
15693
|
e.preventDefault();
|
|
15588
15694
|
const handle = handleRef.current;
|
|
15589
15695
|
const preview = previewRef.current;
|
|
15590
15696
|
if (handle === null || preview === null) return;
|
|
15591
|
-
const center = handle
|
|
15697
|
+
const center = laidOutParent(handle);
|
|
15592
15698
|
if (center === null) return;
|
|
15593
15699
|
const startX = e.clientX;
|
|
15594
15700
|
const startWidth = preview.getBoundingClientRect().width;
|
|
15595
|
-
const centerWidth = center.getBoundingClientRect().width;
|
|
15596
|
-
const max = Math.max(PREVIEW_MIN, centerWidth - CHAT_MIN);
|
|
15597
15701
|
const onMove = (ev) => {
|
|
15598
|
-
|
|
15702
|
+
const centerWidth = center.getBoundingClientRect().width;
|
|
15703
|
+
const max = Math.max(PREVIEW_MIN, centerWidth - CHAT_MIN);
|
|
15704
|
+
const width = clamp(startWidth + ev.clientX - startX, PREVIEW_MIN, max);
|
|
15705
|
+
setPreviewWidth(width);
|
|
15706
|
+
try {
|
|
15707
|
+
window.localStorage.setItem(PREVIEW_WIDTH_KEY, String(width));
|
|
15708
|
+
} catch {}
|
|
15599
15709
|
};
|
|
15600
15710
|
const onUp = () => {
|
|
15601
|
-
window.removeEventListener("pointermove",
|
|
15602
|
-
window.removeEventListener("pointerup",
|
|
15711
|
+
window.removeEventListener("pointermove", dragMoveRef.current);
|
|
15712
|
+
window.removeEventListener("pointerup", dragUpRef.current);
|
|
15713
|
+
window.removeEventListener("pointercancel", dragUpRef.current);
|
|
15603
15714
|
};
|
|
15715
|
+
window.removeEventListener("pointermove", dragMoveRef.current);
|
|
15716
|
+
window.removeEventListener("pointerup", dragUpRef.current);
|
|
15717
|
+
window.removeEventListener("pointercancel", dragUpRef.current);
|
|
15718
|
+
dragMoveRef.current = onMove;
|
|
15719
|
+
dragUpRef.current = onUp;
|
|
15604
15720
|
window.addEventListener("pointermove", onMove);
|
|
15605
15721
|
window.addEventListener("pointerup", onUp);
|
|
15722
|
+
window.addEventListener("pointercancel", onUp);
|
|
15723
|
+
try {
|
|
15724
|
+
handle.setPointerCapture(e.pointerId);
|
|
15725
|
+
} catch {}
|
|
15606
15726
|
}, []);
|
|
15607
15727
|
const onSave = (0, react.useCallback)(async () => {
|
|
15608
15728
|
if (active === void 0) return;
|
|
@@ -15890,7 +16010,7 @@ window.__ModuleLoader__.load({
|
|
|
15890
16010
|
"tab.expand": "弹出文件详情",
|
|
15891
16011
|
"tab.diskChanged": "文件已在磁盘上被修改,点击重新加载(会放弃未保存的编辑)",
|
|
15892
16012
|
"menu.open": "打开预览",
|
|
15893
|
-
"menu.atFile": "
|
|
16013
|
+
"menu.atFile": "@. 在消息中引用",
|
|
15894
16014
|
"menu.newFile": "新建文件",
|
|
15895
16015
|
"menu.newFolder": "新建文件夹",
|
|
15896
16016
|
"menu.rename": "重命名",
|
|
@@ -15948,7 +16068,7 @@ window.__ModuleLoader__.load({
|
|
|
15948
16068
|
"tab.expand": "Expand file details",
|
|
15949
16069
|
"tab.diskChanged": "File changed on disk — click to reload (discards unsaved edits)",
|
|
15950
16070
|
"menu.open": "Open preview",
|
|
15951
|
-
"menu.atFile": "
|
|
16071
|
+
"menu.atFile": "@. Mention in message",
|
|
15952
16072
|
"menu.newFile": "New File",
|
|
15953
16073
|
"menu.newFolder": "New Folder",
|
|
15954
16074
|
"menu.rename": "Rename",
|
|
@@ -15995,11 +16115,11 @@ window.__ModuleLoader__.load({
|
|
|
15995
16115
|
/**
|
|
15996
16116
|
* @-mention linkifier for the conversation.
|
|
15997
16117
|
*
|
|
15998
|
-
* The workbench inserts
|
|
15999
|
-
* gesture "在消息中引用"
|
|
16000
|
-
*
|
|
16001
|
-
* matches the mention grammar is wrapped
|
|
16002
|
-
* the file in the workbench preview.
|
|
16118
|
+
* The workbench inserts `@.\<relative-workspace-path>` into the composer (menu
|
|
16119
|
+
* gesture "在消息中引用"; the `.\` prefix marks a workspace-relative path), and
|
|
16120
|
+
* this module makes the mention VISIBLE as a hyperlink once the message is
|
|
16121
|
+
* rendered: any `@`-prefixed token that matches the mention grammar is wrapped
|
|
16122
|
+
* in an anchor, and clicking it opens the file in the workbench preview.
|
|
16003
16123
|
*
|
|
16004
16124
|
* Grammar (anything else stays plain text with no special meaning):
|
|
16005
16125
|
* - `@` must sit at a token boundary (start of text, whitespace, or
|
|
@@ -16008,8 +16128,12 @@ window.__ModuleLoader__.load({
|
|
|
16008
16128
|
* - the token is the longest run of non-whitespace, non-`@` characters,
|
|
16009
16129
|
* with trailing sentence punctuation trimmed (`.。 ,, ;; :: !! ?? ))`…);
|
|
16010
16130
|
* - the remaining token must be a RELATIVE path (never drive-absolute or
|
|
16011
|
-
* leading-slash)
|
|
16012
|
-
*
|
|
16131
|
+
* leading-slash) — optionally prefixed with a `.\` or `./` workspace
|
|
16132
|
+
* marker — and path-shaped: either contains a `/` or `\` directory
|
|
16133
|
+
* separator, or is a single segment ending in a file extension;
|
|
16134
|
+
* - a path containing whitespace may instead use the quoted `@"token"` form
|
|
16135
|
+
* (the workbench writes it when the rel path has spaces, e.g.
|
|
16136
|
+
* `@"\.\my plan.md"`).
|
|
16013
16137
|
*
|
|
16014
16138
|
* Scanning mirrors the table-zoom enhancer: a MutationObserver on
|
|
16015
16139
|
* `document.body`, rAF-coalesced, walks the text nodes of every
|
|
@@ -16019,6 +16143,8 @@ window.__ModuleLoader__.load({
|
|
|
16019
16143
|
*/
|
|
16020
16144
|
/** Mention pattern: `@` + token (no whitespace, no embedded `@`). */
|
|
16021
16145
|
const MENTION_RE = /@([^\s@]+)/g;
|
|
16146
|
+
/** Quoted mention pattern: `@"token"` — used when the path contains whitespace. */
|
|
16147
|
+
const QUOTED_MENTION_RE = /@"([^"@]+)"/g;
|
|
16022
16148
|
/** Trailing characters trimmed from a mention token before validation. */
|
|
16023
16149
|
const TRAILING = /* @__PURE__ */ new Set([
|
|
16024
16150
|
".",
|
|
@@ -16057,7 +16183,7 @@ window.__ModuleLoader__.load({
|
|
|
16057
16183
|
if (token.startsWith("/") || token.startsWith("\\")) return false;
|
|
16058
16184
|
if (token.startsWith("..")) return false;
|
|
16059
16185
|
if (token.includes("/") || token.includes("\\")) return true;
|
|
16060
|
-
return /^[^\\/]+\.[A-Za-z0-9_][A-Za-z0-9._
|
|
16186
|
+
return /^[^\\/]+\.[A-Za-z0-9_][A-Za-z0-9._~+ -]*$/.test(token);
|
|
16061
16187
|
}
|
|
16062
16188
|
/** Trim trailing sentence punctuation from a raw mention token. */
|
|
16063
16189
|
function trimMentionToken(raw) {
|
|
@@ -16068,7 +16194,9 @@ window.__ModuleLoader__.load({
|
|
|
16068
16194
|
/**
|
|
16069
16195
|
* Extract every valid mention from `text` as [start, end, mention] ranges.
|
|
16070
16196
|
* `end` covers `@` + the TRIMMED token (trailing punctuation stays outside the
|
|
16071
|
-
* link).
|
|
16197
|
+
* link). For the quoted `@"..."` form the span covers `@"..."` including both
|
|
16198
|
+
* quotes, while `mention` carries the inner path (used for resolution).
|
|
16199
|
+
* Pure and testable: the DOM walk uses it and then splits the text node.
|
|
16072
16200
|
*/
|
|
16073
16201
|
function findMentions(text) {
|
|
16074
16202
|
const hits = [];
|
|
@@ -16086,7 +16214,31 @@ window.__ModuleLoader__.load({
|
|
|
16086
16214
|
mention: token
|
|
16087
16215
|
});
|
|
16088
16216
|
}
|
|
16089
|
-
|
|
16217
|
+
QUOTED_MENTION_RE.lastIndex = 0;
|
|
16218
|
+
let quoted;
|
|
16219
|
+
while ((quoted = QUOTED_MENTION_RE.exec(text)) !== null) {
|
|
16220
|
+
const at = quoted.index;
|
|
16221
|
+
const inner = quoted[1];
|
|
16222
|
+
if (!isBoundaryBefore(at > 0 ? text[at - 1] : void 0)) continue;
|
|
16223
|
+
if (!isMentionToken(inner)) continue;
|
|
16224
|
+
hits.push({
|
|
16225
|
+
start: at,
|
|
16226
|
+
end: at + inner.length + 3,
|
|
16227
|
+
mention: inner
|
|
16228
|
+
});
|
|
16229
|
+
}
|
|
16230
|
+
hits.sort((a, b) => a.start - b.start || a.end - b.end);
|
|
16231
|
+
const merged = [];
|
|
16232
|
+
for (const hit of hits) {
|
|
16233
|
+
const prev = merged[merged.length - 1];
|
|
16234
|
+
if (prev !== void 0 && hit.start === prev.start) {
|
|
16235
|
+
merged[merged.length - 1] = hit;
|
|
16236
|
+
continue;
|
|
16237
|
+
}
|
|
16238
|
+
if (prev !== void 0 && hit.start < prev.end) continue;
|
|
16239
|
+
merged.push(hit);
|
|
16240
|
+
}
|
|
16241
|
+
return merged;
|
|
16090
16242
|
}
|
|
16091
16243
|
/** Containers whose text is never linkified (code, existing links, overlays). */
|
|
16092
16244
|
const SKIP_SELECTOR = [
|
|
@@ -16138,11 +16290,11 @@ window.__ModuleLoader__.load({
|
|
|
16138
16290
|
return true;
|
|
16139
16291
|
}
|
|
16140
16292
|
/** Style tag guard (the bundle may re-apply on HMR). */
|
|
16141
|
-
let styleInstalled = false;
|
|
16293
|
+
let styleInstalled$1 = false;
|
|
16142
16294
|
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;
|
|
16295
|
+
function installStyle$1() {
|
|
16296
|
+
if (styleInstalled$1 || typeof document === "undefined") return;
|
|
16297
|
+
styleInstalled$1 = true;
|
|
16146
16298
|
const tagId = "dsh-plugin-workbench/mention.module.css";
|
|
16147
16299
|
if (document.querySelector(`style[data-plugin-css=${JSON.stringify(tagId)}]`) === null) {
|
|
16148
16300
|
const tag = document.createElement("style");
|
|
@@ -16153,7 +16305,7 @@ window.__ModuleLoader__.load({
|
|
|
16153
16305
|
}
|
|
16154
16306
|
}
|
|
16155
16307
|
/** Click handler: open the mentioned file in the workbench preview. */
|
|
16156
|
-
function onDocumentClick(e) {
|
|
16308
|
+
function onDocumentClick$1(e) {
|
|
16157
16309
|
const target = e.target;
|
|
16158
16310
|
if (!(target instanceof Element)) return;
|
|
16159
16311
|
const anchor = target.closest("a.dswb-mention");
|
|
@@ -16171,12 +16323,164 @@ window.__ModuleLoader__.load({
|
|
|
16171
16323
|
if (typeof document === "undefined" || typeof MutationObserver === "undefined") return () => void 0;
|
|
16172
16324
|
if (linkifierInstalled) return () => void 0;
|
|
16173
16325
|
linkifierInstalled = true;
|
|
16326
|
+
installStyle$1();
|
|
16327
|
+
document.addEventListener("click", onDocumentClick$1);
|
|
16328
|
+
let pending = false;
|
|
16329
|
+
const scan = () => {
|
|
16330
|
+
pending = false;
|
|
16331
|
+
for (const root of document.querySelectorAll("[data-conversation-scroll]")) linkifyRoot(root);
|
|
16332
|
+
};
|
|
16333
|
+
const observer = new MutationObserver(() => {
|
|
16334
|
+
if (pending) return;
|
|
16335
|
+
pending = true;
|
|
16336
|
+
requestAnimationFrame(scan);
|
|
16337
|
+
});
|
|
16338
|
+
observer.observe(document.body, {
|
|
16339
|
+
childList: true,
|
|
16340
|
+
subtree: true,
|
|
16341
|
+
characterData: true
|
|
16342
|
+
});
|
|
16343
|
+
scan();
|
|
16344
|
+
return () => {
|
|
16345
|
+
observer.disconnect();
|
|
16346
|
+
document.removeEventListener("click", onDocumentClick$1);
|
|
16347
|
+
linkifierInstalled = false;
|
|
16348
|
+
};
|
|
16349
|
+
}
|
|
16350
|
+
//#endregion
|
|
16351
|
+
//#region src/client/composerMentions.ts
|
|
16352
|
+
/**
|
|
16353
|
+
* Composer @-mention hyperlink enhancement.
|
|
16354
|
+
*
|
|
16355
|
+
* The composer's visible text lives in a core-rendered backdrop (`textarea`
|
|
16356
|
+
* text is transparent) that React re-renders on every keystroke, so plugin
|
|
16357
|
+
* code must not mutate it. Instead this module renders its OWN overlay — an
|
|
16358
|
+
* absolutely-positioned copy of the draft, in the same font/padding/wrap
|
|
16359
|
+
* metrics as the textarea (copied from the core `.input,.mirror,.backdrop`
|
|
16360
|
+
* rule), with every `@.\`-style mention drawn in the link color and
|
|
16361
|
+
* underlined. The overlay's plain text is fully transparent, so the visible
|
|
16362
|
+
* glyphs still come from the core backdrop; mention spans paint on top at the
|
|
16363
|
+
* identical position (same metrics => same layout), which shows the mention as
|
|
16364
|
+
* a real hyperlink inside the chat input.
|
|
16365
|
+
*
|
|
16366
|
+
* The overlay is appended directly to the core `.grow` container (a sibling
|
|
16367
|
+
* of the backdrop/mirror) — React never manages nodes it did not create, and
|
|
16368
|
+
* the overlay is `position:absolute;inset:0` so it always tracks the input
|
|
16369
|
+
* box, including scroll inside `[data-input-scroll]`. It re-syncs whenever the
|
|
16370
|
+
* core `[data-input-mirror]` text changes (a React-written text node, so it
|
|
16371
|
+
* updates for typing AND programmatic inserts such as the core `@` menu or
|
|
16372
|
+
* this plugin's own insertIntoComposer).
|
|
16373
|
+
*
|
|
16374
|
+
* Interaction: Ctrl/Cmd+click inside the composer textarea opens the mention
|
|
16375
|
+
* under the caret in the workbench preview (the visible link is a decoration;
|
|
16376
|
+
* the real input gains the click), mirroring the rendered-message linkifier.
|
|
16377
|
+
*/
|
|
16378
|
+
/** One-time install guard (the client bundle re-applies on HMR). */
|
|
16379
|
+
let installed = false;
|
|
16380
|
+
/** Style-tag guard (the bundle may re-apply on HMR). */
|
|
16381
|
+
let styleInstalled = false;
|
|
16382
|
+
const OVERLAY_CSS = [
|
|
16383
|
+
"[data-wb-composer-mention-overlay]{position:absolute;inset:0;overflow:hidden;pointer-events:none;box-sizing:border-box;font-family:var(--dsw-font-family);font-size:inherit;line-height:inherit;white-space:pre-wrap;word-break:break-word;overflow-wrap:anywhere;padding:4px 12px 0 16px;color:transparent}",
|
|
16384
|
+
"[data-wb-composer-mention-overlay][hidden]{display:none}",
|
|
16385
|
+
".dswb-composer-mention{color:var(--dsw-alias-state-business-primary);-webkit-text-fill-color:var(--dsw-alias-state-business-primary);text-decoration:underline;text-underline-offset:2px}"
|
|
16386
|
+
].join("");
|
|
16387
|
+
function installStyle() {
|
|
16388
|
+
if (styleInstalled || typeof document === "undefined") return;
|
|
16389
|
+
styleInstalled = true;
|
|
16390
|
+
const tagId = "dsh-plugin-workbench/composer-mention.module.css";
|
|
16391
|
+
if (document.querySelector(`style[data-plugin-css=${JSON.stringify(tagId)}]`) === null) {
|
|
16392
|
+
const tag = document.createElement("style");
|
|
16393
|
+
tag.dataset.plugin = "dsh-plugin-workbench";
|
|
16394
|
+
tag.dataset.pluginCss = tagId;
|
|
16395
|
+
tag.textContent = OVERLAY_CSS;
|
|
16396
|
+
document.head.appendChild(tag);
|
|
16397
|
+
}
|
|
16398
|
+
}
|
|
16399
|
+
/** The overlay element for the currently visible composer, or null. */
|
|
16400
|
+
let overlayEl = null;
|
|
16401
|
+
/** Last draft rendered into the overlay (avoids re-rendering on own mutations). */
|
|
16402
|
+
let lastDraft = null;
|
|
16403
|
+
/** Create (once per composer instance) and return the overlay inside `.grow`. */
|
|
16404
|
+
function ensureOverlay() {
|
|
16405
|
+
const seat = document.querySelector("[data-composer-seat]");
|
|
16406
|
+
if (seat === null) return null;
|
|
16407
|
+
const grow = seat.querySelector("[data-input-mirror]")?.parentElement;
|
|
16408
|
+
if (grow === null || grow === void 0) return null;
|
|
16409
|
+
let overlay = grow.querySelector("[data-wb-composer-mention-overlay]");
|
|
16410
|
+
if (overlay === null) {
|
|
16411
|
+
overlay = document.createElement("div");
|
|
16412
|
+
overlay.dataset.wbComposerMentionOverlay = "";
|
|
16413
|
+
overlay.setAttribute("aria-hidden", "true");
|
|
16414
|
+
grow.appendChild(overlay);
|
|
16415
|
+
}
|
|
16416
|
+
if (overlayEl !== overlay) {
|
|
16417
|
+
overlayEl = overlay;
|
|
16418
|
+
lastDraft = null;
|
|
16419
|
+
}
|
|
16420
|
+
return overlay;
|
|
16421
|
+
}
|
|
16422
|
+
/** Render the (transparent) draft with mention spans into the overlay. */
|
|
16423
|
+
function render(overlay, draft) {
|
|
16424
|
+
const hits = findMentions(draft);
|
|
16425
|
+
if (hits.length === 0) {
|
|
16426
|
+
overlay.hidden = true;
|
|
16427
|
+
return;
|
|
16428
|
+
}
|
|
16429
|
+
overlay.hidden = false;
|
|
16430
|
+
const frag = document.createDocumentFragment();
|
|
16431
|
+
let cursor = 0;
|
|
16432
|
+
for (const hit of hits) {
|
|
16433
|
+
if (hit.start > cursor) frag.appendChild(document.createTextNode(draft.slice(cursor, hit.start)));
|
|
16434
|
+
const span = document.createElement("span");
|
|
16435
|
+
span.className = "dswb-composer-mention";
|
|
16436
|
+
span.dataset.wbMention = hit.mention;
|
|
16437
|
+
span.textContent = draft.slice(hit.start, hit.end);
|
|
16438
|
+
frag.appendChild(span);
|
|
16439
|
+
cursor = hit.end;
|
|
16440
|
+
}
|
|
16441
|
+
if (cursor < draft.length) frag.appendChild(document.createTextNode(draft.slice(cursor)));
|
|
16442
|
+
overlay.replaceChildren(frag);
|
|
16443
|
+
}
|
|
16444
|
+
/** Re-sync the overlay with the composer draft (cheap no-op when unchanged). */
|
|
16445
|
+
function sync() {
|
|
16446
|
+
const seat = document.querySelector("[data-composer-seat]");
|
|
16447
|
+
if (seat === null) return;
|
|
16448
|
+
if (seat.querySelector("textarea:not([disabled]):not([readonly])") === null) return;
|
|
16449
|
+
const mirror = seat.querySelector("[data-input-mirror]");
|
|
16450
|
+
const overlay = ensureOverlay();
|
|
16451
|
+
if (mirror === null || overlay === null) return;
|
|
16452
|
+
const draft = (mirror.textContent ?? "").replace(/\n$/, "");
|
|
16453
|
+
if (draft === lastDraft) return;
|
|
16454
|
+
lastDraft = draft;
|
|
16455
|
+
render(overlay, draft);
|
|
16456
|
+
}
|
|
16457
|
+
/** Ctrl/Cmd+click on the composer: open the mention under the caret. */
|
|
16458
|
+
function onDocumentClick(e) {
|
|
16459
|
+
if (!(e.ctrlKey || e.metaKey)) return;
|
|
16460
|
+
const target = e.target;
|
|
16461
|
+
if (!(target instanceof HTMLTextAreaElement)) return;
|
|
16462
|
+
if (target.closest("[data-composer-seat]") === null) return;
|
|
16463
|
+
const pos = target.selectionStart;
|
|
16464
|
+
const draft = target.value;
|
|
16465
|
+
const hits = findMentions(draft);
|
|
16466
|
+
let hit = hits.find((h) => pos >= h.start && pos <= h.end);
|
|
16467
|
+
if (hit === void 0) hit = hits.find((h) => pos - 1 >= h.start && pos - 1 < h.end);
|
|
16468
|
+
if (hit === void 0) return;
|
|
16469
|
+
e.preventDefault();
|
|
16470
|
+
e.stopPropagation();
|
|
16471
|
+
openMention(hit.mention);
|
|
16472
|
+
}
|
|
16473
|
+
/** Start the composer mention overlay + Ctrl+click opener (idempotent). */
|
|
16474
|
+
function installComposerMentions() {
|
|
16475
|
+
if (typeof document === "undefined" || typeof MutationObserver === "undefined") return () => void 0;
|
|
16476
|
+
if (installed) return () => void 0;
|
|
16477
|
+
installed = true;
|
|
16174
16478
|
installStyle();
|
|
16175
16479
|
document.addEventListener("click", onDocumentClick);
|
|
16176
16480
|
let pending = false;
|
|
16177
16481
|
const scan = () => {
|
|
16178
16482
|
pending = false;
|
|
16179
|
-
|
|
16483
|
+
sync();
|
|
16180
16484
|
};
|
|
16181
16485
|
const observer = new MutationObserver(() => {
|
|
16182
16486
|
if (pending) return;
|
|
@@ -16192,7 +16496,7 @@ window.__ModuleLoader__.load({
|
|
|
16192
16496
|
return () => {
|
|
16193
16497
|
observer.disconnect();
|
|
16194
16498
|
document.removeEventListener("click", onDocumentClick);
|
|
16195
|
-
|
|
16499
|
+
installed = false;
|
|
16196
16500
|
};
|
|
16197
16501
|
}
|
|
16198
16502
|
//#endregion
|
|
@@ -16212,6 +16516,7 @@ window.__ModuleLoader__.load({
|
|
|
16212
16516
|
en
|
|
16213
16517
|
}), "files-explorer: dictionaries");
|
|
16214
16518
|
installComposerDrops();
|
|
16519
|
+
installComposerMentions();
|
|
16215
16520
|
ctx.effect(() => installMentionLinkifier(), "files-explorer: @mention linkifier");
|
|
16216
16521
|
const listDir = (path, signal) => unwrap(ctx.connection.rpc.call(CHANNEL, "list", { path }, signal));
|
|
16217
16522
|
const readFile = (path, signal) => unwrap(ctx.connection.rpc.call(CHANNEL, "read", { path }, signal));
|