dsh-quote-followup 0.2.1 → 0.2.2
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 +3 -3
- package/README.zh.md +3 -3
- package/lib/client.js +66 -12
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -9,7 +9,7 @@ A Web-only [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) p
|
|
|
9
9
|
- Select text inside the Web conversation transcript to reveal a floating **Quote** button.
|
|
10
10
|
- Append the selection as a Markdown quote without replacing the existing draft.
|
|
11
11
|
- Repeat the action to collect multiple excerpts before sending.
|
|
12
|
-
-
|
|
12
|
+
- Dispatch DSH's live Lexical `PASTE_COMMAND` directly, keeping the editor model and rendered composer in sync across Chromium and Firefox.
|
|
13
13
|
|
|
14
14
|
TUI is intentionally unsupported.
|
|
15
15
|
|
|
@@ -19,7 +19,7 @@ TUI is intentionally unsupported.
|
|
|
19
19
|
dsh plugin --profile web add dsh-quote-followup
|
|
20
20
|
```
|
|
21
21
|
|
|
22
|
-
Add `dsh-quote-followup` to the Web profile's `dsh.profile.bundles`, then restart `dsh web` so the server rebuilds its boot-time client bundle.
|
|
22
|
+
Add `dsh-quote-followup` to the Web profile's `dsh.profile.bundles`, then restart `dsh web` so the server rebuilds its boot-time client bundle. Reload or reopen browser tabs that stayed open across the restart; their already-loaded JavaScript cannot update itself from a new server process.
|
|
23
23
|
|
|
24
24
|
## Use
|
|
25
25
|
|
|
@@ -34,7 +34,7 @@ Add `dsh-quote-followup` to the Web profile's `dsh.profile.bundles`, then restar
|
|
|
34
34
|
npm test
|
|
35
35
|
```
|
|
36
36
|
|
|
37
|
-
The regression harness
|
|
37
|
+
The regression harness covers repeated quotes, Firefox rejecting constructor-injected `clipboardData`, Lexical reconciliation of raw DOM fallbacks, and takeover from a stale client button/singleton after a hot swap.
|
|
38
38
|
|
|
39
39
|
## License
|
|
40
40
|
|
package/README.zh.md
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
- 在 Web 对话区选中文本后显示浮动的 **❐ 引用** 按钮。
|
|
10
10
|
- 以 Markdown 引用块追加到现有草稿,不覆盖已输入内容。
|
|
11
11
|
- 可连续引用多段内容,再统一编辑并发送。
|
|
12
|
-
-
|
|
12
|
+
- 直接派发 DSH 当前 Lexical editor 的 `PASTE_COMMAND`,在 Chromium 与 Firefox 中同步更新编辑器模型和 DOM。
|
|
13
13
|
|
|
14
14
|
本插件不再适配 TUI。
|
|
15
15
|
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
dsh plugin --profile web add dsh-quote-followup
|
|
20
20
|
```
|
|
21
21
|
|
|
22
|
-
将 `dsh-quote-followup` 加入 Web profile 的 `dsh.profile.bundles`,然后重启 `dsh web`,让服务端重新生成启动时缓存的 client bundle
|
|
22
|
+
将 `dsh-quote-followup` 加入 Web profile 的 `dsh.profile.bundles`,然后重启 `dsh web`,让服务端重新生成启动时缓存的 client bundle。重启后还需刷新或重新打开此前一直未关闭的浏览器页;旧页面已经加载的 JavaScript 不会从新服务进程自动更新。
|
|
23
23
|
|
|
24
24
|
## 使用
|
|
25
25
|
|
|
@@ -34,7 +34,7 @@ dsh plugin --profile web add dsh-quote-followup
|
|
|
34
34
|
npm test
|
|
35
35
|
```
|
|
36
36
|
|
|
37
|
-
|
|
37
|
+
回归测试覆盖连续引用、Firefox 丢弃构造参数中的 `clipboardData`、Lexical 回滚裸 DOM fallback,以及热替换后新版 client 接管旧按钮/单例状态。
|
|
38
38
|
|
|
39
39
|
## 许可证
|
|
40
40
|
|
package/lib/client.js
CHANGED
|
@@ -32,6 +32,8 @@ window.__ModuleLoader__.load({
|
|
|
32
32
|
'[data-input-scroll] textarea'
|
|
33
33
|
];
|
|
34
34
|
const BUTTON_ID = "dsh-quote-followup-btn";
|
|
35
|
+
const BUTTON_VERSION_ATTR = "data-dsh-quote-followup-version";
|
|
36
|
+
const CLIENT_VERSION = "0.2.2";
|
|
35
37
|
/** Per-quote character cap for a bounded composer insertion. */
|
|
36
38
|
const QUOTE_MAX_CHARS = 1600;
|
|
37
39
|
//#endregion
|
|
@@ -91,7 +93,41 @@ window.__ModuleLoader__.load({
|
|
|
91
93
|
}
|
|
92
94
|
return null;
|
|
93
95
|
};
|
|
94
|
-
/**
|
|
96
|
+
/**
|
|
97
|
+
* Use the live Lexical command table when DSH exposes its editor on the root.
|
|
98
|
+
* This bypasses browser security differences around synthetic clipboardData
|
|
99
|
+
* while still entering through DSH's own PASTE_COMMAND → keyboard.paste path.
|
|
100
|
+
*/
|
|
101
|
+
const pasteViaLexical = (element, text) => {
|
|
102
|
+
const editor = element.__lexicalEditor;
|
|
103
|
+
if (editor === null || typeof editor !== "object" || typeof editor.dispatchCommand !== "function")
|
|
104
|
+
return false;
|
|
105
|
+
const commands = editor._commands;
|
|
106
|
+
if (commands === null || typeof commands !== "object" || typeof commands.keys !== "function")
|
|
107
|
+
return false;
|
|
108
|
+
const command = [...commands.keys()].find((candidate) => candidate?.type === "PASTE_COMMAND");
|
|
109
|
+
if (command === undefined)
|
|
110
|
+
return false;
|
|
111
|
+
const clipboardData = {
|
|
112
|
+
files: [],
|
|
113
|
+
items: [],
|
|
114
|
+
types: ["text/plain"],
|
|
115
|
+
getData: (type) => type === "text/plain" ? text : ""
|
|
116
|
+
};
|
|
117
|
+
const event = {
|
|
118
|
+
clipboardData,
|
|
119
|
+
defaultPrevented: false,
|
|
120
|
+
preventDefault() { this.defaultPrevented = true; },
|
|
121
|
+
stopPropagation() {}
|
|
122
|
+
};
|
|
123
|
+
try {
|
|
124
|
+
return editor.dispatchCommand(command, event) === true;
|
|
125
|
+
} catch (error) {
|
|
126
|
+
console.warn("[dsh-quote-followup] Lexical paste command failed; using DOM event fallback", error);
|
|
127
|
+
return false;
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
/** Dispatch a DOM paste fallback for non-DSH stateful editors. */
|
|
95
131
|
const pasteIntoEditor = (element, text) => {
|
|
96
132
|
let clipboardData = null;
|
|
97
133
|
try {
|
|
@@ -149,6 +185,8 @@ window.__ModuleLoader__.load({
|
|
|
149
185
|
range.collapse(false);
|
|
150
186
|
selection?.removeAllRanges();
|
|
151
187
|
selection?.addRange(range);
|
|
188
|
+
if (pasteViaLexical(element, insertText))
|
|
189
|
+
return;
|
|
152
190
|
if (pasteIntoEditor(element, insertText))
|
|
153
191
|
return;
|
|
154
192
|
if (typeof document.execCommand === "function" && document.execCommand("insertText", false, insertText))
|
|
@@ -162,10 +200,12 @@ window.__ModuleLoader__.load({
|
|
|
162
200
|
let pendingQuote = null;
|
|
163
201
|
const ensureButton = () => {
|
|
164
202
|
const existing = document.getElementById(BUTTON_ID);
|
|
165
|
-
if (existing
|
|
203
|
+
if (existing?.getAttribute(BUTTON_VERSION_ATTR) === CLIENT_VERSION)
|
|
166
204
|
return existing;
|
|
205
|
+
existing?.remove();
|
|
167
206
|
const button = document.createElement("button");
|
|
168
207
|
button.id = BUTTON_ID;
|
|
208
|
+
button.setAttribute(BUTTON_VERSION_ATTR, CLIENT_VERSION);
|
|
169
209
|
button.type = "button";
|
|
170
210
|
button.textContent = "❐ 引用";
|
|
171
211
|
button.title = "引用选中内容到输入框(可多次引用,发送前可编辑)";
|
|
@@ -251,11 +291,13 @@ window.__ModuleLoader__.load({
|
|
|
251
291
|
};
|
|
252
292
|
//#endregion
|
|
253
293
|
//#region apply
|
|
254
|
-
const
|
|
294
|
+
const STATE = Symbol.for("dsh-quote-followup.state");
|
|
255
295
|
function apply(ctx) {
|
|
256
|
-
|
|
296
|
+
const previous = globalThis[STATE];
|
|
297
|
+
if (previous?.version === CLIENT_VERSION)
|
|
257
298
|
return;
|
|
258
|
-
|
|
299
|
+
if (typeof previous?.dispose === "function")
|
|
300
|
+
previous.dispose();
|
|
259
301
|
const onScroll = () => hideButton();
|
|
260
302
|
const onKeyDown = (event) => {
|
|
261
303
|
if (event.key === "Escape")
|
|
@@ -265,19 +307,31 @@ window.__ModuleLoader__.load({
|
|
|
265
307
|
if (event.target instanceof Element && event.target.id !== BUTTON_ID)
|
|
266
308
|
hideButton();
|
|
267
309
|
};
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
document.addEventListener("keydown", onKeyDown, true);
|
|
271
|
-
document.addEventListener("mousedown", onMouseDown, true);
|
|
310
|
+
let disposed = false;
|
|
311
|
+
let state;
|
|
272
312
|
const cleanup = () => {
|
|
273
|
-
|
|
313
|
+
if (disposed)
|
|
314
|
+
return;
|
|
315
|
+
disposed = true;
|
|
274
316
|
document.removeEventListener("selectionchange", onSelectionChange);
|
|
275
317
|
window.removeEventListener("scroll", onScroll, true);
|
|
276
318
|
document.removeEventListener("keydown", onKeyDown, true);
|
|
277
319
|
document.removeEventListener("mousedown", onMouseDown, true);
|
|
278
|
-
|
|
279
|
-
|
|
320
|
+
const button = document.getElementById(BUTTON_ID);
|
|
321
|
+
if (button?.getAttribute(BUTTON_VERSION_ATTR) === CLIENT_VERSION)
|
|
322
|
+
button.remove();
|
|
323
|
+
if (globalThis[STATE] === state)
|
|
324
|
+
delete globalThis[STATE];
|
|
280
325
|
};
|
|
326
|
+
state = { version: CLIENT_VERSION, dispose: cleanup };
|
|
327
|
+
globalThis[STATE] = state;
|
|
328
|
+
document.addEventListener("selectionchange", onSelectionChange);
|
|
329
|
+
window.addEventListener("scroll", onScroll, true);
|
|
330
|
+
document.addEventListener("keydown", onKeyDown, true);
|
|
331
|
+
document.addEventListener("mousedown", onMouseDown, true);
|
|
332
|
+
// Claim the shared id immediately so an older listener in a long-lived tab
|
|
333
|
+
// cannot keep its obsolete click handler after a client hot-swap.
|
|
334
|
+
ensureButton();
|
|
281
335
|
if (ctx !== undefined && typeof ctx.effect === "function")
|
|
282
336
|
ctx.effect(() => cleanup, "dsh-quote-followup: selection quote ui");
|
|
283
337
|
}
|