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 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
- * Two gestures land text in the chat composer without touching the core:
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. 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.
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 `@<relative-workspace-path>` at the
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
- * 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).
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 absolute = /^[A-Za-z]:[\\/]/.test(mention) || mention.startsWith("/") || mention.startsWith("\\");
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 mention;
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 + mention : cwd + sep + mention;
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
- /** Insert `@<relative-workspace-path>` into the composer (Claude Code-style mention). */
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(`@${mention} `);
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.parentElement;
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
- setPreviewWidth(clamp(startWidth + ev.clientX - startX, PREVIEW_MIN, max));
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", onMove);
15602
- window.removeEventListener("pointerup", onUp);
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": "@ Mention in message",
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 `@<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.
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), and path-shaped: either contains a `/` or `\` directory
16012
- * separator, or is a single segment ending in a file extension.
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._~+-]*$/.test(token);
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). Pure and testable: the DOM walk uses it and then splits the text node.
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
- return hits;
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
- for (const root of document.querySelectorAll("[data-conversation-scroll]")) linkifyRoot(root);
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
- linkifierInstalled = false;
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));