dsh-quote-followup 0.2.6 → 0.2.7

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
@@ -4,6 +4,7 @@ English | [中文](README.zh.md)
4
4
 
5
5
  [![npm version](https://img.shields.io/npm/v/dsh-quote-followup.svg)](https://www.npmjs.com/package/dsh-quote-followup)
6
6
  [![license](https://img.shields.io/npm/l/dsh-quote-followup.svg)](LICENSE)
7
+ [![DSH Market](https://raw.githubusercontent.com/2BingLing/dsh-market/master/assets/readme/badge-listed-en.svg)](https://dsh.market/)
7
8
 
8
9
  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.
9
10
 
@@ -58,6 +59,8 @@ npm test
58
59
 
59
60
  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.
60
61
 
62
+ An opt-in [assembled DSH Web regression](https://github.com/tr1v3r/dsh-quote-followup/blob/master/test/integration/README.md) runs the plugin through a pinned, built DSH checkout with Chromium. It covers existing drafts, repeated quoting, undo/redo, session isolation, prompt serialization, and Host-driven unavailable input.
63
+
61
64
  ## License
62
65
 
63
66
  MIT
package/README.zh.md CHANGED
@@ -4,6 +4,7 @@
4
4
 
5
5
  [![npm version](https://img.shields.io/npm/v/dsh-quote-followup.svg)](https://www.npmjs.com/package/dsh-quote-followup)
6
6
  [![license](https://img.shields.io/npm/l/dsh-quote-followup.svg)](LICENSE)
7
+ [![DSH Market](https://raw.githubusercontent.com/2BingLing/dsh-market/master/assets/readme/badge-listed-zh.svg)](https://dsh.market/)
7
8
 
8
9
  一个仅支持 Web 的 [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) 插件:选中对话文本,将其追加到输入框,再进行针对性追问。
9
10
 
@@ -58,6 +59,8 @@ npm test
58
59
 
59
60
  回归测试覆盖精简的原生引用 chip、中英文 locale 切换、轮次溯源与降级、连续引用、已有草稿后的间距、codec 序列化、Firefox 文本降级路径,以及热替换后新版 client 接管旧按钮/单例状态。
60
61
 
62
+ 可选的[真实 DSH Web 集成回归](https://github.com/tr1v3r/dsh-quote-followup/blob/master/test/integration/README.md)通过固定版本、已构建的 DSH 和 Chromium 加载插件,验证已有草稿、连续引用、撤销重做、会话隔离、发送序列化及 Host 驱动的不可写输入状态。
63
+
61
64
  ## 许可证
62
65
 
63
66
  MIT
package/lib/client.js CHANGED
@@ -35,7 +35,11 @@ window.__ModuleLoader__.load({
35
35
  ];
36
36
  const BUTTON_ID = "dsh-quote-followup-btn";
37
37
  const BUTTON_VERSION_ATTR = "data-dsh-quote-followup-version";
38
- const CLIENT_VERSION = "0.2.6";
38
+ const CLIENT_VERSION = "0.2.7";
39
+ /** Shared <style> element; versioned like the button so hot-swaps replace it. */
40
+ const STYLE_ID = "dsh-quote-followup-style";
41
+ const STYLE_VERSION_ATTR = "data-dsh-quote-followup-style-version";
42
+ const BUTTON_CLASS = "dsh-qf-btn";
39
43
  /** Codec owner for native DSH reference chips. */
40
44
  const QUOTE_SOURCE = "quote-followup";
41
45
  const LOCALE_NS = "quote-followup";
@@ -358,7 +362,67 @@ window.__ModuleLoader__.load({
358
362
  let pendingQuote = null;
359
363
  let quoteSourceReady = false;
360
364
  let currentT = fallbackT;
365
+ /**
366
+ * Inject the button stylesheet once per client version. Visuals follow
367
+ * the dsh-annotation convention: a single idempotent <style> element
368
+ * built on DSH Web design tokens (--dsw-alias-* / --dsw-specific-*),
369
+ * so the pill tracks the active theme instead of hard-coded colors.
370
+ * The trailing fallbacks only engage when a token is renamed upstream.
371
+ * Without a real document.head (test fakes) the button still works,
372
+ * just unstyled.
373
+ */
374
+ const ensureStyles = () => {
375
+ const head = document.head;
376
+ if (head === null || typeof head.appendChild !== "function")
377
+ return;
378
+ const existing = document.getElementById(STYLE_ID);
379
+ if (existing !== null) {
380
+ if (existing.getAttribute(STYLE_VERSION_ATTR) === CLIENT_VERSION)
381
+ return;
382
+ existing.remove();
383
+ }
384
+ const style = document.createElement("style");
385
+ style.id = STYLE_ID;
386
+ style.setAttribute(STYLE_VERSION_ATTR, CLIENT_VERSION);
387
+ style.textContent = [
388
+ `.${BUTTON_CLASS} {`,
389
+ " position: fixed;",
390
+ " z-index: 2147483600;",
391
+ " display: inline-flex;",
392
+ " align-items: center;",
393
+ " gap: 6px;",
394
+ " height: 28px;",
395
+ " padding: 0 12px;",
396
+ " border: none;",
397
+ " border-radius: 14px;",
398
+ " background: var(--dsw-alias-button-primary-fill, rgba(56, 56, 62, .94));",
399
+ " color: var(--dsw-alias-label-primary-foreground, #f5f5f7);",
400
+ " font-family: var(--dsw-font-family, system-ui);",
401
+ " font-size: 12px;",
402
+ " line-height: 18px;",
403
+ " white-space: nowrap;",
404
+ " cursor: pointer;",
405
+ " box-shadow: var(--dsw-shadow-lv3, 0 2px 10px rgba(0, 0, 0, .28));",
406
+ " user-select: none;",
407
+ " animation: dsh-qf-pop .12s var(--ds-ease-in-out, ease);",
408
+ "}",
409
+ `.${BUTTON_CLASS}:hover:not(:disabled) {`,
410
+ " background: var(--dsw-alias-button-primary-hover, rgba(72, 72, 80, .96));",
411
+ "}",
412
+ `.${BUTTON_CLASS}:active { filter: brightness(.92); }`,
413
+ `.${BUTTON_CLASS}:focus-visible {`,
414
+ " outline: 2px solid var(--dsw-alias-text-accent, #4c9aff);",
415
+ " outline-offset: 1px;",
416
+ "}",
417
+ "@keyframes dsh-qf-pop {",
418
+ " from { opacity: 0; transform: translateY(3px); }",
419
+ " to { opacity: 1; transform: none; }",
420
+ "}"
421
+ ].join("\n");
422
+ head.appendChild(style);
423
+ };
361
424
  const ensureButton = () => {
425
+ ensureStyles();
362
426
  const existing = document.getElementById(BUTTON_ID);
363
427
  if (existing?.getAttribute(BUTTON_VERSION_ATTR) === CLIENT_VERSION)
364
428
  return existing;
@@ -367,23 +431,10 @@ window.__ModuleLoader__.load({
367
431
  button.id = BUTTON_ID;
368
432
  button.setAttribute(BUTTON_VERSION_ATTR, CLIENT_VERSION);
369
433
  button.type = "button";
434
+ button.className = BUTTON_CLASS;
370
435
  button.textContent = currentT("button.label");
371
436
  button.title = currentT("button.title");
372
- Object.assign(button.style, {
373
- position: "fixed",
374
- zIndex: "2147483600",
375
- padding: "3px 10px",
376
- fontSize: "12px",
377
- lineHeight: "20px",
378
- borderRadius: "6px",
379
- border: "1px solid rgba(140,140,150,.45)",
380
- background: "rgba(56,56,62,.94)",
381
- color: "#f5f5f7",
382
- cursor: "pointer",
383
- boxShadow: "0 2px 10px rgba(0,0,0,.28)",
384
- userSelect: "none",
385
- display: "none"
386
- });
437
+ button.style.display = "none";
387
438
  button.addEventListener("mousedown", (event) => {
388
439
  event.preventDefault();
389
440
  event.stopPropagation();
@@ -397,14 +448,21 @@ window.__ModuleLoader__.load({
397
448
  return button;
398
449
  };
399
450
  const placeButton = (button, rect) => {
400
- let x = rect.left + rect.width / 2 - 30;
401
- let y = rect.top - 28;
451
+ // Reveal before measuring: offsetWidth of a display:none element is 0,
452
+ // and the fixed-width guard below keeps fake DOMs (no layout) working.
453
+ button.style.display = "inline-flex";
454
+ const width = Number.isFinite(button.offsetWidth) && button.offsetWidth > 0
455
+ ? button.offsetWidth
456
+ : 64;
457
+ // Center on the selection; measurement guards keep fake/hidden
458
+ // elements (offsetWidth 0 / undefined) on the old fixed-width path.
459
+ let x = rect.left + rect.width / 2 - width / 2;
460
+ let y = rect.top - 34; // pill height (28px) + 6px gap
402
461
  if (y < 4)
403
462
  y = rect.bottom + 6;
404
- x = Math.max(4, Math.min(x, window.innerWidth - 70));
463
+ x = Math.max(4, Math.min(x, window.innerWidth - width - 4));
405
464
  button.style.left = `${Math.round(x)}px`;
406
465
  button.style.top = `${Math.round(y)}px`;
407
- button.style.display = "block";
408
466
  };
409
467
  const hideButton = () => {
410
468
  const button = document.getElementById(BUTTON_ID);
@@ -528,6 +586,9 @@ window.__ModuleLoader__.load({
528
586
  const button = document.getElementById(BUTTON_ID);
529
587
  if (button?.getAttribute(BUTTON_VERSION_ATTR) === CLIENT_VERSION)
530
588
  button.remove();
589
+ const style = document.getElementById(STYLE_ID);
590
+ if (style?.getAttribute(STYLE_VERSION_ATTR) === CLIENT_VERSION)
591
+ style.remove();
531
592
  if (globalThis[STATE] === state)
532
593
  delete globalThis[STATE];
533
594
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-quote-followup",
3
- "version": "0.2.6",
3
+ "version": "0.2.7",
4
4
  "description": "Quote selected Web conversation text into the DeepSeek Harness composer for targeted follow-up turns.",
5
5
  "keywords": [
6
6
  "dsh",
@@ -11,7 +11,8 @@
11
11
  "follow-up"
12
12
  ],
13
13
  "scripts": {
14
- "test": "node --check lib/index.js && node --check lib/client.js && node test/web-repeat-harness.mjs"
14
+ "test": "node --check lib/index.js && node --check lib/client.js && node test/web-repeat-harness.mjs",
15
+ "test:assembled-web": "node test/run-assembled-web.mjs"
15
16
  },
16
17
  "type": "module",
17
18
  "main": "lib/index.js",