dsh-edit-turn 0.3.0 → 0.3.1
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 +11 -1
- package/lib/client.js +127 -12
- package/lib/index.js +23 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -164,7 +164,7 @@ ctx.sessions.registerMessageProjection({
|
|
|
164
164
|
| 运行中的实例是否真的挂载了本插件(只读路由守卫探针,无需 token) | `npm run probe:loaded [端口]` | 通过:`/state` 返 400、`/apply` 返 405 —— 这两个状态码只有本插件会返回 |
|
|
165
165
|
| 浏览器半部到底问了什么、拿回了什么(只读诊断,含失败记录) | `GET /dsh-edit-turn/debug` | 通过:真实浏览器里确认客户端确实拿到 replies;并复现了旧 v3 会话的 session-not-found |
|
|
166
166
|
| 官方 append **与读取**契约(真实校验器,进程内) | `npm run verify:contract` | 96 项通过(每段写入后都用加载器重读整份日志):替换事件被接受、派生历史真的收缩、日志 append-only、工具结果与调用同进同退、空 developer 载体不产生模型消息、**连续两次回退都被接受**、**编辑模型回答的完整机制被接受**、**回复编辑后的尾部重放(后续提问/回答/tool 调用与结果按新 turn 号回到修正回答之后,顺序正确、日志仍可加载)**、**跨插件依赖的不变量(单节点窗口 / 同事件类型 / 全窗口 sourceEventSeqs / 每个替换都带 `editedBy` 标记 / 改写后的 `source.kind` 不是 `user` / 修正回复不带 `sourceEventSeqs`)**,以及**就地补丁:一条 ignorable 记录、surface 不变、去掉投影后原文回来、后续轮次与 tool 结果不动、第二条记录可叠加** |
|
|
167
|
-
| 纯逻辑 + 宿主集成 + 客户端 DOM 行为(真 HTTP、真校验器、桩服务、DOM 桩) | `npm test` |
|
|
167
|
+
| 纯逻辑 + 宿主集成 + 客户端 DOM 行为(真 HTTP、真校验器、桩服务、DOM 桩) | `npm test` | 210 项通过(含回复编辑的尾部重放用例:窗口/重放区间、新 turn 号与新 messageId、tool 调用与结果成对重映射、上一轮修正可再次编辑,以及与 `dsh-rerun-turn` builder 的逐字段对照;另含「文本没变 → 不写」的四条,以及块随消息一起改的 35 条:未动的块逐字节保留、删块少一块、文字位置不变、no-op 语义升级、附件服务缺失时降级且不写坏数据、自定义 `quote-card` 块的通用性、录用与拒绝、`/state` 的块描述与能力、附件字节路由、编辑器芯片/删除/添加/缩略图/降级警告) |
|
|
168
168
|
| 客户端半部静态检查(注册、i18n 完整性、样式、皮肤可读性、版本三处同步、线协议、块契约) | `npm run verify:client` | 184 项全部通过(含 12 条块契约检查:能力上报、服务现场探测、录用是唯一建块路径、录用先于 append、**两侧都不特判块类型**、块逐字节复制、文字归位、`dropped` 上报、只在无服务时警告、芯片靠 `error` 事件自证) |
|
|
169
169
|
| 实机前端产物校验(运行中的 DSH 是否在下发当前代码) | `npm run verify:live -- --token-file ~/path/to/dsh.log` | 全部通过(含**宿主节点形状锚点**) |
|
|
170
170
|
| **真实浏览器渲染冒烟**(CDP 驱动已开着的标签页,只读) | `npm run verify:ui` | 全部通过:下发的字节就是刚改的字节、无槽位崩溃留下的空占位、回复铅笔在平台动作条里、被回滚的行保住动作条、**被回滚的用户行一定显示替代文本(消息不会凭空消失)**、**替代气泡一定排在时间/复制之上(顺序与正常消息一致)**、**改写后那颗铅笔一定在还活着的动作条末尾(已在原始会话、切走的会话、切回后三处现场确认)**、**回复铅笔一定排在动作条最左(复制之前,已现场确认)**、气泡上没有 `.dshet-floating` 压着文字、切走再切回入口仍在 |
|
|
@@ -250,6 +250,16 @@ ln -s /path/to/dsh-install/node_modules ./node_modules
|
|
|
250
250
|
|
|
251
251
|
### 更新日志
|
|
252
252
|
|
|
253
|
+
**0.3.1** —— 修复 0.3.0 的显示与跨插件清理问题。
|
|
254
|
+
|
|
255
|
+
- **编辑后看不到变化**:`turn-process` 行和 `assistant-step` 行都覆盖同一个 seq,旧的 DOM 映射按文档顺序先拿到过程行,补丁落到没有正文的行上。现在优先 assistant 行;极短回答是裸文本节点时也有兜底。
|
|
256
|
+
- **Markdown 匹配**:原文有 `**1**`、渲染后是 `1。`,旧比较把 `**` 换成空格后变成 `1 。`,逐字比较失败。现在比较忽略全部空白;显示时去掉 `**`、反引号和标题标记,不把 Markdown 源码画在正文里。
|
|
257
|
+
- **React 重渲染**:会清掉补丁元素的属性和样式但保留文本;现在按 `seq:block` 记住元素,连续第二次编辑仍能更新并能在补丁消失时恢复原文。
|
|
258
|
+
- **过时补丁**:目标已被 rerun 退役的 patch 不再出现在 `/state.patches`。
|
|
259
|
+
- **轮次丢失**:prompt 替换载体在 turn 括号之外追加,`turnIndex` 取不到轮次;现在从 `sourceEventSeqs` 继承被替换的轮次。
|
|
260
|
+
- **诊断**:客户端在 `<html>` 上写 `data-dshet-version`,可确认页面实际跑的客户端版本。
|
|
261
|
+
- **测试**:`npm test` **210/210**、`npm run verify:contract` **96** 项、`npm run verify:client` **184** 项。
|
|
262
|
+
|
|
253
263
|
**0.3.0** —— 回复编辑不再重放:用官方 plugin record + message projection 就地打补丁,正文与思维链都能改,日志每次只多一条小记录。
|
|
254
264
|
|
|
255
265
|
- **为什么改**:0.2.21 的回复编辑虽然保住了后续轮次,但代价是「遮蔽到末尾 + 逐事件重放」——改一条中间回答,日志会多出整段副本,轮次号、messageId 全换,工具调用/思维链也留不住。用户要的是「像改提示词一样改回答」,而不是一次 infix splice。
|
package/lib/client.js
CHANGED
|
@@ -42,7 +42,7 @@ window.__ModuleLoader__.load({
|
|
|
42
42
|
const RERUN_PREFIX = '/dsh-rerun-turn'
|
|
43
43
|
|
|
44
44
|
/** Keep in sync with package.json and lib/index.js. */
|
|
45
|
-
const PLUGIN_VERSION = '0.3.
|
|
45
|
+
const PLUGIN_VERSION = '0.3.1'
|
|
46
46
|
|
|
47
47
|
// --- copy -----------------------------------------------------------------
|
|
48
48
|
|
|
@@ -2452,6 +2452,11 @@ window.__ModuleLoader__.load({
|
|
|
2452
2452
|
const PATCH_ATTR = 'data-dshet-patch'
|
|
2453
2453
|
const NOTE_ATTR = 'data-dshet-patch-note'
|
|
2454
2454
|
const patchOriginals = new WeakMap()
|
|
2455
|
+
// React may re-render the assistant container and reset its attributes and
|
|
2456
|
+
// style while leaving the text node we replaced. Remember the element by
|
|
2457
|
+
// seq:block so the next pass can restore the marker and, on a later edit,
|
|
2458
|
+
// update it without having to re-find the original text (which is gone).
|
|
2459
|
+
const patchedElements = new Map()
|
|
2455
2460
|
|
|
2456
2461
|
function normalizedText(value) {
|
|
2457
2462
|
return String(value ?? '').replace(/\s+/g, ' ').trim()
|
|
@@ -2464,6 +2469,27 @@ window.__ModuleLoader__.load({
|
|
|
2464
2469
|
return normalizedText(String(value ?? '').replace(/[`*_#>~[\]()!|]/g, ' ').replace(/\s+/g, ' '))
|
|
2465
2470
|
}
|
|
2466
2471
|
|
|
2472
|
+
// Identity, not display: markdown emphasis (`**1**`) renders as `1。`, and
|
|
2473
|
+
// the spaces left by stripping the markers must not defeat the comparison.
|
|
2474
|
+
// Whitespace carries no block identity here, so remove it entirely.
|
|
2475
|
+
function matchText(value) {
|
|
2476
|
+
return plainText(value).replace(/\s+/g, '')
|
|
2477
|
+
}
|
|
2478
|
+
|
|
2479
|
+
// What the reader sees for a patched block. The model context keeps the
|
|
2480
|
+
// raw Markdown the editor wrote; the transcript should not show the markers
|
|
2481
|
+
// as literal text (`**1**`), which is what made a successful edit look like
|
|
2482
|
+
// an unrendered draft. This is deliberately minimal: bold, inline code and
|
|
2483
|
+
// headings are unwrapped; everything else stays as written.
|
|
2484
|
+
function displayText(value) {
|
|
2485
|
+
return String(value ?? '')
|
|
2486
|
+
.replace(/```[\s\S]*?```/g, (fence) => fence.replace(/^```[^\n]*\n?/gm, '').replace(/```$/gm, ''))
|
|
2487
|
+
.replace(/`([^`]*)`/g, '$1')
|
|
2488
|
+
.replace(/\*\*([^*]+)\*\*/g, '$1')
|
|
2489
|
+
.replace(/__([^_]+)__/g, '$1')
|
|
2490
|
+
.replace(/^#{1,6}\s+/gm, '')
|
|
2491
|
+
}
|
|
2492
|
+
|
|
2467
2493
|
function isMarkdownContainer(el) {
|
|
2468
2494
|
if (el === null || el === undefined || !el.children) return false
|
|
2469
2495
|
const kids = el.children
|
|
@@ -2498,18 +2524,69 @@ window.__ModuleLoader__.load({
|
|
|
2498
2524
|
el.style.whiteSpace = ''
|
|
2499
2525
|
}
|
|
2500
2526
|
|
|
2527
|
+
// A short reply can be a bare text node (or a span) rather than the
|
|
2528
|
+
// block-level markdown container the picker looks for. Match the deepest
|
|
2529
|
+
// element in the owning row whose whole text is exactly the recorded text;
|
|
2530
|
+
// never a button, a timestamp, our own chrome, or an ancestor that merely
|
|
2531
|
+
// contains the text.
|
|
2532
|
+
function findPatchTextFallback(owner, wanted) {
|
|
2533
|
+
if (owner === null || owner === undefined || typeof owner.querySelectorAll !== 'function') return null
|
|
2534
|
+
const original = matchText(wanted.original)
|
|
2535
|
+
if (original === '') return null
|
|
2536
|
+
let scope = owner
|
|
2537
|
+
try {
|
|
2538
|
+
const node = owner.querySelector('[data-slot="conversation.chat.node"]')
|
|
2539
|
+
if (node !== null && node !== undefined) scope = node
|
|
2540
|
+
} catch {
|
|
2541
|
+
/* keep the row as the scope */
|
|
2542
|
+
}
|
|
2543
|
+
for (const el of scope.querySelectorAll('*')) {
|
|
2544
|
+
if (el === scope) continue
|
|
2545
|
+
if (el.getAttribute(PATCH_ATTR) !== null) continue
|
|
2546
|
+
try {
|
|
2547
|
+
if (el.closest('.dshet-editor, .dshet-action, .dshet-pick, .dshet-patch-note, button, time') !== null) continue
|
|
2548
|
+
} catch {
|
|
2549
|
+
continue
|
|
2550
|
+
}
|
|
2551
|
+
const text = matchText(el.textContent)
|
|
2552
|
+
if (text !== original) continue
|
|
2553
|
+
let deeper = false
|
|
2554
|
+
for (const child of el.children ?? []) {
|
|
2555
|
+
if (matchText(child.textContent) === text) {
|
|
2556
|
+
deeper = true
|
|
2557
|
+
break
|
|
2558
|
+
}
|
|
2559
|
+
}
|
|
2560
|
+
if (!deeper) return el
|
|
2561
|
+
}
|
|
2562
|
+
return null
|
|
2563
|
+
}
|
|
2564
|
+
|
|
2501
2565
|
function applyPatchDisplay(snapshot, view, t) {
|
|
2502
2566
|
// Map a reply seq to the host row that draws it, so a short answer like
|
|
2503
2567
|
// "好的" cannot be matched in a different row. Fall back to a global
|
|
2504
2568
|
// search when the row is not mounted (virtualised histories).
|
|
2505
2569
|
const rowBySeq = new Map()
|
|
2570
|
+
// A turn-process row also covers the turn's assistant seq (seqsFor reads
|
|
2571
|
+
// its anchor), and it comes before the assistant row in document order.
|
|
2572
|
+
// First-win mapping therefore pointed the patch at the process row, whose
|
|
2573
|
+
// subtree has no message body: zero matches. Prefer the row that actually
|
|
2574
|
+
// draws the message; the process/tail rows are only a last resort.
|
|
2575
|
+
const rowRank = new Map()
|
|
2506
2576
|
try {
|
|
2507
2577
|
for (const row of document.querySelectorAll('[data-chat-flow-key]')) {
|
|
2508
2578
|
const key = row.getAttribute('data-chat-flow-key')
|
|
2509
2579
|
if (!key || !snapshot || !snapshot.nodes || typeof snapshot.nodes.get !== 'function') continue
|
|
2510
2580
|
const node = snapshot.nodes.get(key)
|
|
2511
2581
|
if (!node) continue
|
|
2512
|
-
|
|
2582
|
+
const rank = node.kind === 'assistant-step' ? 0 : node.kind === 'user' ? 1 : 2
|
|
2583
|
+
for (const seq of seqsFor(node)) {
|
|
2584
|
+
const previous = rowRank.get(seq)
|
|
2585
|
+
if (previous === undefined || rank < previous) {
|
|
2586
|
+
rowBySeq.set(seq, row)
|
|
2587
|
+
rowRank.set(seq, rank)
|
|
2588
|
+
}
|
|
2589
|
+
}
|
|
2513
2590
|
}
|
|
2514
2591
|
} catch {
|
|
2515
2592
|
/* a host shape this pass cannot read falls back below */
|
|
@@ -2541,6 +2618,13 @@ window.__ModuleLoader__.load({
|
|
|
2541
2618
|
}
|
|
2542
2619
|
}
|
|
2543
2620
|
|
|
2621
|
+
// A patch that no longer exists must give the host's recorded text back,
|
|
2622
|
+
// even when a re-render already stripped our marker from the element.
|
|
2623
|
+
for (const [key, el] of [...patchedElements]) {
|
|
2624
|
+
if (wantedText.has(key)) continue
|
|
2625
|
+
patchedElements.delete(key)
|
|
2626
|
+
if (el.isConnected === true) restorePatchedBlock(el)
|
|
2627
|
+
}
|
|
2544
2628
|
// Remove overrides whose patch is gone (or was superseded by a prompt
|
|
2545
2629
|
// rollback). The original HTML is what the host rendered.
|
|
2546
2630
|
for (const el of [...root.querySelectorAll(`[${PATCH_ATTR}]`)]) {
|
|
@@ -2550,8 +2634,9 @@ window.__ModuleLoader__.load({
|
|
|
2550
2634
|
restorePatchedBlock(el)
|
|
2551
2635
|
continue
|
|
2552
2636
|
}
|
|
2553
|
-
|
|
2554
|
-
|
|
2637
|
+
const shown = displayText(wanted.text)
|
|
2638
|
+
if (normalizedText(el.textContent) !== normalizedText(shown)) {
|
|
2639
|
+
el.textContent = shown
|
|
2555
2640
|
}
|
|
2556
2641
|
}
|
|
2557
2642
|
for (const note of [...root.querySelectorAll(`[${NOTE_ATTR}]`)]) {
|
|
@@ -2572,13 +2657,33 @@ window.__ModuleLoader__.load({
|
|
|
2572
2657
|
}
|
|
2573
2658
|
}
|
|
2574
2659
|
for (const [key, wanted] of wantedText) {
|
|
2660
|
+
const owner = rowBySeq.get(wanted.seq)
|
|
2661
|
+
const remembered = patchedElements.get(key)
|
|
2662
|
+
if (remembered !== undefined) {
|
|
2663
|
+
const stillThere = remembered.isConnected === true &&
|
|
2664
|
+
(owner === undefined || remembered.closest('[data-chat-flow-key]') === owner)
|
|
2665
|
+
if (!stillThere) {
|
|
2666
|
+
patchedElements.delete(key)
|
|
2667
|
+
} else {
|
|
2668
|
+
const shown = displayText(wanted.text)
|
|
2669
|
+
if (normalizedText(remembered.textContent) !== normalizedText(shown)) {
|
|
2670
|
+
remembered.textContent = shown
|
|
2671
|
+
}
|
|
2672
|
+
if (remembered.getAttribute(PATCH_ATTR) !== key) remembered.setAttribute(PATCH_ATTR, key)
|
|
2673
|
+
remembered.style.whiteSpace = 'pre-wrap'
|
|
2674
|
+
continue
|
|
2675
|
+
}
|
|
2676
|
+
}
|
|
2575
2677
|
const done = [...root.querySelectorAll(`[${PATCH_ATTR}]`)].find((el) => el.getAttribute(PATCH_ATTR) === key)
|
|
2576
|
-
if (done !== undefined)
|
|
2577
|
-
|
|
2678
|
+
if (done !== undefined) {
|
|
2679
|
+
patchedElements.set(key, done)
|
|
2680
|
+
if (done.style.whiteSpace !== 'pre-wrap') done.style.whiteSpace = 'pre-wrap'
|
|
2681
|
+
continue
|
|
2682
|
+
}
|
|
2683
|
+
const original = matchText(wanted.original)
|
|
2578
2684
|
if (original === '') continue
|
|
2579
2685
|
let best = null
|
|
2580
2686
|
let bestScore = Infinity
|
|
2581
|
-
const owner = rowBySeq.get(wanted.seq)
|
|
2582
2687
|
for (const el of containers) {
|
|
2583
2688
|
if (el.getAttribute(PATCH_ATTR) !== null) continue
|
|
2584
2689
|
if (owner !== undefined) {
|
|
@@ -2588,7 +2693,7 @@ window.__ModuleLoader__.load({
|
|
|
2588
2693
|
/* no closest on this node: keep the global match */
|
|
2589
2694
|
}
|
|
2590
2695
|
}
|
|
2591
|
-
const text =
|
|
2696
|
+
const text = matchText(el.textContent)
|
|
2592
2697
|
if (text === '') continue
|
|
2593
2698
|
const exact = text === original
|
|
2594
2699
|
const prefix = text.startsWith(original.slice(0, 60)) || original.startsWith(text.slice(0, 60))
|
|
@@ -2602,11 +2707,13 @@ window.__ModuleLoader__.load({
|
|
|
2602
2707
|
bestScore = score
|
|
2603
2708
|
}
|
|
2604
2709
|
}
|
|
2710
|
+
if (best === null) best = findPatchTextFallback(owner, wanted)
|
|
2605
2711
|
if (best === null) continue
|
|
2606
2712
|
patchOriginals.set(best, { html: best.innerHTML, text: best.textContent })
|
|
2607
|
-
best.textContent = wanted.text
|
|
2713
|
+
best.textContent = displayText(wanted.text)
|
|
2608
2714
|
best.setAttribute(PATCH_ATTR, key)
|
|
2609
2715
|
best.style.whiteSpace = 'pre-wrap'
|
|
2716
|
+
patchedElements.set(key, best)
|
|
2610
2717
|
}
|
|
2611
2718
|
|
|
2612
2719
|
// Reasoning notes: put one under the reply row, found by the reply's own
|
|
@@ -2614,15 +2721,15 @@ window.__ModuleLoader__.load({
|
|
|
2614
2721
|
for (const [key, wanted] of wantedNotes) {
|
|
2615
2722
|
let note = [...root.querySelectorAll(`[${NOTE_ATTR}]`)].find((el) => el.getAttribute(NOTE_ATTR) === key)
|
|
2616
2723
|
let row = rowBySeq.get(wanted.reply.seq) ?? null
|
|
2617
|
-
const replyText =
|
|
2724
|
+
const replyText = matchText(wanted.reply.text)
|
|
2618
2725
|
for (const node of row === null ? nodes : []) {
|
|
2619
|
-
const text =
|
|
2726
|
+
const text = matchText(node.textContent)
|
|
2620
2727
|
if (text === '') continue
|
|
2621
2728
|
if (replyText !== '' && text.includes(replyText)) {
|
|
2622
2729
|
row = node
|
|
2623
2730
|
break
|
|
2624
2731
|
}
|
|
2625
|
-
if (
|
|
2732
|
+
if (matchText(wanted.block.original) !== '' && text.includes(matchText(wanted.block.original))) {
|
|
2626
2733
|
row = node
|
|
2627
2734
|
break
|
|
2628
2735
|
}
|
|
@@ -2967,6 +3074,14 @@ window.__ModuleLoader__.load({
|
|
|
2967
3074
|
// --- plugin ---------------------------------------------------------------
|
|
2968
3075
|
|
|
2969
3076
|
function apply(ctx) {
|
|
3077
|
+
// Diagnostic only: the host restart does not reload the page's JS, so a
|
|
3078
|
+
// stale client bundle and a live host can coexist. This marker says which
|
|
3079
|
+
// browser half actually ran (the old bundle has none).
|
|
3080
|
+
try {
|
|
3081
|
+
document.documentElement.dataset.dshetVersion = PLUGIN_VERSION
|
|
3082
|
+
} catch {
|
|
3083
|
+
/* a non-DOM host is not worth failing over */
|
|
3084
|
+
}
|
|
2970
3085
|
ctx.effect(() => ctx.locale.register(NS, { zh, en }), 'dsh-edit-turn: dictionaries')
|
|
2971
3086
|
|
|
2972
3087
|
const controllers = new Map()
|
package/lib/index.js
CHANGED
|
@@ -80,7 +80,7 @@ export const PLUGIN_ID = 'dsh-edit-turn'
|
|
|
80
80
|
export const name = PLUGIN_ID
|
|
81
81
|
|
|
82
82
|
/** Keep in sync with package.json and lib/client.js. */
|
|
83
|
-
export const PLUGIN_VERSION = '0.3.
|
|
83
|
+
export const PLUGIN_VERSION = '0.3.1'
|
|
84
84
|
|
|
85
85
|
// A ring of the last few requests, answering "did the browser half even ask, and
|
|
86
86
|
// what did it get?" without needing its console. Read-only diagnostics: the
|
|
@@ -1239,7 +1239,20 @@ export function editableTurns(events, surfaceNodes, options = {}) {
|
|
|
1239
1239
|
if (!event || !isHumanPrompt(event)) continue
|
|
1240
1240
|
if (seq === head) continue
|
|
1241
1241
|
const { text, attachments } = readMessageText(event)
|
|
1242
|
-
|
|
1242
|
+
// A replacement carrier is appended after its original turn's bracket has
|
|
1243
|
+
// closed, so turnIndex cannot name its turn. It still stands at that
|
|
1244
|
+
// prompt's position in the conversation: inherit the turn from the event
|
|
1245
|
+
// it replaced, or the editor's turn list loses the row (turn: null).
|
|
1246
|
+
let turn = turnOf.get(seq)
|
|
1247
|
+
if (typeof turn !== 'number' && Array.isArray(event.sourceEventSeqs)) {
|
|
1248
|
+
for (const source of event.sourceEventSeqs) {
|
|
1249
|
+
const inherited = turnOf.get(source)
|
|
1250
|
+
if (typeof inherited === 'number') {
|
|
1251
|
+
turn = inherited
|
|
1252
|
+
break
|
|
1253
|
+
}
|
|
1254
|
+
}
|
|
1255
|
+
}
|
|
1243
1256
|
out.push({
|
|
1244
1257
|
seq,
|
|
1245
1258
|
turn: typeof turn === 'number' ? turn : null,
|
|
@@ -1645,6 +1658,13 @@ async function stateOf(ctx, sessionId, config) {
|
|
|
1645
1658
|
const folded = foldSurface(events)
|
|
1646
1659
|
const ledger = rollbackLedger(events)
|
|
1647
1660
|
const patches = patchesOf(events)
|
|
1661
|
+
// A patch whose target has been retired (a later rerun shadowed it) is inert:
|
|
1662
|
+
// the projection skips it and no row can show it. Reporting it in /state made
|
|
1663
|
+
// the browser draw a patch that could never land, and made "which patches are
|
|
1664
|
+
// live" ambiguous. Keep the internal map for the replies; publish the active
|
|
1665
|
+
// subset only.
|
|
1666
|
+
const surfaceSet = new Set(folded.nodes)
|
|
1667
|
+
const activePatches = new Map([...patches].filter(([seq]) => surfaceSet.has(seq)))
|
|
1648
1668
|
const live = findLiveSession(ctx, sessionId)
|
|
1649
1669
|
// Probed per request: the editor offers what THIS deployment can do right
|
|
1650
1670
|
// now, so a store that is unmounted (or not yet mounted) cannot leave the
|
|
@@ -1676,7 +1696,7 @@ async function stateOf(ctx, sessionId, config) {
|
|
|
1676
1696
|
// The landed in-place patches, latest per block. The browser half uses them
|
|
1677
1697
|
// to draw the projected text in place of the recorded one; the model already
|
|
1678
1698
|
// sees them through the host projection.
|
|
1679
|
-
patches: patchList(
|
|
1699
|
+
patches: patchList(activePatches),
|
|
1680
1700
|
mode: config.patch === true ? 'patch' : 'replay',
|
|
1681
1701
|
live: Boolean(live),
|
|
1682
1702
|
busy: isBusy(events),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-edit-turn",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"description": "Edit any message in DeepSeek Harness in place: a prompt is replaced where it stands; a model reply block (answer text or reasoning) is patched through a plugin record and a message projection, so the turn stays where it is, later turns are untouched and the log grows by one small record. Falls back to the replay splice on hosts without the projection API. The append-only session log is never rewritten.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|