Bundle
dsh-fork-edit
Turn-level conversation editing and retry via clean session forks: forks at the closed-turn boundary before the target turn, then replays edited text (or the original input) as a normal user turn — zero custom events, cold-load safe
- Source
- homily707
- License
- MIT
- Updated
- Updated 4 hours ago
Readme
# dsh-fork-edit
[dsh](https://github.com/deepseek-ai) (DeepSeek Harness) web 客户端的回合级对话编辑 / 重试插件。
在任意一条已落定的历史用户消息上悬停,即可看到 ✎ 编辑 / ↻ 重试图标:
- **编辑** —— 修改那条消息的输入(支持直接粘贴图片),从该回合之前分叉出一个新会话并自动执行;
- **重试** —— 原输入不变,从该回合之前分叉重放。
原会话原样保留,随时可以切回。
## 为什么用 fork 实现
常见的做法是给消息编辑写一套自定义事件(版本树 + undo/redo),但自定义事件写入 session log 后,历史会话可能因事件白名单拒读而无法冷加载。
dsh-fork-edit 反其道而行:**不写入任何自定义事件**。编辑/重试 = 在目标回合之前的闭合边界处 fork 出新会话,编辑后的文本作为新会话里的普通用户回合执行。谱系关系只靠 `parentSession` header meta 记录。
结果:
- 日志里只有核心已知事件,历史会话**永远可读**;
- 编辑语义天然简单 —— 分支就是一个普通会话,没有任何待修复的历史数据;
- 核心事务在源 Agent 的 `runMaintenance()` 缝隙内执行,失败时逐项回滚,不产生半成品会话。
```
inverses = []
child = agents.create({ seed: 目标回合之前的闭合边界前缀, meta: { parentSession, seedLength, cwd, agentPreset } })
inverses.push(() => child.dispose())
workspace? → attachSession(childId)
await sessions.flush(child.session) # 耐久性屏障
child.agent.followup(编辑后文本 || 原回合用户输入) # 新会话自动执行
失败 → recoverOperation(inverses.reverse()) # 无半成品残留
```
## 功能
### 编辑 / 重试
- 悬停任意已落定的用户消息行,官方操作按钮旁出现 ✎ / ↻ 图标;对话进行中按钮也会在数秒内自动出现,无需刷新或切换会话。
- 编辑弹窗预填原输入;**输入框内可直接粘贴图片**(持久化后作为 image block 并入新消息),纯图片编辑亦可提交。
- **Enter 直接提交,Shift+Enter 换行**;输入法组合态不误触发。
- 弹窗内带**会话 timeline**:纵排列出所有已闭合的用户回合(序号 + 单行预览),点击即在弹窗内跳转切换编辑目标。
- 新分支自动以首条用户输入命名,各分支一眼可辨;新会话自动打开。
### 侧边栏分支分组
官方侧边栏是平铺展示的,fork 出的后代挤在顶部难以辨认。本插件以 `sessions.list` 谱系为权威,把根会话的全部后代分组渲染在根行下方:
- 单一层级,视觉缩进 + 树状连接线(`└` 形),一组连线自成一体;
- 行识别基于 React fiber 的会话 id,标题只作降级兜底 —— 不受 fork 子会话暂无标题、标题重名的影响;
- 根行(或根不可见时的组头行)带折叠开关,折叠状态存 localStorage,随会话切换智能展开;
- 官方只渲染最近 N 个会话时,后代自动锚定到最近可见的祖先,整组不散架。
### 级联归档
官方的「归档会话」只归档单个会话。本插件在 client 侧对服务方法做级联包装:归档任一会话时,自动收集并归档其整棵后代子树(部分失败不静默)。
注意:**归档目前没有官方 unarchive 入口,不可逆**。
## HTTP API
Host 侧通过 `ctx.webServer.register` 暴露路由,Client 以同源 `fetch` 调用:
| 请求 | 作用 |
|---|---|
| `GET /fork-edit?sessionId=…` | 已落定回合列表 |
| `POST /fork-edit { kind: "prefill", sessionId, turn? }` | 目标回合原文本 |
| `POST /fork-edit { sessionId, turn?, mode, text? }` | 执行分叉 |
`mode` 取值:`edit-user`(编辑用户输入)/ `retry-turn`(重试回合)。出错返回结构化错误(400 参数 / 409 业务)。
## 安装
`~/.dsh/profiles/web/package.json`:
```jsonc
{
"dependencies": {
"dsh-fork-edit": "link:/path/to/dsh-fork-edit"
}
// 并在 bundles 数组加入 "dsh-fork-edit"
}
```
> 若同时装有同类消息编辑插件(如 dsh-message-edit),请移除 —— 相同注入点会出双按钮。
## 工程结构
```
dsh-fork-edit/
├── package.json # dsh.bundle.patch / dsh.client 注入清单
├── cordis.patch.yml # 插件注册入口
├── index.mjs # host 侧:fork 事务 + HTTP 路由
├── client.js # client 侧:按钮注入 + 编辑弹窗 + 侧边栏分组
└── README.md
```
## License
[MIT](./LICENSE)
Install
dsh plugin --profile web add github:homily707/dsh-fork-edit
Profile: web
With the hub plugin installed, ask your agent to install it by name — it resolves the same plan shown here.
dsh plugin --profile web add github:stvlynn/dsh.fish#path:packages/dsh-plugin-hub
install dsh-fork-edit from the hub
- This source has no pinned commit, so a later push upstream changes what installs. Prefer pinning a commit.