dsh-edit-turn 0.2.11 → 0.2.12
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 +20 -26
- package/lib/client.js +110 -10
- package/lib/index.js +8 -65
- package/package.json +1 -2
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# dsh-edit-turn
|
|
2
2
|
|
|
3
|
-
**DeepSeek Harness 的「编辑某一轮」插件 —— 点用户消息旁的编辑按钮,就地改写那一句。保存即生效:改后的内容成为模型后续读到的版本,这一条消息下面的对话原样不动。** 改写走官方 surface-replace 契约(一条替换事件同时完成"删旧 + 立新"
|
|
3
|
+
**DeepSeek Harness 的「编辑某一轮」插件 —— 点用户消息旁的编辑按钮,就地改写那一句。保存即生效:改后的内容成为模型后续读到的版本,这一条消息下面的对话原样不动。** 改写走官方 surface-replace 契约(一条替换事件同时完成"删旧 + 立新",被替换的内容从模型上下文里消失),**保存本身永不调用模型**。会话日志是 append-only 的,**原始字节一个都不改写**。
|
|
4
4
|
|
|
5
5
|
[中文](#中文) · [English](#english)
|
|
6
6
|
|
|
@@ -18,14 +18,14 @@ DSH 的会话日志是 append-only 的事件流:说错的提示词、问偏的
|
|
|
18
18
|
- 点击后在该消息下方就地展开编辑器,预填原文;
|
|
19
19
|
- 保存后:**只有这一条消息被替换**——旧措辞从模型上下文里消失,改后的内容占住它的位置(因此后续每一轮读到的都是改后的版本);
|
|
20
20
|
- **它下面的助手回复、以及之后的全部对话,原样保留**,不重跑、不清空;
|
|
21
|
-
-
|
|
21
|
+
- 装了姊妹插件 **dsh-rerun-turn** 时,编辑器里会多一个「**重跑**」:先保存这次修改,再让它用改后的提示词重新生成这一轮(后续轮次逐事件重放回来,不丢)。
|
|
22
22
|
|
|
23
23
|
### 特性
|
|
24
24
|
|
|
25
25
|
- **官方 seam,不改日志** —— 回退 = 追加一条带 `surfaceOp: { op: 'replace', startSeq, endSeq }` 的替换事件。原始事件全部留在会话文件里,只是不再进入 `deriveMessages()`。与官方 `/compact` 用的是同一套契约。
|
|
26
26
|
- **改写内容就是载体本身** —— 提示词编辑落下的是一条 `user/message` 替换事件,内容就是你改后的那句话。它既让模型读到你改后的版本,也在转录里显示成你自己的气泡;平台不会把它当成新输入去回答(这一点踩过坑:**追加**一条 user 消息会触发平台自动回答,越改越多)。
|
|
27
27
|
- **窗口最小化** —— 提示词编辑只遮蔽目标那一格(`shadowed = [target.seq]`);回复编辑才需要"到末尾"的窗口(回答变了,建立在它之上的一切都不再成立),因此助手消息(内含 tool_use)与它产生的 tool/result 永远一起走,**不可能留下悬空的调用/结果对**。
|
|
28
|
-
-
|
|
28
|
+
- **保存永不调用模型** —— 保存只写上下文,不产生任何模型调用。"改完立刻重答"是另一件事,交给姊妹插件 **dsh-rerun-turn**(它遮蔽该轮、用表面上的提示词重新生成、再把后续轮次逐事件重放回来);本插件只在它装着时提供一个「重跑」按钮转发过去。
|
|
29
29
|
- **模型回答也能编辑** —— 回答无法被"替换":官方格式禁止 `assistant/message` 携带 `sourceEventSeqs`(已在真实校验器上验证)。做法是回退该回答及其后的内容,再**追加**一条带改写文本的助手消息——模型会把改写后的内容当成自己说过的话,对话可以继续。`source.editedBy` 会如实记录这段文字由插件写入。
|
|
30
30
|
- **改过的那条还能再改** —— 它仍然显示在原来的位置(由插件渲染成气泡),悬停就有编辑按钮。
|
|
31
31
|
- **一次点击即执行** —— 保存后不再有二次确认(`confirm: true` 可恢复两步确认,确认步里会写明这次保存会丢弃什么)。编辑器本身已经是用户主动打开的动作,保存的后果在 README 与确认步里说明,输入框本身保持干净。
|
|
@@ -50,7 +50,7 @@ dsh plugin --profile web add /path/to/dsh-edit-turn
|
|
|
50
50
|
### 使用
|
|
51
51
|
|
|
52
52
|
1. 把鼠标移到你想改的那条**用户消息**上,点右侧的编辑图标(铅笔)。
|
|
53
|
-
2.
|
|
53
|
+
2. 消息下方展开编辑器,原文已预填。改完点「**保存**」——**一次点击即执行**:旧措辞从模型上下文里移除,改后的内容占住它的位置,**不叫模型**。装了 `dsh-rerun-turn` 时旁边还有一个「**重跑**」:先保存,再让那一轮用改后的内容重新生成(后续轮次不丢)。
|
|
54
54
|
3. 编辑器收起,那条消息就地变成你改后的内容;它下面的回复与之后的对话**原样不动**。
|
|
55
55
|
|
|
56
56
|
编辑器是**平台输入框那样的一个圆角盒子**:输入区在上、取消与主按钮在右下,聚焦时整圈描边高亮;**随文字长高**(空的时候不留空白),没有标题栏、没有常驻说明、没有拖拽手柄。键盘也和输入框一致:**Enter 保存、Shift+Enter 换行、Esc 取消**(确认步里 Esc 是退回上一步)。**输入法优先**:组合中的 Enter 与"候选选词的 Enter"(`isComposing` 或 `keyCode 229`,平台自己的输入框也是这两条一起挡)都不会被当成保存,组合期间也不重排输入框——此前少挡了 229 那半,拼音选词按回车会直接保存/关闭,看起来就是"打不了中文"。
|
|
@@ -69,13 +69,11 @@ dsh plugin --profile web add /path/to/dsh-edit-turn
|
|
|
69
69
|
- id: dsh-edit-turn
|
|
70
70
|
config:
|
|
71
71
|
confirm: false # 默认 false:一次点击即执行;置 true 才要求二次确认
|
|
72
|
-
rerun: false # 默认 false:保存只写上下文;置 true 则同时重跑该轮
|
|
73
72
|
```
|
|
74
73
|
|
|
75
74
|
| 字段 | 默认 | 含义 |
|
|
76
75
|
|---|---|---|
|
|
77
76
|
| `confirm` | `false` | 客户端保存后是否先要求一次确认。默认**关闭**(一次点击即执行:编辑器本身已是用户主动打开的动作;设为 `true` 时确认步里会写明这次保存会丢弃什么); |
|
|
78
|
-
| `rerun` | `false` | 保存后是否立刻重跑该轮。默认**关闭**:改后的内容进上下文即可,等你下次发消息时模型才读到它。设为 `true` 则保存即重跑(走官方 `sessionController.prompt()`)。 |
|
|
79
77
|
|
|
80
78
|
### 工作原理
|
|
81
79
|
|
|
@@ -96,11 +94,7 @@ session.append('developer/message', { turn, step, message: { role: 'developer',
|
|
|
96
94
|
|
|
97
95
|
落盘后等一次持久化检查点(`sessions.flush`),保证重启也能看到。
|
|
98
96
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
```js
|
|
102
|
-
await ctx.sessionController.prompt({ requestId, sessionId, mode: 'queue', content: [{ type: 'text', text: 新内容 }] })
|
|
103
|
-
```
|
|
97
|
+
**重跑不由本插件做。** "改完立刻重答"是姊妹插件 `dsh-rerun-turn` 的操作:它遮蔽该轮、用表面现在显示的提示词(也就是你改后的文本)重新生成,再把后续轮次逐事件重放回来。本插件只负责把编辑落地;编辑器里的「重跑」按钮仅在检测到它挂载时出现,点击后先保存、再调用它的 `POST /dsh-rerun-turn/apply`。它不在(例如桌面版没装)时,没有这个按钮。
|
|
104
98
|
|
|
105
99
|
**为什么不做文件截断?** 因为那不是官方能力。`dsh-session-persistence-jsonl` 只暴露残帧崩溃修复用的 `truncateTornTail`,正常路径下「Committed events are never rewritten」;运行中的宿主把会话放在内存 append-only 日志 + 投影缓存里,改磁盘不会让它重新读取。截断还会破坏 `sourceEventSeqs` 的 `[start,end]` 压缩表示与多帧 zstd 结构。surface-replace 是官方为这件事准备的机制。
|
|
106
100
|
|
|
@@ -109,7 +103,7 @@ await ctx.sessionController.prompt({ requestId, sessionId, mode: 'queue', conten
|
|
|
109
103
|
| 类型 | 名称 | 说明 |
|
|
110
104
|
|---|---|---|
|
|
111
105
|
| 路由 | `GET /dsh-edit-turn/state?sessionId=` | 可编辑轮次、已遮蔽行账本(`hidden` + `revisions`)、surface、忙碌状态 |
|
|
112
|
-
| 路由 | `POST /dsh-edit-turn/apply` | `{ sessionId, seq \| messageId \| turn, text }` →
|
|
106
|
+
| 路由 | `POST /dsh-edit-turn/apply` | `{ sessionId, seq \| messageId \| turn, text }` → 写入改后内容;响应含 `applied` / `shadowed` / `replacementSeq` |
|
|
113
107
|
| 工具 | `edit_turn_targets` | 只读:列出该会话当前可编辑的轮次与原文(供 agent 自查) |
|
|
114
108
|
| 前端 | `conversation.input.overlay` | 每会话控制器:编辑入口、就地编辑器、被遮蔽行的隐藏账本 |
|
|
115
109
|
|
|
@@ -170,7 +164,7 @@ ln -s /path/to/dsh-install/node_modules ./node_modules
|
|
|
170
164
|
|
|
171
165
|
- 可以编辑**你自己输入的消息**与**模型的回答**;注入的上下文行与系统提示词没有编辑入口。
|
|
172
166
|
- 编辑一条回答会把它替换为**纯文本**:该回答里的工具调用与思考过程会被移除(编辑器会提示),因为它们的结果已不再成立。
|
|
173
|
-
-
|
|
167
|
+
- 编辑一条用户消息**只替换那一条**,**保存永不调用模型**;编辑一条模型回答只替换内容,同样不问模型。想立刻重答,用 `dsh-rerun-turn` 的「重跑」。
|
|
174
168
|
- **提示词编辑只动那一条**:它下面的回复与之后的对话**原样保留**。**回复编辑**才会移除该回答及其后的内容——回答变了,建立在它之上的一切都不再成立。想保留原文形成分支,需要走 `sessionController.fork({ sessionId, atSeq })`,尚未实现。
|
|
175
169
|
- **只改写文本**。消息里含图片/文件附件时,改写后只保留文字(编辑器会提示)。
|
|
176
170
|
- **会话必须当前在 DSH 中打开**,否则返回 `409 session-not-active`。
|
|
@@ -219,6 +213,12 @@ ln -s /path/to/dsh-install/node_modules ./node_modules
|
|
|
219
213
|
|
|
220
214
|
### 更新日志
|
|
221
215
|
|
|
216
|
+
**0.2.12** —— 编辑器里的「重跑」(转发给 dsh-rerun-turn),并删除旧的 `rerun` 配置。
|
|
217
|
+
|
|
218
|
+
- **新增:「重跑」按钮(提示词编辑专用)**。装了姊妹插件 **dsh-rerun-turn** 时,取消/保存 旁多一个「重跑」:先走本插件的保存(就地替换、不花模型调用),紧接着调用它的 `POST /dsh-rerun-turn/apply { sessionId, seq: 该轮作答 }`——它按表面现在显示的提示词(你改后的文本)重新生成,并把后续轮次逐事件重放回来。探测方式与它的加载器探针一致(不带 sessionId 请求它的 `/state`:400=装着、404=没装),**没装就没有这个按钮**(例如桌面版没装时)。回答编辑不加这个按钮——回答动作条里本来就有它自己的 ↻。**它的 apply 不接受改写文本,所以顺序永远是先保存、后重跑**;该轮没有可重跑的作答、或它拒绝(`not-rerunnable`/`already-retired`/`busy`/`rerunning`…)时,按错误码提示,不影响已经落地的保存。
|
|
219
|
+
- **删除:旧的 `rerun: true` 配置及其实现**。旧路径在保存后用 `sessionController.prompt()` 在末尾追加一条同文提问——那不是"重跑这一轮",语义不干净(会在上下文里多出一条重复提问)。现在 `POST /apply` 的响应不再有 `reran` / `rerunError`,`/state` 的 `config` 不再有 `rerun`,i18n 的「保存并重跑」与文档一并移除;**保存永不调用模型**,重跑一律交给 dsh-rerun-turn。
|
|
220
|
+
- **诚实修正(流程问题)**:静态检查里「编辑器锚定到替代行」那条断言自 0.2.7 起写的还是被替换前的旧表达式(`view.revisions.get(seq)`,代码已改为 `headRevision(...)`),**一直红着**;我在那几轮里只 grep「全部通过」而漏看 `✗`,所以 0.2.7–0.2.11 的提交说明里"静态全绿"的说法**有一处不实**(单测、契约、真机冒烟这些当时确实都过,功能未受影响)。本条断言已按现表达式修正,并且从此用**退出码**判定 `npm run check`。本版:114 单测 / 80 契约 / 静态(exit 0)/ verify:live / verify:ui 全绿。
|
|
221
|
+
|
|
222
222
|
**0.2.11** —— 修「字怎么变大了」。
|
|
223
223
|
|
|
224
224
|
- **修复:改写后的气泡与编辑器不跟随平台的"内容字号"设置**。平台的气泡用 `font-size: var(--dsh-content-font-size,14px)` + `line-height: calc(22px + var(--dsh-content-font-delta,0px))`;我们的替代气泡写成 `font:inherit`(继承了行的 16px,比气泡的 15px 大),编辑器又写死 15px——读者一改内容字号,两处都对不上,看起来就是"字变大了"。现在两者都取平台同一个变量/公式(该变量定义在 body 上,编辑器所在的浮层是其子节点,可直接继承)。
|
|
@@ -336,7 +336,7 @@ This plugin adds it:
|
|
|
336
336
|
- saving **replaces that one message in place**: only the old wording leaves the model context, the revised text takes its place, and every later turn is answered against the new version;
|
|
337
337
|
- **the reply under it and the whole conversation after it stay exactly as they were** - nothing is re-run and nothing is cleared;
|
|
338
338
|
- model replies are editable too: replacing one rolls back that answer and everything built on it (a changed answer invalidates what followed) and appends your text as a fresh reply;
|
|
339
|
-
-
|
|
339
|
+
- with the sister plugin **dsh-rerun-turn** installed, the editor also offers a "**Re-run**" button: it saves your change first, then has that plugin regenerate the turn from the revised prompt (replaying what followed, so nothing after it is lost).
|
|
340
340
|
|
|
341
341
|
### Features
|
|
342
342
|
|
|
@@ -344,7 +344,7 @@ This plugin adds it:
|
|
|
344
344
|
- **The revision is the carrier.** A prompt edit lands a `user/message` replacement whose content **is** your revised text: it updates the model context and shows in the transcript in place of the old wording. The platform does not answer it as a new input (that was tried: *appending* a user message makes the platform reply, and the next save added another copy). Reply edits stay silent too: their carrier is an empty `developer/message` - the format admits only system-prompt sources on `system/message`, and an empty plugin-owned system node is exactly what used to make sessions unreadable (see below).
|
|
345
345
|
- **The smallest window that is correct.** A prompt edit shadows exactly one node (`shadowed = [target.seq]`); a reply edit shadows the reply through the end of the surface - a changed answer invalidates everything built on it - which also keeps an assistant message (carrying its own tool_use blocks) and the tool/result it produced together. A dangling call/result pair is impossible.
|
|
346
346
|
- **A rewritten message stays editable.** It still shows where the original stood, and hovering it offers the pencil again.
|
|
347
|
-
- **
|
|
347
|
+
- **Saving never calls the model.** It only writes the context. Regenerating a turn after the edit is a different operation, owned by the sister plugin **dsh-rerun-turn** (it shadows the turn, regenerates from the prompt the surface now shows, and replays the turns that followed); this plugin only forwards to it, through the "Re-run" button, when it is mounted.
|
|
348
348
|
- **The model's replies are editable too.** A reply cannot be swapped in place: the format refuses `sourceEventSeqs` on an `assistant/message` (verified against the real validator). The answer is rolled back together with everything after it, and the corrected text is **appended** as a fresh reply - the model goes on treating it as its own. `source.editedBy` records honestly that the plugin wrote those words.
|
|
349
349
|
- **One click applies** - no second confirmation by default. The editor is already an explicit action the user opened; with `confirm: true` the confirmation step states what this save will discard, while the input box itself stays clean.
|
|
350
350
|
- **Bilingual UI** that follows the current DSH locale.
|
|
@@ -368,7 +368,7 @@ Restart the DSH process that serves that profile to pick it up.
|
|
|
368
368
|
### Usage
|
|
369
369
|
|
|
370
370
|
1. Hover the **user message** you want to change and click the pencil action.
|
|
371
|
-
2. An editor opens below it with the original text pre-filled. Click "**Save**" - **one click applies**: the old wording leaves the model context and the revised text takes its place. **No model call.**
|
|
371
|
+
2. An editor opens below it with the original text pre-filled. Click "**Save**" - **one click applies**: the old wording leaves the model context and the revised text takes its place. **No model call.** With `dsh-rerun-turn` installed there is also a "**Re-run**" button: save first, then let that turn regenerate from your revised wording (nothing after it is lost).
|
|
372
372
|
3. The editor closes and the message shows your text in place; the reply under it and everything after it stay exactly where they were.
|
|
373
373
|
|
|
374
374
|
The editor is **shaped like the platform's own input box**: one rounded
|
|
@@ -397,13 +397,11 @@ longer valid once you have changed what the model said.
|
|
|
397
397
|
- id: dsh-edit-turn
|
|
398
398
|
config:
|
|
399
399
|
confirm: false # default: apply in one click; true restores the two-step flow
|
|
400
|
-
rerun: false # default: saving only writes the context; true re-runs the turn
|
|
401
400
|
```
|
|
402
401
|
|
|
403
402
|
| Field | Default | Meaning |
|
|
404
403
|
|---|---|---|
|
|
405
404
|
| `confirm` | `false` | Whether the client asks for a second confirmation before applying. Off by default - one click applies, because the editor is already an explicit action the user opened; with `true` the confirmation step states what this save will discard. |
|
|
406
|
-
| `rerun` | `false` | Whether the edited turn is re-run right after saving. Off by default: the revision enters the context and the model reads it on your next message. `true` re-runs through the official `sessionController.prompt()`. |
|
|
407
405
|
|
|
408
406
|
### How it works
|
|
409
407
|
|
|
@@ -434,11 +432,7 @@ session.append('developer/message', { turn, step, message: { role: 'developer',
|
|
|
434
432
|
The append then waits for the official durability checkpoint (`sessions.flush`)
|
|
435
433
|
so a reload or a DSH restart still sees the rollback.
|
|
436
434
|
|
|
437
|
-
**
|
|
438
|
-
|
|
439
|
-
```js
|
|
440
|
-
await ctx.sessionController.prompt({ requestId, sessionId, mode: 'queue', content: [{ type: 'text', text }] })
|
|
441
|
-
```
|
|
435
|
+
**The re-run is not this plugin's operation.** Regenerating the turn is the sister plugin `dsh-rerun-turn`'s job: it shadows the turn, regenerates from the prompt the surface now shows (your revised text), and replays the turns that followed. This plugin only lands the edit; its editor offers a "Re-run" button when that plugin is detected, and the button chains save-then-`POST /dsh-rerun-turn/apply`. Without it (the desktop profile, say) there is no such button.
|
|
442
436
|
|
|
443
437
|
**Why not truncate the log?** Because that is not an official capability.
|
|
444
438
|
`dsh-session-persistence-jsonl` exposes only `truncateTornTail`, for crash
|
|
@@ -454,7 +448,7 @@ for exactly this.
|
|
|
454
448
|
| Kind | Name | Purpose |
|
|
455
449
|
|---|---|---|
|
|
456
450
|
| Route | `GET /dsh-edit-turn/state?sessionId=` | Editable turns, the hidden-row ledger (`hidden` + `revisions`), the surface, busy state |
|
|
457
|
-
| Route | `POST /dsh-edit-turn/apply` | `{ sessionId, seq \| messageId \| turn, text }` → write the revised text
|
|
451
|
+
| Route | `POST /dsh-edit-turn/apply` | `{ sessionId, seq \| messageId \| turn, text }` → write the revised text; the response carries `applied` / `shadowed` / `replacementSeq` |
|
|
458
452
|
| Tool | `edit_turn_targets` | Read-only: list the session's editable turns and their text |
|
|
459
453
|
| Client | `conversation.input.overlay` | Per-session controller: edit entry, in-place editor, hidden-row ledger |
|
|
460
454
|
|
|
@@ -703,8 +697,8 @@ ln -s /path/to/dsh-install/node_modules ./node_modules
|
|
|
703
697
|
|
|
704
698
|
- **Human prompts and model replies** are both editable; injected context rows and the system prompt offer no edit entry.
|
|
705
699
|
- **Editing a reply replaces it with plain text**: the tool calls and reasoning inside it are removed (the editor warns first), because their results are no longer valid.
|
|
706
|
-
- **Editing a user message replaces that one message and leaves the rest of the conversation untouched
|
|
707
|
-
- **A prompt edit touches only that message.** The reply under it and the whole conversation after it are kept
|
|
700
|
+
- **Editing a user message replaces that one message and leaves the rest of the conversation untouched; saving never calls the model.** For an immediate answer, use `dsh-rerun-turn`'s "Re-run"; editing a model reply only replaces the text, too.
|
|
701
|
+
- **A prompt edit touches only that message.** The reply under it and the whole conversation after it are kept; saving never calls the model. A **reply edit** removes that answer and everything built on it, because a changed answer invalidates what followed. Keeping the original as a branch needs `sessionController.fork({ sessionId, atSeq })`, which is not implemented.
|
|
708
702
|
- **Text only.** A message carrying image or file attachments keeps its text and drops them (the editor warns first).
|
|
709
703
|
- **The session must be open in DSH**, otherwise the route answers `409 session-not-active`.
|
|
710
704
|
- **Running work is refused**: an unclosed turn or an in-flight compaction answers `409 busy`.
|
package/lib/client.js
CHANGED
|
@@ -28,9 +28,13 @@ window.__ModuleLoader__.load({
|
|
|
28
28
|
|
|
29
29
|
const NS = 'dsh-edit-turn'
|
|
30
30
|
const ROUTE_PREFIX = '/dsh-edit-turn'
|
|
31
|
+
// The sibling that owns re-running: it shadows a turn and regenerates it
|
|
32
|
+
// from the prompt the surface now shows, then replays what followed. The
|
|
33
|
+
// editor's re-run button is only offered when it is mounted.
|
|
34
|
+
const RERUN_PREFIX = '/dsh-rerun-turn'
|
|
31
35
|
|
|
32
36
|
/** Keep in sync with package.json and lib/index.js. */
|
|
33
|
-
const PLUGIN_VERSION = '0.2.
|
|
37
|
+
const PLUGIN_VERSION = '0.2.12'
|
|
34
38
|
|
|
35
39
|
// --- copy -----------------------------------------------------------------
|
|
36
40
|
|
|
@@ -42,7 +46,7 @@ window.__ModuleLoader__.load({
|
|
|
42
46
|
'editor.placeholder': '改成你希望自己当时说的内容。保存后这条消息之后的轮次会从模型上下文中移除,改后的内容成为新的起点。',
|
|
43
47
|
'editor.placeholder-reply': '改成你希望模型说过的内容,保存后它就成为这条回答。',
|
|
44
48
|
'editor.save': '保存',
|
|
45
|
-
'editor.
|
|
49
|
+
'editor.rerun': '重跑',
|
|
46
50
|
'editor.save-reply': '保存',
|
|
47
51
|
'editor.review': '确认执行',
|
|
48
52
|
'editor.cancel': '取消',
|
|
@@ -63,6 +67,11 @@ window.__ModuleLoader__.load({
|
|
|
63
67
|
'error.prompt': '已回退,但改后的内容没能写进上下文。请重试。',
|
|
64
68
|
'error.reply': '已回退,但替换没有落地。请重试。',
|
|
65
69
|
'error.empty': '改写后的内容不能为空。',
|
|
70
|
+
'error.rerun-nothing': '已保存,但这一轮没有可重跑的作答。',
|
|
71
|
+
'error.rerun-unavailable': '已保存,但重跑没有开始,请稍后重试。',
|
|
72
|
+
'error.not-rerunnable': '这一轮没有可重跑的作答。',
|
|
73
|
+
'error.already-retired': '这一轮的内容已经不在当前上下文里了。',
|
|
74
|
+
'error.rerunning': '已经有一次重跑在进行中,请等它结束。',
|
|
66
75
|
'error.internal': '服务器内部错误,请稍后重试。',
|
|
67
76
|
}
|
|
68
77
|
|
|
@@ -74,7 +83,7 @@ window.__ModuleLoader__.load({
|
|
|
74
83
|
'editor.placeholder': 'Saving rolls the conversation back to just before this message and this turn runs again.',
|
|
75
84
|
'editor.placeholder-reply': 'Write what you want the model to have said; saving makes it this reply.',
|
|
76
85
|
'editor.save': 'Save',
|
|
77
|
-
'editor.
|
|
86
|
+
'editor.rerun': 'Re-run',
|
|
78
87
|
'editor.save-reply': 'Save',
|
|
79
88
|
'editor.review': 'Confirm',
|
|
80
89
|
'editor.cancel': 'Cancel',
|
|
@@ -95,6 +104,11 @@ window.__ModuleLoader__.load({
|
|
|
95
104
|
'error.prompt': 'Rolled back, but the re-run did not start. Send the revised text manually.',
|
|
96
105
|
'error.reply': 'Rolled back, but the replacement did not land. Try again.',
|
|
97
106
|
'error.empty': 'The revised message cannot be empty.',
|
|
107
|
+
'error.rerun-nothing': 'Saved, but this turn has no reply that can be re-run.',
|
|
108
|
+
'error.rerun-unavailable': 'Saved, but the re-run did not start; try again.',
|
|
109
|
+
'error.not-rerunnable': 'This turn has no reply that can be re-run.',
|
|
110
|
+
'error.already-retired': 'This turn is no longer part of the current context.',
|
|
111
|
+
'error.rerunning': 'A re-run is already in flight; wait for it to finish.',
|
|
98
112
|
'error.internal': 'The host hit an internal error; try again shortly.',
|
|
99
113
|
}
|
|
100
114
|
|
|
@@ -270,6 +284,8 @@ window.__ModuleLoader__.load({
|
|
|
270
284
|
this.editorFocus = false
|
|
271
285
|
this.editorDraft = ''
|
|
272
286
|
this.clearTimer = null
|
|
287
|
+
this.pendingRerun = false
|
|
288
|
+
this.siblingRerun = 'unknown'
|
|
273
289
|
this.confirming = false
|
|
274
290
|
this.noticeTimer = null
|
|
275
291
|
this.view = Object.freeze({
|
|
@@ -290,7 +306,9 @@ window.__ModuleLoader__.load({
|
|
|
290
306
|
// Matches the host's default: one click applies. Only an explicit
|
|
291
307
|
// `confirm: true` in the profile turns the second step back on.
|
|
292
308
|
confirmStep: false,
|
|
293
|
-
rerun
|
|
309
|
+
// Whether the sibling that owns re-running (dsh-rerun-turn) is
|
|
310
|
+
// mounted; probed when an editor opens, updated only on change.
|
|
311
|
+
siblingRerun: 'unknown',
|
|
294
312
|
revision: 0,
|
|
295
313
|
})
|
|
296
314
|
}
|
|
@@ -369,9 +387,6 @@ window.__ModuleLoader__.load({
|
|
|
369
387
|
loaded: true,
|
|
370
388
|
loadError: false,
|
|
371
389
|
confirmStep: !(data.config && data.config.confirm === false),
|
|
372
|
-
// Off by default: saving writes the revision into the context and waits
|
|
373
|
-
// for the next message rather than spending a model call.
|
|
374
|
-
rerun: Boolean(data.config && data.config.rerun === true),
|
|
375
390
|
})
|
|
376
391
|
})
|
|
377
392
|
.catch(() => {
|
|
@@ -409,10 +424,29 @@ window.__ModuleLoader__.load({
|
|
|
409
424
|
}
|
|
410
425
|
}
|
|
411
426
|
this.publish({ editing: target, draft: this.editorDraft, confirming: false, failure: null })
|
|
427
|
+
this.probeSiblingRerun()
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
// The sibling is probed the way our own loader is probed: its state route
|
|
431
|
+
// answers 400 without a sessionId when it is mounted, 404 when the route
|
|
432
|
+
// does not exist. A change is published - the editor's marker carries it,
|
|
433
|
+
// so the button appears (or never does).
|
|
434
|
+
async probeSiblingRerun() {
|
|
435
|
+
let status = 0
|
|
436
|
+
try {
|
|
437
|
+
const response = await fetch(`${RERUN_PREFIX}/state`, { headers: { accept: 'application/json' } })
|
|
438
|
+
status = response.status
|
|
439
|
+
} catch {
|
|
440
|
+
status = 0
|
|
441
|
+
}
|
|
442
|
+
const next = status === 400 || status === 405 ? 'present' : 'absent'
|
|
443
|
+
this.siblingRerun = next
|
|
444
|
+
if (this.view.siblingRerun !== next) this.publish({ siblingRerun: next })
|
|
412
445
|
}
|
|
413
446
|
|
|
414
447
|
close() {
|
|
415
448
|
if (this.view.pending) return
|
|
449
|
+
this.pendingRerun = false
|
|
416
450
|
this.editorElement = null
|
|
417
451
|
this.editorBox = null
|
|
418
452
|
this.editorRow = null
|
|
@@ -429,6 +463,17 @@ window.__ModuleLoader__.load({
|
|
|
429
463
|
}
|
|
430
464
|
|
|
431
465
|
review() {
|
|
466
|
+
this.pendingRerun = false
|
|
467
|
+
this.runReview()
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
/** The same save, plus the intent to re-run the turn afterwards. */
|
|
471
|
+
reviewRerun() {
|
|
472
|
+
this.pendingRerun = true
|
|
473
|
+
this.runReview()
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
runReview() {
|
|
432
477
|
if (this.editorDraft.trim() === '') {
|
|
433
478
|
this.publish({ failure: 'empty' })
|
|
434
479
|
return
|
|
@@ -450,6 +495,8 @@ window.__ModuleLoader__.load({
|
|
|
450
495
|
async confirm() {
|
|
451
496
|
const target = this.editor
|
|
452
497
|
if (target === null || this.view.pending) return
|
|
498
|
+
const rerunIntent = this.pendingRerun
|
|
499
|
+
this.pendingRerun = false
|
|
453
500
|
const text = this.editorDraft
|
|
454
501
|
if (text.trim() === '') {
|
|
455
502
|
this.publish({ failure: 'empty' })
|
|
@@ -526,6 +573,11 @@ window.__ModuleLoader__.load({
|
|
|
526
573
|
// failed second half has to be reported somewhere that survives that.
|
|
527
574
|
if (data.kind === 'reply' && data.applied === false) this.notify('reply')
|
|
528
575
|
if (data.kind !== 'reply' && data.applied === false) this.notify('prompt')
|
|
576
|
+
// Save first, re-run second - always: the sibling re-runs the turn
|
|
577
|
+
// from the prompt the surface NOW shows, so the revised wording has
|
|
578
|
+
// to be on the surface before it is asked to regenerate. Not
|
|
579
|
+
// awaited: its own client takes the screen from here.
|
|
580
|
+
if (rerunIntent) this.startSiblingRerun(target.turn)
|
|
529
581
|
// The view above is complete enough to render, but the host is the
|
|
530
582
|
// authority on what the session now looks like: only its answer knows
|
|
531
583
|
// how many rows the platform decided to draw, and what the appended
|
|
@@ -538,6 +590,37 @@ window.__ModuleLoader__.load({
|
|
|
538
590
|
}
|
|
539
591
|
}
|
|
540
592
|
|
|
593
|
+
// Hand the re-run to the sibling: find the turn's reply through its own
|
|
594
|
+
// state (the freshest view of what it can rerun) and ask it to start.
|
|
595
|
+
async startSiblingRerun(turn) {
|
|
596
|
+
try {
|
|
597
|
+
const response = await fetch(
|
|
598
|
+
`${RERUN_PREFIX}/state?sessionId=${encodeURIComponent(this.sessionId)}`,
|
|
599
|
+
{ headers: { accept: 'application/json' } },
|
|
600
|
+
)
|
|
601
|
+
const state = response.ok ? await response.json().catch(() => ({})) : {}
|
|
602
|
+
const replies = Array.isArray(state.replies)
|
|
603
|
+
? state.replies.filter((entry) => entry && entry.turn === turn && typeof entry.seq === 'number')
|
|
604
|
+
: []
|
|
605
|
+
if (replies.length === 0) {
|
|
606
|
+
this.notify('rerun-nothing')
|
|
607
|
+
return
|
|
608
|
+
}
|
|
609
|
+
const head = replies.reduce((best, entry) => (entry.seq > best.seq ? entry : best))
|
|
610
|
+
const started = await fetch(`${RERUN_PREFIX}/apply`, {
|
|
611
|
+
method: 'POST',
|
|
612
|
+
headers: { 'content-type': 'application/json' },
|
|
613
|
+
body: JSON.stringify({ sessionId: this.sessionId, seq: head.seq }),
|
|
614
|
+
})
|
|
615
|
+
const result = await started.json().catch(() => ({}))
|
|
616
|
+
if (!started.ok || !result.ok) {
|
|
617
|
+
this.notify(result && result.code ? String(result.code) : 'rerun-unavailable')
|
|
618
|
+
}
|
|
619
|
+
} catch {
|
|
620
|
+
this.notify('rerun-unavailable')
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
|
|
541
624
|
notify(code) {
|
|
542
625
|
if (this.noticeTimer !== null) window.clearTimeout(this.noticeTimer)
|
|
543
626
|
this.publish({ notice: code })
|
|
@@ -1101,13 +1184,16 @@ window.__ModuleLoader__.load({
|
|
|
1101
1184
|
view.confirming ? 1 : 0,
|
|
1102
1185
|
view.pending ? 1 : 0,
|
|
1103
1186
|
view.failure === null ? '' : view.failure,
|
|
1187
|
+
// The re-run button appears when the sibling probe lands; without it in
|
|
1188
|
+
// the marker the editor would never be rebuilt to show the button.
|
|
1189
|
+
view.siblingRerun === 'present' ? 'r' : 'n',
|
|
1104
1190
|
].join('|')
|
|
1105
1191
|
}
|
|
1106
1192
|
|
|
1107
1193
|
// Every string the editor shows depends on what is being edited: a prompt is
|
|
1108
1194
|
// re-run, a reply is replaced. Keeping them in one place keeps the two modes
|
|
1109
1195
|
// from drifting apart in wording.
|
|
1110
|
-
function editorTexts(t, target
|
|
1196
|
+
function editorTexts(t, target) {
|
|
1111
1197
|
const reply = target.mode === 'reply'
|
|
1112
1198
|
return {
|
|
1113
1199
|
title: t(reply ? 'editor.title-reply' : 'editor.title'),
|
|
@@ -1115,13 +1201,13 @@ window.__ModuleLoader__.load({
|
|
|
1115
1201
|
warn: t(reply ? 'editor.warn-reply' : 'editor.warn.attachments'),
|
|
1116
1202
|
// Saving writes the revision into the context; only the opt-in config asks
|
|
1117
1203
|
// the model again, and the button says which one will happen.
|
|
1118
|
-
save: t(reply ? 'editor.save-reply' :
|
|
1204
|
+
save: t(reply ? 'editor.save-reply' : 'editor.save'),
|
|
1119
1205
|
}
|
|
1120
1206
|
}
|
|
1121
1207
|
|
|
1122
1208
|
function renderEditor(row, controller, view, t) {
|
|
1123
1209
|
const target = view.editing
|
|
1124
|
-
const texts = editorTexts(t, target
|
|
1210
|
+
const texts = editorTexts(t, target)
|
|
1125
1211
|
const marker = editorMarker(target, view)
|
|
1126
1212
|
const existing = controller.editorBox !== null && controller.editorBox.isConnected === true
|
|
1127
1213
|
? controller.editorBox
|
|
@@ -1243,6 +1329,20 @@ window.__ModuleLoader__.load({
|
|
|
1243
1329
|
})
|
|
1244
1330
|
|
|
1245
1331
|
footer.appendChild(cancel)
|
|
1332
|
+
// The re-run button belongs to the sibling (dsh-rerun-turn): it appears
|
|
1333
|
+
// only when that plugin is mounted, only for a prompt edit, and only
|
|
1334
|
+
// outside the confirmation step - the intent is carried through it. Save
|
|
1335
|
+
// first, re-run second, always. A reply edit has no re-run here: the
|
|
1336
|
+
// action strip of the reply carries the sibling's own button.
|
|
1337
|
+
if (target.mode === 'prompt' && view.siblingRerun === 'present' && view.confirming !== true) {
|
|
1338
|
+
const rerun = document.createElement('button')
|
|
1339
|
+
rerun.type = 'button'
|
|
1340
|
+
rerun.className = 'dshet-btn dshet-rerun'
|
|
1341
|
+
rerun.textContent = t('editor.rerun')
|
|
1342
|
+
rerun.disabled = view.pending
|
|
1343
|
+
pressable(rerun, () => controller.reviewRerun())
|
|
1344
|
+
footer.appendChild(rerun)
|
|
1345
|
+
}
|
|
1246
1346
|
footer.appendChild(submit)
|
|
1247
1347
|
box.appendChild(footer)
|
|
1248
1348
|
|
package/lib/index.js
CHANGED
|
@@ -18,11 +18,8 @@
|
|
|
18
18
|
// tool/result it produced are always shadowed together: no call/result
|
|
19
19
|
// pair can be left dangling.
|
|
20
20
|
//
|
|
21
|
-
// 2. RE-RUN - `ctx.sessionController.prompt(...)`, the one official prompt
|
|
22
|
-
// admission path. It resumes the Session when cold and starts exactly one
|
|
23
|
-
// new turn from the revised text.
|
|
24
21
|
//
|
|
25
|
-
// The silent replacement carrier is an EMPTY `developer/message` (0.2.
|
|
22
|
+
// The silent replacement carrier is an EMPTY `developer/message` (0.2.12;
|
|
26
23
|
// before that an empty `system/message`, which no reader accepts - see the
|
|
27
24
|
// README). Empty content projects to no model message, so the rollback adds
|
|
28
25
|
// nothing the model can see and the derived context after an edit is exactly
|
|
@@ -60,7 +57,7 @@ export const PLUGIN_ID = 'dsh-edit-turn'
|
|
|
60
57
|
export const name = PLUGIN_ID
|
|
61
58
|
|
|
62
59
|
/** Keep in sync with package.json and lib/client.js. */
|
|
63
|
-
export const PLUGIN_VERSION = '0.2.
|
|
60
|
+
export const PLUGIN_VERSION = '0.2.12'
|
|
64
61
|
|
|
65
62
|
// A ring of the last few requests, answering "did the browser half even ask, and
|
|
66
63
|
// what did it get?" without needing its console. Read-only diagnostics: the
|
|
@@ -82,7 +79,6 @@ export function recentRequests() {
|
|
|
82
79
|
const ROUTE_PREFIX = '/dsh-edit-turn'
|
|
83
80
|
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
|
|
84
81
|
const MAX_TEXT = 200_000
|
|
85
|
-
const PROMPT_DEADLINE_MS = 30_000
|
|
86
82
|
const FLUSH_DEADLINE_MS = 5_000
|
|
87
83
|
|
|
88
84
|
/**
|
|
@@ -113,12 +109,6 @@ export const Config =
|
|
|
113
109
|
* step.
|
|
114
110
|
*/
|
|
115
111
|
confirm: z.boolean().default(false),
|
|
116
|
-
/**
|
|
117
|
-
* Whether saving a revised prompt also runs the turn again. Off by default:
|
|
118
|
-
* the revision becomes the standing wording and waits for the next message,
|
|
119
|
-
* so saving costs no model call. Set it to `true` to re-run on save.
|
|
120
|
-
*/
|
|
121
|
-
rerun: z.boolean().default(false),
|
|
122
112
|
})
|
|
123
113
|
|
|
124
114
|
// ---------------------------------------------------------------------------
|
|
@@ -700,7 +690,7 @@ async function stateOf(ctx, sessionId, config) {
|
|
|
700
690
|
live: Boolean(live),
|
|
701
691
|
busy: isBusy(events),
|
|
702
692
|
lastSeq: events.length > 0 ? events[events.length - 1].seq : -1,
|
|
703
|
-
config: { confirm: config.confirm
|
|
693
|
+
config: { confirm: config.confirm },
|
|
704
694
|
version: PLUGIN_VERSION,
|
|
705
695
|
}
|
|
706
696
|
}
|
|
@@ -907,60 +897,14 @@ async function applyEdit(ctx, sessionId, body, config) {
|
|
|
907
897
|
|
|
908
898
|
// The rollback above already did the whole job: the carrier that replaced the
|
|
909
899
|
// window *is* the revised prompt, so the model reads it as the user's wording
|
|
910
|
-
// from here on
|
|
911
|
-
//
|
|
912
|
-
//
|
|
913
|
-
//
|
|
914
|
-
if (config.rerun !== true) {
|
|
915
|
-
return {
|
|
916
|
-
kind: 'prompt',
|
|
917
|
-
replacementSeq: replacement.seq,
|
|
918
|
-
applied: true,
|
|
919
|
-
reran: false,
|
|
920
|
-
shadowed: plan.shadowed,
|
|
921
|
-
...flush,
|
|
922
|
-
}
|
|
923
|
-
}
|
|
924
|
-
|
|
925
|
-
const controller = ctx.get('sessionController')
|
|
926
|
-
if (!controller || typeof controller.prompt !== 'function') {
|
|
927
|
-
return {
|
|
928
|
-
kind: 'prompt',
|
|
929
|
-
replacementSeq: replacement.seq,
|
|
930
|
-
applied: true,
|
|
931
|
-
reran: false,
|
|
932
|
-
rerunError: 'sessionController.prompt unavailable',
|
|
933
|
-
shadowed: plan.shadowed,
|
|
934
|
-
...flush,
|
|
935
|
-
}
|
|
936
|
-
}
|
|
937
|
-
try {
|
|
938
|
-
await controller.prompt(
|
|
939
|
-
{
|
|
940
|
-
requestId: randomUUID(),
|
|
941
|
-
sessionId,
|
|
942
|
-
mode: 'queue',
|
|
943
|
-
content: [{ type: 'text', text }],
|
|
944
|
-
},
|
|
945
|
-
AbortSignal.timeout(PROMPT_DEADLINE_MS),
|
|
946
|
-
)
|
|
947
|
-
} catch (error) {
|
|
948
|
-
return {
|
|
949
|
-
kind: 'prompt',
|
|
950
|
-
replacementSeq: replacement.seq,
|
|
951
|
-
applied: true,
|
|
952
|
-
reran: false,
|
|
953
|
-
rerunError: String((error && error.message) || error),
|
|
954
|
-
shadowed: plan.shadowed,
|
|
955
|
-
...flush,
|
|
956
|
-
}
|
|
957
|
-
}
|
|
958
|
-
|
|
900
|
+
// from here on. Saving therefore costs no model call, always: regenerating
|
|
901
|
+
// the turn is a different operation with a different owner - the sibling
|
|
902
|
+
// plugin dsh-rerun-turn shadows the turn and regenerates it from the prompt
|
|
903
|
+
// the surface now shows, then replays what followed. Nothing here prompts.
|
|
959
904
|
return {
|
|
960
905
|
kind: 'prompt',
|
|
961
906
|
replacementSeq: replacement.seq,
|
|
962
907
|
applied: true,
|
|
963
|
-
reran: true,
|
|
964
908
|
shadowed: plan.shadowed,
|
|
965
909
|
...flush,
|
|
966
910
|
}
|
|
@@ -1027,7 +971,7 @@ function registerTool(ctx) {
|
|
|
1027
971
|
lines.push(`- reply (seq ${reply.seq}, ${reply.attachments} non-text part(s)): ${preview}`)
|
|
1028
972
|
}
|
|
1029
973
|
if (replies.length > shownReplies.length) lines.push(`(${replies.length - shownReplies.length} earlier reply/replies omitted)`)
|
|
1030
|
-
lines.push('editing a turn
|
|
974
|
+
lines.push('editing a turn replaces its wording in place (no model call); editing a reply replaces its text and discards what follows. Re-running a turn is dsh-rerun-turn\'s button.')
|
|
1031
975
|
return lines.join('\n')
|
|
1032
976
|
},
|
|
1033
977
|
}),
|
|
@@ -1140,7 +1084,6 @@ export function apply(ctx, config) {
|
|
|
1140
1084
|
: DEFAULT_MARKER,
|
|
1141
1085
|
carrier: config && config.carrier === 'user/message' ? 'user/message' : 'system/message',
|
|
1142
1086
|
confirm: config && typeof config.confirm === 'boolean' ? config.confirm : false,
|
|
1143
|
-
rerun: config && config.rerun === true,
|
|
1144
1087
|
}
|
|
1145
1088
|
|
|
1146
1089
|
const registerRoutes = (webServer, fiber) => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-edit-turn",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.12",
|
|
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",
|
|
@@ -21,7 +21,6 @@
|
|
|
21
21
|
"edit",
|
|
22
22
|
"message",
|
|
23
23
|
"turn",
|
|
24
|
-
"rerun",
|
|
25
24
|
"surface-replace",
|
|
26
25
|
"rollback"
|
|
27
26
|
],
|