dsh-turn-undo 0.0.1 → 0.0.2
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 +3 -3
- package/client.js +14 -3
- package/index.js +40 -28
- package/package.json +6 -3
package/README.md
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
## 功能
|
|
6
6
|
|
|
7
7
|
- ✅ 每条用户消息的操作栏注入一个“撤销”按钮(↺)。
|
|
8
|
-
- ✅
|
|
8
|
+
- ✅ 弹窗显示撤销该消息后会影响的全部文件——包括该消息之后所有轮次的改动(不只是消息本轮的),并给出警告。
|
|
9
9
|
- ✅ 确认后:
|
|
10
10
|
- 恢复工作区文件到**发送该消息之前**的快照状态。
|
|
11
11
|
- 通过 DSH 原生的 `sessionController.fork` 从该消息之前 fork 出新会话。
|
|
@@ -24,7 +24,7 @@ dsh plugin --profile <name> add dsh-turn-undo
|
|
|
24
24
|
## 使用
|
|
25
25
|
|
|
26
26
|
1. 在对话中,每条用户消息的操作栏会出现撤销按钮。
|
|
27
|
-
2.
|
|
27
|
+
2. 点击按钮,弹窗列出撤销该消息后会影响的文件(该消息及之后所有轮次的改动)。
|
|
28
28
|
3. 点击“确认恢复并继续”:
|
|
29
29
|
- 文件恢复。
|
|
30
30
|
- 旧会话被标记为 `(已撤销)`。
|
|
@@ -47,7 +47,7 @@ dsh plugin --profile <name> add dsh-turn-undo
|
|
|
47
47
|
- 对“第一条用户消息”前面没有 completed turn 时,fall back 到 `ctx.sessionController.create({ cwd, agentPreset })` 创建空白会话。
|
|
48
48
|
- 旧会话通过 `ctx.sessionController.rename` 加上 `(已撤销)` 前缀。
|
|
49
49
|
- **HTTP API**:
|
|
50
|
-
- `GET /api/turn-undo?sessionId=...&messageSeq=...&promptText
|
|
50
|
+
- `GET /api/turn-undo?sessionId=...&messageSeq=...&promptText=...`:预览撤销该消息后会影响的文件(该消息之后所有轮次改动的并集)。
|
|
51
51
|
- `POST /api/turn-undo`(body `{ sessionId, messageSeq, promptText }`):执行恢复 + fork。
|
|
52
52
|
|
|
53
53
|
### 客户端(`client.js`)
|
package/client.js
CHANGED
|
@@ -90,6 +90,7 @@ window.__ModuleLoader__.load({
|
|
|
90
90
|
'.dtu-status{margin:0;overflow-wrap:anywhere;color:var(--dsw-alias-label-secondary);font-size:13px;line-height:20px}',
|
|
91
91
|
'.dtu-files{min-width:0;max-width:100%;box-sizing:border-box;max-height:220px;overflow:auto;border:1px solid var(--dsw-alias-border-l2);border-radius:10px}',
|
|
92
92
|
'.dtu-file{display:flex;justify-content:space-between;gap:16px;min-width:0;padding:8px 10px;border-bottom:1px solid var(--dsw-alias-border-l1);font-size:12px}.dtu-file:last-child{border-bottom:0}',
|
|
93
|
+
'.dtu-more{display:flex;justify-content:center;gap:8px;padding:8px 10px;font-size:12px;color:var(--dsw-alias-label-tertiary)}',
|
|
93
94
|
'.dtu-file code{min-width:0;overflow:hidden;text-overflow:ellipsis;color:var(--dsw-alias-label-secondary)}',
|
|
94
95
|
'.dtu-kind{flex:none;color:var(--dsw-alias-label-tertiary)}',
|
|
95
96
|
'.dtu-warning,.dtu-error{box-sizing:border-box;max-width:100%;margin:0;padding:10px 12px;overflow-wrap:anywhere;word-break:break-word;border-radius:10px;font-size:12px;line-height:18px}',
|
|
@@ -329,7 +330,7 @@ window.__ModuleLoader__.load({
|
|
|
329
330
|
),
|
|
330
331
|
),
|
|
331
332
|
open ? h(RestoreDialog, {
|
|
332
|
-
sessionId: sessionId, messageText: messageText,
|
|
333
|
+
sessionId: sessionId, messageText: messageText,
|
|
333
334
|
onClose: close, preview: preview, loading: loading, error: error,
|
|
334
335
|
applying: applying, done: done, changes: changes,
|
|
335
336
|
previewError: previewError, noSnapshot: noSnapshot, canApply: canApply, applyRestore: applyRestore,
|
|
@@ -340,7 +341,6 @@ window.__ModuleLoader__.load({
|
|
|
340
341
|
function RestoreDialog(props) {
|
|
341
342
|
var sessionId = props.sessionId
|
|
342
343
|
var messageText = props.messageText
|
|
343
|
-
var turn = props.turn
|
|
344
344
|
var onClose = props.onClose
|
|
345
345
|
var preview = props.preview
|
|
346
346
|
var loading = props.loading
|
|
@@ -353,6 +353,13 @@ window.__ModuleLoader__.load({
|
|
|
353
353
|
var canApply = props.canApply
|
|
354
354
|
var applyRestore = props.applyRestore
|
|
355
355
|
|
|
356
|
+
// 文件很多时避免一次性渲染大量 DOM(性能优化):只渲染前 200 个,
|
|
357
|
+
// 其余折叠成一条提示。总数仍在标题里显示。
|
|
358
|
+
var MAX_PREVIEW_FILES = 200
|
|
359
|
+
var shownChanges = changes.length > MAX_PREVIEW_FILES
|
|
360
|
+
? changes.slice(0, MAX_PREVIEW_FILES)
|
|
361
|
+
: changes
|
|
362
|
+
|
|
356
363
|
return reactDom.createPortal(
|
|
357
364
|
h('div', { className: 'dtu-overlay', onClick: onClose },
|
|
358
365
|
h('div', { className: 'dtu-dialog', onClick: function (e) { e.stopPropagation() } },
|
|
@@ -377,12 +384,16 @@ window.__ModuleLoader__.load({
|
|
|
377
384
|
? h('div', { className: 'dtu-section' },
|
|
378
385
|
h('div', { className: 'dtu-section-label' }, '将影响的文件 (' + changes.length + ' 个)'),
|
|
379
386
|
h('div', { className: 'dtu-files' },
|
|
380
|
-
|
|
387
|
+
shownChanges.map(function (change, idx) {
|
|
381
388
|
return h('div', { key: idx, className: 'dtu-file' },
|
|
382
389
|
h('code', {}, change.path),
|
|
383
390
|
h('span', { className: 'dtu-kind' }, kindLabel(change.kind)),
|
|
384
391
|
)
|
|
385
392
|
}),
|
|
393
|
+
(changes.length > shownChanges.length)
|
|
394
|
+
? h('div', { className: 'dtu-more' },
|
|
395
|
+
'… 还有 ' + (changes.length - shownChanges.length) + ' 个文件未显示'
|
|
396
|
+
) : null,
|
|
386
397
|
),
|
|
387
398
|
) : null,
|
|
388
399
|
(!loading && !previewError && changes.length > 0)
|
package/index.js
CHANGED
|
@@ -887,6 +887,9 @@ function createHandler(ctx, runtime, sessions, agents) {
|
|
|
887
887
|
}
|
|
888
888
|
}
|
|
889
889
|
|
|
890
|
+
// Wait for any in-flight snapshot capture so the preview reflects the
|
|
891
|
+
// latest committed workspace state (avoids showing a stale turn/end).
|
|
892
|
+
try { await runtime.waitForSnapshots() } catch {}
|
|
890
893
|
const preview = runtime.store.preview(sessionId, targetTurn)
|
|
891
894
|
return json(response, 200, preview)
|
|
892
895
|
}
|
|
@@ -922,6 +925,18 @@ function createHandler(ctx, runtime, sessions, agents) {
|
|
|
922
925
|
// empty snapshot chain and skip file restoration.
|
|
923
926
|
try { await runtime.waitForSnapshots() } catch {}
|
|
924
927
|
|
|
928
|
+
// 3.5 Safety snapshot: before this irreversible restore wipes files,
|
|
929
|
+
// force-capture the CURRENT workspace into the session chain (as a
|
|
930
|
+
// fractional turn just past the newest snapshot) so the user can
|
|
931
|
+
// restore again to the pre-undo state if needed. Non-fatal on failure.
|
|
932
|
+
try {
|
|
933
|
+
const chain = runtime.store.loadChain(sessionId)
|
|
934
|
+
const lastTurn = chain.length ? chain[chain.length - 1].turn : 0
|
|
935
|
+
await runtime.captureNow(cwd, sessionId, lastTurn + 0.5)
|
|
936
|
+
} catch (e) {
|
|
937
|
+
console.warn('[turn-undo] safety snapshot failed (non-fatal):', e?.message)
|
|
938
|
+
}
|
|
939
|
+
|
|
925
940
|
// 4. Restore files to the best snapshot at/before restoreTurn.
|
|
926
941
|
let restoreResult
|
|
927
942
|
if (restoreTurn !== null) {
|
|
@@ -1026,49 +1041,46 @@ SnapshotStore.prototype.preview = function (sessionId, targetTurn) {
|
|
|
1026
1041
|
return { ok: true, status: 'ready', targetTurn: null, totalChanges: 0, changes: [] }
|
|
1027
1042
|
}
|
|
1028
1043
|
|
|
1029
|
-
//
|
|
1030
|
-
|
|
1044
|
+
// 撤销"发送这条消息之前"会一并回退该消息之后的所有改动,因此影响范围 =
|
|
1045
|
+
// baseline(targetTurn 之前最近的快照,即恢复到什么状态)与 latest
|
|
1046
|
+
// (会话最新快照,即撤销点之后累积到当前的状态终点)之差。
|
|
1047
|
+
// baseline 取小于 targetTurn 的最新快照:正常是该 turn/start 快照
|
|
1048
|
+
// (T - 0.5),若中间某 turn 无文件变化没写 manifest,则回退到更早的最近
|
|
1049
|
+
// 快照;对于没有前置快照的首条消息,用空对象 {} 作为基线(即回到空状态)。
|
|
1050
|
+
let baseline = null
|
|
1031
1051
|
for (const m of chain) {
|
|
1032
|
-
if (m.turn ===
|
|
1033
|
-
|
|
1034
|
-
break
|
|
1052
|
+
if (m.turn < targetTurn && (baseline === null || m.turn > baseline.turn)) {
|
|
1053
|
+
baseline = m
|
|
1035
1054
|
}
|
|
1036
1055
|
}
|
|
1037
|
-
|
|
1056
|
+
const baselineManifest = baseline ? baseline.manifest : {}
|
|
1057
|
+
|
|
1058
|
+
if (chain.length === 0) {
|
|
1038
1059
|
return { ok: true, status: 'ready', targetTurn, totalChanges: 0, changes: [], noSnapshot: true }
|
|
1039
1060
|
}
|
|
1040
1061
|
|
|
1041
|
-
//
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
// most recent available baseline. For the very first turn we use an empty
|
|
1045
|
-
// baseline.
|
|
1046
|
-
let prev = null
|
|
1047
|
-
for (const m of chain) {
|
|
1048
|
-
if (m.turn < targetTurn && (prev === null || m.turn > prev.turn)) {
|
|
1049
|
-
prev = m
|
|
1050
|
-
}
|
|
1051
|
-
}
|
|
1052
|
-
const prevManifest = prev ? prev.manifest : {}
|
|
1062
|
+
// 会话最新快照 = 撤销点之后所有改动的累积终点。
|
|
1063
|
+
const latest = chain[chain.length - 1]
|
|
1064
|
+
const latestManifest = latest.manifest
|
|
1053
1065
|
|
|
1054
|
-
// Calculate changes
|
|
1066
|
+
// Calculate changes between latest and baseline: these are the files that
|
|
1067
|
+
// undo (restoring to baseline) will affect — every change made at or after
|
|
1068
|
+
// the target turn.
|
|
1055
1069
|
const changes = []
|
|
1056
|
-
const
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
const entry = targetManifest[rel]
|
|
1060
|
-
if (!prevManifest[rel]) {
|
|
1070
|
+
for (const rel of Object.keys(latestManifest)) {
|
|
1071
|
+
const entry = latestManifest[rel]
|
|
1072
|
+
if (!baselineManifest[rel]) {
|
|
1061
1073
|
changes.push({ path: rel, kind: 'created' })
|
|
1062
1074
|
} else {
|
|
1063
|
-
const prevEntry =
|
|
1075
|
+
const prevEntry = baselineManifest[rel]
|
|
1064
1076
|
if (entry.hash !== prevEntry.hash) {
|
|
1065
1077
|
changes.push({ path: rel, kind: 'modified' })
|
|
1066
1078
|
}
|
|
1067
1079
|
}
|
|
1068
1080
|
}
|
|
1069
1081
|
|
|
1070
|
-
for (const rel of Object.keys(
|
|
1071
|
-
if (!
|
|
1082
|
+
for (const rel of Object.keys(baselineManifest)) {
|
|
1083
|
+
if (!latestManifest[rel]) {
|
|
1072
1084
|
changes.push({ path: rel, kind: 'deleted' })
|
|
1073
1085
|
}
|
|
1074
1086
|
}
|
|
@@ -1076,7 +1088,7 @@ SnapshotStore.prototype.preview = function (sessionId, targetTurn) {
|
|
|
1076
1088
|
return {
|
|
1077
1089
|
ok: true,
|
|
1078
1090
|
status: 'ready',
|
|
1079
|
-
targetTurn
|
|
1091
|
+
targetTurn,
|
|
1080
1092
|
totalChanges: changes.length,
|
|
1081
1093
|
changes,
|
|
1082
1094
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-turn-undo",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.2",
|
|
4
4
|
"description": "Undo to any point in a DSH conversation — revert files and fork a new session",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.js",
|
|
@@ -24,7 +24,10 @@
|
|
|
24
24
|
"client.js",
|
|
25
25
|
"cordis.patch.yml"
|
|
26
26
|
],
|
|
27
|
-
"repository":
|
|
27
|
+
"repository": {
|
|
28
|
+
"type": "git",
|
|
29
|
+
"url": "git+https://github.com/gege9527/dsh-turn-undo.git"
|
|
30
|
+
},
|
|
28
31
|
"bugs": "https://github.com/gege9527/dsh-turn-undo/issues",
|
|
29
32
|
"homepage": "https://github.com/gege9527/dsh-turn-undo#readme",
|
|
30
33
|
"license": "MIT",
|
|
@@ -39,4 +42,4 @@
|
|
|
39
42
|
"optional": false
|
|
40
43
|
}
|
|
41
44
|
}
|
|
42
|
-
}
|
|
45
|
+
}
|