dsh-quote-annotate-pro 0.2.4 → 0.2.6
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/CHANGELOG.md +17 -0
- package/README.en.md +2 -2
- package/README.md +2 -2
- package/lib/client.js +94 -13
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,23 @@
|
|
|
2
2
|
|
|
3
3
|
本项目的所有重要变更都记在这里。版本号遵循 [SemVer](https://semver.org/lang/zh-CN/)。
|
|
4
4
|
|
|
5
|
+
## 0.2.6 — 输入框与消息统一为紧凑引用(消除插入瞬间的闪烁)
|
|
6
|
+
|
|
7
|
+
- **输入框里的引用**:插入后立刻渲染成**紧凑原文块**(位置行隐藏),不再"先露出完整引用块、稍后才变紧凑"
|
|
8
|
+
- 实现:装饰函数暴露为模块级钩子,插入成功即调用一次;MutationObserver 的防抖从 250ms 降到 40ms
|
|
9
|
+
- **引用文本去掉 `> ` 前缀**(`【引用 · 第N轮 · 角色】` + 原文),编辑器里不再多露一个符号
|
|
10
|
+
- **文本保持完整**:界面只是隐藏位置行(加样式类,不动编辑器文本),因此**发送出去的内容仍带轮次/角色** → 模型侧定位能力不变
|
|
11
|
+
- 消息侧(0.2.3)的紧凑渲染保持不变,输入与输出观感一致
|
|
12
|
+
|
|
13
|
+
## 0.2.5 — 引用块样式:贴在输入框上沿
|
|
14
|
+
|
|
15
|
+
- 0.2.4 曾把引用块挪进 `conversation.input.overlay`,实测该槽位是**浮动弹层**(放的是 slash-menu / command-popup 这类弹窗),静态内容放进去不可见 → **改回 `conversation.input.dock`**
|
|
16
|
+
- DSH 没有"输入框卡片内部、文字上方"的静态槽位:
|
|
17
|
+
- `conversation.input.overlay` = 浮动弹层(不适用)
|
|
18
|
+
- `conversation.input.attachments` = 官方草稿附件栏(single 槽位,占用会顶掉官方附件 UI,不能碰)
|
|
19
|
+
- 折中方案:引用块留在卡片上沿,用样式做成**贴着输入框**(顶部大圆角、底部小圆角、无间隙),视觉上与输入框连成一体
|
|
20
|
+
- 对话页里的紧凑引用(0.2.3)不受影响
|
|
21
|
+
|
|
5
22
|
## 0.2.4 — 输入框里的引用块(挪进输入框卡片内)
|
|
6
23
|
|
|
7
24
|
- 引用块从「输入框卡片**上方**」挪到「输入框卡片**内部**、文字上方」(`conversation.input.overlay` 槽位),与参考产品一致:引用跟着输入内容走,看起来是一体的
|
package/README.en.md
CHANGED
|
@@ -17,8 +17,8 @@
|
|
|
17
17
|
| --- | --- |
|
|
18
18
|
| Select & annotate | Select text in the transcript → a bubble appears above the selection → write a comment (optional) → `Enter` |
|
|
19
19
|
| Quote chip | An atomic `引用1` chip (short label) lands in the composer; it expands to the full quote block on send |
|
|
20
|
-
| **Quote block** |
|
|
21
|
-
| **Compact quotes in the transcript** | A sent message shows only a **compact quote block** (up to 3 lines)
|
|
20
|
+
| **Quote block (in the composer)** | After inserting a quote the composer shows only the **quoted text** (the turn/role line is hidden); click it to jump back to the source and highlight it — no visible "jump" affordance |
|
|
21
|
+
| **Compact quotes in the transcript** | A sent message shows only a **compact quote block** (up to 3 lines). The message text still starts with `【引用 · 第N轮 · 角色】` — hidden in the UI, so the model keeps the position context |
|
|
22
22
|
| **Turn-accurate context** | The quote block starts with `【引用 · 第3轮 · 你的消息】` followed by `> quoted text`, so the model knows exactly which turn and whose message |
|
|
23
23
|
| Turn-level quoting | Every finished turn gets `引用提问` / `引用回答` buttons that quote the whole question or answer |
|
|
24
24
|
| Anchors survive reload | The `ref → anchor` map is persisted in `localStorage` (per session, 60 entries max), so quotes still jump after a page refresh |
|
package/README.md
CHANGED
|
@@ -17,8 +17,8 @@
|
|
|
17
17
|
|---|---|
|
|
18
18
|
| 划词批注 | 会话里选中任意文字 → 选区上方浮出气泡(预览 + 「批注」)→ 编辑框里写评语(可留空)→ `Enter` 插入 |
|
|
19
19
|
| 引用 chip | 输入框里出现 `引用1` 的原子 chip(短标签);发送时展开为完整引用块 |
|
|
20
|
-
|
|
|
21
|
-
| **对话页里的紧凑引用** |
|
|
20
|
+
| **引用块(输入框内)** | 插入引用后输入框里只显示**原文片段**(位置行隐藏);**点一下即跳回原文并高亮** —— 不做任何可见的"跳转"提示 |
|
|
21
|
+
| **对话页里的紧凑引用** | 已发送消息里的引用同样只显示**紧凑原文块**(最多 3 行)。**消息文本仍以 `【引用 · 第N轮 · 角色】` 开头** —— 界面隐藏、模型照旧知道出处 |
|
|
22
22
|
| **模型侧精确定位** | 引用块首行是 `【引用 · 第5轮 · 你的消息】`,后接 `> 原文`,让模型明确知道**哪一轮、谁说的** |
|
|
23
23
|
| 回合级引用 | 每条已完成回合尾部有「引用提问 / 引用回答」,整段引用该轮 |
|
|
24
24
|
| 锚点持久化 | `ref → 锚点` 映射写入 `localStorage`(按会话,最多 60 条),**刷新页面后引用块仍能跳回原文** |
|
package/lib/client.js
CHANGED
|
@@ -147,6 +147,8 @@ window.__ModuleLoader__.load({
|
|
|
147
147
|
const refTargets = new Map()
|
|
148
148
|
// ref -> { body, clipboardText } 提交时经 codec 序列化
|
|
149
149
|
const refCodecs = new Map()
|
|
150
|
+
// 输入框引用装饰的"立即执行"钩子:插入后马上贴样式,避免先露出旧样子再变(用户反馈的瞬间闪烁)
|
|
151
|
+
let composerDecorateNow = null
|
|
150
152
|
|
|
151
153
|
// 页面内反馈 toast(点击 chip 时显示结果)
|
|
152
154
|
const toastStore = {
|
|
@@ -735,8 +737,8 @@ window.__ModuleLoader__.load({
|
|
|
735
737
|
try {
|
|
736
738
|
const c = (comment || '').trim()
|
|
737
739
|
const header = buildQuoteHeader(item)
|
|
738
|
-
|
|
739
|
-
const quoteBody = header === '' ?
|
|
740
|
+
// 引用正文不再加 "> " 前缀:输入框与消息里都由插件渲染成紧凑块(前缀会在编辑器里露出一截符号)
|
|
741
|
+
const quoteBody = header === '' ? item.text : header + '\n' + item.text
|
|
740
742
|
const conversation = rootCtx ? rootCtx.get('conversation') : undefined
|
|
741
743
|
const sessions = rootCtx ? rootCtx.get('sessions') : undefined
|
|
742
744
|
if (!conversation || !conversation.input || !sessions) {
|
|
@@ -762,6 +764,8 @@ window.__ModuleLoader__.load({
|
|
|
762
764
|
const ref = 'q' + REF_RUN + (++refSeq)
|
|
763
765
|
// 输入框里的 chip 文案只要短、可区分即可(轮次/角色写在发送后的引用块里,不在输入框展示)
|
|
764
766
|
const chipLabel = '引用' + refSeq
|
|
767
|
+
// 输入框与消息里显示的就是这段文本(实测:宿主消息 = 草稿文本,WYSIWYG);
|
|
768
|
+
// 因此位置行(【引用 · 第N轮 · 角色】)保留在文本里给模型用,界面上由插件渲染时隐藏。
|
|
765
769
|
refTargets.set(ref, { sessionId, anchorKey: item.anchorKey || null, body: quoteBody, label: chipLabel })
|
|
766
770
|
refCodecs.set(ref, { body: quoteBody, clipboardText: quoteBody })
|
|
767
771
|
saveRef(sessionId, ref, { anchorKey: item.anchorKey || null, body: quoteBody, label: chipLabel })
|
|
@@ -791,7 +795,18 @@ window.__ModuleLoader__.load({
|
|
|
791
795
|
const st3 = input.state.getSnapshot()
|
|
792
796
|
input.setDraft(st3.draft.replace(/[ \t]+$/, '') + commentTail)
|
|
793
797
|
}
|
|
798
|
+
try {
|
|
799
|
+
const snap2 = input.state.getSnapshot()
|
|
800
|
+
const occ2 = (snap2 && snap2.occurrences) || []
|
|
801
|
+
report('insert:draft', {
|
|
802
|
+
draft: String((snap2 && snap2.draft) || '').replace(/\s+/g, ' ').slice(0, 120),
|
|
803
|
+
occ: occ2.length,
|
|
804
|
+
occLabels: occ2.map((o) => (o && o.label) || '').join('|').slice(0, 80),
|
|
805
|
+
})
|
|
806
|
+
} catch (e) { /* ignore */ }
|
|
794
807
|
report('insert:chip', { ref, anchorKey: item.anchorKey || null, header, chars: item.text.length })
|
|
808
|
+
// 立刻给刚插入的内容贴样式,避免"先露出旧样子、稍后才变紧凑"的闪烁
|
|
809
|
+
try { if (typeof composerDecorateNow === 'function') composerDecorateNow() } catch (e) { /* ignore */ }
|
|
795
810
|
setStatus({
|
|
796
811
|
ok: true,
|
|
797
812
|
text: '已插入引用 ✓' + (item.anchorKey ? '(输入框上方的引用块可点回原文)' : '(未定位到消息行,可能无法跳回)'),
|
|
@@ -987,9 +1002,9 @@ window.__ModuleLoader__.load({
|
|
|
987
1002
|
/* 根因:textarea(透明文字、absolute、inset:0)渲染在 backdrop 之后,整层盖住 chip。
|
|
988
1003
|
给 chip 提升 z-index 使其浮到 textarea 之上,点击才能落到 chip 上。 */
|
|
989
1004
|
[data-occurrence]{pointer-events:auto;cursor:pointer;position:relative;z-index:1}
|
|
990
|
-
/*
|
|
991
|
-
.dshq-dock{display:flex;flex-direction:column;gap:4px;padding:0;margin:
|
|
992
|
-
.dshq-quote-block{display:flex;align-items:center;min-width:0;max-width:100%;box-sizing:border-box;padding:
|
|
1005
|
+
/* 引用块:贴在输入框卡片上沿(点一下跳回原文,不显示任何"跳转"提示) */
|
|
1006
|
+
.dshq-dock{display:flex;flex-direction:column;gap:4px;padding:0 6px;margin:6px 0 -2px;min-width:0;box-sizing:border-box}
|
|
1007
|
+
.dshq-quote-block{display:flex;align-items:center;min-width:0;max-width:100%;box-sizing:border-box;padding:6px 10px;border-radius:12px 12px 4px 4px;background:var(--dsw-alias-bg-layer-2);background:color-mix(in oklab,var(--dsw-alias-brand-primary) 8%,var(--dsw-alias-bg-layer-1));color:var(--dsw-alias-label-secondary);font-size:12px;line-height:18px;cursor:pointer;transition:background .12s ease,color .12s ease}
|
|
993
1008
|
.dshq-quote-block:hover{color:var(--dsw-alias-label-primary)}
|
|
994
1009
|
.dshq-quote-block-text{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
995
1010
|
/* 已发送消息里的引用:隐藏位置标签,只显示紧凑的原文块(点击可跳回原文,无可见引导) */
|
|
@@ -997,6 +1012,11 @@ window.__ModuleLoader__.load({
|
|
|
997
1012
|
.dshq-sent-quote{display:block;margin:3px 0;padding:6px 10px;border-radius:8px;background:rgba(127,127,127,.10);color:var(--dsw-alias-label-secondary);font-size:12px;line-height:1.5;cursor:pointer;overflow:hidden;transition:background .12s ease,color .12s ease}
|
|
998
1013
|
.dshq-sent-quote:hover{color:var(--dsw-alias-label-primary)}
|
|
999
1014
|
.dshq-sent-quote-text{display:-webkit-box;-webkit-line-clamp:3;-webkit-box-orient:vertical;overflow:hidden;white-space:pre-wrap;word-break:break-word}
|
|
1015
|
+
/* 输入框里的引用:隐藏位置行,正文显示为紧凑块(输入框与消息观感一致) */
|
|
1016
|
+
.dshq-cmp-header{display:none !important}
|
|
1017
|
+
.dshq-cmp-quote{background:rgba(127,127,127,.10);border-radius:8px;padding:2px 10px;margin:2px 0}
|
|
1018
|
+
.dshq-cmp-quote-first{margin-top:4px}
|
|
1019
|
+
.dshq-cmp-quote-last{margin-bottom:4px}
|
|
1000
1020
|
`
|
|
1001
1021
|
|
|
1002
1022
|
// ============ 插件主体 ============
|
|
@@ -1270,18 +1290,22 @@ window.__ModuleLoader__.load({
|
|
|
1270
1290
|
})
|
|
1271
1291
|
return wrap
|
|
1272
1292
|
}
|
|
1273
|
-
/** 把承载「【引用…】\n
|
|
1293
|
+
/** 把承载「【引用…】\n原文…」的文本节点拆成:隐藏头部 + 紧凑引用块 + 其余文本 */
|
|
1274
1294
|
const transformTextNode = (node) => {
|
|
1275
1295
|
const raw = node.nodeValue || ''
|
|
1276
1296
|
const m = HEADER_RE.exec(raw)
|
|
1277
1297
|
if (!m) return false
|
|
1278
|
-
const
|
|
1279
|
-
const nl =
|
|
1298
|
+
const after = raw.slice(m.index + m[0].length)
|
|
1299
|
+
const nl = after.indexOf('\n')
|
|
1280
1300
|
if (nl < 0) return false
|
|
1281
|
-
const lines =
|
|
1301
|
+
const lines = after.slice(nl + 1).split('\n')
|
|
1302
|
+
// 引用正文 = 头部之后、直到空行之前的连续非空行(兼容旧格式的 "> " 前缀)
|
|
1282
1303
|
const quote = []
|
|
1283
1304
|
let i = 0
|
|
1284
|
-
|
|
1305
|
+
for (; i < lines.length; i++) {
|
|
1306
|
+
if (lines[i].trim() === '') break
|
|
1307
|
+
quote.push(lines[i].replace(/^>\s?/, ''))
|
|
1308
|
+
}
|
|
1285
1309
|
if (quote.length === 0) return false
|
|
1286
1310
|
const rest = lines.slice(i).join('\n')
|
|
1287
1311
|
const frag = document.createDocumentFragment()
|
|
@@ -1331,6 +1355,60 @@ window.__ModuleLoader__.load({
|
|
|
1331
1355
|
} catch (e) {
|
|
1332
1356
|
report('apply:sent-decor-throw', String((e && e.message) || e))
|
|
1333
1357
|
}
|
|
1358
|
+
|
|
1359
|
+
// ============ 输入框里的引用:隐藏位置行 + 紧凑块(与消息里的渲染保持一致) ============
|
|
1360
|
+
// 只给编辑器的行元素加 class,**不改动编辑器文本** → 发送出去的内容不变(模型仍拿到位置信息)。
|
|
1361
|
+
// 编辑器重渲染会丢掉 class,因此用 MutationObserver 重新贴上。
|
|
1362
|
+
try {
|
|
1363
|
+
ctx.effect(() => {
|
|
1364
|
+
let timer = null
|
|
1365
|
+
let observer = null
|
|
1366
|
+
const isSingleLine = (t) => t !== '' && t.indexOf('\n') === -1
|
|
1367
|
+
const decorate = () => {
|
|
1368
|
+
timer = null
|
|
1369
|
+
try {
|
|
1370
|
+
for (const ed of document.querySelectorAll('[contenteditable="true"]')) {
|
|
1371
|
+
const kids = Array.from(ed.children || [])
|
|
1372
|
+
let headerEl = null
|
|
1373
|
+
const quoteEls = []
|
|
1374
|
+
for (const el of kids) {
|
|
1375
|
+
const t = String(el.textContent || '').trim()
|
|
1376
|
+
if (!headerEl) {
|
|
1377
|
+
// 位置行必须"整行且只有一行",避免误把整个编辑器当成头部
|
|
1378
|
+
if (isSingleLine(t) && /^【引用\s*·[^\n]*】$/.test(t)) headerEl = el
|
|
1379
|
+
continue
|
|
1380
|
+
}
|
|
1381
|
+
if (t === '') break
|
|
1382
|
+
if (!isSingleLine(t)) break
|
|
1383
|
+
quoteEls.push(el)
|
|
1384
|
+
}
|
|
1385
|
+
if (!headerEl) continue
|
|
1386
|
+
headerEl.classList.add('dshq-cmp-header')
|
|
1387
|
+
quoteEls.forEach((el, idx) => {
|
|
1388
|
+
el.classList.add('dshq-cmp-quote')
|
|
1389
|
+
el.classList.toggle('dshq-cmp-quote-first', idx === 0)
|
|
1390
|
+
el.classList.toggle('dshq-cmp-quote-last', idx === quoteEls.length - 1)
|
|
1391
|
+
})
|
|
1392
|
+
report('cmp:decorated', { lines: quoteEls.length })
|
|
1393
|
+
}
|
|
1394
|
+
} catch (e) { report('cmp:scan-throw', String((e && e.message) || e)) }
|
|
1395
|
+
}
|
|
1396
|
+
const schedule = () => { if (timer === null) timer = setTimeout(decorate, 40) }
|
|
1397
|
+
decorate()
|
|
1398
|
+
composerDecorateNow = decorate
|
|
1399
|
+
try {
|
|
1400
|
+
observer = new MutationObserver(schedule)
|
|
1401
|
+
observer.observe(document.body, { childList: true, subtree: true, characterData: true })
|
|
1402
|
+
} catch (e) { report('cmp:observer-throw', String((e && e.message) || e)) }
|
|
1403
|
+
return () => {
|
|
1404
|
+
if (composerDecorateNow === decorate) composerDecorateNow = null
|
|
1405
|
+
if (timer !== null) clearTimeout(timer)
|
|
1406
|
+
if (observer) { try { observer.disconnect() } catch (e) { /* ignore */ } }
|
|
1407
|
+
}
|
|
1408
|
+
}, 'dsh-quote-annotate-pro: composer quote decoration')
|
|
1409
|
+
} catch (e) {
|
|
1410
|
+
report('apply:cmp-decor-throw', String((e && e.message) || e))
|
|
1411
|
+
}
|
|
1334
1412
|
const tryRegister = (tag, fn) => {
|
|
1335
1413
|
try { fn(); report('register:ok', tag); return true } catch (e) {
|
|
1336
1414
|
report('register:throw', tag + ' :: ' + String((e && e.stack) || (e && e.message) || e))
|
|
@@ -1344,9 +1422,12 @@ window.__ModuleLoader__.load({
|
|
|
1344
1422
|
{ name: 'conversation.input.dock', id: 'quote-pro-session-probe', order: 1 },
|
|
1345
1423
|
SessionProbe,
|
|
1346
1424
|
)))
|
|
1347
|
-
//
|
|
1348
|
-
|
|
1349
|
-
|
|
1425
|
+
// 引用块回到「输入框卡片上沿」的 dock 槽位(官方说明:Full-width entries above the composer card),
|
|
1426
|
+
// 用样式把它做成"贴在输入框上沿"的样子 —— DSH 没有"卡片内部、文字上方"的静态槽位:
|
|
1427
|
+
// conversation.input.overlay 是浮动弹层(放的是 slash-menu / command-popup 这类),
|
|
1428
|
+
// conversation.input.attachments 是官方草稿附件栏(single,占用会顶掉官方附件 UI,不能碰)。
|
|
1429
|
+
tryRegister('dock:quote-nav', () => slots.inject('conversation.input.dock', () => slots.register(
|
|
1430
|
+
{ name: 'conversation.input.dock', id: 'quote-pro-quote-block', order: 2 },
|
|
1350
1431
|
QuoteDock,
|
|
1351
1432
|
)))
|
|
1352
1433
|
tryRegister('turnTail', () => slots.inject('conversation.chat.turnTail', () => slots.register(
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-quote-annotate-pro",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.6",
|
|
4
4
|
"description": "选区引用与锚点批注(增强 fork):划词 → 批注 → 带精确锚点的引用;输入框上方只显示原文片段,点一下跳回原文;发送时把轮次/角色写入引用块,让模型精确知道你指的是哪一处。",
|
|
5
5
|
"keywords": ["dsh", "dsh-plugin", "deepseek-harness", "quote", "annotate", "selection", "reference", "anchor"],
|
|
6
6
|
"license": "MIT",
|