@kangyishuai/dsh-session-rewind 0.4.0 → 0.5.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 +16 -5
- package/README.zh-CN.md +16 -5
- package/lib/client.js +85 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
Unofficial plugin for [DeepSeek Harness (DSH)](https://github.com/deepseek-ai/deepseek-harness). Not affiliated with DeepSeek.
|
|
10
10
|
|
|
11
|
-
> **Status: early.** Verified end to end on DSH 0.2.2-rc.1:
|
|
11
|
+
> **Status: early.** Verified end to end on DSH 0.2.2-rc.1: 185 unit tests, 34 GUI assertions in a throwaway DSH home, and **36 assertions against the real desktop app** (read-only surface plus file mode: created-file trash, modified-file restore, rescue restore, timeline). `pnpm gate:session-rewind` runs the whole release gate in one command. Young software — keep backups of anything you cannot lose.
|
|
12
12
|
|
|
13
13
|
## What it does
|
|
14
14
|
|
|
@@ -21,6 +21,7 @@ Long conversations drift. A few turns later you wish you had answered a prompt d
|
|
|
21
21
|
- **Conversation.** The target message and everything after it are withdrawn from the model-visible context and from the transcript. The session id, title and earlier history are untouched. Nothing is deleted from the log — one marker event is appended.
|
|
22
22
|
- **Files (optional).** Files changed by `write` / `edit` / `apply_patch` in the withdrawn turns are restored to their pre-change content. Files created after the target are moved to the plugin's trash instead of deleted. A rescue point of the current files is created before anything is written; failures roll back from it.
|
|
23
23
|
- **Composer.** The target message's text (and images) is put back into the composer, ready to edit and send again. An existing draft is never overwritten. Tick **Send this message again right after rewinding** in the confirmation dialog to retry the turn in one click (conversation-only rewinds).
|
|
24
|
+
- **Every view stays in sync.** Rewind in one tab or window and other open views of the same session hide the withdrawn rows immediately, without a reload (the notice travels through `localStorage`; the host's cuts remain the only source of truth).
|
|
24
25
|
- **Rewind panel.** A new header action opens a panel with four tabs: **Withdrawn** (every message each rewind removed, searchable, copy or put back in the composer), **History** (a timeline of each rewind and each rescue-point file restore, newest first: scope, message count, tokens, file results), **Rescue points** (restore files from an earlier rescue), and **Settings** (default scope, page size, confirm-on-list, diagnostics).
|
|
25
26
|
|
|
26
27
|
## Read-only agent tool
|
|
@@ -34,6 +35,16 @@ The plugin also registers one **read-only** tool, `session_rewind_candidates`, s
|
|
|
34
35
|
|
|
35
36
|
The tool never rewinds, never edits files and never appends to the session log — the rewind itself stays a user action (that is the point of the ADR 0006 design). Set `readOnlyTool: false` to unregister it; **Settings → Diagnostics** reports whether it registered.
|
|
36
37
|
|
|
38
|
+
## What it looks like
|
|
39
|
+
|
|
40
|
+

|
|
41
|
+
|
|
42
|
+

|
|
43
|
+
|
|
44
|
+
Both screenshots come from a throwaway DSH home containing only a synthetic workspace and session (`pnpm shots:session-rewind` regenerates them), cropped to the dialog and the panel.
|
|
45
|
+
|
|
46
|
+
> Image links are absolute (`raw.githubusercontent.com`) on purpose: npm does not ship `assets/` in the tarball, so a relative path would render as a broken image on npmjs.com.
|
|
47
|
+
|
|
37
48
|
## Install
|
|
38
49
|
|
|
39
50
|
```bash
|
|
@@ -105,9 +116,9 @@ On startup the plugin also removes data for sessions that no longer exist (they
|
|
|
105
116
|
|
|
106
117
|
## Compatibility
|
|
107
118
|
|
|
108
|
-
| DSH version | Status
|
|
109
|
-
| ----------- |
|
|
110
|
-
| 0.2.2-rc.1 | Verified: `
|
|
119
|
+
| DSH version | Status |
|
|
120
|
+
| ----------- | ------------------------------------------------------------------------------------------------------ |
|
|
121
|
+
| 0.2.2-rc.1 | Verified: 185 unit tests + `smoke:session-rewind` 34/34 + desktop 36/36 (`--deep`, file mode included) |
|
|
111
122
|
|
|
112
123
|
The plugin declares no `engines.dsh` and no `@deepseek-ai/*` peer range: DSH is pre-release and semver ranges over prereleases are unreliable (see [ADR 0005](../../docs/adr/0005-dsh-version-compatibility.md)). Treat the table as the compatibility statement. After a DSH upgrade run `pnpm probe:runtime`, `pnpm smoke:session-rewind`, and check the plugin's diagnostics tab; if a private seam changed, the plugin degrades instead of breaking the session.
|
|
113
124
|
|
|
@@ -117,7 +128,7 @@ The plugin declares no `engines.dsh` and no `@deepseek-ai/*` peer range: DSH is
|
|
|
117
128
|
pnpm test # unit tests (host engine, host RPC, client helpers)
|
|
118
129
|
pnpm verify # repo-wide gates
|
|
119
130
|
pnpm smoke:session-rewind # temp DSH home + real GUI end-to-end (needs dsh & ego-browser)
|
|
120
|
-
pnpm smoke:session-rewind-desktop # read-only
|
|
131
|
+
pnpm smoke:session-rewind-desktop # desktop (Electron) check: 21 read-only + 12 with --deep --workspace <dir>
|
|
121
132
|
```
|
|
122
133
|
|
|
123
134
|
Design notes: [ADR 0006](../../docs/adr/0006-session-rewind-architecture.md) and [ADR 0007](../../docs/adr/0007-session-rewind-file-restore.md) (Chinese; the repository docs are written in Chinese).
|
package/README.zh-CN.md
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
非官方插件,与 DeepSeek 无关。面向 [DeepSeek Harness (DSH)](https://github.com/deepseek-ai/deepseek-harness)。
|
|
10
10
|
|
|
11
|
-
> **状态:早期。** 已在 DSH 0.2.2-rc.1
|
|
11
|
+
> **状态:早期。** 已在 DSH 0.2.2-rc.1 上端到端验证:185 个单测、一次性 DSH 目录里 34 项 GUI 断言、以及**真实桌面版 36 项断言**(只读界面 + 文件模式:新建文件进回收站、修改文件还原、救援点恢复、时间线)。`pnpm gate:session-rewind` 一条命令跑完发版门禁。插件还年轻,重要数据请自行备份。
|
|
12
12
|
|
|
13
13
|
## 它做什么
|
|
14
14
|
|
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
- **状态变更提示**:如果要撤回的区间里改过会话状态(plan 模式、goal、待办、模型选择、权限/沙箱预设、智能体预设、定时任务、工作目录),确认框会列出来并明确说明——回溯只撤回对话,**不会**回滚这些状态。
|
|
22
22
|
- **运行中也能回退**:确认回退时如果这一轮正在跑,会先取消它(保留队列)、等它空闲,再对新历史重新求值。
|
|
23
23
|
- **输入框**:目标消息文本(以及图片)放回输入框,改完可直接重发;已有草稿绝不覆盖。勾选「回退后立即重发」可一键重试这一轮(仅对话模式)。
|
|
24
|
+
- **多视图自动同步**:在一个标签页/窗口里回溯后,其它已打开的同会话视图会立刻隐藏被撤回的行,无需刷新(通知走 `localStorage`,渲染只认宿主 cuts)。
|
|
24
25
|
- **回溯面板**:会话头部新增一个入口,四个页签:**已撤回**(每次回溯撤回的全部消息,可搜索、复制、放回输入框)、**回溯记录**(时间线:每次回溯与每次救援点恢复,新在上;范围、条数、token、文件结果)、**救援点**(手动恢复文件)、**设置**(默认范围、每页数量、列表确认、诊断)。
|
|
25
26
|
|
|
26
27
|
## 只读 agent 工具
|
|
@@ -34,6 +35,16 @@
|
|
|
34
35
|
|
|
35
36
|
这个工具不回溯、不碰文件、不写会话日志——真正的回退始终由用户在界面确认(这是 ADR 0006 的设计前提)。设 `readOnlyTool: false` 可关闭;**设置 → 诊断**里能看到它是否注册成功。
|
|
36
37
|
|
|
38
|
+
## 长什么样
|
|
39
|
+
|
|
40
|
+

|
|
41
|
+
|
|
42
|
+

|
|
43
|
+
|
|
44
|
+
两张图都来自一个只含**合成工作区与合成会话**的临时 DSH 目录(`pnpm shots:session-rewind` 可重新生成),并且按 DOM 矩形裁剪到对话框/面板本身。
|
|
45
|
+
|
|
46
|
+
> 图片用绝对地址(`raw.githubusercontent.com`)而不是相对路径:npm 的 tarball 不含 `assets/`,相对路径在 npmjs.com 上会是破图。
|
|
47
|
+
|
|
37
48
|
## 安装
|
|
38
49
|
|
|
39
50
|
```bash
|
|
@@ -105,9 +116,9 @@ dsh plugin --profile desktop add /tmp/kangyishuai-dsh-session-rewind-*.tgz
|
|
|
105
116
|
|
|
106
117
|
## 兼容性
|
|
107
118
|
|
|
108
|
-
| DSH 版本 | 状态
|
|
109
|
-
| ---------- |
|
|
110
|
-
| 0.2.2-rc.1 |
|
|
119
|
+
| DSH 版本 | 状态 |
|
|
120
|
+
| ---------- | ---------------------------------------------------------------------------------------- |
|
|
121
|
+
| 0.2.2-rc.1 | 已验证:185 个单测 + `smoke:session-rewind` 34/34 + 桌面版 36/36(`--deep`,含文件模式) |
|
|
111
122
|
|
|
112
123
|
插件不声明 `engines.dsh`,也不声明 `@deepseek-ai/*` 的 peer 范围:DSH 还在预发布阶段,预发布版本的 semver 范围不可靠(见 [ADR 0005](../../docs/adr/0005-dsh-version-compatibility.md))。以这张表为准。DSH 升级后跑 `pnpm probe:runtime`、`pnpm smoke:session-rewind`,并看插件诊断页签;私有 seam 变了会降级而不是让会话出错。
|
|
113
124
|
|
|
@@ -117,7 +128,7 @@ dsh plugin --profile desktop add /tmp/kangyishuai-dsh-session-rewind-*.tgz
|
|
|
117
128
|
pnpm test # 单测(宿主文件引擎、宿主 RPC、客户端 helper)
|
|
118
129
|
pnpm verify # 仓库统一门禁
|
|
119
130
|
pnpm smoke:session-rewind # 临时 DSH 目录 + 真实 GUI 端到端(需要 dsh 与 ego-browser)
|
|
120
|
-
pnpm smoke:session-rewind-desktop #
|
|
131
|
+
pnpm smoke:session-rewind-desktop # 桌面(Electron)回归:只读 21 项,加 --deep --workspace <dir> 再 +12 项
|
|
121
132
|
```
|
|
122
133
|
|
|
123
134
|
设计记录:[ADR 0006](../../docs/adr/0006-session-rewind-architecture.md)、[ADR 0007](../../docs/adr/0007-session-rewind-file-restore.md)。
|
package/lib/client.js
CHANGED
|
@@ -574,6 +574,33 @@ window.__ModuleLoader__.load({
|
|
|
574
574
|
/** localStorage 偏好键;版本化,方便未来迁移。 */
|
|
575
575
|
let PREF_STORAGE_KEY = "dsh-session-rewind.prefs.v1";
|
|
576
576
|
|
|
577
|
+
/** 跨视图广播键:某个视图回溯成功后写入,其它视图据此重新拉宿主 cuts。 */
|
|
578
|
+
let CUT_BROADCAST_KEY = "dsh-session-rewind.cut-broadcast.v1";
|
|
579
|
+
|
|
580
|
+
/**
|
|
581
|
+
* 解析跨视图广播载荷(脏数据一律返回 null)。
|
|
582
|
+
* @param {any} raw localStorage 里的字符串
|
|
583
|
+
* @returns {{sessionId: string, targetSeq: number|null, markerSeq: number|null, at: number} | null}
|
|
584
|
+
*/
|
|
585
|
+
function parseCutBroadcast(raw) {
|
|
586
|
+
if (typeof raw !== "string" || raw === "") return null;
|
|
587
|
+
let value;
|
|
588
|
+
try {
|
|
589
|
+
value = JSON.parse(raw);
|
|
590
|
+
} catch (_error) {
|
|
591
|
+
return null;
|
|
592
|
+
}
|
|
593
|
+
if (!value || typeof value !== "object") return null;
|
|
594
|
+
let sessionId = typeof value.sessionId === "string" ? value.sessionId : "";
|
|
595
|
+
if (sessionId === "") return null;
|
|
596
|
+
return {
|
|
597
|
+
sessionId: sessionId,
|
|
598
|
+
targetSeq: typeof value.targetSeq === "number" ? value.targetSeq : null,
|
|
599
|
+
markerSeq: typeof value.markerSeq === "number" ? value.markerSeq : null,
|
|
600
|
+
at: typeof value.at === "number" ? value.at : 0,
|
|
601
|
+
};
|
|
602
|
+
}
|
|
603
|
+
|
|
577
604
|
/** 客户端偏好默认值:默认仅回退对话、每页 100 条、列表选择先确认。 */
|
|
578
605
|
let PREF_DEFAULTS = Object.freeze({
|
|
579
606
|
defaultMode: "chat",
|
|
@@ -3770,6 +3797,8 @@ window.__ModuleLoader__.load({
|
|
|
3770
3797
|
});
|
|
3771
3798
|
rebuildSessionById(sessionId);
|
|
3772
3799
|
let outcome = fillComposer(sessionId, summary.text || target.text || "");
|
|
3800
|
+
// 同源其它视图也要立刻隐藏这些行(它们不会被这次 append 唤醒)。
|
|
3801
|
+
broadcastCut(sessionId, summary);
|
|
3773
3802
|
let text = t("toast.rewound", {
|
|
3774
3803
|
n: summary.removedCount,
|
|
3775
3804
|
tokens: formatCount(summary.tokens),
|
|
@@ -4250,6 +4279,57 @@ window.__ModuleLoader__.load({
|
|
|
4250
4279
|
}
|
|
4251
4280
|
|
|
4252
4281
|
/** 拉一次宿主 cuts 作为可见性兜底(静默失败)。 */
|
|
4282
|
+
/**
|
|
4283
|
+
* 把「这个会话刚回溯过」广播给同源其它视图(多标签/多窗口一致性)。
|
|
4284
|
+
* localStorage 的 storage 事件只在**其它** document 触发,不会回环到自己。
|
|
4285
|
+
* @param {any} sessionId
|
|
4286
|
+
* @param {any} summary
|
|
4287
|
+
*/
|
|
4288
|
+
function broadcastCut(sessionId, summary) {
|
|
4289
|
+
try {
|
|
4290
|
+
if (!window.localStorage) return;
|
|
4291
|
+
window.localStorage.setItem(
|
|
4292
|
+
CUT_BROADCAST_KEY,
|
|
4293
|
+
JSON.stringify({
|
|
4294
|
+
sessionId: sessionId,
|
|
4295
|
+
targetSeq:
|
|
4296
|
+
summary && typeof summary.targetSeq === "number" ? summary.targetSeq : null,
|
|
4297
|
+
markerSeq:
|
|
4298
|
+
summary && typeof summary.markerSeq === "number" ? summary.markerSeq : null,
|
|
4299
|
+
at: Date.now(),
|
|
4300
|
+
}),
|
|
4301
|
+
);
|
|
4302
|
+
} catch (_error) {
|
|
4303
|
+
/* 存储不可用时退化为「只有本视图知道」 */
|
|
4304
|
+
}
|
|
4305
|
+
}
|
|
4306
|
+
|
|
4307
|
+
/**
|
|
4308
|
+
* 监听其它视图的广播:收到就按宿主 cuts 重新隐藏(宿主是权威来源,不信任广播内容)。
|
|
4309
|
+
* @returns {() => void} disposer
|
|
4310
|
+
*/
|
|
4311
|
+
function installCutBroadcast() {
|
|
4312
|
+
/** @param {any} event */
|
|
4313
|
+
function onStorage(event) {
|
|
4314
|
+
if (!event || event.key !== CUT_BROADCAST_KEY) return;
|
|
4315
|
+
let payload = parseCutBroadcast(event.newValue);
|
|
4316
|
+
if (!payload) return;
|
|
4317
|
+
seedCuts(payload.sessionId);
|
|
4318
|
+
}
|
|
4319
|
+
try {
|
|
4320
|
+
window.addEventListener("storage", onStorage);
|
|
4321
|
+
} catch (_error) {
|
|
4322
|
+
return function noop() {};
|
|
4323
|
+
}
|
|
4324
|
+
return function dispose() {
|
|
4325
|
+
try {
|
|
4326
|
+
window.removeEventListener("storage", onStorage);
|
|
4327
|
+
} catch (_error) {
|
|
4328
|
+
/* ignore */
|
|
4329
|
+
}
|
|
4330
|
+
};
|
|
4331
|
+
}
|
|
4332
|
+
|
|
4253
4333
|
function seedCuts(/** @type {any} */ sessionId) {
|
|
4254
4334
|
return call("cuts", { sessionId: sessionId })
|
|
4255
4335
|
.then((/** @type {any} */ value) => {
|
|
@@ -4303,6 +4383,9 @@ window.__ModuleLoader__.load({
|
|
|
4303
4383
|
|
|
4304
4384
|
installChatVisibility(ctx, cutStore);
|
|
4305
4385
|
|
|
4386
|
+
// 多视图一致性:别的标签页回溯后,本视图按宿主 cuts 重新隐藏。
|
|
4387
|
+
ctx.effect(() => installCutBroadcast(), "session-rewind: cut broadcast");
|
|
4388
|
+
|
|
4306
4389
|
ctx.effect(
|
|
4307
4390
|
() =>
|
|
4308
4391
|
installRowAction(ctx.slots, {
|
|
@@ -4535,6 +4618,8 @@ window.__ModuleLoader__.load({
|
|
|
4535
4618
|
normalizeFiles: normalizeFiles,
|
|
4536
4619
|
normalizePreview: normalizePreview,
|
|
4537
4620
|
normalizeStateChanges: normalizeStateChanges,
|
|
4621
|
+
parseCutBroadcast: parseCutBroadcast,
|
|
4622
|
+
CUT_BROADCAST_KEY: CUT_BROADCAST_KEY,
|
|
4538
4623
|
fileModeState: fileModeState,
|
|
4539
4624
|
effectiveMode: effectiveMode,
|
|
4540
4625
|
conflictPathsOf: conflictPathsOf,
|
package/package.json
CHANGED