Skip to content
dsh.fish
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

  • This source has no pinned commit, so a later push upstream changes what installs. Prefer pinning a commit.
Source