dsh-quote-followup 0.2.3 → 0.2.4

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/README.md CHANGED
@@ -2,8 +2,17 @@
2
2
 
3
3
  English | [中文](README.zh.md)
4
4
 
5
+ [![npm version](https://img.shields.io/npm/v/dsh-quote-followup.svg)](https://www.npmjs.com/package/dsh-quote-followup)
6
+ [![license](https://img.shields.io/npm/l/dsh-quote-followup.svg)](LICENSE)
7
+
5
8
  A Web-only [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) plugin for quoting selected conversation text into the composer and asking a focused follow-up.
6
9
 
10
+ <p align="center">
11
+ <img src="docs/assets/quote-followup-demo.gif" width="960" alt="Select conversation excerpts, insert native DSH quote chips, and write a focused follow-up">
12
+ </p>
13
+
14
+ <p align="center"><strong>Select. Quote. Follow up.</strong></p>
15
+
7
16
  ## Features
8
17
 
9
18
  - Select text inside the Web conversation transcript to reveal a floating **Quote** button.
@@ -16,6 +25,8 @@ TUI is intentionally unsupported.
16
25
 
17
26
  ## Install
18
27
 
28
+ Requires DSH `>=0.1.2-rc.1`.
29
+
19
30
  ```bash
20
31
  dsh plugin --profile web add dsh-quote-followup
21
32
  ```
package/README.zh.md CHANGED
@@ -2,8 +2,17 @@
2
2
 
3
3
  [English](README.md) | 中文
4
4
 
5
+ [![npm version](https://img.shields.io/npm/v/dsh-quote-followup.svg)](https://www.npmjs.com/package/dsh-quote-followup)
6
+ [![license](https://img.shields.io/npm/l/dsh-quote-followup.svg)](LICENSE)
7
+
5
8
  一个仅支持 Web 的 [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) 插件:选中对话文本,将其追加到输入框,再进行针对性追问。
6
9
 
10
+ <p align="center">
11
+ <img src="docs/assets/quote-followup-demo.gif" width="960" alt="划选对话片段,插入 DSH 原生引用 chip,再输入针对性追问">
12
+ </p>
13
+
14
+ <p align="center"><strong>划选、引用、继续追问。</strong></p>
15
+
7
16
  ## 功能
8
17
 
9
18
  - 在 Web 对话区选中文本后显示浮动的 **❐ 引用** 按钮。
@@ -16,6 +25,8 @@
16
25
 
17
26
  ## 安装
18
27
 
28
+ 要求 DSH `>=0.1.2-rc.1`。
29
+
19
30
  ```bash
20
31
  dsh plugin --profile web add dsh-quote-followup
21
32
  ```
package/lib/client.js CHANGED
@@ -33,9 +33,28 @@ window.__ModuleLoader__.load({
33
33
  ];
34
34
  const BUTTON_ID = "dsh-quote-followup-btn";
35
35
  const BUTTON_VERSION_ATTR = "data-dsh-quote-followup-version";
36
- const CLIENT_VERSION = "0.2.3";
36
+ const CLIENT_VERSION = "0.2.4";
37
37
  /** Codec owner for native DSH reference chips. */
38
38
  const QUOTE_SOURCE = "quote-followup";
39
+ const LOCALE_NS = "quote-followup";
40
+ /** UI + serialized frame copy; the active DSH locale picks the language. */
41
+ const LOCALE_DICT = {
42
+ zh: {
43
+ "button.label": "❐ 引用",
44
+ "button.title": "引用选中内容到输入框(可多次引用,发送前可编辑)",
45
+ "quote.header": "引用",
46
+ "conversation.role": "对话",
47
+ "quote.truncated": ",已截断"
48
+ },
49
+ en: {
50
+ "button.label": "❐ Quote",
51
+ "button.title": "Quote the selection into the composer (repeatable; editable before sending)",
52
+ "quote.header": "Quote",
53
+ "conversation.role": "conversation",
54
+ "quote.truncated": ", truncated"
55
+ }
56
+ };
57
+ const fallbackT = (key) => LOCALE_DICT.en[key] ?? key;
39
58
  /** Per-quote character cap for a bounded composer insertion. */
40
59
  const QUOTE_MAX_CHARS = 1600;
41
60
  //#endregion
@@ -75,7 +94,7 @@ window.__ModuleLoader__.load({
75
94
  }
76
95
  return null;
77
96
  };
78
- const roleLabel = (role) => role === "user" ? "user" : role === "assistant" ? "assistant" : "对话";
97
+ const roleLabel = (role, t) => role === "user" ? "user" : role === "assistant" ? "assistant" : t("conversation.role");
79
98
  /** Bound one quote before it becomes editor state. */
80
99
  const quotePayload = (text, role) => {
81
100
  let body = String(text ?? "");
@@ -87,9 +106,9 @@ window.__ModuleLoader__.load({
87
106
  return { text: body, role, truncated };
88
107
  };
89
108
  /** Model / clipboard projection. The composer shows a chip, not this frame. */
90
- const quoteFrame = ({ text, role, truncated }) => {
109
+ const quoteFrame = ({ text, role, truncated }, t) => {
91
110
  const lines = text.split("\n").map((line) => `> ${line}`.trimEnd());
92
- return `> [引用 · ${roleLabel(role)}${truncated ? ",已截断" : ""}]\n${lines.join("\n")}\n\n`;
111
+ return `> [${t("quote.header")} · ${roleLabel(role, t)}${truncated ? t("quote.truncated") : ""}]\n${lines.join("\n")}\n\n`;
93
112
  };
94
113
  const decodeQuote = (ref) => {
95
114
  const value = JSON.parse(ref);
@@ -101,29 +120,31 @@ window.__ModuleLoader__.load({
101
120
  truncated: value.truncated === true
102
121
  };
103
122
  };
104
- const quoteInsert = (payload) => {
105
- const clipboardText = quoteFrame(payload);
123
+ const quoteInsert = (payload, t) => {
124
+ const clipboardText = quoteFrame(payload, t);
106
125
  const preview = payload.text.replace(/\s+/g, " ").trim();
107
126
  return {
108
127
  source: QUOTE_SOURCE,
109
128
  ref: JSON.stringify(payload),
110
- label: `引用 · ${roleLabel(payload.role)}: ${preview}${payload.truncated ? "…" : ""}`,
129
+ // The bubble icon carries the quote meaning; keep only the excerpt
130
+ // visible. Role/truncation stay in the serialized Markdown frame.
131
+ label: preview,
111
132
  appearance: "session",
112
133
  clipboardText
113
134
  };
114
135
  };
115
136
  /** Codec-only source: it owns quote chips but intentionally adds no @ candidates. */
116
- const QUOTE_TRIGGER_SOURCE = {
137
+ const makeQuoteSource = (t) => ({
117
138
  trigger: "@",
118
139
  name: QUOTE_SOURCE,
119
140
  order: 1000,
120
141
  candidates: () => Promise.resolve([]),
121
142
  onPick: () => void 0,
122
143
  codec: {
123
- clipboardText: (ref) => quoteFrame(decodeQuote(ref)),
124
- serialize: (ref) => Promise.resolve(quoteFrame(decodeQuote(ref)))
144
+ clipboardText: (ref) => quoteFrame(decodeQuote(ref), t),
145
+ serialize: (ref) => Promise.resolve(quoteFrame(decodeQuote(ref), t))
125
146
  }
126
- };
147
+ });
127
148
  const findComposer = () => {
128
149
  for (const selector of COMPOSER_SELECTORS) {
129
150
  const element = document.querySelector(selector);
@@ -138,7 +159,7 @@ window.__ModuleLoader__.load({
138
159
  * The node class is already registered on the live composer; no second
139
160
  * Lexical or React runtime is loaded by this plugin.
140
161
  */
141
- const appendQuoteChip = (element, payload) => {
162
+ const appendQuoteChip = (element, payload, t) => {
142
163
  const editor = element.__lexicalEditor;
143
164
  const registry = editor?._nodes;
144
165
  const ChipNode = registry?.get?.("reference-chip")?.klass;
@@ -162,7 +183,7 @@ window.__ModuleLoader__.load({
162
183
  const tail = typeof block.getTextContent === "function" ? block.getTextContent().slice(-1) : "";
163
184
  if (tail !== "" && !/\s/u.test(tail))
164
185
  nodes.push(new TextNode(" "));
165
- nodes.push(new ChipNode(quoteInsert(payload)));
186
+ nodes.push(new ChipNode(quoteInsert(payload, t)));
166
187
  const trailing = new TextNode(" ");
167
188
  nodes.push(trailing);
168
189
  block.append(...nodes);
@@ -284,6 +305,7 @@ window.__ModuleLoader__.load({
284
305
  //#region floating button
285
306
  let pendingQuote = null;
286
307
  let quoteSourceReady = false;
308
+ let currentT = fallbackT;
287
309
  const ensureButton = () => {
288
310
  const existing = document.getElementById(BUTTON_ID);
289
311
  if (existing?.getAttribute(BUTTON_VERSION_ATTR) === CLIENT_VERSION)
@@ -293,8 +315,8 @@ window.__ModuleLoader__.load({
293
315
  button.id = BUTTON_ID;
294
316
  button.setAttribute(BUTTON_VERSION_ATTR, CLIENT_VERSION);
295
317
  button.type = "button";
296
- button.textContent = "❐ 引用";
297
- button.title = "引用选中内容到输入框(可多次引用,发送前可编辑)";
318
+ button.textContent = currentT("button.label");
319
+ button.title = currentT("button.title");
298
320
  Object.assign(button.style, {
299
321
  position: "fixed",
300
322
  zIndex: "2147483600",
@@ -374,8 +396,8 @@ window.__ModuleLoader__.load({
374
396
  // insertion destroys Lexical's caret and makes the next quote unreliable.
375
397
  document.getSelection()?.removeAllRanges();
376
398
  const payload = quotePayload(quote.text, quote.role);
377
- if (!(quoteSourceReady && appendQuoteChip(composer, payload)))
378
- appendToComposer(composer, quoteFrame(payload));
399
+ if (!(quoteSourceReady && appendQuoteChip(composer, payload, currentT)))
400
+ appendToComposer(composer, quoteFrame(payload, currentT));
379
401
  };
380
402
  //#endregion
381
403
  //#region apply
@@ -387,10 +409,34 @@ window.__ModuleLoader__.load({
387
409
  if (typeof previous?.dispose === "function")
388
410
  previous.dispose();
389
411
  let unregisterSource = null;
412
+ let unregisterLocale = null;
413
+ let unsubscribeLocale = null;
414
+ const refreshButtonCopy = () => {
415
+ const button = document.getElementById(BUTTON_ID);
416
+ if (button !== null) {
417
+ button.textContent = currentT("button.label");
418
+ button.title = currentT("button.title");
419
+ }
420
+ };
421
+ try {
422
+ const locale = typeof ctx?.get === "function" ? ctx.get("locale") : null;
423
+ if (typeof locale?.register === "function" && typeof locale?.bind === "function") {
424
+ unregisterLocale = locale.register(LOCALE_NS, LOCALE_DICT);
425
+ currentT = locale.bind(LOCALE_NS);
426
+ if (typeof locale.subscribe === "function") {
427
+ unsubscribeLocale = locale.subscribe(refreshButtonCopy);
428
+ }
429
+ } else {
430
+ currentT = fallbackT;
431
+ }
432
+ } catch (error) {
433
+ console.warn("[dsh-quote-followup] locale unavailable; using English copy", error);
434
+ currentT = fallbackT;
435
+ }
390
436
  try {
391
437
  const inputTriggers = typeof ctx?.get === "function" ? ctx.get("inputTriggers") : null;
392
438
  if (typeof inputTriggers?.registerSource === "function") {
393
- unregisterSource = inputTriggers.registerSource(QUOTE_TRIGGER_SOURCE);
439
+ unregisterSource = inputTriggers.registerSource(makeQuoteSource(currentT));
394
440
  quoteSourceReady = true;
395
441
  }
396
442
  } catch (error) {
@@ -412,6 +458,11 @@ window.__ModuleLoader__.load({
412
458
  return;
413
459
  disposed = true;
414
460
  quoteSourceReady = false;
461
+ if (typeof unsubscribeLocale === "function")
462
+ unsubscribeLocale();
463
+ if (typeof unregisterLocale === "function")
464
+ unregisterLocale();
465
+ currentT = fallbackT;
415
466
  if (typeof unregisterSource === "function")
416
467
  unregisterSource();
417
468
  document.removeEventListener("selectionchange", onSelectionChange);
@@ -437,7 +488,7 @@ window.__ModuleLoader__.load({
437
488
  ctx.effect(() => cleanup, "dsh-quote-followup: selection quote ui");
438
489
  }
439
490
  //#endregion
440
- exports.inject = ["inputTriggers"];
491
+ exports.inject = ["inputTriggers", "locale"];
441
492
  exports.apply = apply;
442
493
  return module.exports;
443
494
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-quote-followup",
3
- "version": "0.2.3",
3
+ "version": "0.2.4",
4
4
  "description": "Quote selected Web conversation text into the DeepSeek Harness composer for targeted follow-up turns.",
5
5
  "keywords": [
6
6
  "dsh",
@@ -28,6 +28,7 @@
28
28
  "files": [
29
29
  "lib",
30
30
  "cordis.patch.yml",
31
+ "docs",
31
32
  "README.md",
32
33
  "README.zh.md"
33
34
  ],