dsh-quote-followup 0.2.2 → 0.2.3
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 +6 -5
- package/README.zh.md +6 -5
- package/lib/client.js +109 -8
- package/package.json +5 -3
package/README.md
CHANGED
|
@@ -7,9 +7,10 @@ A Web-only [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) p
|
|
|
7
7
|
## Features
|
|
8
8
|
|
|
9
9
|
- Select text inside the Web conversation transcript to reveal a floating **Quote** button.
|
|
10
|
-
- Append
|
|
11
|
-
-
|
|
12
|
-
-
|
|
10
|
+
- Append a native DSH conversation-reference chip without replacing the existing draft.
|
|
11
|
+
- Reuse the same atomic `ReferenceChipNode`, conversation icon, and business color as `@file` / `@session`.
|
|
12
|
+
- Quote multiple excerpts; on send, the plugin codec expands each chip into a model-readable Markdown blockquote.
|
|
13
|
+
- Fall back to a plain-text quote when the host lacks native chip support.
|
|
13
14
|
|
|
14
15
|
TUI is intentionally unsupported.
|
|
15
16
|
|
|
@@ -26,7 +27,7 @@ Add `dsh-quote-followup` to the Web profile's `dsh.profile.bundles`, then restar
|
|
|
26
27
|
1. Select text in a conversation message.
|
|
27
28
|
2. Click **❐ Quote**.
|
|
28
29
|
3. Select and quote more excerpts if needed.
|
|
29
|
-
4. Type the follow-up
|
|
30
|
+
4. Type the follow-up after the inserted chips and send.
|
|
30
31
|
|
|
31
32
|
## Development
|
|
32
33
|
|
|
@@ -34,7 +35,7 @@ Add `dsh-quote-followup` to the Web profile's `dsh.profile.bundles`, then restar
|
|
|
34
35
|
npm test
|
|
35
36
|
```
|
|
36
37
|
|
|
37
|
-
The regression harness covers repeated
|
|
38
|
+
The regression harness covers native quote chips, repeated quoting, spacing after an existing draft, codec serialization, the Firefox text fallback, and current-client takeover of stale singleton/button state after a hot swap.
|
|
38
39
|
|
|
39
40
|
## License
|
|
40
41
|
|
package/README.zh.md
CHANGED
|
@@ -7,9 +7,10 @@
|
|
|
7
7
|
## 功能
|
|
8
8
|
|
|
9
9
|
- 在 Web 对话区选中文本后显示浮动的 **❐ 引用** 按钮。
|
|
10
|
-
- 以
|
|
11
|
-
-
|
|
12
|
-
-
|
|
10
|
+
- 以 DSH 原生对话引用 chip 追加到现有草稿,不覆盖已输入内容。
|
|
11
|
+
- chip 使用与 `@文件` / `@对话` 相同的 `ReferenceChipNode`、对话图标和业务色,可整体删除。
|
|
12
|
+
- 可连续引用多段内容;发送时由插件 codec 将各 chip 展开为模型可读的 Markdown 引用块。
|
|
13
|
+
- 旧版 DSH 缺少原生 chip 能力时,自动降级为纯文本引用。
|
|
13
14
|
|
|
14
15
|
本插件不再适配 TUI。
|
|
15
16
|
|
|
@@ -26,7 +27,7 @@ dsh plugin --profile web add dsh-quote-followup
|
|
|
26
27
|
1. 在对话消息中划选任意片段。
|
|
27
28
|
2. 点击 **❐ 引用**。
|
|
28
29
|
3. 如需补充,可继续选中并引用其他片段。
|
|
29
|
-
4.
|
|
30
|
+
4. 在引用 chip 后输入追问并发送。
|
|
30
31
|
|
|
31
32
|
## 开发验证
|
|
32
33
|
|
|
@@ -34,7 +35,7 @@ dsh plugin --profile web add dsh-quote-followup
|
|
|
34
35
|
npm test
|
|
35
36
|
```
|
|
36
37
|
|
|
37
|
-
|
|
38
|
+
回归测试覆盖原生引用 chip、连续引用、已有草稿后的间距、codec 序列化、Firefox 文本降级路径,以及热替换后新版 client 接管旧按钮/单例状态。
|
|
38
39
|
|
|
39
40
|
## 许可证
|
|
40
41
|
|
package/lib/client.js
CHANGED
|
@@ -33,7 +33,9 @@ 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.3";
|
|
37
|
+
/** Codec owner for native DSH reference chips. */
|
|
38
|
+
const QUOTE_SOURCE = "quote-followup";
|
|
37
39
|
/** Per-quote character cap for a bounded composer insertion. */
|
|
38
40
|
const QUOTE_MAX_CHARS = 1600;
|
|
39
41
|
//#endregion
|
|
@@ -73,17 +75,54 @@ window.__ModuleLoader__.load({
|
|
|
73
75
|
}
|
|
74
76
|
return null;
|
|
75
77
|
};
|
|
76
|
-
|
|
77
|
-
|
|
78
|
+
const roleLabel = (role) => role === "user" ? "user" : role === "assistant" ? "assistant" : "对话";
|
|
79
|
+
/** Bound one quote before it becomes editor state. */
|
|
80
|
+
const quotePayload = (text, role) => {
|
|
78
81
|
let body = String(text ?? "");
|
|
79
82
|
let truncated = false;
|
|
80
83
|
if (body.length > QUOTE_MAX_CHARS) {
|
|
81
84
|
body = body.slice(0, QUOTE_MAX_CHARS);
|
|
82
85
|
truncated = true;
|
|
83
86
|
}
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
+
return { text: body, role, truncated };
|
|
88
|
+
};
|
|
89
|
+
/** Model / clipboard projection. The composer shows a chip, not this frame. */
|
|
90
|
+
const quoteFrame = ({ text, role, truncated }) => {
|
|
91
|
+
const lines = text.split("\n").map((line) => `> ${line}`.trimEnd());
|
|
92
|
+
return `> [引用 · ${roleLabel(role)}${truncated ? ",已截断" : ""}]\n${lines.join("\n")}\n\n`;
|
|
93
|
+
};
|
|
94
|
+
const decodeQuote = (ref) => {
|
|
95
|
+
const value = JSON.parse(ref);
|
|
96
|
+
if (value === null || typeof value !== "object" || typeof value.text !== "string")
|
|
97
|
+
throw new Error("invalid quote-followup reference");
|
|
98
|
+
return {
|
|
99
|
+
text: value.text,
|
|
100
|
+
role: value.role === "user" || value.role === "assistant" ? value.role : null,
|
|
101
|
+
truncated: value.truncated === true
|
|
102
|
+
};
|
|
103
|
+
};
|
|
104
|
+
const quoteInsert = (payload) => {
|
|
105
|
+
const clipboardText = quoteFrame(payload);
|
|
106
|
+
const preview = payload.text.replace(/\s+/g, " ").trim();
|
|
107
|
+
return {
|
|
108
|
+
source: QUOTE_SOURCE,
|
|
109
|
+
ref: JSON.stringify(payload),
|
|
110
|
+
label: `引用 · ${roleLabel(payload.role)}: ${preview}${payload.truncated ? "…" : ""}`,
|
|
111
|
+
appearance: "session",
|
|
112
|
+
clipboardText
|
|
113
|
+
};
|
|
114
|
+
};
|
|
115
|
+
/** Codec-only source: it owns quote chips but intentionally adds no @ candidates. */
|
|
116
|
+
const QUOTE_TRIGGER_SOURCE = {
|
|
117
|
+
trigger: "@",
|
|
118
|
+
name: QUOTE_SOURCE,
|
|
119
|
+
order: 1000,
|
|
120
|
+
candidates: () => Promise.resolve([]),
|
|
121
|
+
onPick: () => void 0,
|
|
122
|
+
codec: {
|
|
123
|
+
clipboardText: (ref) => quoteFrame(decodeQuote(ref)),
|
|
124
|
+
serialize: (ref) => Promise.resolve(quoteFrame(decodeQuote(ref)))
|
|
125
|
+
}
|
|
87
126
|
};
|
|
88
127
|
const findComposer = () => {
|
|
89
128
|
for (const selector of COMPOSER_SELECTORS) {
|
|
@@ -93,6 +132,52 @@ window.__ModuleLoader__.load({
|
|
|
93
132
|
}
|
|
94
133
|
return null;
|
|
95
134
|
};
|
|
135
|
+
/**
|
|
136
|
+
* Insert through DSH's registered ReferenceChipNode so the quote uses the
|
|
137
|
+
* exact same atomic editor entity and visual treatment as @file/@session.
|
|
138
|
+
* The node class is already registered on the live composer; no second
|
|
139
|
+
* Lexical or React runtime is loaded by this plugin.
|
|
140
|
+
*/
|
|
141
|
+
const appendQuoteChip = (element, payload) => {
|
|
142
|
+
const editor = element.__lexicalEditor;
|
|
143
|
+
const registry = editor?._nodes;
|
|
144
|
+
const ChipNode = registry?.get?.("reference-chip")?.klass;
|
|
145
|
+
const TextNode = registry?.get?.("text")?.klass;
|
|
146
|
+
const ParagraphNode = registry?.get?.("paragraph")?.klass;
|
|
147
|
+
if (typeof editor?.update !== "function" || typeof ChipNode !== "function" || typeof TextNode !== "function" || typeof ParagraphNode !== "function")
|
|
148
|
+
return false;
|
|
149
|
+
try {
|
|
150
|
+
let inserted = false;
|
|
151
|
+
editor.update(() => {
|
|
152
|
+
const state = editor._pendingEditorState ?? editor._editorState;
|
|
153
|
+
const root = state?._nodeMap?.get?.("root");
|
|
154
|
+
if (root === void 0 || root === null || typeof root.append !== "function")
|
|
155
|
+
return;
|
|
156
|
+
let block = typeof root.getLastChild === "function" ? root.getLastChild() : null;
|
|
157
|
+
if (block === null || typeof block.append !== "function") {
|
|
158
|
+
block = new ParagraphNode();
|
|
159
|
+
root.append(block);
|
|
160
|
+
}
|
|
161
|
+
const nodes = [];
|
|
162
|
+
const tail = typeof block.getTextContent === "function" ? block.getTextContent().slice(-1) : "";
|
|
163
|
+
if (tail !== "" && !/\s/u.test(tail))
|
|
164
|
+
nodes.push(new TextNode(" "));
|
|
165
|
+
nodes.push(new ChipNode(quoteInsert(payload)));
|
|
166
|
+
const trailing = new TextNode(" ");
|
|
167
|
+
nodes.push(trailing);
|
|
168
|
+
block.append(...nodes);
|
|
169
|
+
if (typeof trailing.selectEnd === "function")
|
|
170
|
+
trailing.selectEnd();
|
|
171
|
+
inserted = true;
|
|
172
|
+
}, { discrete: true });
|
|
173
|
+
if (inserted)
|
|
174
|
+
element.focus();
|
|
175
|
+
return inserted;
|
|
176
|
+
} catch (error) {
|
|
177
|
+
console.warn("[dsh-quote-followup] native reference chip failed; using text fallback", error);
|
|
178
|
+
return false;
|
|
179
|
+
}
|
|
180
|
+
};
|
|
96
181
|
/**
|
|
97
182
|
* Use the live Lexical command table when DSH exposes its editor on the root.
|
|
98
183
|
* This bypasses browser security differences around synthetic clipboardData
|
|
@@ -198,6 +283,7 @@ window.__ModuleLoader__.load({
|
|
|
198
283
|
//#endregion
|
|
199
284
|
//#region floating button
|
|
200
285
|
let pendingQuote = null;
|
|
286
|
+
let quoteSourceReady = false;
|
|
201
287
|
const ensureButton = () => {
|
|
202
288
|
const existing = document.getElementById(BUTTON_ID);
|
|
203
289
|
if (existing?.getAttribute(BUTTON_VERSION_ATTR) === CLIENT_VERSION)
|
|
@@ -287,7 +373,9 @@ window.__ModuleLoader__.load({
|
|
|
287
373
|
// Clear the transcript range BEFORE focusing the composer. Clearing after
|
|
288
374
|
// insertion destroys Lexical's caret and makes the next quote unreliable.
|
|
289
375
|
document.getSelection()?.removeAllRanges();
|
|
290
|
-
|
|
376
|
+
const payload = quotePayload(quote.text, quote.role);
|
|
377
|
+
if (!(quoteSourceReady && appendQuoteChip(composer, payload)))
|
|
378
|
+
appendToComposer(composer, quoteFrame(payload));
|
|
291
379
|
};
|
|
292
380
|
//#endregion
|
|
293
381
|
//#region apply
|
|
@@ -298,6 +386,16 @@ window.__ModuleLoader__.load({
|
|
|
298
386
|
return;
|
|
299
387
|
if (typeof previous?.dispose === "function")
|
|
300
388
|
previous.dispose();
|
|
389
|
+
let unregisterSource = null;
|
|
390
|
+
try {
|
|
391
|
+
const inputTriggers = typeof ctx?.get === "function" ? ctx.get("inputTriggers") : null;
|
|
392
|
+
if (typeof inputTriggers?.registerSource === "function") {
|
|
393
|
+
unregisterSource = inputTriggers.registerSource(QUOTE_TRIGGER_SOURCE);
|
|
394
|
+
quoteSourceReady = true;
|
|
395
|
+
}
|
|
396
|
+
} catch (error) {
|
|
397
|
+
console.warn("[dsh-quote-followup] native reference codec unavailable; using text fallback", error);
|
|
398
|
+
}
|
|
301
399
|
const onScroll = () => hideButton();
|
|
302
400
|
const onKeyDown = (event) => {
|
|
303
401
|
if (event.key === "Escape")
|
|
@@ -313,6 +411,9 @@ window.__ModuleLoader__.load({
|
|
|
313
411
|
if (disposed)
|
|
314
412
|
return;
|
|
315
413
|
disposed = true;
|
|
414
|
+
quoteSourceReady = false;
|
|
415
|
+
if (typeof unregisterSource === "function")
|
|
416
|
+
unregisterSource();
|
|
316
417
|
document.removeEventListener("selectionchange", onSelectionChange);
|
|
317
418
|
window.removeEventListener("scroll", onScroll, true);
|
|
318
419
|
document.removeEventListener("keydown", onKeyDown, true);
|
|
@@ -336,7 +437,7 @@ window.__ModuleLoader__.load({
|
|
|
336
437
|
ctx.effect(() => cleanup, "dsh-quote-followup: selection quote ui");
|
|
337
438
|
}
|
|
338
439
|
//#endregion
|
|
339
|
-
exports.inject = [];
|
|
440
|
+
exports.inject = ["inputTriggers"];
|
|
340
441
|
exports.apply = apply;
|
|
341
442
|
return module.exports;
|
|
342
443
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-quote-followup",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.3",
|
|
4
4
|
"description": "Quote selected Web conversation text into the DeepSeek Harness composer for targeted follow-up turns.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"dsh",
|
|
@@ -44,13 +44,15 @@
|
|
|
44
44
|
},
|
|
45
45
|
"dsh": {
|
|
46
46
|
"engines": {
|
|
47
|
-
"dsh": ">=0.1.
|
|
47
|
+
"dsh": ">=0.1.2-rc.1"
|
|
48
48
|
},
|
|
49
49
|
"bundle": {
|
|
50
50
|
"patch": "./cordis.patch.yml"
|
|
51
51
|
},
|
|
52
52
|
"client": {
|
|
53
|
-
"inject": [
|
|
53
|
+
"inject": [
|
|
54
|
+
"@deepseek-ai/dsh-client-ui-input-trigger"
|
|
55
|
+
],
|
|
54
56
|
"platform": "web"
|
|
55
57
|
}
|
|
56
58
|
}
|