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 +5 -0
- package/README.md +1 -0
- package/lib/client.js +262 -35
- package/lib/client.js.map +1 -1
- package/lib/index.js +5 -1
- package/package.json +1 -1
- package/src/client/FileExplorer.tsx +6 -3
- 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 +17 -1
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
|
-
*
|
|
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;
|
|
@@ -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": "
|
|
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
|
|
15999
|
-
* gesture "在消息中引用"
|
|
16000
|
-
*
|
|
16001
|
-
* matches the mention grammar is wrapped
|
|
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)
|
|
16012
|
-
*
|
|
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._
|
|
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).
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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));
|