dsh-chat-link-actions 0.1.0 → 0.2.1

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.
Files changed (3) hide show
  1. package/README.md +9 -2
  2. package/client.js +149 -21
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -4,12 +4,17 @@
4
4
 
5
5
  长任务跑完,记录里一串路径;想引用回去得手打,想在访达里找到又是另一回事。这个插件把右键菜单接到对话里的文件链接上。
6
6
 
7
- ## 两个动作
7
+ ## 三个动作
8
8
 
9
9
  | 右键文件链接 | 做什么 |
10
10
  | --- | --- |
11
- | **引用到对话** | `@路径` 原样插进输入框,光标跟在后面;一次撤销就能回到原样 |
11
+ | **引用到对话** | 插成输入框里的引用芯片(`(</> 文件名)`),和拖文件、`@` 选文件的结果一样 |
12
12
  | **在访达中查看** | 打开所在目录并选中这个文件(Windows 用资源管理器;Linux 打开所在目录) |
13
+ | **复制路径** | 绝对路径进剪贴板,直接粘给别的窗口 |
14
+
15
+ ## 认哪些链接
16
+
17
+ 右键不是只认一种写法,常见渲染都走同一条解析:`href` / `src` 里的 `?path=`(图片、附件卡片)、`data-path` 之类的绝对路径属性、`title` 里的路径(宿主自绘的文件链接、文件夹链接)、以及直接写在正文里的绝对路径(光标落在路径上才算)。Windows 的 `C:\...` 与含空格、中文的路径同样能认。
13
18
 
14
19
  ## 装
15
20
 
@@ -23,6 +28,8 @@ dsh plugin install dsh-chat-link-actions
23
28
 
24
29
  点下菜单之后,如果你又改了输入框或换了会话,插入会停下来不覆盖已写的内容——重新右键一次即可。
25
30
 
31
+ 芯片由宿主渲染:图和视频进的是附件,文件夹显示成 `文件夹名/`,其他文件按扩展名走同一个芯片。
32
+
26
33
  ## 不做什么
27
34
 
28
35
  - 只认本机存在的绝对路径:网络路径、已删掉的文件,会直接说找不到。
package/client.js CHANGED
@@ -33,6 +33,8 @@ window.__ModuleLoader__.load({
33
33
 
34
34
  /** SessionId -> inputActions for that session's composer. */
35
35
  var drafts = new Map();
36
+ /** Client root context, kept for the session-scoped chip event. */
37
+ var rootContext = null;
36
38
  /** Session whose composer rendered most recently. */
37
39
  var activeSession = null;
38
40
 
@@ -61,9 +63,17 @@ window.__ModuleLoader__.load({
61
63
 
62
64
  function draftCandidates() {
63
65
  var ordered = [];
64
- if (activeSession !== null && drafts.has(activeSession)) ordered.push(drafts.get(activeSession));
65
- drafts.forEach(function (actions) {
66
- if (ordered.indexOf(actions) === -1) ordered.push(actions);
66
+ var seen = [];
67
+ function add(sessionId) {
68
+ if (sessionId === undefined || sessionId === null) return;
69
+ var actions = drafts.get(sessionId);
70
+ if (actions === undefined || seen.indexOf(actions) !== -1) return;
71
+ seen.push(actions);
72
+ ordered.push({ sessionId: sessionId, actions: actions });
73
+ }
74
+ add(activeSession);
75
+ drafts.forEach(function (_actions, sessionId) {
76
+ add(sessionId);
67
77
  });
68
78
  return ordered;
69
79
  }
@@ -73,13 +83,70 @@ window.__ModuleLoader__.load({
73
83
  return /[\s"]/.test(path) ? '@"' + path.replace(/"/g, '\\"') + '"' : "@" + path;
74
84
  }
75
85
 
86
+ /** Directory links carry a trailing separator; files never do. */
87
+ function looksLikeDirectory(path) {
88
+ return path.charAt(path.length - 1) === "/" || path.charAt(path.length - 1) === "\\";
89
+ }
90
+
91
+ /** Last path segment, the same label the composer's own reference chips show. */
92
+ function displayLabel(path, directory) {
93
+ var trimmed = path.replace(/[\\/]+$/, "");
94
+ var cut = Math.max(trimmed.lastIndexOf("/"), trimmed.lastIndexOf("\\"));
95
+ var name = cut === -1 ? trimmed : trimmed.slice(cut + 1);
96
+ if (name === "") name = trimmed;
97
+ return directory ? name + "/" : name;
98
+ }
99
+
100
+ /**
101
+ * Insert the reference as the composer's own reference chip (the `(</> name)`
102
+ * node), which the shell only builds for its file-drop and `@`-picker paths.
103
+ * The shell owns that insertion behind a session-scoped event; when the event
104
+ * is not reachable the caller falls back to plain `insertText`.
105
+ */
106
+ function insertChip(sessionId, actions, path) {
107
+ if (rootContext === null || typeof rootContext.sessions !== "object" || rootContext.sessions === null) return false;
108
+ var binding = null;
109
+ try {
110
+ binding = rootContext.sessions.binding(sessionId);
111
+ } catch (error) {
112
+ return false;
113
+ }
114
+ var sessionContext = binding === null || binding === undefined ? null : binding.ctx;
115
+ if (sessionContext === null || sessionContext === undefined || typeof sessionContext.emit !== "function") return false;
116
+ var before = actions.captureInsertion();
117
+ if (before === null || before === undefined) return false;
118
+ var directory = looksLikeDirectory(path);
119
+ var mention = referenceText(path);
120
+ var reference = {};
121
+ reference.source = "reference";
122
+ reference.ref = mention;
123
+ reference.label = displayLabel(path, directory);
124
+ reference.appearance = directory ? "folder" : "file";
125
+ reference.clipboardText = mention;
126
+ var request = {};
127
+ request.reference = reference;
128
+ request.span = before;
129
+ var delivered = false;
130
+ try {
131
+ delivered = sessionContext.emit("slash/input-insert-reference", request);
132
+ } catch (error) {
133
+ return false;
134
+ }
135
+ if (delivered === false) return false;
136
+ var after = actions.captureInsertion();
137
+ if (after === null || after === undefined) return true;
138
+ return after.start !== before.start || after.end !== before.end;
139
+ }
140
+
76
141
  /** Insert the reference into a live composer. Returns true on success. */
77
142
  function quoteIntoComposer(path) {
78
143
  var candidates = draftCandidates();
79
144
  for (var i = 0; i < candidates.length; i += 1) {
80
- var actions = candidates[i];
145
+ var sessionId = candidates[i].sessionId;
146
+ var actions = candidates[i].actions;
81
147
  try {
82
148
  if (typeof actions.captureInsertion !== "function") continue;
149
+ if (insertChip(sessionId, actions, path)) return true;
83
150
  var span = actions.captureInsertion();
84
151
  if (span === null || span === undefined) continue;
85
152
  if (actions.insertText(referenceText(path) + " ", span) === true) return true;
@@ -102,6 +169,9 @@ window.__ModuleLoader__.load({
102
169
  candidate = candidate.slice(1, -1);
103
170
  }
104
171
  candidate = candidate.replace(/[.,;:!?,。;:!?]+$/u, "");
172
+ candidate = candidate.replace(/^file:\/\//, "/");
173
+ if (/^[A-Za-z]:(?!\/)/.test(candidate)) candidate = candidate.slice(0, 2) + "/" + candidate.slice(2);
174
+ candidate = candidate.replace(/^([A-Za-z]):\//, "$1:/");
105
175
  return looksAbsolute(candidate) ? candidate : null;
106
176
  }
107
177
 
@@ -116,6 +186,10 @@ window.__ModuleLoader__.load({
116
186
  var url = new URL(href, document.baseURI);
117
187
  var fromQuery = normalizeCandidate(url.searchParams.get("path"));
118
188
  if (fromQuery !== null) return fromQuery;
189
+ for (var i = 0; i < ["file", "filepath", "abs", "target"].length; i += 1) {
190
+ var fromNamed = normalizeCandidate(url.searchParams.get(["file", "filepath", "abs", "target"][i]));
191
+ if (fromNamed !== null) return fromNamed;
192
+ }
119
193
  if (url.protocol === "file:") return normalizeCandidate(decodeURIComponent(url.pathname));
120
194
  if (url.origin === location.origin && looksAbsolute(url.pathname) && /\.[A-Za-z0-9]{1,6}$/.test(url.pathname)) {
121
195
  return normalizeCandidate(decodeURIComponent(url.pathname));
@@ -130,20 +204,71 @@ window.__ModuleLoader__.load({
130
204
  * Walk up from the clicked node looking for a link that names a file: href
131
205
  * first, then an absolute `data-*` value, then the reference chip title.
132
206
  */
133
- function pathFromNode(start) {
207
+ var PATH_ATTRS = ["href", "src"];
208
+
209
+ /** `data-*` values that may carry a path, in confidence order. */
210
+ function datasetPath(node) {
211
+ var dataset = node.dataset || {};
212
+ var keys = Object.keys(dataset);
213
+ for (var i = 0; i < keys.length; i += 1) {
214
+ if (!/path|file|href|src|dir|folder|abs/i.test(keys[i])) continue;
215
+ var candidate = normalizeCandidate(dataset[keys[i]]);
216
+ if (candidate !== null) return candidate;
217
+ }
218
+ return null;
219
+ }
220
+
221
+ /** An absolute path written as plain text inside the clicked element. */
222
+ var PATH_IN_TEXT = /(?:^|[\s"'“”「」()()\[\]])(@?"?(?:\/|[A-Za-z]:[\\/])[^\s"'“”「」<>,。;:!?]*)/u;
223
+
224
+ /** 正文纯文本里,取光标落点所在的那段路径。 */
225
+ function pathFromCaret(x, y) {
226
+ if (typeof document.caretRangeFromPoint !== "function") return null;
227
+ var range = null;
228
+ try {
229
+ range = document.caretRangeFromPoint(x, y);
230
+ } catch (error) {
231
+ return null;
232
+ }
233
+ if (range === null || range.startContainer === null || range.startContainer.nodeType !== 3) return null;
234
+ var data = range.startContainer.data;
235
+ var offset = Math.min(range.startOffset, data.length);
236
+ var bounds = [];
237
+ var matcher = new RegExp(PATH_IN_TEXT.source, "gu");
238
+ var hit = null;
239
+ while ((hit = matcher.exec(data)) !== null) {
240
+ bounds.push({ text: hit[0], start: hit.index, end: hit.index + hit[0].length });
241
+ if (hit[0].length === 0) matcher.lastIndex += 1;
242
+ }
243
+ for (var i = 0; i < bounds.length; i += 1) {
244
+ if (offset >= bounds[i].start && offset <= bounds[i].end) {
245
+ return normalizeCandidate(bounds[i].text);
246
+ }
247
+ }
248
+ return null;
249
+ }
250
+
251
+ /**
252
+ * Walk up from the clicked node looking for a file link: href/src first, then
253
+ * an absolute `data-*` value, then a title, then the element's own text. The
254
+ * text pass covers paths the transcript renders as plain prose.
255
+ */
256
+ function pathFromNode(start, point) {
134
257
  for (var node = start; node !== null && node !== undefined && node !== document.body; node = node.parentElement) {
135
258
  if (node.nodeType !== 1) continue;
136
- var fromHref = decodePathFromHref(node.getAttribute("href"));
137
- if (fromHref !== null) return fromHref;
138
- var dataset = node.dataset || {};
139
- var keys = Object.keys(dataset);
140
- for (var k = 0; k < keys.length; k += 1) {
141
- var fromData = normalizeCandidate(dataset[keys[k]]);
142
- if (fromData !== null && /\.[A-Za-z0-9]{1,6}$/.test(fromData)) return fromData;
259
+ for (var a = 0; a < PATH_ATTRS.length; a += 1) {
260
+ var fromHref = decodePathFromHref(node.getAttribute(PATH_ATTRS[a]));
261
+ if (fromHref !== null) return fromHref;
143
262
  }
263
+ var fromData = datasetPath(node);
264
+ if (fromData !== null) return fromData;
144
265
  var fromTitle = normalizeCandidate(node.getAttribute("title"));
145
266
  if (fromTitle !== null) return fromTitle;
146
267
  }
268
+ if (point !== null && point !== undefined) {
269
+ var fromCaret = pathFromCaret(point.x, point.y);
270
+ if (fromCaret !== null) return fromCaret;
271
+ }
147
272
  return null;
148
273
  }
149
274
 
@@ -157,7 +282,8 @@ window.__ModuleLoader__.load({
157
282
 
158
283
  var STYLE_ID = "dsh-chat-link-actions-style";
159
284
  var CSS = [
160
- ".dcla-menu{position:fixed;z-index:1100;box-sizing:border-box;padding:6px;min-width:200px;",
285
+ ".dcla-layer{position:fixed;inset:0;z-index:1100;pointer-events:none;}",
286
+ ".dcla-menu{position:fixed;z-index:1100;pointer-events:auto;box-sizing:border-box;padding:6px;min-width:200px;",
161
287
  "max-width:min(320px,calc(100vw - 24px));background:var(--dsw-specific-menu,rgba(30,32,38,.96));",
162
288
  "backdrop-filter:var(--dsw-menu-backdrop-filter,blur(20px) saturate(180%));",
163
289
  "-webkit-backdrop-filter:var(--dsw-menu-backdrop-filter,blur(20px) saturate(180%));",
@@ -281,19 +407,19 @@ window.__ModuleLoader__.load({
281
407
 
282
408
  function onViewportChange() {
283
409
  if (open === null) return;
284
- placeMenu(open.layer, open.x, open.y);
410
+ placeMenu(open.panel, open.x, open.y);
285
411
  }
286
412
 
287
413
  /** Place the panel at the pointer, flipped inward at the viewport edges. */
288
- function placeMenu(layer, x, y) {
414
+ function placeMenu(panel, x, y) {
289
415
  var margin = 8;
290
- var rect = layer.getBoundingClientRect();
416
+ var rect = panel.getBoundingClientRect();
291
417
  var left = x;
292
418
  var top = y;
293
419
  if (left + rect.width > window.innerWidth - margin) left = Math.max(margin, x - rect.width);
294
420
  if (top + rect.height > window.innerHeight - margin) top = Math.max(margin, y - rect.height);
295
- layer.style.left = Math.round(left) + "px";
296
- layer.style.top = Math.round(top) + "px";
421
+ panel.style.left = Math.round(left) + "px";
422
+ panel.style.top = Math.round(top) + "px";
297
423
  }
298
424
 
299
425
  function paintActive() {
@@ -383,6 +509,7 @@ window.__ModuleLoader__.load({
383
509
 
384
510
  var state = {};
385
511
  state.layer = layer;
512
+ state.panel = built.panel;
386
513
  state.buttons = built.buttons;
387
514
  state.items = items;
388
515
  state.index = 0;
@@ -391,7 +518,7 @@ window.__ModuleLoader__.load({
391
518
  state.y = y;
392
519
  state.restoreFocus = document.activeElement instanceof HTMLElement ? document.activeElement : null;
393
520
  open = state;
394
- placeMenu(layer, x, y);
521
+ placeMenu(built.panel, x, y);
395
522
  document.addEventListener("pointerdown", onPointerDown, true);
396
523
  document.addEventListener("keydown", onKeyDown, true);
397
524
  document.addEventListener("contextmenu", onContextDuringMenu, true);
@@ -432,7 +559,7 @@ window.__ModuleLoader__.load({
432
559
  function onContextMenu(event) {
433
560
  if (!(event.target instanceof Element)) return;
434
561
  var target = event.target;
435
- var path = pathFromNode(target);
562
+ var path = pathFromNode(target, { x: event.clientX, y: event.clientY });
436
563
  if (path === null) return;
437
564
  event.preventDefault();
438
565
  event.stopPropagation();
@@ -441,9 +568,10 @@ window.__ModuleLoader__.load({
441
568
 
442
569
  /* ── plugin entry ───────────────────────────────────────────────────── */
443
570
 
444
- var inject = ["slots"];
571
+ var inject = ["slots", "sessions"];
445
572
 
446
573
  function apply(ctx) {
574
+ rootContext = ctx;
447
575
  ctx.effect(function () {
448
576
  document.addEventListener("contextmenu", onContextMenu, true);
449
577
  return function () {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-chat-link-actions",
3
- "version": "0.1.0",
3
+ "version": "0.2.1",
4
4
  "description": "Right-click actions on file links in the DSH transcript: quote the path into the composer, or reveal the file in the OS file manager. Menus are drawn in the app's own style, never the system one.",
5
5
  "type": "module",
6
6
  "main": "index.js",