dsh-edit-turn 0.2.0 → 0.2.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 +19 -2
- package/lib/client.js +139 -25
- package/lib/index.js +61 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -110,11 +110,12 @@ await ctx.sessionController.prompt({ requestId, sessionId, mode: 'queue', conten
|
|
|
110
110
|
|
|
111
111
|
### 验证状态
|
|
112
112
|
|
|
113
|
-
本插件在 DSH `0.1.6-alpha.2`
|
|
113
|
+
本插件在 DSH `0.1.6-alpha.2` 与 `0.1.7-rc.1` 上完成以下验证:
|
|
114
114
|
|
|
115
115
|
| 验证 | 命令 | 结果 |
|
|
116
116
|
|---|---|---|
|
|
117
117
|
| 运行中的实例是否真的挂载了本插件(只读路由守卫探针,无需 token) | `npm run probe:loaded [端口]` | 通过:`/state` 返 400、`/apply` 返 405 —— 这两个状态码只有本插件会返回 |
|
|
118
|
+
| 浏览器半部到底问了什么、拿回了什么(只读诊断,含失败记录) | `GET /dsh-edit-turn/debug` | 通过:真实浏览器里确认客户端确实拿到 replies;并复现了旧 v3 会话的 session-not-found |
|
|
118
119
|
| 官方 append 契约(真实校验器,进程内) | `npm run verify:contract` | 61 项通过:替换事件被接受、派生历史真的收缩、日志 append-only、工具结果与调用同进同退、空 system 载体不产生模型消息、**连续两次回退都被接受**、**编辑模型回答的完整机制被接受** |
|
|
119
120
|
| 纯逻辑 + 宿主集成 + 客户端 DOM 行为(真 HTTP、真校验器、桩服务、DOM 桩) | `npm test` | 83 项通过 |
|
|
120
121
|
| 客户端半部静态检查(注册、i18n 完整性、样式、皮肤可读性、版本三处同步、线协议) | `npm run verify:client` | 全部通过 |
|
|
@@ -149,6 +150,22 @@ ln -s /path/to/dsh-install/node_modules ./node_modules
|
|
|
149
150
|
- **进行中拒绝编辑**:未闭合的轮次或正在压缩时返回 `409 busy`。
|
|
150
151
|
- **回退是持久的,隐藏不是**。回退写进日志后,模型上下文永久改变;转录里那些行的隐藏是本插件客户端半部做的。卸载插件后,旧行会重新显示出来(而模型上下文里的回退仍然生效)——因为替换事件是官方事件,不会随插件消失。
|
|
151
152
|
- 回退后**系统提示词保持不变**(窗口永不包含 surface 节点 0,该节点也永不可编辑)。
|
|
153
|
+
- **旧会话(v3 格式日志)在 DSH 0.1.7 下读不到**:DSH 的会话读取层对这类日志返回 `session-not-found`,本插件因此在其中完全不工作(表现为没有编辑入口)。新写的会话是 v4,正常。
|
|
154
|
+
|
|
155
|
+
### 更新日志
|
|
156
|
+
|
|
157
|
+
**0.2.1** —— 修 0.1.7 上的两处界面问题,并补上让问题可自证的诊断手段。
|
|
158
|
+
|
|
159
|
+
- 修复:模型回复的编辑入口改注册进官方插槽 `conversation.chat.assistant-actions`,与平台自己的 复制 / 赞 / 踩 / 分支 / 用量 同排(此前是 DOM 注入,位置不一致、且会随工具调用行卸载而闪烁)。
|
|
160
|
+
- 修复:编辑模型回答后**重新闭合该轮**(`step/end` + `turn/end`)。此前修正消息被追加到 `turn/end` 之后、落在轮外,导致那一轮的尾巴(用时、动作条)渲染到回复正文**上面**。
|
|
161
|
+
- 修复:DSH 0.1.7 兼容——会话格式 v4 的 producer-owned source kind(`plugin:<id>`)、助手消息文本取自 `data.message.content`。
|
|
162
|
+
- 新增:只读诊断路由 `GET /dsh-edit-turn/debug`;`npm run probe:loaded [端口]`。
|
|
163
|
+
|
|
164
|
+
**0.2.0** —— 可以编辑模型的回答。回退该回答及其后内容,再追加一条带改写文本的助手消息(回答无法原地替换:官方格式禁止 `assistant/message` 携带 `sourceEventSeqs`)。不重跑模型;`source.editedBy` 如实标记文字由插件写入。
|
|
165
|
+
|
|
166
|
+
**0.1.1** —— 去掉多余的二次确认(默认一次点击即执行);修皮肤下界面可读性。
|
|
167
|
+
|
|
168
|
+
**0.1.0** —— 首版:回退并重跑你自己发过的消息。
|
|
152
169
|
|
|
153
170
|
### 姊妹插件
|
|
154
171
|
|
|
@@ -158,7 +175,7 @@ ln -s /path/to/dsh-install/node_modules ./node_modules
|
|
|
158
175
|
|
|
159
176
|
### 兼容性
|
|
160
177
|
|
|
161
|
-
- `dsh.engines.dsh`: `>=0.1.6-alpha.2`(本插件依赖该版本的消息行定义与 surface
|
|
178
|
+
- `dsh.engines.dsh`: `>=0.1.6-alpha.2`(本插件依赖该版本的消息行定义与 surface 语义;`0.1.6-alpha.2` 与 `0.1.7-rc.1` 均已实测)。
|
|
162
179
|
- 客户端依赖:`dsh-client-locale`、`dsh-client-ui-chat`、`dsh-client-ui-conversation`、`dsh-client-ui-primitives`。
|
|
163
180
|
- 宿主依赖:`dsh-settings`、`dsh-tools`(peer);`@deepseek-ai/schemastery`(直接依赖)。
|
|
164
181
|
|
package/lib/client.js
CHANGED
|
@@ -30,7 +30,7 @@ window.__ModuleLoader__.load({
|
|
|
30
30
|
const ROUTE_PREFIX = '/dsh-edit-turn'
|
|
31
31
|
|
|
32
32
|
/** Keep in sync with package.json and lib/index.js. */
|
|
33
|
-
const PLUGIN_VERSION = '0.2.
|
|
33
|
+
const PLUGIN_VERSION = '0.2.1'
|
|
34
34
|
|
|
35
35
|
// --- copy -----------------------------------------------------------------
|
|
36
36
|
|
|
@@ -216,6 +216,7 @@ window.__ModuleLoader__.load({
|
|
|
216
216
|
hidden: new Map(),
|
|
217
217
|
editable: new Map(),
|
|
218
218
|
replies: new Map(),
|
|
219
|
+
repliesByMessage: new Map(),
|
|
219
220
|
editing: null,
|
|
220
221
|
draft: '',
|
|
221
222
|
confirming: false,
|
|
@@ -278,13 +279,19 @@ window.__ModuleLoader__.load({
|
|
|
278
279
|
// rewritten rather than re-run, so the editor words the consequence
|
|
279
280
|
// differently.
|
|
280
281
|
const replies = new Map()
|
|
282
|
+
const repliesByMessage = new Map()
|
|
281
283
|
for (const reply of Array.isArray(data.replies) ? data.replies : []) {
|
|
282
|
-
if (reply
|
|
284
|
+
if (!reply || typeof reply.seq !== 'number') continue
|
|
285
|
+
replies.set(reply.seq, reply)
|
|
286
|
+
// The official assistant-actions strip hands us a messageId, so
|
|
287
|
+
// replies are indexed by it too.
|
|
288
|
+
if (typeof reply.messageId === 'string' && reply.messageId !== '') repliesByMessage.set(reply.messageId, reply)
|
|
283
289
|
}
|
|
284
290
|
this.publish({
|
|
285
291
|
hidden,
|
|
286
292
|
editable,
|
|
287
293
|
replies,
|
|
294
|
+
repliesByMessage,
|
|
288
295
|
surfaceReady: true,
|
|
289
296
|
loaded: true,
|
|
290
297
|
loadError: false,
|
|
@@ -431,34 +438,26 @@ window.__ModuleLoader__.load({
|
|
|
431
438
|
// The editable target of a Chat view node, or null when the row stands for
|
|
432
439
|
// something that cannot be rewritten.
|
|
433
440
|
//
|
|
434
|
-
//
|
|
435
|
-
//
|
|
436
|
-
//
|
|
437
|
-
//
|
|
438
|
-
// so both candidates are tried rather than assumed.
|
|
441
|
+
// Only human prompts come through here: a reply's entry comes from the
|
|
442
|
+
// official assistant-actions strip instead, which is where the host puts
|
|
443
|
+
// every other action for that row. Injecting one here used to put the pencil
|
|
444
|
+
// in a different place from every other reply action.
|
|
439
445
|
function targetFor(node, view) {
|
|
440
|
-
if (!node) return null
|
|
446
|
+
if (!node || node.kind !== 'user') return null
|
|
441
447
|
const data = node.data || {}
|
|
442
448
|
const candidates = []
|
|
443
449
|
const push = (value) => {
|
|
444
450
|
if (typeof value === 'number' && !candidates.includes(value)) candidates.push(value)
|
|
445
451
|
}
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
push(node.anchorSeq)
|
|
449
|
-
} else if (node.kind === 'assistant-step') {
|
|
450
|
-
push(data.finalNode && data.finalNode.seq)
|
|
451
|
-
push(node.anchorSeq)
|
|
452
|
-
} else {
|
|
453
|
-
return null
|
|
454
|
-
}
|
|
452
|
+
push(data.seq)
|
|
453
|
+
push(node.anchorSeq)
|
|
455
454
|
for (const seq of candidates) {
|
|
456
455
|
if (view.hidden.has(seq)) continue
|
|
457
|
-
const entry = view.editable.get(seq)
|
|
456
|
+
const entry = view.editable.get(seq)
|
|
458
457
|
if (!entry) continue
|
|
459
458
|
return {
|
|
460
459
|
seq,
|
|
461
|
-
mode:
|
|
460
|
+
mode: 'prompt',
|
|
462
461
|
turn: entry.turn,
|
|
463
462
|
messageId: entry.messageId,
|
|
464
463
|
text: typeof entry.text === 'string' ? entry.text : '',
|
|
@@ -468,6 +467,13 @@ window.__ModuleLoader__.load({
|
|
|
468
467
|
return null
|
|
469
468
|
}
|
|
470
469
|
|
|
470
|
+
// The element that carries the turn-tail marker: either the flow-keyed row
|
|
471
|
+
// itself or a wrapper inside it. Its own action strip is a direct child.
|
|
472
|
+
function rootOfTurnTail(row) {
|
|
473
|
+
if (typeof row.matches === 'function' && row.matches('[data-turn-tail]')) return row
|
|
474
|
+
return row.querySelector('[data-turn-tail]')
|
|
475
|
+
}
|
|
476
|
+
|
|
471
477
|
// Every surface seq a row stands for, so one rollback can hide the rows it
|
|
472
478
|
// shadowed however the host UI chose to group them.
|
|
473
479
|
function seqsFor(node) {
|
|
@@ -559,7 +565,7 @@ window.__ModuleLoader__.load({
|
|
|
559
565
|
rowActions.delete(row)
|
|
560
566
|
}
|
|
561
567
|
|
|
562
|
-
function injectRowAction(row, target, controller, t) {
|
|
568
|
+
function injectRowAction(row, target, controller, t, options = {}) {
|
|
563
569
|
const label = t(target.mode === 'reply' ? 'action.edit-reply' : 'action.edit')
|
|
564
570
|
let entry = rowActions.get(row)
|
|
565
571
|
if (!entry) {
|
|
@@ -569,6 +575,11 @@ window.__ModuleLoader__.load({
|
|
|
569
575
|
button.type = 'button'
|
|
570
576
|
button.className = 'dshet-action dshet-row-action'
|
|
571
577
|
button.innerHTML = ICON_MARKUP
|
|
578
|
+
// The marker goes on the BUTTON, not only on its wrapper: the pass that
|
|
579
|
+
// looks for the official strip's entry queries buttons, so a marker one
|
|
580
|
+
// level up made this pencil look like the strip's own and it deleted
|
|
581
|
+
// itself - then the next pass put it back, which reads as flicker.
|
|
582
|
+
if (options.fallback === true) button.dataset.dshetFallback = '1'
|
|
572
583
|
host.appendChild(button)
|
|
573
584
|
entry = { host, button }
|
|
574
585
|
rowActions.set(row, entry)
|
|
@@ -583,7 +594,9 @@ window.__ModuleLoader__.load({
|
|
|
583
594
|
event.stopPropagation()
|
|
584
595
|
controller.open(target)
|
|
585
596
|
}
|
|
586
|
-
|
|
597
|
+
// A caller that already picked the right bar (the turn tail's own strip)
|
|
598
|
+
// hands it over; otherwise the first actions container in the row wins.
|
|
599
|
+
const anchor = options.anchor ?? row.querySelector('[class*="_actions"]')
|
|
587
600
|
if (anchor) {
|
|
588
601
|
host.classList.remove('dshet-floating')
|
|
589
602
|
row.classList.remove('dshet-row')
|
|
@@ -754,14 +767,62 @@ window.__ModuleLoader__.load({
|
|
|
754
767
|
const hidden = isRowHidden(view.hidden, seqsFor(node))
|
|
755
768
|
setRowHidden(row, hidden, animate)
|
|
756
769
|
const target = hidden ? null : targetFor(node, view)
|
|
757
|
-
if (target !== null)
|
|
758
|
-
|
|
770
|
+
if (target !== null) {
|
|
771
|
+
injectRowAction(row, target, controller, t)
|
|
772
|
+
} else if (!hidden && node.kind === 'turn-tail') {
|
|
773
|
+
// A reply's entry belongs in the host's own action strip, and that is
|
|
774
|
+
// where it is registered. A slot entry that never renders leaves no
|
|
775
|
+
// trace at all, though - no error, no gap - so the same pencil is also
|
|
776
|
+
// offered through the turn tail's own strip, and only while that strip
|
|
777
|
+
// does not already carry one. Both land in the platform's own bar, so
|
|
778
|
+
// the position is identical either way.
|
|
779
|
+
const covered = seqsFor(node).find((seq) => view.replies.has(seq) && !view.hidden.has(seq))
|
|
780
|
+
// The strip lives inside the turn-tail root (`data-turn-tail`) as its
|
|
781
|
+
// LAST direct child, while the row carrying the flow key may be a
|
|
782
|
+
// wrapper around that root. Bounding the search by the tail root keeps
|
|
783
|
+
// the tool-call bars inside the tail out of reach: those unmount as a
|
|
784
|
+
// call expands, so a pencil parked there blinks, and it is not where
|
|
785
|
+
// the reply's actions live anyway.
|
|
786
|
+
const tailRoot = rootOfTurnTail(row)
|
|
787
|
+
const bar =
|
|
788
|
+
covered === undefined || tailRoot === null
|
|
789
|
+
? undefined
|
|
790
|
+
: Array.from(tailRoot.children)
|
|
791
|
+
.reverse()
|
|
792
|
+
.find((child) => typeof child.className === 'string' && child.className.includes('_actions'))
|
|
793
|
+
const slotPencil = bar === undefined ? null : bar.querySelector('.dshet-row-action:not([data-dshet-fallback])')
|
|
794
|
+
if (covered !== undefined && bar !== undefined && slotPencil === null) {
|
|
795
|
+
const entry = view.replies.get(covered)
|
|
796
|
+
injectRowAction(
|
|
797
|
+
row,
|
|
798
|
+
{
|
|
799
|
+
seq: covered,
|
|
800
|
+
mode: 'reply',
|
|
801
|
+
turn: entry.turn,
|
|
802
|
+
messageId: entry.messageId,
|
|
803
|
+
text: typeof entry.text === 'string' ? entry.text : '',
|
|
804
|
+
attachments: typeof entry.attachments === 'number' ? entry.attachments : 0,
|
|
805
|
+
},
|
|
806
|
+
controller,
|
|
807
|
+
t,
|
|
808
|
+
{ fallback: true, anchor: bar },
|
|
809
|
+
)
|
|
810
|
+
} else {
|
|
811
|
+
// Either the strip's own entry is already there, or this row holds
|
|
812
|
+
// nothing editable: any spare pencil of ours has to go.
|
|
813
|
+
removeRowAction(row)
|
|
814
|
+
}
|
|
815
|
+
} else {
|
|
816
|
+
removeRowAction(row)
|
|
817
|
+
}
|
|
818
|
+
// The editor anchors to whichever row covers the seq being edited, not to
|
|
819
|
+
// the row that owns the entry: a reply's entry now lives in the turn-tail
|
|
820
|
+
// action strip, while its editor still belongs under the reply text.
|
|
759
821
|
if (
|
|
760
822
|
!hidden &&
|
|
761
823
|
view.editing !== null &&
|
|
762
824
|
typeof view.editing.seq === 'number' &&
|
|
763
|
-
|
|
764
|
-
target.seq === view.editing.seq
|
|
825
|
+
seqsFor(node).includes(view.editing.seq)
|
|
765
826
|
) {
|
|
766
827
|
renderEditor(row, controller, view, t)
|
|
767
828
|
}
|
|
@@ -769,6 +830,40 @@ window.__ModuleLoader__.load({
|
|
|
769
830
|
if (view.editing === null) clearEditor()
|
|
770
831
|
}
|
|
771
832
|
|
|
833
|
+
// The reply edit entry, registered in the host's own assistant-actions strip.
|
|
834
|
+
//
|
|
835
|
+
// It has to live there rather than be injected next to the message: the host
|
|
836
|
+
// renders that strip per assistant message, hands the entry the messageId, and
|
|
837
|
+
// keeps the platform's own reveal behaviour (always for the latest turn, hover
|
|
838
|
+
// for older ones). Injecting instead put the pencil in a different place from
|
|
839
|
+
// where every other action for that row lives, which is what users noticed.
|
|
840
|
+
function ReplyActionEntry({ messageId, controller, useEditTurn, t }) {
|
|
841
|
+
const view = useEditTurn((state) => state)
|
|
842
|
+
const entry = view.repliesByMessage.get(messageId)
|
|
843
|
+
if (entry === undefined || view.hidden.has(entry.seq)) return null
|
|
844
|
+
const label = t('action.edit-reply')
|
|
845
|
+
const open = (event) => {
|
|
846
|
+
event.preventDefault()
|
|
847
|
+
event.stopPropagation()
|
|
848
|
+
controller.open({
|
|
849
|
+
seq: entry.seq,
|
|
850
|
+
mode: 'reply',
|
|
851
|
+
turn: entry.turn,
|
|
852
|
+
messageId: entry.messageId,
|
|
853
|
+
text: typeof entry.text === 'string' ? entry.text : '',
|
|
854
|
+
attachments: typeof entry.attachments === 'number' ? entry.attachments : 0,
|
|
855
|
+
})
|
|
856
|
+
}
|
|
857
|
+
return jsx('button', {
|
|
858
|
+
type: 'button',
|
|
859
|
+
className: 'dshet-action dshet-row-action',
|
|
860
|
+
'aria-label': label,
|
|
861
|
+
title: label,
|
|
862
|
+
onClick: open,
|
|
863
|
+
children: jsx(PencilIcon, null),
|
|
864
|
+
})
|
|
865
|
+
}
|
|
866
|
+
|
|
772
867
|
// --- react entries --------------------------------------------------------
|
|
773
868
|
|
|
774
869
|
function OverlayEntry({ useChat, useEditTurn, controller, t }) {
|
|
@@ -835,6 +930,25 @@ window.__ModuleLoader__.load({
|
|
|
835
930
|
'dsh-edit-turn: per-session controllers',
|
|
836
931
|
)
|
|
837
932
|
|
|
933
|
+
// A reply's edit entry lives in the official assistant-actions strip, one
|
|
934
|
+
// entry per assistant message. order 5 puts it ahead of the feedback
|
|
935
|
+
// entries (order 10), so the pencil sits with the leading icons.
|
|
936
|
+
ctx.slots.inject('conversation.chat.assistant-actions', () =>
|
|
937
|
+
ctx.slots.register(
|
|
938
|
+
{
|
|
939
|
+
name: 'conversation.chat.assistant-actions',
|
|
940
|
+
id: 'edit-turn-reply',
|
|
941
|
+
order: 5,
|
|
942
|
+
locale: NS,
|
|
943
|
+
inject: (sessionId) => ({
|
|
944
|
+
hooks: { editTurn: controllerFor(sessionId) },
|
|
945
|
+
controller: controllerFor(sessionId),
|
|
946
|
+
}),
|
|
947
|
+
},
|
|
948
|
+
ReplyActionEntry,
|
|
949
|
+
),
|
|
950
|
+
)
|
|
951
|
+
|
|
838
952
|
ctx.slots.inject('conversation.input.overlay', () =>
|
|
839
953
|
ctx.slots.register(
|
|
840
954
|
{
|
package/lib/index.js
CHANGED
|
@@ -48,7 +48,24 @@ export const PLUGIN_ID = 'dsh-edit-turn'
|
|
|
48
48
|
export const name = PLUGIN_ID
|
|
49
49
|
|
|
50
50
|
/** Keep in sync with package.json and lib/client.js. */
|
|
51
|
-
export const PLUGIN_VERSION = '0.2.
|
|
51
|
+
export const PLUGIN_VERSION = '0.2.1'
|
|
52
|
+
|
|
53
|
+
// A ring of the last few requests, answering "did the browser half even ask, and
|
|
54
|
+
// what did it get?" without needing its console. Read-only diagnostics: the
|
|
55
|
+
// browser half's own state is otherwise invisible from the host side.
|
|
56
|
+
const REQUEST_LOG_LIMIT = 40
|
|
57
|
+
const requestLog = []
|
|
58
|
+
|
|
59
|
+
/** Record one request for the diagnostic route. */
|
|
60
|
+
export function noteRequest(entry) {
|
|
61
|
+
requestLog.push({ at: Date.now(), ...entry })
|
|
62
|
+
if (requestLog.length > REQUEST_LOG_LIMIT) requestLog.shift()
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** The recorded requests, oldest first. */
|
|
66
|
+
export function recentRequests() {
|
|
67
|
+
return requestLog.slice()
|
|
68
|
+
}
|
|
52
69
|
|
|
53
70
|
const ROUTE_PREFIX = '/dsh-edit-turn'
|
|
54
71
|
const SESSION_ID_RE = /^(session-)?[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i
|
|
@@ -595,16 +612,31 @@ async function flushSession(ctx, session) {
|
|
|
595
612
|
|
|
596
613
|
async function stateOf(ctx, sessionId, config) {
|
|
597
614
|
const events = await readEvents(ctx, sessionId)
|
|
598
|
-
if (!events)
|
|
615
|
+
if (!events) {
|
|
616
|
+
noteRequest({ kind: 'state', sessionId, ok: false, code: 'session-not-found' })
|
|
617
|
+
throw new HttpError(404, 'session-not-found', 'no session log for this id')
|
|
618
|
+
}
|
|
599
619
|
const folded = foldSurface(events)
|
|
600
620
|
const ledger = rollbackLedger(events)
|
|
601
621
|
const live = findLiveSession(ctx, sessionId)
|
|
622
|
+
const turns = editableTurns(events, folded.nodes)
|
|
623
|
+
const replies = editableReplies(events, folded.nodes)
|
|
624
|
+
noteRequest({
|
|
625
|
+
kind: 'state',
|
|
626
|
+
sessionId,
|
|
627
|
+
ok: true,
|
|
628
|
+
events: events.length,
|
|
629
|
+
turns: turns.length,
|
|
630
|
+
replies: replies.length,
|
|
631
|
+
hidden: ledger.hidden.length,
|
|
632
|
+
live: Boolean(live),
|
|
633
|
+
})
|
|
602
634
|
return {
|
|
603
635
|
hidden: ledger.hidden,
|
|
604
636
|
edits: ledger.edits.length,
|
|
605
637
|
surface: folded.nodes,
|
|
606
|
-
turns
|
|
607
|
-
replies
|
|
638
|
+
turns,
|
|
639
|
+
replies,
|
|
608
640
|
live: Boolean(live),
|
|
609
641
|
busy: isBusy(events),
|
|
610
642
|
lastSeq: events.length > 0 ? events[events.length - 1].seq : -1,
|
|
@@ -752,6 +784,13 @@ async function applyEdit(ctx, sessionId, body, config) {
|
|
|
752
784
|
let appended
|
|
753
785
|
try {
|
|
754
786
|
appended = session.append(correction.type, correction.data, { surfaceOp: 'append' })
|
|
787
|
+
// Close the turn again, now that the corrected reply is the last thing in
|
|
788
|
+
// it. The host anchors a turn's tail (its duration and action strip) at the
|
|
789
|
+
// turn's LAST `turn/end`, so a correction appended after the old one lands
|
|
790
|
+
// OUTSIDE the turn and the tail renders above the corrected text. These two
|
|
791
|
+
// events put the tail back behind it, exactly as a normal turn ends.
|
|
792
|
+
session.append('step/end', { turn: appended.data.turn, step: appended.data.step })
|
|
793
|
+
session.append('turn/end', { turn: appended.data.turn, reason: { kind: 'completed' } })
|
|
755
794
|
} catch (error) {
|
|
756
795
|
return {
|
|
757
796
|
kind: 'reply',
|
|
@@ -1013,6 +1052,24 @@ export function apply(ctx, config) {
|
|
|
1013
1052
|
}),
|
|
1014
1053
|
)
|
|
1015
1054
|
|
|
1055
|
+
// Read-only diagnostics. The browser half's own state cannot be observed
|
|
1056
|
+
// from here, so the next best thing is an honest record of what its requests
|
|
1057
|
+
// asked for and what they got back.
|
|
1058
|
+
fiber.effect(() =>
|
|
1059
|
+
webServer.register({
|
|
1060
|
+
kind: 'exact',
|
|
1061
|
+
path: `${ROUTE_PREFIX}/debug`,
|
|
1062
|
+
handler: (req, res) => {
|
|
1063
|
+
if (!guard(req, res)) return
|
|
1064
|
+
if (req.method !== 'GET') {
|
|
1065
|
+
sendJson(res, 405, { ok: false, code: 'method', error: 'GET only' })
|
|
1066
|
+
return
|
|
1067
|
+
}
|
|
1068
|
+
sendJson(res, 200, { ok: true, version: PLUGIN_VERSION, requests: recentRequests() })
|
|
1069
|
+
},
|
|
1070
|
+
}),
|
|
1071
|
+
)
|
|
1072
|
+
|
|
1016
1073
|
fiber.effect(() =>
|
|
1017
1074
|
webServer.register({
|
|
1018
1075
|
kind: 'exact',
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-edit-turn",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "Edit any user turn in DeepSeek Harness and re-run it: an inline editor on the message row rolls the conversation back to just before that turn through the official surface-replace contract, then re-prompts the agent with the revised text. The append-only session log is never rewritten.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|