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 +11 -0
- package/README.zh.md +11 -0
- package/docs/assets/quote-followup-demo.gif +0 -0
- package/docs/assets/quote-followup-demo.webm +0 -0
- package/lib/client.js +70 -19
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -2,8 +2,17 @@
|
|
|
2
2
|
|
|
3
3
|
English | [中文](README.zh.md)
|
|
4
4
|
|
|
5
|
+
[](https://www.npmjs.com/package/dsh-quote-followup)
|
|
6
|
+
[](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
|
+
[](https://www.npmjs.com/package/dsh-quote-followup)
|
|
6
|
+
[](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
|
```
|
|
Binary file
|
|
Binary file
|
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.
|
|
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 `> [
|
|
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
|
-
|
|
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
|
|
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(
|
|
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
|
+
"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
|
],
|