dsh-quote-followup 0.2.4 → 0.2.5

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
@@ -18,11 +18,21 @@ A Web-only [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) p
18
18
  - Select text inside the Web conversation transcript to reveal a floating **Quote** button.
19
19
  - Append a native DSH conversation-reference chip without replacing the existing draft.
20
20
  - Reuse the same atomic `ReferenceChipNode`, conversation icon, and business color as `@file` / `@session`.
21
+ - Keep each chip compact by showing only the excerpt; the bubble icon already conveys that it is a quote.
22
+ - Follow the active DSH locale for the floating action and serialized quote frame.
23
+ - Attach the conversation turn number to the serialized frame when the selection comes from a DSH chat row, so follow-ups like "revisit turn 3" stay resolvable. Selections without a turn marker keep the provenance-free frame.
21
24
  - Quote multiple excerpts; on send, the plugin codec expands each chip into a model-readable Markdown blockquote.
22
25
  - Fall back to a plain-text quote when the host lacks native chip support.
23
26
 
24
27
  TUI is intentionally unsupported.
25
28
 
29
+ ## Scope
30
+
31
+ This is a Web client-side extension. The composer and send path are owned by the browser client: chips exist only in the Lexical editor, and the model only ever sees the Markdown blockquotes the codec expands at send time — never the chips themselves. No host-side plugin surface is involved.
32
+
33
+ Chips are text-only by design. They carry the excerpt, a display-only role hint, the conversation turn ordinal when it is available, and a truncation flag — no global session-message references — so they stay valid across compaction folds and session rotation.
34
+
35
+
26
36
  ## Install
27
37
 
28
38
  Requires DSH `>=0.1.2-rc.1`.
@@ -46,7 +56,7 @@ Add `dsh-quote-followup` to the Web profile's `dsh.profile.bundles`, then restar
46
56
  npm test
47
57
  ```
48
58
 
49
- 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.
59
+ The regression harness covers compact native quote chips, locale switching, turn-number provenance with graceful degradation, 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.
50
60
 
51
61
  ## License
52
62
 
package/README.zh.md CHANGED
@@ -18,11 +18,21 @@
18
18
  - 在 Web 对话区选中文本后显示浮动的 **❐ 引用** 按钮。
19
19
  - 以 DSH 原生对话引用 chip 追加到现有草稿,不覆盖已输入内容。
20
20
  - chip 使用与 `@文件` / `@对话` 相同的 `ReferenceChipNode`、对话图标和业务色,可整体删除。
21
+ - chip 只展示摘录正文,引用语义由气泡图标表达,减少重复标签。
22
+ - 浮动按钮和发送时的引用框架跟随 DSH 当前语言。
23
+ - 选中内容来自 DSH 聊天行时,序列化引用会附带对话轮次编号,模型可定位"第 3 轮引用的内容";无轮次标记时保持原有框架。
21
24
  - 可连续引用多段内容;发送时由插件 codec 将各 chip 展开为模型可读的 Markdown 引用块。
22
25
  - 旧版 DSH 缺少原生 chip 能力时,自动降级为纯文本引用。
23
26
 
24
27
  本插件不再适配 TUI。
25
28
 
29
+ ## 边界
30
+
31
+ 这是一个 Web 客户端侧扩展。composer 和发送路径由浏览器 client 拥有:chip 只存在于 Lexical 编辑器中,模型只会看到发送时 codec 展开的 Markdown 引用块,永远不会看到 chip 本身,不涉及任何 host 侧插件接口。
32
+
33
+ chip 刻意只存文本:包含摘录正文、仅供显示的角色提示、可用的对话轮次编号和截断标记,不含全局 session-message 引用,因此在 compaction 折叠与会话轮转后依然有效。
34
+
35
+
26
36
  ## 安装
27
37
 
28
38
  要求 DSH `>=0.1.2-rc.1`。
@@ -46,7 +56,7 @@ dsh plugin --profile web add dsh-quote-followup
46
56
  npm test
47
57
  ```
48
58
 
49
- 回归测试覆盖原生引用 chip、连续引用、已有草稿后的间距、codec 序列化、Firefox 文本降级路径,以及热替换后新版 client 接管旧按钮/单例状态。
59
+ 回归测试覆盖精简的原生引用 chip、中英文 locale 切换、轮次溯源与降级、连续引用、已有草稿后的间距、codec 序列化、Firefox 文本降级路径,以及热替换后新版 client 接管旧按钮/单例状态。
50
60
 
51
61
  ## 许可证
52
62
 
Binary file
package/lib/client.js CHANGED
@@ -33,7 +33,7 @@ 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.4";
36
+ const CLIENT_VERSION = "0.2.5";
37
37
  /** Codec owner for native DSH reference chips. */
38
38
  const QUOTE_SOURCE = "quote-followup";
39
39
  const LOCALE_NS = "quote-followup";
@@ -44,14 +44,16 @@ window.__ModuleLoader__.load({
44
44
  "button.title": "引用选中内容到输入框(可多次引用,发送前可编辑)",
45
45
  "quote.header": "引用",
46
46
  "conversation.role": "对话",
47
- "quote.truncated": ",已截断"
47
+ "quote.turn": "第 {n} 轮",
48
+ "quote.truncated": "已截断"
48
49
  },
49
50
  en: {
50
51
  "button.label": "❐ Quote",
51
52
  "button.title": "Quote the selection into the composer (repeatable; editable before sending)",
52
53
  "quote.header": "Quote",
53
54
  "conversation.role": "conversation",
54
- "quote.truncated": ", truncated"
55
+ "quote.turn": "turn {n}",
56
+ "quote.truncated": "truncated"
55
57
  }
56
58
  };
57
59
  const fallbackT = (key) => LOCALE_DICT.en[key] ?? key;
@@ -95,20 +97,45 @@ window.__ModuleLoader__.load({
95
97
  return null;
96
98
  };
97
99
  const roleLabel = (role, t) => role === "user" ? "user" : role === "assistant" ? "assistant" : t("conversation.role");
100
+ /**
101
+ * Conversation-turn provenance: DSH's chat renderer stamps every message
102
+ * row with `data-chat-turn` (a per-session monotonic ordinal). Resolving
103
+ * it lets the model address "what you quoted in turn N" without any
104
+ * host-side message-id API. Absent / non-numeric markers degrade to null
105
+ * and the serialized frame stays exactly as before.
106
+ */
107
+ const detectTurn = (range, transcript) => {
108
+ let element = elementOf(range.startContainer);
109
+ while (element !== null && element !== transcript) {
110
+ const raw = element.getAttribute?.("data-chat-turn");
111
+ if (typeof raw === "string" && raw !== "") {
112
+ const turn = Number(raw);
113
+ if (Number.isSafeInteger(turn) && turn >= 0)
114
+ return turn;
115
+ }
116
+ element = element.parentElement;
117
+ }
118
+ return null;
119
+ };
98
120
  /** Bound one quote before it becomes editor state. */
99
- const quotePayload = (text, role) => {
121
+ const quotePayload = (text, role, turn) => {
100
122
  let body = String(text ?? "");
101
123
  let truncated = false;
102
124
  if (body.length > QUOTE_MAX_CHARS) {
103
125
  body = body.slice(0, QUOTE_MAX_CHARS);
104
126
  truncated = true;
105
127
  }
106
- return { text: body, role, truncated };
128
+ return { text: body, role, truncated, turn: Number.isSafeInteger(turn) && turn >= 0 ? turn : null };
107
129
  };
108
130
  /** Model / clipboard projection. The composer shows a chip, not this frame. */
109
- const quoteFrame = ({ text, role, truncated }, t) => {
131
+ const quoteFrame = ({ text, role, truncated, turn }, t) => {
110
132
  const lines = text.split("\n").map((line) => `> ${line}`.trimEnd());
111
- return `> [${t("quote.header")} · ${roleLabel(role, t)}${truncated ? t("quote.truncated") : ""}]\n${lines.join("\n")}\n\n`;
133
+ const meta = [t("quote.header"), roleLabel(role, t)];
134
+ if (Number.isSafeInteger(turn) && turn >= 0)
135
+ meta.push(String(t("quote.turn")).replace("{n}", String(turn)));
136
+ if (truncated)
137
+ meta.push(t("quote.truncated"));
138
+ return `> [${meta.join(" · ")}]\n${lines.join("\n")}\n\n`;
112
139
  };
113
140
  const decodeQuote = (ref) => {
114
141
  const value = JSON.parse(ref);
@@ -117,7 +144,8 @@ window.__ModuleLoader__.load({
117
144
  return {
118
145
  text: value.text,
119
146
  role: value.role === "user" || value.role === "assistant" ? value.role : null,
120
- truncated: value.truncated === true
147
+ truncated: value.truncated === true,
148
+ turn: Number.isSafeInteger(value.turn) && value.turn >= 0 ? value.turn : null
121
149
  };
122
150
  };
123
151
  const quoteInsert = (payload, t) => {
@@ -378,7 +406,11 @@ window.__ModuleLoader__.load({
378
406
  hideButton();
379
407
  return;
380
408
  }
381
- pendingQuote = { text: text.trim(), role: detectRole(selection.getRangeAt(0), transcript) };
409
+ pendingQuote = {
410
+ text: text.trim(),
411
+ role: detectRole(selection.getRangeAt(0), transcript),
412
+ turn: detectTurn(selection.getRangeAt(0), transcript)
413
+ };
382
414
  placeButton(ensureButton(), rect);
383
415
  };
384
416
  const onQuoteClick = () => {
@@ -395,7 +427,7 @@ window.__ModuleLoader__.load({
395
427
  // Clear the transcript range BEFORE focusing the composer. Clearing after
396
428
  // insertion destroys Lexical's caret and makes the next quote unreliable.
397
429
  document.getSelection()?.removeAllRanges();
398
- const payload = quotePayload(quote.text, quote.role);
430
+ const payload = quotePayload(quote.text, quote.role, quote.turn);
399
431
  if (!(quoteSourceReady && appendQuoteChip(composer, payload, currentT)))
400
432
  appendToComposer(composer, quoteFrame(payload, currentT));
401
433
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-quote-followup",
3
- "version": "0.2.4",
3
+ "version": "0.2.5",
4
4
  "description": "Quote selected Web conversation text into the DeepSeek Harness composer for targeted follow-up turns.",
5
5
  "keywords": [
6
6
  "dsh",