dsh-plugin-workbench 0.0.11 → 0.0.12

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,10 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.0.12] - 2026-08-25
4
+
5
+ - 相关插件段新增 dsh-plugin-windows-guard(Windows 环境防坑守则 skill 插件,互相引流)。
6
+
7
+
3
8
  本项目所有重要变更都会记录在此文件。
4
9
  格式基于 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.1.0/),
5
10
  版本号遵循 [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;
@@ -15890,7 +15932,7 @@ window.__ModuleLoader__.load({
15890
15932
  "tab.expand": "弹出文件详情",
15891
15933
  "tab.diskChanged": "文件已在磁盘上被修改,点击重新加载(会放弃未保存的编辑)",
15892
15934
  "menu.open": "打开预览",
15893
- "menu.atFile": "@ 在消息中引用",
15935
+ "menu.atFile": "@. 在消息中引用",
15894
15936
  "menu.newFile": "新建文件",
15895
15937
  "menu.newFolder": "新建文件夹",
15896
15938
  "menu.rename": "重命名",
@@ -15948,7 +15990,7 @@ window.__ModuleLoader__.load({
15948
15990
  "tab.expand": "Expand file details",
15949
15991
  "tab.diskChanged": "File changed on disk — click to reload (discards unsaved edits)",
15950
15992
  "menu.open": "Open preview",
15951
- "menu.atFile": "@ Mention in message",
15993
+ "menu.atFile": "@. Mention in message",
15952
15994
  "menu.newFile": "New File",
15953
15995
  "menu.newFolder": "New Folder",
15954
15996
  "menu.rename": "Rename",
@@ -15995,11 +16037,11 @@ window.__ModuleLoader__.load({
15995
16037
  /**
15996
16038
  * @-mention linkifier for the conversation.
15997
16039
  *
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.
16040
+ * The workbench inserts `@.\<relative-workspace-path>` into the composer (menu
16041
+ * gesture "在消息中引用"; the `.\` prefix marks a workspace-relative path), and
16042
+ * this module makes the mention VISIBLE as a hyperlink once the message is
16043
+ * rendered: any `@`-prefixed token that matches the mention grammar is wrapped
16044
+ * in an anchor, and clicking it opens the file in the workbench preview.
16003
16045
  *
16004
16046
  * Grammar (anything else stays plain text with no special meaning):
16005
16047
  * - `@` must sit at a token boundary (start of text, whitespace, or
@@ -16008,8 +16050,12 @@ window.__ModuleLoader__.load({
16008
16050
  * - the token is the longest run of non-whitespace, non-`@` characters,
16009
16051
  * with trailing sentence punctuation trimmed (`.。 ,, ;; :: !! ?? ))`…);
16010
16052
  * - 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.
16053
+ * leading-slash) optionally prefixed with a `.\` or `./` workspace
16054
+ * marker and path-shaped: either contains a `/` or `\` directory
16055
+ * separator, or is a single segment ending in a file extension;
16056
+ * - a path containing whitespace may instead use the quoted `@"token"` form
16057
+ * (the workbench writes it when the rel path has spaces, e.g.
16058
+ * `@"\.\my plan.md"`).
16013
16059
  *
16014
16060
  * Scanning mirrors the table-zoom enhancer: a MutationObserver on
16015
16061
  * `document.body`, rAF-coalesced, walks the text nodes of every
@@ -16019,6 +16065,8 @@ window.__ModuleLoader__.load({
16019
16065
  */
16020
16066
  /** Mention pattern: `@` + token (no whitespace, no embedded `@`). */
16021
16067
  const MENTION_RE = /@([^\s@]+)/g;
16068
+ /** Quoted mention pattern: `@"token"` — used when the path contains whitespace. */
16069
+ const QUOTED_MENTION_RE = /@"([^"@]+)"/g;
16022
16070
  /** Trailing characters trimmed from a mention token before validation. */
16023
16071
  const TRAILING = /* @__PURE__ */ new Set([
16024
16072
  ".",
@@ -16057,7 +16105,7 @@ window.__ModuleLoader__.load({
16057
16105
  if (token.startsWith("/") || token.startsWith("\\")) return false;
16058
16106
  if (token.startsWith("..")) return false;
16059
16107
  if (token.includes("/") || token.includes("\\")) return true;
16060
- return /^[^\\/]+\.[A-Za-z0-9_][A-Za-z0-9._~+-]*$/.test(token);
16108
+ return /^[^\\/]+\.[A-Za-z0-9_][A-Za-z0-9._~+ -]*$/.test(token);
16061
16109
  }
16062
16110
  /** Trim trailing sentence punctuation from a raw mention token. */
16063
16111
  function trimMentionToken(raw) {
@@ -16068,7 +16116,9 @@ window.__ModuleLoader__.load({
16068
16116
  /**
16069
16117
  * Extract every valid mention from `text` as [start, end, mention] ranges.
16070
16118
  * `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.
16119
+ * link). For the quoted `@"..."` form the span covers `@"..."` including both
16120
+ * quotes, while `mention` carries the inner path (used for resolution).
16121
+ * Pure and testable: the DOM walk uses it and then splits the text node.
16072
16122
  */
16073
16123
  function findMentions(text) {
16074
16124
  const hits = [];
@@ -16086,7 +16136,31 @@ window.__ModuleLoader__.load({
16086
16136
  mention: token
16087
16137
  });
16088
16138
  }
16089
- return hits;
16139
+ QUOTED_MENTION_RE.lastIndex = 0;
16140
+ let quoted;
16141
+ while ((quoted = QUOTED_MENTION_RE.exec(text)) !== null) {
16142
+ const at = quoted.index;
16143
+ const inner = quoted[1];
16144
+ if (!isBoundaryBefore(at > 0 ? text[at - 1] : void 0)) continue;
16145
+ if (!isMentionToken(inner)) continue;
16146
+ hits.push({
16147
+ start: at,
16148
+ end: at + inner.length + 3,
16149
+ mention: inner
16150
+ });
16151
+ }
16152
+ hits.sort((a, b) => a.start - b.start || a.end - b.end);
16153
+ const merged = [];
16154
+ for (const hit of hits) {
16155
+ const prev = merged[merged.length - 1];
16156
+ if (prev !== void 0 && hit.start === prev.start) {
16157
+ merged[merged.length - 1] = hit;
16158
+ continue;
16159
+ }
16160
+ if (prev !== void 0 && hit.start < prev.end) continue;
16161
+ merged.push(hit);
16162
+ }
16163
+ return merged;
16090
16164
  }
16091
16165
  /** Containers whose text is never linkified (code, existing links, overlays). */
16092
16166
  const SKIP_SELECTOR = [
@@ -16138,11 +16212,11 @@ window.__ModuleLoader__.load({
16138
16212
  return true;
16139
16213
  }
16140
16214
  /** Style tag guard (the bundle may re-apply on HMR). */
16141
- let styleInstalled = false;
16215
+ let styleInstalled$1 = false;
16142
16216
  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;
16217
+ function installStyle$1() {
16218
+ if (styleInstalled$1 || typeof document === "undefined") return;
16219
+ styleInstalled$1 = true;
16146
16220
  const tagId = "dsh-plugin-workbench/mention.module.css";
16147
16221
  if (document.querySelector(`style[data-plugin-css=${JSON.stringify(tagId)}]`) === null) {
16148
16222
  const tag = document.createElement("style");
@@ -16153,7 +16227,7 @@ window.__ModuleLoader__.load({
16153
16227
  }
16154
16228
  }
16155
16229
  /** Click handler: open the mentioned file in the workbench preview. */
16156
- function onDocumentClick(e) {
16230
+ function onDocumentClick$1(e) {
16157
16231
  const target = e.target;
16158
16232
  if (!(target instanceof Element)) return;
16159
16233
  const anchor = target.closest("a.dswb-mention");
@@ -16171,12 +16245,164 @@ window.__ModuleLoader__.load({
16171
16245
  if (typeof document === "undefined" || typeof MutationObserver === "undefined") return () => void 0;
16172
16246
  if (linkifierInstalled) return () => void 0;
16173
16247
  linkifierInstalled = true;
16248
+ installStyle$1();
16249
+ document.addEventListener("click", onDocumentClick$1);
16250
+ let pending = false;
16251
+ const scan = () => {
16252
+ pending = false;
16253
+ for (const root of document.querySelectorAll("[data-conversation-scroll]")) linkifyRoot(root);
16254
+ };
16255
+ const observer = new MutationObserver(() => {
16256
+ if (pending) return;
16257
+ pending = true;
16258
+ requestAnimationFrame(scan);
16259
+ });
16260
+ observer.observe(document.body, {
16261
+ childList: true,
16262
+ subtree: true,
16263
+ characterData: true
16264
+ });
16265
+ scan();
16266
+ return () => {
16267
+ observer.disconnect();
16268
+ document.removeEventListener("click", onDocumentClick$1);
16269
+ linkifierInstalled = false;
16270
+ };
16271
+ }
16272
+ //#endregion
16273
+ //#region src/client/composerMentions.ts
16274
+ /**
16275
+ * Composer @-mention hyperlink enhancement.
16276
+ *
16277
+ * The composer's visible text lives in a core-rendered backdrop (`textarea`
16278
+ * text is transparent) that React re-renders on every keystroke, so plugin
16279
+ * code must not mutate it. Instead this module renders its OWN overlay — an
16280
+ * absolutely-positioned copy of the draft, in the same font/padding/wrap
16281
+ * metrics as the textarea (copied from the core `.input,.mirror,.backdrop`
16282
+ * rule), with every `@.\`-style mention drawn in the link color and
16283
+ * underlined. The overlay's plain text is fully transparent, so the visible
16284
+ * glyphs still come from the core backdrop; mention spans paint on top at the
16285
+ * identical position (same metrics => same layout), which shows the mention as
16286
+ * a real hyperlink inside the chat input.
16287
+ *
16288
+ * The overlay is appended directly to the core `.grow` container (a sibling
16289
+ * of the backdrop/mirror) — React never manages nodes it did not create, and
16290
+ * the overlay is `position:absolute;inset:0` so it always tracks the input
16291
+ * box, including scroll inside `[data-input-scroll]`. It re-syncs whenever the
16292
+ * core `[data-input-mirror]` text changes (a React-written text node, so it
16293
+ * updates for typing AND programmatic inserts such as the core `@` menu or
16294
+ * this plugin's own insertIntoComposer).
16295
+ *
16296
+ * Interaction: Ctrl/Cmd+click inside the composer textarea opens the mention
16297
+ * under the caret in the workbench preview (the visible link is a decoration;
16298
+ * the real input gains the click), mirroring the rendered-message linkifier.
16299
+ */
16300
+ /** One-time install guard (the client bundle re-applies on HMR). */
16301
+ let installed = false;
16302
+ /** Style-tag guard (the bundle may re-apply on HMR). */
16303
+ let styleInstalled = false;
16304
+ const OVERLAY_CSS = [
16305
+ "[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}",
16306
+ "[data-wb-composer-mention-overlay][hidden]{display:none}",
16307
+ ".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}"
16308
+ ].join("");
16309
+ function installStyle() {
16310
+ if (styleInstalled || typeof document === "undefined") return;
16311
+ styleInstalled = true;
16312
+ const tagId = "dsh-plugin-workbench/composer-mention.module.css";
16313
+ if (document.querySelector(`style[data-plugin-css=${JSON.stringify(tagId)}]`) === null) {
16314
+ const tag = document.createElement("style");
16315
+ tag.dataset.plugin = "dsh-plugin-workbench";
16316
+ tag.dataset.pluginCss = tagId;
16317
+ tag.textContent = OVERLAY_CSS;
16318
+ document.head.appendChild(tag);
16319
+ }
16320
+ }
16321
+ /** The overlay element for the currently visible composer, or null. */
16322
+ let overlayEl = null;
16323
+ /** Last draft rendered into the overlay (avoids re-rendering on own mutations). */
16324
+ let lastDraft = null;
16325
+ /** Create (once per composer instance) and return the overlay inside `.grow`. */
16326
+ function ensureOverlay() {
16327
+ const seat = document.querySelector("[data-composer-seat]");
16328
+ if (seat === null) return null;
16329
+ const grow = seat.querySelector("[data-input-mirror]")?.parentElement;
16330
+ if (grow === null || grow === void 0) return null;
16331
+ let overlay = grow.querySelector("[data-wb-composer-mention-overlay]");
16332
+ if (overlay === null) {
16333
+ overlay = document.createElement("div");
16334
+ overlay.dataset.wbComposerMentionOverlay = "";
16335
+ overlay.setAttribute("aria-hidden", "true");
16336
+ grow.appendChild(overlay);
16337
+ }
16338
+ if (overlayEl !== overlay) {
16339
+ overlayEl = overlay;
16340
+ lastDraft = null;
16341
+ }
16342
+ return overlay;
16343
+ }
16344
+ /** Render the (transparent) draft with mention spans into the overlay. */
16345
+ function render(overlay, draft) {
16346
+ const hits = findMentions(draft);
16347
+ if (hits.length === 0) {
16348
+ overlay.hidden = true;
16349
+ return;
16350
+ }
16351
+ overlay.hidden = false;
16352
+ const frag = document.createDocumentFragment();
16353
+ let cursor = 0;
16354
+ for (const hit of hits) {
16355
+ if (hit.start > cursor) frag.appendChild(document.createTextNode(draft.slice(cursor, hit.start)));
16356
+ const span = document.createElement("span");
16357
+ span.className = "dswb-composer-mention";
16358
+ span.dataset.wbMention = hit.mention;
16359
+ span.textContent = draft.slice(hit.start, hit.end);
16360
+ frag.appendChild(span);
16361
+ cursor = hit.end;
16362
+ }
16363
+ if (cursor < draft.length) frag.appendChild(document.createTextNode(draft.slice(cursor)));
16364
+ overlay.replaceChildren(frag);
16365
+ }
16366
+ /** Re-sync the overlay with the composer draft (cheap no-op when unchanged). */
16367
+ function sync() {
16368
+ const seat = document.querySelector("[data-composer-seat]");
16369
+ if (seat === null) return;
16370
+ if (seat.querySelector("textarea:not([disabled]):not([readonly])") === null) return;
16371
+ const mirror = seat.querySelector("[data-input-mirror]");
16372
+ const overlay = ensureOverlay();
16373
+ if (mirror === null || overlay === null) return;
16374
+ const draft = (mirror.textContent ?? "").replace(/\n$/, "");
16375
+ if (draft === lastDraft) return;
16376
+ lastDraft = draft;
16377
+ render(overlay, draft);
16378
+ }
16379
+ /** Ctrl/Cmd+click on the composer: open the mention under the caret. */
16380
+ function onDocumentClick(e) {
16381
+ if (!(e.ctrlKey || e.metaKey)) return;
16382
+ const target = e.target;
16383
+ if (!(target instanceof HTMLTextAreaElement)) return;
16384
+ if (target.closest("[data-composer-seat]") === null) return;
16385
+ const pos = target.selectionStart;
16386
+ const draft = target.value;
16387
+ const hits = findMentions(draft);
16388
+ let hit = hits.find((h) => pos >= h.start && pos <= h.end);
16389
+ if (hit === void 0) hit = hits.find((h) => pos - 1 >= h.start && pos - 1 < h.end);
16390
+ if (hit === void 0) return;
16391
+ e.preventDefault();
16392
+ e.stopPropagation();
16393
+ openMention(hit.mention);
16394
+ }
16395
+ /** Start the composer mention overlay + Ctrl+click opener (idempotent). */
16396
+ function installComposerMentions() {
16397
+ if (typeof document === "undefined" || typeof MutationObserver === "undefined") return () => void 0;
16398
+ if (installed) return () => void 0;
16399
+ installed = true;
16174
16400
  installStyle();
16175
16401
  document.addEventListener("click", onDocumentClick);
16176
16402
  let pending = false;
16177
16403
  const scan = () => {
16178
16404
  pending = false;
16179
- for (const root of document.querySelectorAll("[data-conversation-scroll]")) linkifyRoot(root);
16405
+ sync();
16180
16406
  };
16181
16407
  const observer = new MutationObserver(() => {
16182
16408
  if (pending) return;
@@ -16192,7 +16418,7 @@ window.__ModuleLoader__.load({
16192
16418
  return () => {
16193
16419
  observer.disconnect();
16194
16420
  document.removeEventListener("click", onDocumentClick);
16195
- linkifierInstalled = false;
16421
+ installed = false;
16196
16422
  };
16197
16423
  }
16198
16424
  //#endregion
@@ -16212,6 +16438,7 @@ window.__ModuleLoader__.load({
16212
16438
  en
16213
16439
  }), "files-explorer: dictionaries");
16214
16440
  installComposerDrops();
16441
+ installComposerMentions();
16215
16442
  ctx.effect(() => installMentionLinkifier(), "files-explorer: @mention linkifier");
16216
16443
  const listDir = (path, signal) => unwrap(ctx.connection.rpc.call(CHANNEL, "list", { path }, signal));
16217
16444
  const readFile = (path, signal) => unwrap(ctx.connection.rpc.call(CHANNEL, "read", { path }, signal));