dsh-rewind-plugin 0.1.9 → 0.1.10
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 +18 -19
- package/lib/client.js +97 -39
- package/lib/index.js +4 -19
- package/lib/types/client/index.d.ts +5 -0
- package/lib/types/client/locales.d.ts +2 -0
- package/lib/types/client/styles.d.ts +2 -1
- package/package.json +1 -1
- package/scripts/verify-host.mjs +18 -10
package/README.md
CHANGED
|
@@ -2,19 +2,19 @@
|
|
|
2
2
|
|
|
3
3
|
DeepSeek Harness 插件:**同一会话窗口的 in-place 对话回退**(Claude Code `/rewind` 语义)。主交互为**用户消息旁的「回退」按钮**,点击后选择回退模式;命令仅作辅助。
|
|
4
4
|
|
|
5
|
-
> 状态:v0.1.
|
|
5
|
+
> 状态:v0.1.10 已实现并发布(`dsh-rewind-plugin`,npm + GitHub Actions Trusted Publishing)。交互以 Claude Code 行为为参考,并贴合 dsh Web 实际 UI(利用现有 DOM 锚点与运行时快照,纯插件、不改仓库核心)。
|
|
6
6
|
|
|
7
|
-
## 实现状态(v0.1.
|
|
7
|
+
## 实现状态(v0.1.10)
|
|
8
8
|
|
|
9
|
-
- ✅ host 端 `/rewind`
|
|
9
|
+
- ✅ host 端 `/rewind` 命令(手动单动作:不接受参数、撤回最近一条;参数化形式保留为按钮内部调用)
|
|
10
10
|
- ✅ host 端变更台账(`tools/execute` 捕获 before、`tools/post-execute` 提交),按会话隔离
|
|
11
11
|
- ✅ **与其他审批类插件共存**:捕获在 around-dispatch 阶段,`tools/pre-execute` 被 `ask` 短路(如 dsh-edit-approval)后批准仍能记录;被拒绝的调用不留 pending 残留
|
|
12
12
|
- ✅ **路径按会话 cwd 解析**(复刻 `dsh-tool-fs` 的 session-cwd 规则),相对路径台账/还原指向真实文件;台账记录解析后的 display path
|
|
13
13
|
- ✅ **fs 服务动态获取**(`ctx.inject(['fs'])`):fs 后挂载也不失效,无 fs 部署时命令仍可用
|
|
14
14
|
- ✅ 同窗口 in-place 回退:追加空内容标记 + `surfaceOp: replace` 替换目标及之后全部 surface(真实 `dsh-session` 集成测试通过)
|
|
15
|
-
- ✅ client 端「回退」按钮(MutationObserver 注入用户消息行操作区)+ 模式选择浮层(含 both
|
|
15
|
+
- ✅ client 端「回退」按钮(MutationObserver 注入用户消息行操作区)+ 模式选择浮层(含 both 模式影响清单确认)+ 手动 `/rewind` 参数拦截
|
|
16
16
|
- ✅ 测试:纯函数单测 + 真实 `dsh-session` 集成测试 + `verify-host` 端到端(14 项,含审批短路/会话 cwd 场景)
|
|
17
|
-
- ⏳ 二期:快捷键、git-first
|
|
17
|
+
- ⏳ 二期:快捷键、git-first 快照式文件回退
|
|
18
18
|
|
|
19
19
|
## 安装
|
|
20
20
|
|
|
@@ -68,12 +68,12 @@ git push --tags # push v<version> tag → 触发 .github/workflows/publish.
|
|
|
68
68
|
|
|
69
69
|
- 每条用户消息 hover 出现「↶ 回退」按钮:点击 → 选择「仅回退对话」或
|
|
70
70
|
「回退对话和代码」(后者先展示影响清单再确认)。
|
|
71
|
-
- **回退 =
|
|
71
|
+
- **回退 = 撤回(时间回溯)**:对**任意**用户消息回退(按钮,或 `/rewind` 撤回最近一条),
|
|
72
72
|
效果是**撤回该消息及它之后的所有内容**(含 agent 回复、工具调用)——对话界面与
|
|
73
73
|
Agent 上下文都回到这条消息发送之前;**该消息的文本自动填入输入框(编辑区)**,
|
|
74
74
|
可直接修改后重发。命令结果提示"已撤回 seq N 及之后内容"。
|
|
75
|
-
-
|
|
76
|
-
|
|
75
|
+
- 手动 `/rewind` **不接受参数**:直接撤回最近一条用户消息(内容填回输入框可修改重发);
|
|
76
|
+
回退到更早的消息请用该消息旁的 ↶ 按钮(参数化命令保留为按钮内部调用)。
|
|
77
77
|
- **回退后前端与 Agent 一致**:回退标记是空内容消息(deriveMessages 会跳过,模型
|
|
78
78
|
上下文无任何标记噪音);client 端隐藏被撤回范围内的消息行与 `/rewind` 命令结果,
|
|
79
79
|
可见对话即"撤回点之前的内容"。会话日志(append-only 审计)不受影响。
|
|
@@ -93,9 +93,10 @@ git push --tags # push v<version> tag → 触发 .github/workflows/publish.
|
|
|
93
93
|
|
|
94
94
|
社区 rewind 类插件(`dsh-recall-plugin`、`dsh-checkpoint-rewind`、`dsh-turn-rewind`)均为 **fork 路线**(回退 = fork 出新会话,用户切换会话继续),且没有「仅回退对话 / 对话+代码」的选项。本插件提供:**在当前会话窗口内**改写模型上下文 + 可选还原工作区文件。
|
|
95
95
|
|
|
96
|
-
##
|
|
96
|
+
## 交互设计(按钮两步;手动命令为单动作)
|
|
97
97
|
|
|
98
|
-
|
|
98
|
+
按钮流程遵循两步:**第一步选择要回退到的 user 消息,第二步选择回退模式**。手动
|
|
99
|
+
`/rewind` 命令不参与两步流程——它不接受参数,只撤回最近一条消息。
|
|
99
100
|
|
|
100
101
|
### 1. 用户消息旁的「回退」按钮(主入口)
|
|
101
102
|
|
|
@@ -108,18 +109,16 @@ git push --tags # push v<version> tag → 触发 .github/workflows/publish.
|
|
|
108
109
|
- 选「回退对话和代码」时,浮层内先显示将受影响的内容清单(将还原/删除的文件名与数量),确认后执行。
|
|
109
110
|
- 执行结果以一条对话内消息呈现(如「已回退到 seq N,移除 M 条上下文;还原 2 个文件」)。
|
|
110
111
|
|
|
111
|
-
### 2.
|
|
112
|
+
### 2. 命令(辅助入口,仅支持手动撤回最近一条)
|
|
112
113
|
|
|
113
114
|
```
|
|
114
|
-
/rewind
|
|
115
|
-
/rewind <序号|@seq> 第二步:对选中的消息展示模式选项,等待选择
|
|
116
|
-
/rewind <序号|@seq> chat 执行:仅回退对话
|
|
117
|
-
/rewind <序号|@seq> both 执行:对话 + 代码
|
|
118
|
-
/rewind preview <目标> 只输出影响清单,不执行
|
|
115
|
+
/rewind 撤回最近一条用户消息(不接受参数;内容填回输入框可修改重发)
|
|
119
116
|
```
|
|
120
117
|
|
|
121
|
-
-
|
|
122
|
-
|
|
118
|
+
- 手动 `/rewind` 是**单动作**:不接受任何参数,直接撤回最近一条用户消息(对话回到
|
|
119
|
+
上一条消息之前)。参数化形式(`@seq chat|both`、`preview`)仍存在于 host 端,
|
|
120
|
+
但**仅供 ↶ 按钮内部调用**——client 端会在输入框拦截带参数的手动 `/rewind` 并提示
|
|
121
|
+
改用按钮。回退到更早的消息请使用该消息旁的「回退」按钮。
|
|
123
122
|
- UI 按钮与命令共享同一套 host 端回退逻辑(`/rewind @seq <mode>`)。
|
|
124
123
|
|
|
125
124
|
## 回退机制(host 端,全部公开 API)
|
|
@@ -153,7 +152,7 @@ git push --tags # push v<version> tag → 触发 .github/workflows/publish.
|
|
|
153
152
|
|
|
154
153
|
- 按钮注入锚点:用户行 `[data-chat-flow-kind="user"]`(行容器 `data-chat-anchor-key` 为节点 key);用 MutationObserver 跟踪新增行。
|
|
155
154
|
- 消息 seq 获取:从行元素的 `data-chat-anchor-key` → 运行时快照 `session.getSnapshot().chat.nodes.get(key)` → `UserMessageNode.seq`(DOM 只用于定位,数据取自 runtime,不解析 DOM 文本)。
|
|
156
|
-
-
|
|
155
|
+
- **按钮两步选择浮层**:点击消息旁按钮时,客户端接管交互——目标即该消息,第二步展示模式选项(仅回退对话 / 回退对话和代码 / 取消);确认后调 `session.command('/rewind @<seq> <mode>')` 执行。**手动 `/rewind` 不经过浮层**:它不接受参数,直接撤回最近一条消息;client 端在输入框拦截带参数的手动 `/rewind` 并提示改用按钮。
|
|
157
156
|
- 执行结果以命令节点出现在对话中。
|
|
158
157
|
- 注入按钮与「在新对话中分支」等官方操作并排,样式遵循 dsh 设计 token。
|
|
159
158
|
|
package/lib/client.js
CHANGED
|
@@ -47,7 +47,8 @@ var zh = {
|
|
|
47
47
|
"popover.impact.failed": "\u65E0\u6CD5\u83B7\u53D6\u5F71\u54CD\u6E05\u5355\uFF1A{message}",
|
|
48
48
|
"popover.impact.none": "\u76EE\u6807\u4E4B\u540E\u6CA1\u6709\u53F0\u8D26\u8BB0\u5F55\u7684\u5199\u7C7B\u53D8\u66F4\uFF0C\u65E0\u9700\u8FD8\u539F\u6587\u4EF6\u3002",
|
|
49
49
|
"popover.confirm": "\u786E\u8BA4\u56DE\u9000",
|
|
50
|
-
"popover.back": "\u8FD4\u56DE"
|
|
50
|
+
"popover.back": "\u8FD4\u56DE",
|
|
51
|
+
"guard.hint": "/rewind \u4E0D\u63A5\u53D7\u53C2\u6570\uFF0C\u53EA\u80FD\u64A4\u56DE\u6700\u8FD1\u4E00\u6761\u6D88\u606F\uFF1B\u56DE\u9000\u5230\u66F4\u65E9\u6D88\u606F\u8BF7\u7528\u8BE5\u6D88\u606F\u65C1\u7684 \u21B6 \u6309\u94AE"
|
|
51
52
|
};
|
|
52
53
|
var en = {
|
|
53
54
|
"button.aria": "Rewind to this message",
|
|
@@ -63,7 +64,8 @@ var en = {
|
|
|
63
64
|
"popover.impact.failed": "Could not fetch the impact list: {message}",
|
|
64
65
|
"popover.impact.none": "No ledger-recorded file changes after the target; nothing to restore.",
|
|
65
66
|
"popover.confirm": "Confirm rewind",
|
|
66
|
-
"popover.back": "Back"
|
|
67
|
+
"popover.back": "Back",
|
|
68
|
+
"guard.hint": "/rewind takes no parameters \u2014 it only withdraws the most recent message. To rewind to an earlier message, use the \u21B6 button on that message."
|
|
67
69
|
};
|
|
68
70
|
|
|
69
71
|
// src/client/styles.ts
|
|
@@ -79,7 +81,8 @@ var CLASS = {
|
|
|
79
81
|
popoverImpact: "dsh-rewind-popover-impact",
|
|
80
82
|
popoverActions: "dsh-rewind-popover-actions",
|
|
81
83
|
popoverPrimary: "dsh-rewind-popover-primary",
|
|
82
|
-
popoverGhost: "dsh-rewind-popover-ghost"
|
|
84
|
+
popoverGhost: "dsh-rewind-popover-ghost",
|
|
85
|
+
guardHint: "dsh-rewind-guard-hint"
|
|
83
86
|
};
|
|
84
87
|
var REWIND_ICON_SVG = [
|
|
85
88
|
'<svg width="16" height="16" viewBox="0 0 16 16" fill="none" aria-hidden="true">',
|
|
@@ -198,6 +201,20 @@ var STYLE = `
|
|
|
198
201
|
.dsh-rewind-popover-ghost:hover {
|
|
199
202
|
background: var(--dsw-alias-interactive-bg-hover);
|
|
200
203
|
}
|
|
204
|
+
|
|
205
|
+
.dsh-rewind-guard-hint {
|
|
206
|
+
position: fixed;
|
|
207
|
+
z-index: 1000;
|
|
208
|
+
max-width: min(440px, calc(100vw - 24px));
|
|
209
|
+
padding: 8px 12px;
|
|
210
|
+
border-radius: 10px;
|
|
211
|
+
background: var(--dsw-specific-surface-1, var(--dsw-alias-surface-1, #1f2127));
|
|
212
|
+
box-shadow: 0 8px 28px rgba(0, 0, 0, 0.32);
|
|
213
|
+
font-size: 13px;
|
|
214
|
+
line-height: 18px;
|
|
215
|
+
color: var(--dsw-alias-label-primary);
|
|
216
|
+
pointer-events: none;
|
|
217
|
+
}
|
|
201
218
|
`;
|
|
202
219
|
|
|
203
220
|
// src/client/popover.ts
|
|
@@ -406,10 +423,11 @@ function hiddenSeqsOf(session) {
|
|
|
406
423
|
const command = node.data;
|
|
407
424
|
if (command.name !== "rewind") continue;
|
|
408
425
|
if (command.outcome?.kind !== "success") continue;
|
|
426
|
+
if (command.outcome.sourceEventSeq === void 0) continue;
|
|
409
427
|
hidden.add(command.seq);
|
|
410
|
-
const marker = command.outcome
|
|
411
|
-
const target = targetOfOutcome(command.outcome
|
|
412
|
-
if (
|
|
428
|
+
const marker = command.outcome.sourceEventSeq;
|
|
429
|
+
const target = targetOfOutcome(command.outcome.text);
|
|
430
|
+
if (target !== void 0 && (latest === null || marker > latest.marker)) {
|
|
413
431
|
latest = { marker, target };
|
|
414
432
|
}
|
|
415
433
|
}
|
|
@@ -501,45 +519,27 @@ function apply(ctx) {
|
|
|
501
519
|
buttons.set(key, button);
|
|
502
520
|
seat.setAttribute(REWIND_ATTACHED, "");
|
|
503
521
|
};
|
|
504
|
-
let lastRewindSeq = -1;
|
|
505
|
-
let hiddenSeqsCache = null;
|
|
506
522
|
const scan = () => {
|
|
507
523
|
for (const [key, button] of buttons) {
|
|
508
524
|
if (!button.isConnected) buttons.delete(key);
|
|
509
525
|
}
|
|
510
526
|
const session = sessionFor();
|
|
511
|
-
|
|
512
|
-
if (session !== void 0) {
|
|
513
|
-
const snap = session.getSnapshot();
|
|
514
|
-
for (const key of snap.chat.order) {
|
|
515
|
-
const node = snap.chat.nodes.get(key);
|
|
516
|
-
if (node === void 0 || node.kind !== "command") continue;
|
|
517
|
-
const command = node.data;
|
|
518
|
-
if (command.name === "rewind" && command.outcome?.kind === "success" && command.seq > newestRewind) {
|
|
519
|
-
newestRewind = command.seq;
|
|
520
|
-
}
|
|
521
|
-
}
|
|
522
|
-
}
|
|
523
|
-
if (newestRewind > lastRewindSeq) {
|
|
524
|
-
lastRewindSeq = newestRewind;
|
|
525
|
-
hiddenSeqsCache = session !== void 0 ? hiddenSeqsOf(session) : null;
|
|
526
|
-
if (session !== void 0) fillComposerForRewind(session, filledTargets);
|
|
527
|
-
}
|
|
528
|
-
const hiddenSeqs = hiddenSeqsCache;
|
|
527
|
+
const hiddenSeqs = session !== void 0 ? hiddenSeqsOf(session) : /* @__PURE__ */ new Set();
|
|
529
528
|
let hiddenCount = 0;
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
hidden.delete(seat);
|
|
541
|
-
}
|
|
529
|
+
for (const seat of document.querySelectorAll(CHAT_SEAT_SELECTOR)) {
|
|
530
|
+
const key = seat.dataset.chatAnchorKey;
|
|
531
|
+
const anchor = key !== void 0 && session !== void 0 ? session.getSnapshot().chat.nodes.get(key)?.anchorSeq : void 0;
|
|
532
|
+
if (anchor !== void 0 && hiddenSeqs.has(anchor)) {
|
|
533
|
+
seat.style.display = "none";
|
|
534
|
+
hidden.add(seat);
|
|
535
|
+
hiddenCount += 1;
|
|
536
|
+
} else if (hidden.has(seat)) {
|
|
537
|
+
seat.style.display = "";
|
|
538
|
+
hidden.delete(seat);
|
|
542
539
|
}
|
|
540
|
+
}
|
|
541
|
+
if (session !== void 0) fillComposerForRewind(session, filledTargets);
|
|
542
|
+
if (hiddenSeqs.size > 0 || hiddenCount > 0) {
|
|
543
543
|
console.info(
|
|
544
544
|
`[dsh-rewind] hiding: ${hiddenCount} rows, seqs [${[...hiddenSeqs].slice(0, 20).join(", ")}${hiddenSeqs.size > 20 ? "\u2026" : ""}]`
|
|
545
545
|
);
|
|
@@ -556,6 +556,7 @@ function apply(ctx) {
|
|
|
556
556
|
if (node === void 0 || node.kind !== "command") continue;
|
|
557
557
|
const command = node.data;
|
|
558
558
|
if (command.name !== "rewind" || command.outcome?.kind !== "success") continue;
|
|
559
|
+
if (command.outcome.sourceEventSeq === void 0) continue;
|
|
559
560
|
const target = targetOfOutcome(command.outcome.text);
|
|
560
561
|
if (target === void 0 || filled.has(target)) continue;
|
|
561
562
|
const text = userTextAt(session, target);
|
|
@@ -563,10 +564,67 @@ function apply(ctx) {
|
|
|
563
564
|
if (fillComposer(text)) filled.add(target);
|
|
564
565
|
}
|
|
565
566
|
};
|
|
567
|
+
const REWIND_WITH_ARGS = /^\s*\/rewind\s+\S/i;
|
|
568
|
+
const composerTextarea = () => document.querySelector(COMPOSER_SELECTOR);
|
|
569
|
+
const hasBlockedRewindDraft = () => {
|
|
570
|
+
const textarea = composerTextarea();
|
|
571
|
+
return textarea !== null && REWIND_WITH_ARGS.test(textarea.value);
|
|
572
|
+
};
|
|
573
|
+
let guardHintEl = null;
|
|
574
|
+
let guardHintTimer;
|
|
575
|
+
const showGuardHint = () => {
|
|
576
|
+
if (guardHintEl !== null) guardHintEl.remove();
|
|
577
|
+
if (guardHintTimer !== void 0) window.clearTimeout(guardHintTimer);
|
|
578
|
+
const textarea = composerTextarea();
|
|
579
|
+
if (textarea === null) return;
|
|
580
|
+
const card = textarea.closest("[data-composer-card]");
|
|
581
|
+
const hint = document.createElement("div");
|
|
582
|
+
hint.className = CLASS.guardHint;
|
|
583
|
+
hint.setAttribute("role", "status");
|
|
584
|
+
hint.textContent = t("guard.hint");
|
|
585
|
+
document.body.appendChild(hint);
|
|
586
|
+
const rect = card instanceof HTMLElement ? card.getBoundingClientRect() : textarea.getBoundingClientRect();
|
|
587
|
+
hint.style.left = `${Math.round(rect.left)}px`;
|
|
588
|
+
hint.style.bottom = `${Math.round(window.innerHeight - rect.top + 8)}px`;
|
|
589
|
+
guardHintEl = hint;
|
|
590
|
+
guardHintTimer = window.setTimeout(() => {
|
|
591
|
+
hint.remove();
|
|
592
|
+
if (guardHintEl === hint) guardHintEl = null;
|
|
593
|
+
guardHintTimer = void 0;
|
|
594
|
+
}, 3200);
|
|
595
|
+
};
|
|
596
|
+
const onKeyDownGuard = (event) => {
|
|
597
|
+
if (event.key !== "Enter" || event.shiftKey || event.isComposing) return;
|
|
598
|
+
if (!hasBlockedRewindDraft()) return;
|
|
599
|
+
event.preventDefault();
|
|
600
|
+
event.stopPropagation();
|
|
601
|
+
showGuardHint();
|
|
602
|
+
};
|
|
603
|
+
const onClickGuard = (event) => {
|
|
604
|
+
if (event.button !== 0 || !hasBlockedRewindDraft()) return;
|
|
605
|
+
const target = event.target;
|
|
606
|
+
if (!(target instanceof Element)) return;
|
|
607
|
+
const button = target.closest("button");
|
|
608
|
+
if (button === null) return;
|
|
609
|
+
const card = button.closest("[data-composer-card]");
|
|
610
|
+
if (card === null) return;
|
|
611
|
+
const all = card.querySelectorAll("button");
|
|
612
|
+
if (all[all.length - 1] !== button) return;
|
|
613
|
+
if (button.querySelector("rect") !== null) return;
|
|
614
|
+
event.preventDefault();
|
|
615
|
+
event.stopPropagation();
|
|
616
|
+
showGuardHint();
|
|
617
|
+
};
|
|
618
|
+
document.addEventListener("keydown", onKeyDownGuard, true);
|
|
619
|
+
document.addEventListener("click", onClickGuard, true);
|
|
566
620
|
observer = new MutationObserver(scan);
|
|
567
|
-
observer.observe(document.body, { childList: true, subtree: true });
|
|
621
|
+
observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ["style"] });
|
|
568
622
|
scan();
|
|
569
623
|
yield () => {
|
|
624
|
+
document.removeEventListener("keydown", onKeyDownGuard, true);
|
|
625
|
+
document.removeEventListener("click", onClickGuard, true);
|
|
626
|
+
if (guardHintEl !== null) guardHintEl.remove();
|
|
627
|
+
if (guardHintTimer !== void 0) window.clearTimeout(guardHintTimer);
|
|
570
628
|
observer?.disconnect();
|
|
571
629
|
for (const button of buttons.values()) button.remove();
|
|
572
630
|
buttons.clear();
|
package/lib/index.js
CHANGED
|
@@ -178,12 +178,6 @@ function planRewind(events, surface, target) {
|
|
|
178
178
|
surfaceEnd: shadowedSeqs[shadowedSeqs.length - 1]
|
|
179
179
|
};
|
|
180
180
|
}
|
|
181
|
-
function formatCandidate(candidate) {
|
|
182
|
-
const time = new Date(candidate.time);
|
|
183
|
-
const hh = String(time.getHours()).padStart(2, "0");
|
|
184
|
-
const mm = String(time.getMinutes()).padStart(2, "0");
|
|
185
|
-
return `${candidate.index}. ${hh}:${mm} ${candidate.preview || "(\u65E0\u6587\u672C)"}`;
|
|
186
|
-
}
|
|
187
181
|
|
|
188
182
|
// src/index.ts
|
|
189
183
|
var name = "dsh-rewind";
|
|
@@ -192,11 +186,8 @@ var TRACKED_TOOLS = /* @__PURE__ */ new Set(["write", "edit", "str_replace_edito
|
|
|
192
186
|
var MUTATING_EDITOR_COMMANDS = /* @__PURE__ */ new Set(["create", "str_replace", "insert"]);
|
|
193
187
|
var USAGE = [
|
|
194
188
|
"Usage:",
|
|
195
|
-
" /rewind
|
|
196
|
-
"
|
|
197
|
-
" /rewind <\u5E8F\u53F7|@seq> chat rewind the conversation only",
|
|
198
|
-
" /rewind <\u5E8F\u53F7|@seq> both rewind conversation and restore files",
|
|
199
|
-
" /rewind preview <\u76EE\u6807> show the impact list without executing"
|
|
189
|
+
" /rewind \u64A4\u56DE\u6700\u8FD1\u4E00\u6761\u7528\u6237\u6D88\u606F\uFF08\u4E0D\u63A5\u53D7\u53C2\u6570\uFF09",
|
|
190
|
+
" \u56DE\u9000\u5230\u66F4\u65E9\u7684\u6D88\u606F\u8BF7\u4F7F\u7528\u8BE5\u6D88\u606F\u65C1\u7684\u300C\u56DE\u9000\u300D\u6309\u94AE"
|
|
200
191
|
].join("\n");
|
|
201
192
|
function mutationPathOf(exec) {
|
|
202
193
|
const args = exec.arguments;
|
|
@@ -387,17 +378,11 @@ async function handleRewind(ctx, ledger, invocation) {
|
|
|
387
378
|
const session = invocation.agent.session;
|
|
388
379
|
const input = invocation.rawInput.trim();
|
|
389
380
|
if (input === "") {
|
|
390
|
-
const candidates = listRewindCandidates(session.events, session.surface.nodes);
|
|
381
|
+
const candidates = listRewindCandidates(session.events, session.surface.nodes, 1);
|
|
391
382
|
if (candidates.length === 0) {
|
|
392
383
|
return { kind: "error", text: "\u5F53\u524D\u4F1A\u8BDD\u8FD8\u6CA1\u6709\u53EF\u56DE\u9000\u7684\u7528\u6237\u6D88\u606F\u3002" };
|
|
393
384
|
}
|
|
394
|
-
return {
|
|
395
|
-
kind: "success",
|
|
396
|
-
text: `\u9009\u62E9\u8981\u56DE\u9000\u5230\u7684\u7528\u6237\u6D88\u606F\uFF08\u6700\u8FD1\u5728\u4E0A\uFF09\uFF1A
|
|
397
|
-
${candidates.map(formatCandidate).join("\n")}
|
|
398
|
-
|
|
399
|
-
\u7EE7\u7EED\uFF1A/rewind <\u5E8F\u53F7|@seq>\uFF0C\u6216\u76F4\u63A5 /rewind <\u5E8F\u53F7|@seq> chat|both \u6267\u884C\u3002`
|
|
400
|
-
};
|
|
385
|
+
return executeRewind(ctx, ledger, invocation, `@${candidates[0].seq}`, "chat");
|
|
401
386
|
}
|
|
402
387
|
const parts = input.split(/\s+/);
|
|
403
388
|
if (parts[0] === "preview") {
|
|
@@ -16,6 +16,11 @@
|
|
|
16
16
|
* before executing. Execution always goes through `session.command(...)`, the
|
|
17
17
|
* same host path the `/rewind` command uses.
|
|
18
18
|
*
|
|
19
|
+
* Manual composer input of `/rewind` is deliberately restricted (the guard
|
|
20
|
+
* below): it takes no parameters — a bare `/rewind` withdraws the most recent
|
|
21
|
+
* message, and any `/rewind <args>` line is blocked with a hint pointing at
|
|
22
|
+
* the per-message button.
|
|
23
|
+
*
|
|
19
24
|
* @module dsh-rewind/client
|
|
20
25
|
*/
|
|
21
26
|
import type { ClientContext } from '@deepseek-ai/dsh-client-runtime/client';
|
|
@@ -15,6 +15,7 @@ export declare const zh: {
|
|
|
15
15
|
'popover.impact.none': string;
|
|
16
16
|
'popover.confirm': string;
|
|
17
17
|
'popover.back': string;
|
|
18
|
+
'guard.hint': string;
|
|
18
19
|
};
|
|
19
20
|
/** The rewind namespace key union. */
|
|
20
21
|
export type RewindKey = keyof typeof zh;
|
|
@@ -40,4 +41,5 @@ export declare const en: {
|
|
|
40
41
|
'popover.impact.none': string;
|
|
41
42
|
'popover.confirm': string;
|
|
42
43
|
'popover.back': string;
|
|
44
|
+
'guard.hint': string;
|
|
43
45
|
};
|
|
@@ -20,8 +20,9 @@ export declare const CLASS: {
|
|
|
20
20
|
readonly popoverActions: 'dsh-rewind-popover-actions';
|
|
21
21
|
readonly popoverPrimary: 'dsh-rewind-popover-primary';
|
|
22
22
|
readonly popoverGhost: 'dsh-rewind-popover-ghost';
|
|
23
|
+
readonly guardHint: 'dsh-rewind-guard-hint';
|
|
23
24
|
};
|
|
24
25
|
/** The ↶ glyph, drawn inline so the bundle stays dependency-free. */
|
|
25
26
|
export declare const REWIND_ICON_SVG: string;
|
|
26
27
|
/** One injected stylesheet (scoped under `.dsh-rewind-*`). */
|
|
27
|
-
export declare const STYLE = "\n.dsh-rewind-btn {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 28px;\n height: 28px;\n padding: 6px;\n border: none;\n border-radius: 28px;\n background: transparent;\n color: var(--dsw-alias-label-tertiary);\n cursor: pointer;\n}\n.dsh-rewind-btn:hover {\n background: var(--dsw-alias-interactive-bg-hover);\n color: var(--dsw-alias-label-secondary);\n}\n\n.dsh-rewind-popover {\n position: fixed;\n z-index: 1000;\n width: 288px;\n padding: 12px;\n border-radius: 12px;\n background: var(--dsw-specific-surface-1, var(--dsw-alias-surface-1, #1f2127));\n box-shadow: 0 8px 28px rgba(0, 0, 0, 0.32);\n font-size: 14px;\n line-height: 20px;\n color: var(--dsw-alias-label-primary);\n}\n.dsh-rewind-popover-title {\n font-size: 14px;\n font-weight: 600;\n line-height: 20px;\n}\n.dsh-rewind-popover-target {\n margin: 4px 0 10px;\n font-size: 12px;\n line-height: 16px;\n color: var(--dsw-alias-label-tertiary);\n word-break: break-all;\n}\n.dsh-rewind-popover-option {\n display: flex;\n flex-direction: column;\n gap: 2px;\n width: 100%;\n margin: 0 0 6px;\n padding: 8px 10px;\n border: 1px solid transparent;\n border-radius: 8px;\n background: transparent;\n color: inherit;\n font: inherit;\n text-align: left;\n cursor: pointer;\n}\n.dsh-rewind-popover-option:hover {\n background: var(--dsw-alias-interactive-bg-hover);\n}\n.dsh-rewind-popover-option-label {\n font-weight: 500;\n}\n.dsh-rewind-popover-option-hint {\n font-size: 12px;\n line-height: 16px;\n color: var(--dsw-alias-label-tertiary);\n}\n.dsh-rewind-popover-impact {\n margin: 4px 0 10px;\n padding: 8px 10px;\n border-radius: 8px;\n background: var(--dsw-alias-interactive-bg-hover);\n font-size: 12px;\n line-height: 16px;\n color: var(--dsw-alias-label-secondary);\n white-space: pre-wrap;\n max-height: 160px;\n overflow: auto;\n}\n.dsh-rewind-popover-actions {\n display: flex;\n justify-content: flex-end;\n gap: 8px;\n}\n.dsh-rewind-popover-primary,\n.dsh-rewind-popover-ghost {\n padding: 5px 12px;\n border: none;\n border-radius: 8px;\n font: inherit;\n font-size: 13px;\n line-height: 18px;\n cursor: pointer;\n}\n.dsh-rewind-popover-primary {\n background: var(--dsw-alias-accent, var(--dsw-accent, #5b8cff));\n color: var(--dsw-alias-on-accent, #fff);\n}\n.dsh-rewind-popover-primary:disabled {\n opacity: 0.5;\n cursor: default;\n}\n.dsh-rewind-popover-ghost {\n background: transparent;\n color: var(--dsw-alias-label-secondary);\n}\n.dsh-rewind-popover-ghost:hover {\n background: var(--dsw-alias-interactive-bg-hover);\n}\n";
|
|
28
|
+
export declare const STYLE = "\n.dsh-rewind-btn {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 28px;\n height: 28px;\n padding: 6px;\n border: none;\n border-radius: 28px;\n background: transparent;\n color: var(--dsw-alias-label-tertiary);\n cursor: pointer;\n}\n.dsh-rewind-btn:hover {\n background: var(--dsw-alias-interactive-bg-hover);\n color: var(--dsw-alias-label-secondary);\n}\n\n.dsh-rewind-popover {\n position: fixed;\n z-index: 1000;\n width: 288px;\n padding: 12px;\n border-radius: 12px;\n background: var(--dsw-specific-surface-1, var(--dsw-alias-surface-1, #1f2127));\n box-shadow: 0 8px 28px rgba(0, 0, 0, 0.32);\n font-size: 14px;\n line-height: 20px;\n color: var(--dsw-alias-label-primary);\n}\n.dsh-rewind-popover-title {\n font-size: 14px;\n font-weight: 600;\n line-height: 20px;\n}\n.dsh-rewind-popover-target {\n margin: 4px 0 10px;\n font-size: 12px;\n line-height: 16px;\n color: var(--dsw-alias-label-tertiary);\n word-break: break-all;\n}\n.dsh-rewind-popover-option {\n display: flex;\n flex-direction: column;\n gap: 2px;\n width: 100%;\n margin: 0 0 6px;\n padding: 8px 10px;\n border: 1px solid transparent;\n border-radius: 8px;\n background: transparent;\n color: inherit;\n font: inherit;\n text-align: left;\n cursor: pointer;\n}\n.dsh-rewind-popover-option:hover {\n background: var(--dsw-alias-interactive-bg-hover);\n}\n.dsh-rewind-popover-option-label {\n font-weight: 500;\n}\n.dsh-rewind-popover-option-hint {\n font-size: 12px;\n line-height: 16px;\n color: var(--dsw-alias-label-tertiary);\n}\n.dsh-rewind-popover-impact {\n margin: 4px 0 10px;\n padding: 8px 10px;\n border-radius: 8px;\n background: var(--dsw-alias-interactive-bg-hover);\n font-size: 12px;\n line-height: 16px;\n color: var(--dsw-alias-label-secondary);\n white-space: pre-wrap;\n max-height: 160px;\n overflow: auto;\n}\n.dsh-rewind-popover-actions {\n display: flex;\n justify-content: flex-end;\n gap: 8px;\n}\n.dsh-rewind-popover-primary,\n.dsh-rewind-popover-ghost {\n padding: 5px 12px;\n border: none;\n border-radius: 8px;\n font: inherit;\n font-size: 13px;\n line-height: 18px;\n cursor: pointer;\n}\n.dsh-rewind-popover-primary {\n background: var(--dsw-alias-accent, var(--dsw-accent, #5b8cff));\n color: var(--dsw-alias-on-accent, #fff);\n}\n.dsh-rewind-popover-primary:disabled {\n opacity: 0.5;\n cursor: default;\n}\n.dsh-rewind-popover-ghost {\n background: transparent;\n color: var(--dsw-alias-label-secondary);\n}\n.dsh-rewind-popover-ghost:hover {\n background: var(--dsw-alias-interactive-bg-hover);\n}\n\n.dsh-rewind-guard-hint {\n position: fixed;\n z-index: 1000;\n max-width: min(440px, calc(100vw - 24px));\n padding: 8px 12px;\n border-radius: 10px;\n background: var(--dsw-specific-surface-1, var(--dsw-alias-surface-1, #1f2127));\n box-shadow: 0 8px 28px rgba(0, 0, 0, 0.32);\n font-size: 13px;\n line-height: 18px;\n color: var(--dsw-alias-label-primary);\n pointer-events: none;\n}\n";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-rewind-plugin",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.10",
|
|
4
4
|
"description": "DeepSeek Harness plugin: in-place conversation rewind in the same session window (Claude Code /rewind semantics) with optional workspace file restore",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"deepseek-harness",
|
package/scripts/verify-host.mjs
CHANGED
|
@@ -8,8 +8,8 @@
|
|
|
8
8
|
*
|
|
9
9
|
* What it proves:
|
|
10
10
|
* 1. the plugin registers a `rewind` command on the ctx;
|
|
11
|
-
* 2. `/rewind` (no args)
|
|
12
|
-
* 3. `/rewind @<seq> chat` cuts the surface in-place (log untouched);
|
|
11
|
+
* 2. `/rewind` (no args) withdraws the most recent user message;
|
|
12
|
+
* 3. `/rewind @<seq> chat` (the button's call form) cuts the surface in-place (log untouched);
|
|
13
13
|
* 4. the ledger captures through `tools/execute` (NOT pre-execute): a
|
|
14
14
|
* pre-execute `ask` short-circuit still gets captured after approval, and
|
|
15
15
|
* a denied call never captures (no pending leak);
|
|
@@ -86,17 +86,25 @@ const check = (name, ok, detail) => {
|
|
|
86
86
|
// 1. command registered
|
|
87
87
|
check('command registered', typeof registered?.handler === 'function' && registered.name === 'rewind', JSON.stringify(registered))
|
|
88
88
|
|
|
89
|
-
// 2. bare /rewind
|
|
90
|
-
|
|
91
|
-
|
|
89
|
+
// 2. bare /rewind (manual, no parameters) withdraws the most recent user
|
|
90
|
+
// message (seq 2 "second question") and everything after it
|
|
91
|
+
const bareBefore = [...session.surface.nodes]
|
|
92
|
+
const bareResult = await call(agent, '')
|
|
93
|
+
const bareAfter = [...session.surface.nodes]
|
|
94
|
+
check('bare /rewind succeeds', bareResult.kind === 'success', bareResult.text)
|
|
95
|
+
check('bare /rewind withdraws the latest message', bareAfter.length === 3 && bareAfter[0] === 0 && bareAfter[1] === 1 && bareAfter[2] > 3, `before ${JSON.stringify(bareBefore)} -> after ${JSON.stringify(bareAfter)}`)
|
|
96
|
+
check('log stays append-only (5 events)', session.events.length === 5, `events=${session.events.length}`)
|
|
92
97
|
|
|
93
|
-
// 3. /rewind
|
|
94
|
-
|
|
95
|
-
const
|
|
96
|
-
const
|
|
98
|
+
// 3. /rewind @<seq> chat (the button's exact call form) cuts the surface on a
|
|
99
|
+
// fresh session
|
|
100
|
+
const paramSession = buildSession('verify-param')
|
|
101
|
+
const paramAgent = { id: paramSession.id, session: paramSession, status: 'idle' }
|
|
102
|
+
const before = [...paramSession.surface.nodes]
|
|
103
|
+
const chatResult = await call(paramAgent, '@2 chat')
|
|
104
|
+
const after = [...paramSession.surface.nodes]
|
|
97
105
|
check('rewind chat succeeds', chatResult.kind === 'success', chatResult.text)
|
|
98
106
|
check('surface cut to [0,1,marker] (target withdrawn)', after.length === 3 && after[0] === 0 && after[1] === 1 && after[2] > 3, `before ${JSON.stringify(before)} -> after ${JSON.stringify(after)}`)
|
|
99
|
-
check('log stays append-only (5 events)',
|
|
107
|
+
check('log stays append-only (5 events)', paramSession.events.length === 5, `events=${paramSession.events.length}`)
|
|
100
108
|
|
|
101
109
|
const writeExec = (callId, filePath, content) => ({
|
|
102
110
|
callId, name: 'write', arguments: { file_path: filePath, content }, agent, signal: aborted(),
|