dsh-rewind-plugin 0.7.0 → 0.7.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 +4 -0
- package/docs/README.md +1 -0
- package/docs/compat/audit.md +15 -24
- package/docs/compat/diagnostics.md +82 -0
- package/docs/compat/diagnostics.zh.md +63 -0
- package/lib/client.js +145 -76
- package/lib/index.js +15 -7
- package/lib/types/client/log.d.ts +43 -0
- package/lib/types/client/portals.d.ts +22 -0
- package/lib/types/client/styles.d.ts +1 -1
- package/lib/types/session-events.d.ts +34 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -145,6 +145,10 @@ dsh plugin --profile web add dsh-rewind-plugin
|
|
|
145
145
|
4. **v0.3.3 及更早版本**回退过的会话,压缩对话(compact)不可用。新版本已兼容;受影响的旧会话建议新建会话。
|
|
146
146
|
5. **导轨显示已回退轮次**——DSH `v0.1.2-alpha.1` 新增的右侧导轨,会为已撤回消息保留刻度:点击不跳转、悬浮显示已撤回正文。仅显示差异,无功能影响。
|
|
147
147
|
|
|
148
|
+
> [!NOTE]
|
|
149
|
+
> 若使用中遇到问题,可开启详细诊断输出,复现后附上控制台 `[dsh-rewind]` 输出便于定位。详见
|
|
150
|
+
> [浏览器诊断与详细输出开关](docs/compat/diagnostics.zh.md)。
|
|
151
|
+
|
|
148
152
|
## 安全
|
|
149
153
|
|
|
150
154
|
本插件只向会话日志追加回退标记事件,从不删除或改写已记录的历史。工作区文件仅在「回退对话和代码」时被改写,备份存储于 `~/.dsh/rewind-snapshots/`;还原以备份为唯一来源。不触碰你的 git 仓库,无网络请求,不访问任何凭据。对**长期不活跃**的会话,另有默认关闭的全局自动清理可整目录移除其快照,不影响活动会话与对话日志。完整安全模型:[SECURITY.md](SECURITY.md)。
|
package/docs/README.md
CHANGED
|
@@ -15,6 +15,7 @@ index/navigation entry point.
|
|
|
15
15
|
| `contract/client-contract.md` | Rewind visibility contract for third-party DOM plugins (`.zh` mirror) | integrators |
|
|
16
16
|
| `compat/audit.md` | Compatibility audit: verified surfaces, recorded findings, probe matrix | maintainers |
|
|
17
17
|
| `compat/troubleshooting.md` | Known issues & repair steps (`.zh` mirror) | users / maintainers |
|
|
18
|
+
| `compat/diagnostics.md` | Browser diagnostics & the verbose output switch (`.zh` mirror) | users / maintainers |
|
|
18
19
|
| `release/release.md` | Release workflow & DSH peer-version alignment (`.zh` mirror) | maintainers |
|
|
19
20
|
|
|
20
21
|
Repo-root docs outside `docs/`: `SECURITY.md` (security model) and
|
package/docs/compat/audit.md
CHANGED
|
@@ -22,30 +22,21 @@
|
|
|
22
22
|
>
|
|
23
23
|
> `0.1.2-alpha.1` was never published to npm, so the peer OR-union is declared only from `0.1.2-alpha.2`; the plugin code still supports the `alpha.1`+ client.
|
|
24
24
|
>
|
|
25
|
-
> `0.1.2-alpha.
|
|
26
|
-
>
|
|
27
|
-
>
|
|
28
|
-
>
|
|
29
|
-
>
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
> The composer refill is dual-channel the same way: on alpha the withdrawn text is
|
|
41
|
-
> written through the `conversation` service's `input` resolver's `setDraft` (the
|
|
42
|
-
> harness's own Lexical editor), else the rc.2 `<textarea>` / alpha `contenteditable`
|
|
43
|
-
> DOM write (`writeComposer` in `src/client/portals.tsx`); pinned by
|
|
44
|
-
> `tests/client-composer.test.ts`. The session-seat button DOM is dual-channel
|
|
45
|
-
> the same way: the actions (copy/branch) row was located by `[data-time-hover-root]`
|
|
46
|
-
> on ≤ 0.1.1-rc.x and by `[data-actions-reveal]` on 0.1.2-alpha.2
|
|
47
|
-
> (`ACTIONS_ROOT_SELECTOR` / `PENDING_SEAT_SELECTOR` in `src/client/portals.tsx`);
|
|
48
|
-
> pinned by `tests/client-dom.test.ts`.
|
|
25
|
+
> `0.1.2-alpha.2` … `0.1.2-alpha.4` verified-compatible. Built/tested on the rc.2
|
|
26
|
+
> baseline (the `latest` dist-tag); the same published build also drives the alpha
|
|
27
|
+
> line, and `alpha.2`/`alpha.3`/`alpha.4` share the `0.1.2` tuple, so the single
|
|
28
|
+
> `^0.1.2-alpha.2` peer member covers all three. `rc.2` (`latest`) remains the
|
|
29
|
+
> primary baseline; four versions verified: rc.2, alpha.2, alpha.3, alpha.4.
|
|
30
|
+
|
|
31
|
+
### Channeled seams (version × channel)
|
|
32
|
+
|
|
33
|
+
| Seam | rc.2 (`0.1.1-rc.2`) | alpha.2 / alpha.3 | alpha.4 |
|
|
34
|
+
| --- | --- | --- | --- |
|
|
35
|
+
| Host session log (`eventsOf`) | `Session.events` | `Session.events` | `session.snapshotEvents()` |
|
|
36
|
+
| Client chat snapshot (`chatSnapshotOf`) | session-face `chat` field | `uiConversation` `chat` view | `uiConversation` `chat` view |
|
|
37
|
+
| Client composer refill (`writeComposer`) | `<textarea>` DOM write | `conversation.input.setDraft` | `conversation.input.setDraft` |
|
|
38
|
+
| Client settings card | nested `ctx.inject(['settingsScope'])` | nested `ctx.inject(['settingsScope'])` | nested `ctx.inject(['settingsScope'])` |
|
|
39
|
+
| Client seat-button DOM (`actionsContainerOf`) | `[data-time-hover-root]` | `[data-actions-reveal]` | structural locate of the copy-`<button>` container |
|
|
49
40
|
|
|
50
41
|
## Definition of "fully compatible" (invariants)
|
|
51
42
|
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# Browser diagnostics and the verbose switch
|
|
2
|
+
|
|
3
|
+
[简体中文](diagnostics.zh.md)
|
|
4
|
+
|
|
5
|
+
Every browser-side diagnostic this plugin emits goes through one small
|
|
6
|
+
channel, so a report can be captured with a single console filter and a
|
|
7
|
+
verbose mode can be switched on without a plugin rebuild.
|
|
8
|
+
|
|
9
|
+
## Scope-prefixed output
|
|
10
|
+
|
|
11
|
+
Diagnostics are printed as `[dsh-rewind:<scope>] ...`. The scope names the
|
|
12
|
+
subsystem, and is also what the verbose switch filters on:
|
|
13
|
+
|
|
14
|
+
| Scope | What it reports |
|
|
15
|
+
| --- | --- |
|
|
16
|
+
| `hiding` | The row-hiding path (which rows a rewind cuts; the `rewind not hidden` anomaly) |
|
|
17
|
+
| `refill` | The composer refill after a rewind (target seq, mode, used channel, write result) |
|
|
18
|
+
| `portals` | Per-message button mount issues (e.g. no session binding) |
|
|
19
|
+
| `settings` | The snapshot-cleanup settings card |
|
|
20
|
+
|
|
21
|
+
## Levels
|
|
22
|
+
|
|
23
|
+
| Level | Default | Meaning |
|
|
24
|
+
| --- | --- | --- |
|
|
25
|
+
| `error` | on | Unexpected/breaking; always printed |
|
|
26
|
+
| `warn` | on | Recoverable anomaly guard (`rewind not hidden`, `refill skipped/refused/threw`); always printed |
|
|
27
|
+
| `info` | off | Event-level lifecycle (one line per rewind / refill) |
|
|
28
|
+
| `debug` | off | Additional event-level detail behind the switch (the hide-set line, one per rewind) |
|
|
29
|
+
|
|
30
|
+
`error`/`warn` are always printed so an anomaly surfaces even for a user who
|
|
31
|
+
never touched the switch; `info`/`debug` are gated so a normal user's console
|
|
32
|
+
stays clean. Both gated levels emit once per relevant event (a rewind, a
|
|
33
|
+
refill), never per streaming frame — so a rewound session does not flood the
|
|
34
|
+
console even with verbose output switched on.
|
|
35
|
+
|
|
36
|
+
## Enabling verbose output
|
|
37
|
+
|
|
38
|
+
The switch lives in the **browser's** `localStorage` under the plugin's own
|
|
39
|
+
key, so it can never enable another plugin/feature and nothing else can wake
|
|
40
|
+
this one. Set it, reload the page, reproduce, then filter the console for
|
|
41
|
+
`[dsh-rewind]`:
|
|
42
|
+
|
|
43
|
+
```js
|
|
44
|
+
// Every dsh-rewind namespace.
|
|
45
|
+
localStorage['dsh-rewind.debug'] = 'dsh-rewind*'
|
|
46
|
+
|
|
47
|
+
// Or just the subsystem you care about (exact scope match).
|
|
48
|
+
localStorage['dsh-rewind.debug'] = 'dsh-rewind:refill'
|
|
49
|
+
|
|
50
|
+
// Several at once (comma-separated).
|
|
51
|
+
localStorage['dsh-rewind.debug'] = 'dsh-rewind:refill,dsh-rewind:hiding'
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Reload (`F5`) after setting it. To switch it off:
|
|
55
|
+
|
|
56
|
+
```js
|
|
57
|
+
delete localStorage['dsh-rewind.debug']
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Capturing a report
|
|
61
|
+
|
|
62
|
+
1. On the machine/browser page that reproduces the problem, enable the
|
|
63
|
+
relevant namespace (see above) and reload.
|
|
64
|
+
2. Reproduce once.
|
|
65
|
+
3. In DevTools, filter the Console for `[dsh-rewind]` and copy the output
|
|
66
|
+
(with the plugin version and the DSH/kernel version).
|
|
67
|
+
|
|
68
|
+
For a rewind that did not refill the composer, the `refill` scope is what
|
|
69
|
+
matters: its `composer write` line reports whether the draft was restored
|
|
70
|
+
through the harness facade (`facade`) or the DOM fallback (`dom`) and whether
|
|
71
|
+
the write succeeded, which tells a plugin fault apart from a harness-side
|
|
72
|
+
render desync.
|
|
73
|
+
|
|
74
|
+
## Notes
|
|
75
|
+
|
|
76
|
+
- The switch is an aid for maintainers and cooperating reporters, **not** a
|
|
77
|
+
stable public interface; its exact keys and output may change without notice.
|
|
78
|
+
- It is scoped to one browser origin and one browser; enable it where the
|
|
79
|
+
problem actually occurs.
|
|
80
|
+
|
|
81
|
+
Related: [audit.md](audit.md) for the verified-compatibility matrix and
|
|
82
|
+
[troubleshooting.md](troubleshooting.md) for the legacy repair steps.
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# 浏览器诊断与详细输出开关
|
|
2
|
+
|
|
3
|
+
[English](diagnostics.md)
|
|
4
|
+
|
|
5
|
+
本插件在浏览器端输出的所有诊断信息都经由同一条通道,这样可用一个控制台过滤器抓取报告,且无需重建插件即可开启详细输出模式。
|
|
6
|
+
|
|
7
|
+
## 按作用域前缀输出
|
|
8
|
+
|
|
9
|
+
诊断以 `[dsh-rewind:<scope>] ...` 形式打印。`scope` 标明子系统,同时也正是详细输出开关的过滤依据:
|
|
10
|
+
|
|
11
|
+
| scope | 报告内容 |
|
|
12
|
+
| --- | --- |
|
|
13
|
+
| `hiding` | 行隐藏路径(回退切断哪些行;`rewind not hidden` 异常) |
|
|
14
|
+
| `refill` | 回退后的输入框回填(目标 seq、模式、所用通道、写入结果) |
|
|
15
|
+
| `portals` | 每条消息按钮的挂载问题(如无会话绑定) |
|
|
16
|
+
| `settings` | 快照清理设置卡片 |
|
|
17
|
+
|
|
18
|
+
## 分级
|
|
19
|
+
|
|
20
|
+
| 级别 | 默认 | 含义 |
|
|
21
|
+
| --- | --- | --- |
|
|
22
|
+
| `error` | 开 | 意外/破坏性;总是打印 |
|
|
23
|
+
| `warn` | 开 | 可恢复的异常护栏(`rewind not hidden`、`refill skipped/refused/threw`);总是打印 |
|
|
24
|
+
| `info` | 关 | 事件级生命周期(每次回退/回填一行) |
|
|
25
|
+
| `debug` | 关 | 开关控制下的追加事件级细节(隐藏集一行,每次回退一行) |
|
|
26
|
+
|
|
27
|
+
`error`/`warn` 总是打印,这样即使没碰过开关的用户,异常也会浮现;`info`/`debug` 则受开关控制,保持普通用户控制台干净。两个受控级别都按**相关事件触发一次**(一次回退、一次回填),不会随流式逐帧打印——所以即使开了详细输出,回退后的会话也不会刷屏。
|
|
28
|
+
|
|
29
|
+
## 开启详细输出
|
|
30
|
+
|
|
31
|
+
开关位于**浏览器**的 `localStorage`、且使用插件专属键,因此它不会开启其它插件/功能,别的功能也无法唤醒本插件。设置后刷新页面、复现一次,再把控制台按 `[dsh-rewind]` 过滤即可:
|
|
32
|
+
|
|
33
|
+
```js
|
|
34
|
+
// 全部 dsh-rewind 命名空间。
|
|
35
|
+
localStorage['dsh-rewind.debug'] = 'dsh-rewind*'
|
|
36
|
+
|
|
37
|
+
// 或只开所关心的子系统(精确 scope 匹配)。
|
|
38
|
+
localStorage['dsh-rewind.debug'] = 'dsh-rewind:refill'
|
|
39
|
+
|
|
40
|
+
// 同时开多个(逗号分隔)。
|
|
41
|
+
localStorage['dsh-rewind.debug'] = 'dsh-rewind:refill,dsh-rewind:hiding'
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
设置后刷新(`F5`)。关闭:
|
|
45
|
+
|
|
46
|
+
```js
|
|
47
|
+
delete localStorage['dsh-rewind.debug']
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## 采集一段报告
|
|
51
|
+
|
|
52
|
+
1. 在**复现问题的那台机器/那个浏览器页面**,开启相应命名空间(见上)并刷新。
|
|
53
|
+
2. 复现一次。
|
|
54
|
+
3. 在 DevTools 里把 Console 按 `[dsh-rewind]` 过滤,复制输出(连同插件版本、DSH/内核版本)。
|
|
55
|
+
|
|
56
|
+
对「回退后没回填输入框」这类问题,重点是 `refill` 作用域:其 `composer write` 一行会报告草稿是通过 harness facade(`facade`)还是 DOM 回退(`dom`)恢复、写入是否成功,从而区分「插件没写入」与「harness 侧渲染不同步」。
|
|
57
|
+
|
|
58
|
+
## 备注
|
|
59
|
+
|
|
60
|
+
- 该开关是维护者与配合排查者使用的工具,**不是**稳定公开接口;其具体键与输出可能不经通知而改变。
|
|
61
|
+
- 它受单个浏览器 origin、单个浏览器限定;请在实际出问题的地方开启。
|
|
62
|
+
|
|
63
|
+
相关:[audit.md](audit.md) 为已验证兼容矩阵,[troubleshooting.md](troubleshooting.md) 为历史修复步骤。
|
package/lib/client.js
CHANGED
|
@@ -483,6 +483,7 @@ var STYLE = `
|
|
|
483
483
|
width: 16px;
|
|
484
484
|
height: 16px;
|
|
485
485
|
border-radius: 50%;
|
|
486
|
+
corner-shape: round;
|
|
486
487
|
background: var(--dsw-alias-label-primary-foreground);
|
|
487
488
|
transition: transform 120ms ease;
|
|
488
489
|
}
|
|
@@ -649,8 +650,8 @@ async function previewImpact(session, chatOf, seq) {
|
|
|
649
650
|
if (!result.ok || result.value?.matched !== true) return null;
|
|
650
651
|
return waitForCommand(session, chatOf, (node) => isPreviewFor(node, seq) && !known.has(node.seq));
|
|
651
652
|
}
|
|
652
|
-
function el(
|
|
653
|
-
const node = document.createElement(
|
|
653
|
+
function el(tag2, className, text) {
|
|
654
|
+
const node = document.createElement(tag2);
|
|
654
655
|
node.className = className;
|
|
655
656
|
if (text !== void 0) node.textContent = text;
|
|
656
657
|
return node;
|
|
@@ -930,6 +931,48 @@ function retractSpan(steering, targetId) {
|
|
|
930
931
|
return steering.slice(index).map((item) => item.id);
|
|
931
932
|
}
|
|
932
933
|
|
|
934
|
+
// src/client/log.ts
|
|
935
|
+
var NS = "dsh-rewind";
|
|
936
|
+
var DEBUG_KEY = "dsh-rewind.debug";
|
|
937
|
+
var ALWAYS_ON = /* @__PURE__ */ new Set(["error", "warn"]);
|
|
938
|
+
function switchValue() {
|
|
939
|
+
try {
|
|
940
|
+
return window.localStorage.getItem(DEBUG_KEY) ?? "";
|
|
941
|
+
} catch {
|
|
942
|
+
return "";
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
function matches(value, ns) {
|
|
946
|
+
for (const entry of value.split(",")) {
|
|
947
|
+
const part = entry.trim();
|
|
948
|
+
if (part === "") continue;
|
|
949
|
+
if (part === "*" || part === `${NS}*`) return true;
|
|
950
|
+
if (part.endsWith("*")) {
|
|
951
|
+
if (ns.startsWith(part.slice(0, -1))) return true;
|
|
952
|
+
} else if (ns === part) {
|
|
953
|
+
return true;
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
return false;
|
|
957
|
+
}
|
|
958
|
+
function tag(scope) {
|
|
959
|
+
return `[${NS}:${scope}]`;
|
|
960
|
+
}
|
|
961
|
+
function log(level, scope, message, data) {
|
|
962
|
+
if (ALWAYS_ON.has(level)) {
|
|
963
|
+
console[level](tag(scope), message, data);
|
|
964
|
+
return;
|
|
965
|
+
}
|
|
966
|
+
if (!matches(switchValue(), `${NS}:${scope}`)) return;
|
|
967
|
+
console.info(tag(scope), message, data);
|
|
968
|
+
}
|
|
969
|
+
var rewindLog = {
|
|
970
|
+
error: (scope, message, data) => log("error", scope, message, data),
|
|
971
|
+
warn: (scope, message, data) => log("warn", scope, message, data),
|
|
972
|
+
info: (scope, message, data) => log("info", scope, message, data),
|
|
973
|
+
debug: (scope, message, data) => log("debug", scope, message, data)
|
|
974
|
+
};
|
|
975
|
+
|
|
933
976
|
// src/client/portals.tsx
|
|
934
977
|
var import_jsx_runtime = require("react/jsx-runtime");
|
|
935
978
|
function fillComposerTextarea(text) {
|
|
@@ -984,18 +1027,73 @@ function userNodeOf(chat, key) {
|
|
|
984
1027
|
}
|
|
985
1028
|
async function runRewindAndFill(session, seq, mode, currentSessionId, chatOf, setComposerText) {
|
|
986
1029
|
const known = knownCommandSeqs(session, chatOf, (node) => isExecutedRewindCommand(node, seq));
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
1030
|
+
let result;
|
|
1031
|
+
try {
|
|
1032
|
+
result = await session.command(`/rewind @${seq} ${mode}`);
|
|
1033
|
+
} catch (error) {
|
|
1034
|
+
rewindLog.warn("refill", `rewind command threw, skipping refill @${seq}`, error);
|
|
1035
|
+
return;
|
|
1036
|
+
}
|
|
1037
|
+
if (!result.ok || result.value?.matched !== true) {
|
|
1038
|
+
rewindLog.info("refill", `rewind @${seq} not matched, no refill`);
|
|
1039
|
+
return;
|
|
1040
|
+
}
|
|
1041
|
+
let outcome;
|
|
1042
|
+
try {
|
|
1043
|
+
outcome = await waitForCommand(session, chatOf, (node) => isExecutedRewindCommand(node, seq) && !known.has(node.seq), 2e4);
|
|
1044
|
+
} catch (error) {
|
|
1045
|
+
rewindLog.warn("refill", `waiting for rewind @${seq} outcome threw`, error);
|
|
1046
|
+
return;
|
|
1047
|
+
}
|
|
1048
|
+
if (outcome === null) {
|
|
1049
|
+
rewindLog.warn("refill", `rewind @${seq} never settled within timeout, no refill`);
|
|
1050
|
+
return;
|
|
1051
|
+
}
|
|
991
1052
|
if (outcome.kind !== "success") {
|
|
1053
|
+
rewindLog.warn("refill", `rewind @${seq} refused`, outcome.text);
|
|
992
1054
|
showHint(outcome.text ?? "rewind failed");
|
|
993
1055
|
return;
|
|
994
1056
|
}
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
1057
|
+
const hidSeqs = (() => {
|
|
1058
|
+
const chat = chatOf(session);
|
|
1059
|
+
return chat === void 0 ? /* @__PURE__ */ new Set() : hiddenSeqsOf(chat);
|
|
1060
|
+
})();
|
|
1061
|
+
if (hidSeqs.size === 0) {
|
|
1062
|
+
rewindLog.warn("hiding", `rewind not hidden (target @${seq})`, { target: seq });
|
|
1063
|
+
} else {
|
|
1064
|
+
rewindLog.debug(
|
|
1065
|
+
"hiding",
|
|
1066
|
+
`rewind hides seqs [${[...hidSeqs].slice(0, 20).join(", ")}${hidSeqs.size > 20 ? "\u2026" : ""}]`,
|
|
1067
|
+
{ target: seq }
|
|
1068
|
+
);
|
|
1069
|
+
}
|
|
1070
|
+
if (currentSessionId() !== session.sessionId) {
|
|
1071
|
+
rewindLog.info("refill", `skipped refill @${seq}: session switched during rewind`);
|
|
1072
|
+
return;
|
|
1073
|
+
}
|
|
1074
|
+
let text;
|
|
1075
|
+
try {
|
|
1076
|
+
text = messageTextAt(chatOf(session), seq);
|
|
1077
|
+
} catch (error) {
|
|
1078
|
+
rewindLog.warn("refill", `reading target text for @${seq} threw`, error);
|
|
1079
|
+
return;
|
|
1080
|
+
}
|
|
1081
|
+
if (text === void 0 || text === "") {
|
|
1082
|
+
rewindLog.info("refill", `skipped refill @${seq}: no editable text`);
|
|
1083
|
+
return;
|
|
1084
|
+
}
|
|
1085
|
+
if (composerText().trim() !== "") {
|
|
1086
|
+
rewindLog.info("refill", `skipped refill @${seq}: composer already has a draft`);
|
|
1087
|
+
return;
|
|
1088
|
+
}
|
|
1089
|
+
let ok = false;
|
|
1090
|
+
try {
|
|
1091
|
+
ok = setComposerText(session.sessionId, text);
|
|
1092
|
+
} catch (error) {
|
|
1093
|
+
rewindLog.warn("refill", `composer refill @${seq} threw`, error);
|
|
1094
|
+
return;
|
|
1095
|
+
}
|
|
1096
|
+
rewindLog.info("refill", `rewound to @${seq} (${mode})`, { ok, text: text.slice(0, 80) });
|
|
999
1097
|
}
|
|
1000
1098
|
function composerSurface() {
|
|
1001
1099
|
return document.querySelector(COMPOSER_TEXTAREA_SELECTOR) ?? document.querySelector(COMPOSER_EDITABLE_SELECTOR);
|
|
@@ -1020,7 +1118,16 @@ var COMPOSER_EDITABLE_SELECTOR = "[data-composer-input]";
|
|
|
1020
1118
|
var USER_SEAT_SELECTOR = '[data-chat-flow-kind="user"][data-chat-anchor-key], [data-chat-flow-kind="steering"][data-chat-anchor-key]';
|
|
1021
1119
|
var CHAT_SEAT_SELECTOR = "[data-chat-anchor-key]";
|
|
1022
1120
|
var ACTIONS_ROOT_SELECTOR = "[data-time-hover-root], [data-actions-reveal]";
|
|
1023
|
-
var PENDING_SEAT_SELECTOR = "[data-pending-steering]
|
|
1121
|
+
var PENDING_SEAT_SELECTOR = "[data-pending-steering]";
|
|
1122
|
+
function actionsContainerOf(row) {
|
|
1123
|
+
const root = row?.matches(ACTIONS_ROOT_SELECTOR) ? row : row?.querySelector(ACTIONS_ROOT_SELECTOR);
|
|
1124
|
+
const actions = root?.lastElementChild;
|
|
1125
|
+
if (actions instanceof HTMLElement && actions.querySelector("button") !== null) return actions;
|
|
1126
|
+
const copy = row?.querySelector("button");
|
|
1127
|
+
const structural = copy?.parentElement;
|
|
1128
|
+
if (structural instanceof HTMLElement && structural.querySelector("button") !== null) return structural;
|
|
1129
|
+
return void 0;
|
|
1130
|
+
}
|
|
1024
1131
|
function collectTargets(chat, hiddenSeqs) {
|
|
1025
1132
|
const rows = /* @__PURE__ */ new Map();
|
|
1026
1133
|
for (const element of document.querySelectorAll(USER_SEAT_SELECTOR)) {
|
|
@@ -1034,9 +1141,8 @@ function collectTargets(chat, hiddenSeqs) {
|
|
|
1034
1141
|
const user = node.data;
|
|
1035
1142
|
if (hiddenSeqs.has(node.anchorSeq ?? user.seq)) continue;
|
|
1036
1143
|
const row = rows.get(key);
|
|
1037
|
-
const
|
|
1038
|
-
|
|
1039
|
-
if (!(actions instanceof HTMLElement) || actions.querySelector("button") === null) continue;
|
|
1144
|
+
const actions = actionsContainerOf(row);
|
|
1145
|
+
if (actions === void 0) continue;
|
|
1040
1146
|
targets.push({ kind: "durable", key, container: actions, seq: user.seq, time: user.time, preview: messagePreviewOf(user) });
|
|
1041
1147
|
}
|
|
1042
1148
|
return targets;
|
|
@@ -1061,9 +1167,8 @@ function collectPendingTargets(snapshot) {
|
|
|
1061
1167
|
if (itemId === null) continue;
|
|
1062
1168
|
const row = rows[i];
|
|
1063
1169
|
if (row === void 0) continue;
|
|
1064
|
-
const
|
|
1065
|
-
|
|
1066
|
-
if (!(actions instanceof HTMLElement) || actions.querySelector("button") === null) continue;
|
|
1170
|
+
const actions = actionsContainerOf(row);
|
|
1171
|
+
if (actions === void 0) continue;
|
|
1067
1172
|
const item = steering[i];
|
|
1068
1173
|
targets.push({
|
|
1069
1174
|
kind: "pending",
|
|
@@ -1084,40 +1189,6 @@ function sameTargets(left, right) {
|
|
|
1084
1189
|
return other.kind === "pending" && target.itemId === other.itemId;
|
|
1085
1190
|
});
|
|
1086
1191
|
}
|
|
1087
|
-
function hasExecutedRewindCommand(chat) {
|
|
1088
|
-
for (const key of chat.order) {
|
|
1089
|
-
const node = chat.nodes.get(key);
|
|
1090
|
-
if (node === void 0 || node.kind !== "command") continue;
|
|
1091
|
-
const command = node.data;
|
|
1092
|
-
if (command.name !== "rewind") continue;
|
|
1093
|
-
const args = command.args ?? "";
|
|
1094
|
-
if (args.includes("preview") || args.includes("__candidates")) continue;
|
|
1095
|
-
return true;
|
|
1096
|
-
}
|
|
1097
|
-
return false;
|
|
1098
|
-
}
|
|
1099
|
-
function describeHiding(chat, hiddenSeqs) {
|
|
1100
|
-
const commands = [];
|
|
1101
|
-
for (const key of chat.order) {
|
|
1102
|
-
const node = chat.nodes.get(key);
|
|
1103
|
-
if (node === void 0 || node.kind !== "command") continue;
|
|
1104
|
-
const c = node.data;
|
|
1105
|
-
commands.push(`#${c.seq} name=${c.name} outcome=${c.outcome?.kind ?? "-"} marker=${c.outcome?.sourceEventSeq ?? "-"} args=${JSON.stringify(c.args)}`);
|
|
1106
|
-
}
|
|
1107
|
-
const scoped = hiddenSeqs.size > 0 ? `hidden=[${[...hiddenSeqs].slice(0, 20).join(",")}${hiddenSeqs.size > 20 ? "\u2026" : ""}]` : "";
|
|
1108
|
-
let seats = 0;
|
|
1109
|
-
let resolved = 0;
|
|
1110
|
-
let inHidden = 0;
|
|
1111
|
-
for (const seat of document.querySelectorAll(CHAT_SEAT_SELECTOR)) {
|
|
1112
|
-
seats += 1;
|
|
1113
|
-
const key = seat.dataset.chatAnchorKey;
|
|
1114
|
-
const node = key === void 0 ? void 0 : chat.nodes.get(key);
|
|
1115
|
-
if (node === void 0) continue;
|
|
1116
|
-
resolved += 1;
|
|
1117
|
-
if (hiddenSeqs.has(node.anchorSeq)) inHidden += 1;
|
|
1118
|
-
}
|
|
1119
|
-
return `commands=[${commands.join(" | ")}] ${scoped} seats=${seats} resolved=${resolved} inHidden=${inHidden}`;
|
|
1120
|
-
}
|
|
1121
1192
|
function RewindPortals({ sessionId, sessionOf, chatOf, currentSessionId, t, subscribeLocale, setComposerText }) {
|
|
1122
1193
|
const [targets, setTargets] = (0, import_react.useState)([]);
|
|
1123
1194
|
const hidden = (0, import_react.useRef)(/* @__PURE__ */ new WeakSet());
|
|
@@ -1138,7 +1209,6 @@ function RewindPortals({ sessionId, sessionOf, chatOf, currentSessionId, t, subs
|
|
|
1138
1209
|
const snapshot = session.getSnapshot();
|
|
1139
1210
|
const chat = chatOf(session);
|
|
1140
1211
|
const hiddenSeqs = chat === void 0 ? /* @__PURE__ */ new Set() : hiddenSeqsOf(chat);
|
|
1141
|
-
let hiddenCount = 0;
|
|
1142
1212
|
for (const seat of chat === void 0 ? [] : document.querySelectorAll(CHAT_SEAT_SELECTOR)) {
|
|
1143
1213
|
const key = seat.dataset.chatAnchorKey;
|
|
1144
1214
|
const anchor = key !== void 0 ? chat?.nodes.get(key)?.anchorSeq : void 0;
|
|
@@ -1146,21 +1216,12 @@ function RewindPortals({ sessionId, sessionOf, chatOf, currentSessionId, t, subs
|
|
|
1146
1216
|
seat.style.display = "none";
|
|
1147
1217
|
seat.dataset.dshRewindHidden = "true";
|
|
1148
1218
|
hidden.current.add(seat);
|
|
1149
|
-
hiddenCount += 1;
|
|
1150
1219
|
} else if (hidden.current.has(seat)) {
|
|
1151
1220
|
seat.style.display = "";
|
|
1152
1221
|
delete seat.dataset.dshRewindHidden;
|
|
1153
1222
|
hidden.current.delete(seat);
|
|
1154
1223
|
}
|
|
1155
1224
|
}
|
|
1156
|
-
if (hiddenSeqs.size > 0 || hiddenCount > 0) {
|
|
1157
|
-
console.info(
|
|
1158
|
-
`[dsh-rewind] hiding: ${hiddenCount} rows, seqs [${[...hiddenSeqs].slice(0, 20).join(", ")}${hiddenSeqs.size > 20 ? "\u2026" : ""}]`
|
|
1159
|
-
);
|
|
1160
|
-
}
|
|
1161
|
-
if (chat !== void 0 && hiddenCount === 0 && hasExecutedRewindCommand(chat)) {
|
|
1162
|
-
console.warn(`[dsh-rewind] rewind not hidden: ${describeHiding(chat, hiddenSeqs)}`);
|
|
1163
|
-
}
|
|
1164
1225
|
const durable = chat === void 0 ? [] : collectTargets(chat, hiddenSeqs);
|
|
1165
1226
|
const next = [...durable, ...collectPendingTargets(snapshot)];
|
|
1166
1227
|
setTargets((current) => sameTargets(current, next) ? current : next);
|
|
@@ -1215,7 +1276,7 @@ function RewindButton({ target, sessionId, sessionOf, chatOf, currentSessionId,
|
|
|
1215
1276
|
event.stopPropagation();
|
|
1216
1277
|
const session = sessionOf(sessionId);
|
|
1217
1278
|
if (session === void 0) {
|
|
1218
|
-
|
|
1279
|
+
rewindLog.warn("portals", "rewind button clicked with no session binding");
|
|
1219
1280
|
return;
|
|
1220
1281
|
}
|
|
1221
1282
|
const node = userNodeOf(chatOf(session), target.key);
|
|
@@ -1551,14 +1612,14 @@ function SettingsCleanupCard({ api, t }) {
|
|
|
1551
1612
|
// src/client/index.ts
|
|
1552
1613
|
var name = "dsh-rewind";
|
|
1553
1614
|
var inject = ["slots", "sessions", "locale", "commandUi"];
|
|
1554
|
-
var
|
|
1615
|
+
var NS2 = "rewind";
|
|
1555
1616
|
var HEADER_ACTIONS_SLOT = "conversation.session.header.actions";
|
|
1556
1617
|
var COMPOSER_TEXTAREA_SELECTOR2 = "[data-input-scroll] textarea, textarea[data-phase]";
|
|
1557
1618
|
var COMPOSER_EDITABLE_SELECTOR2 = "[data-composer-input]";
|
|
1558
1619
|
function apply(ctx) {
|
|
1559
1620
|
ctx.effect(function* () {
|
|
1560
|
-
yield ctx.locale.register(
|
|
1561
|
-
const t = ctx.locale.bind(
|
|
1621
|
+
yield ctx.locale.register(NS2, { zh, en });
|
|
1622
|
+
const t = ctx.locale.bind(NS2);
|
|
1562
1623
|
const style = document.createElement("style");
|
|
1563
1624
|
style.dataset.plugin = "dsh-rewind";
|
|
1564
1625
|
style.textContent = STYLE;
|
|
@@ -1578,15 +1639,23 @@ function apply(ctx) {
|
|
|
1578
1639
|
}
|
|
1579
1640
|
};
|
|
1580
1641
|
const setComposerText = (sessionId, text) => {
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1642
|
+
try {
|
|
1643
|
+
const conversation = ctx.get("conversation");
|
|
1644
|
+
const input = conversation?.input;
|
|
1645
|
+
const scope = ctx.sessions.scope?.(sessionId);
|
|
1646
|
+
const facade = input !== void 0 && scope !== void 0;
|
|
1647
|
+
const ok = writeComposer(
|
|
1648
|
+
text,
|
|
1649
|
+
facade ? { setDraft: (draft) => {
|
|
1650
|
+
input.for(scope).setDraft(draft);
|
|
1651
|
+
} } : void 0
|
|
1652
|
+
);
|
|
1653
|
+
rewindLog.info("refill", "composer write", { channel: facade ? "facade" : "dom", ok });
|
|
1654
|
+
return ok;
|
|
1655
|
+
} catch (error) {
|
|
1656
|
+
rewindLog.warn("refill", "composer write threw", error);
|
|
1657
|
+
return false;
|
|
1658
|
+
}
|
|
1590
1659
|
};
|
|
1591
1660
|
const slots = ctx.slots;
|
|
1592
1661
|
yield slots.inject(HEADER_ACTIONS_SLOT, () => slots.register(
|
|
@@ -1624,13 +1693,13 @@ function apply(ctx) {
|
|
|
1624
1693
|
// Match the official cards / dsh-market: locale + inject provide
|
|
1625
1694
|
// the card its props through the slot renderer (the keyed card owns
|
|
1626
1695
|
// its internals, but the page feeds it locale + the bound api).
|
|
1627
|
-
locale:
|
|
1696
|
+
locale: NS2,
|
|
1628
1697
|
inject: () => ({ t, api: cardApi })
|
|
1629
1698
|
},
|
|
1630
1699
|
SettingsCleanupCard
|
|
1631
1700
|
));
|
|
1632
1701
|
} catch (error) {
|
|
1633
|
-
|
|
1702
|
+
rewindLog.error("settings", "settings card register failed", error);
|
|
1634
1703
|
}
|
|
1635
1704
|
});
|
|
1636
1705
|
const commandUi = ctx.get("commandUi");
|
package/lib/index.js
CHANGED
|
@@ -111,6 +111,14 @@ function translate(lang, key, params = {}) {
|
|
|
111
111
|
return text;
|
|
112
112
|
}
|
|
113
113
|
|
|
114
|
+
// src/session-events.ts
|
|
115
|
+
function eventsOf(session) {
|
|
116
|
+
const s = session;
|
|
117
|
+
if (s.snapshotEvents !== void 0) return s.snapshotEvents();
|
|
118
|
+
if (s.events !== void 0) return s.events;
|
|
119
|
+
return [];
|
|
120
|
+
}
|
|
121
|
+
|
|
114
122
|
// src/settings-locale.ts
|
|
115
123
|
function readSettingsSection(provider, ns, brand) {
|
|
116
124
|
const key = brand?.(ns) ?? ns;
|
|
@@ -1516,7 +1524,7 @@ function mutationPathOf(exec) {
|
|
|
1516
1524
|
return void 0;
|
|
1517
1525
|
}
|
|
1518
1526
|
function anchorSeqOf(session, cache) {
|
|
1519
|
-
const events = session
|
|
1527
|
+
const events = eventsOf(session);
|
|
1520
1528
|
const cached = cache.get(session);
|
|
1521
1529
|
if (cached !== void 0 && cached.eventsLength === events.length) return cached.anchor;
|
|
1522
1530
|
let anchor = cached?.anchor;
|
|
@@ -1694,15 +1702,15 @@ async function executeRewind(ctx, store, fs, invocation, rawTarget, mode, inflig
|
|
|
1694
1702
|
}
|
|
1695
1703
|
let plan;
|
|
1696
1704
|
try {
|
|
1697
|
-
plan = resolveOrError(agent.session
|
|
1705
|
+
plan = resolveOrError(eventsOf(agent.session), agent.session.surface.nodes, rawTarget);
|
|
1698
1706
|
} catch (error) {
|
|
1699
1707
|
return rewindErrorResult(error);
|
|
1700
1708
|
}
|
|
1701
1709
|
const marker = buildMarker();
|
|
1702
1710
|
let event;
|
|
1703
1711
|
try {
|
|
1704
|
-
const turn = markerTurnOf(agent.session
|
|
1705
|
-
const step = markerStepOf(agent.session
|
|
1712
|
+
const turn = markerTurnOf(eventsOf(agent.session));
|
|
1713
|
+
const step = markerStepOf(eventsOf(agent.session), turn);
|
|
1706
1714
|
agent.session.append("step/start", { turn, step });
|
|
1707
1715
|
try {
|
|
1708
1716
|
event = agent.session.append("assistant/message", { turn, step, message: marker }, {
|
|
@@ -1756,7 +1764,7 @@ async function handleRewind(ctx, store, fs, invocation, inflight) {
|
|
|
1756
1764
|
const session = invocation.agent.session;
|
|
1757
1765
|
const input = invocation.rawInput.trim();
|
|
1758
1766
|
if (input === "") {
|
|
1759
|
-
const candidates = listRewindCandidates(session
|
|
1767
|
+
const candidates = listRewindCandidates(eventsOf(session), session.surface.nodes, 1);
|
|
1760
1768
|
if (candidates.length === 0) {
|
|
1761
1769
|
return { kind: "error", text: t("noUserMessages") };
|
|
1762
1770
|
}
|
|
@@ -1768,7 +1776,7 @@ async function handleRewind(ctx, store, fs, invocation, inflight) {
|
|
|
1768
1776
|
if (target2 === void 0) return { kind: "error", text: usage() };
|
|
1769
1777
|
let plan;
|
|
1770
1778
|
try {
|
|
1771
|
-
plan = resolveOrError(session
|
|
1779
|
+
plan = resolveOrError(eventsOf(session), session.surface.nodes, target2);
|
|
1772
1780
|
} catch (error) {
|
|
1773
1781
|
return rewindErrorResult(error);
|
|
1774
1782
|
}
|
|
@@ -1776,7 +1784,7 @@ async function handleRewind(ctx, store, fs, invocation, inflight) {
|
|
|
1776
1784
|
return { kind: "success", text: formatPlan(plan, impacts) };
|
|
1777
1785
|
}
|
|
1778
1786
|
if (parts[0] === "__candidates") {
|
|
1779
|
-
const candidates = listRewindCandidates(session
|
|
1787
|
+
const candidates = listRewindCandidates(eventsOf(session), session.surface.nodes);
|
|
1780
1788
|
return { kind: "success", text: formatCandidateList(candidates) };
|
|
1781
1789
|
}
|
|
1782
1790
|
const target = parts[0];
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-rewind client logger: a single, namespaced, level-filtered logging
|
|
3
|
+
* channel for every browser-side diagnostic in this plugin.
|
|
4
|
+
*
|
|
5
|
+
* Design (industry-normal layering, kept dependency-free):
|
|
6
|
+
* - `error` / `warn` are ALWAYS emitted (they are the anomaly guard: rare,
|
|
7
|
+
* cheap, and must surface even for a user who never touched the switch).
|
|
8
|
+
* - `info` / `debug` are gated by a DEBUG switch and further filtered by
|
|
9
|
+
* namespace, so verbose detail never floods a normal user's console.
|
|
10
|
+
*
|
|
11
|
+
* The DEBUG switch is a runtime, per-origin knob read from
|
|
12
|
+
* `localStorage['dsh-rewind.debug']` — the convention-debug-scan pattern
|
|
13
|
+
* (namespace match, comma-separated, `*` wildcard), scoped to an
|
|
14
|
+
* exclusively-own key so it can never enable any other plugin/feature and no
|
|
15
|
+
* other feature can wake this one. Because it is read on every call (never
|
|
16
|
+
* cached), flipping it and reloading takes effect on any published build
|
|
17
|
+
* without a plugin rebuild.
|
|
18
|
+
*
|
|
19
|
+
* Values accepted by the switch (empty/unset = off):
|
|
20
|
+
* - `dsh-rewind*` — every dsh-rewind namespace.
|
|
21
|
+
* - `dsh-rewind:refill` — just one subsystem (exact match).
|
|
22
|
+
* - `dsh-rewind:refill,dsh-rewind:hiding` — several (comma-separated).
|
|
23
|
+
*
|
|
24
|
+
* @module dsh-rewind/client/log
|
|
25
|
+
*/
|
|
26
|
+
export type LogLevel = 'error' | 'warn' | 'info' | 'debug';
|
|
27
|
+
/**
|
|
28
|
+
* Emit one diagnostic line. `error`/`warn` always print; `info`/`debug` print
|
|
29
|
+
* only when the DEBUG switch selects the namespace. Both gated levels are
|
|
30
|
+
* routed to the always-visible `console.info` rather than `console.debug`,
|
|
31
|
+
* whose "Verbose" level Chrome filters out by default — otherwise a reporter
|
|
32
|
+
* who turns the switch on still would not see the line without also changing
|
|
33
|
+
* the DevTools level filter (mapped to `console.debug`). `data` is spread last
|
|
34
|
+
* so DevTools' structured view keeps it inspectable (never stringified).
|
|
35
|
+
*/
|
|
36
|
+
export declare function log(level: LogLevel, scope: string, message: string, data?: unknown): void;
|
|
37
|
+
/** Convenience shorthands (typed so call sites read cleanly). */
|
|
38
|
+
export declare const rewindLog: {
|
|
39
|
+
readonly error: (scope: string, message: string, data?: unknown) => void;
|
|
40
|
+
readonly warn: (scope: string, message: string, data?: unknown) => void;
|
|
41
|
+
readonly info: (scope: string, message: string, data?: unknown) => void;
|
|
42
|
+
readonly debug: (scope: string, message: string, data?: unknown) => void;
|
|
43
|
+
};
|
|
@@ -125,6 +125,28 @@ export declare function writeComposer(text: string, facade: ComposerDraftWriter
|
|
|
125
125
|
* after switching sessions or restarting dsh.
|
|
126
126
|
*/
|
|
127
127
|
export declare function runRewindAndFill(session: SessionFace, seq: number, mode: 'chat' | 'both', currentSessionId: () => string | undefined, chatOf: ChatOf, setComposerText: (sessionId: string, text: string) => boolean): Promise<void>;
|
|
128
|
+
/**
|
|
129
|
+
* Locate the actions container of a user/steering seat row — the element the
|
|
130
|
+
* ↶ button portals into (the copy/branch IconActions row).
|
|
131
|
+
*
|
|
132
|
+
* Dual channel:
|
|
133
|
+
* - rc.2 (0.1.1-rc.2) / alpha.2+ (0.1.2-alpha.2): the hover-reveal root is
|
|
134
|
+
* attribute-marked (`[data-time-hover-root]` ≤ rc.x, `[data-actions-reveal]`
|
|
135
|
+
* alpha.2+) and mounts the actions row as its LAST child. A pending row IS
|
|
136
|
+
* that root; a durable row CONTAINS it as a descendant.
|
|
137
|
+
* - alpha.4 (0.1.2-alpha.4): the marker was removed from user rows (it now
|
|
138
|
+
* lives only on the per-turn tail footer, `TurnTailNodeView`), and the user
|
|
139
|
+
* action row is revealed via CSS `:has()`. The container is instead located
|
|
140
|
+
* structurally: the direct holder of the copy `<button>` (the
|
|
141
|
+
* `MessageIconActions` container, which mounts that button as a direct
|
|
142
|
+
* child — `MessageIconActions.tsx:83,86`).
|
|
143
|
+
*
|
|
144
|
+
* Returns undefined when no qualifying container is found; the caller refuses
|
|
145
|
+
* to portal (never a crash, never a wrong attachment). Exported as a test seam
|
|
146
|
+
* (see `collectTargets`) so the dual-channel finder is exercised directly for
|
|
147
|
+
* both durable and pending row shapes without a full React portal render.
|
|
148
|
+
*/
|
|
149
|
+
export declare function actionsContainerOf(row: HTMLElement | undefined): HTMLElement | undefined;
|
|
128
150
|
/**
|
|
129
151
|
* Collect the portal targets of one session: user rows × snapshot nodes.
|
|
130
152
|
* Exported as a test seam — the DOM→targets pairing that drives the ↶ button
|
|
@@ -23,4 +23,4 @@ export declare const CLASS: {
|
|
|
23
23
|
/** The ↶ glyph, drawn inline so the bundle stays dependency-free. */
|
|
24
24
|
export declare const REWIND_ICON_SVG: string;
|
|
25
25
|
/** One injected stylesheet (scoped under `.dsh-rewind-*`). */
|
|
26
|
-
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: 1px solid var(--dsw-alias-border-l2);\n border-radius: 12px;\n background: var(--dsw-specific-menu, var(--dsw-alias-bg-layer-3));\n box-shadow: var(--dsw-shadow-lv3);\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:disabled {\n opacity: 0.5;\n cursor: default;\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-button-primary-fill);\n color: var(--dsw-alias-label-primary-foreground);\n}\n.dsh-rewind-popover-primary:hover:not(:disabled) {\n background: var(--dsw-alias-button-primary-hover);\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: 1px solid var(--dsw-alias-border-l2);\n border-radius: 10px;\n background: var(--dsw-specific-menu, var(--dsw-alias-bg-layer-3));\n box-shadow: var(--dsw-shadow-lv3);\n font-size: 13px;\n line-height: 18px;\n color: var(--dsw-alias-label-primary);\n pointer-events: none;\n}\n\n/* ---- Snapshot-cleanup settings card (mirrors the harness PluginCard look) ---- */\n.dsh-rewind-cleanup-card {\n list-style: none;\n border: 1px solid var(--dsw-alias-border-l2);\n border-radius: 12px;\n background: var(--dsw-alias-bg-layer-3);\n transition: border-color .16s, background .16s;\n}\n.dsh-rewind-cleanup-card:hover {\n border-color: var(--dsw-alias-label-dimmed);\n}\n.dsh-rewind-cleanup-card-open {\n background: var(--dsw-alias-bg-layer-2);\n border-color: var(--dsw-alias-label-dimmed);\n}\n.dsh-rewind-cleanup-header {\n width: 100%;\n appearance: none;\n border: 0;\n background: none;\n font: inherit;\n color: inherit;\n text-align: left;\n cursor: pointer;\n display: flex;\n align-items: center;\n gap: 12px;\n padding: 14px 16px;\n border-radius: 12px;\n}\n.dsh-rewind-cleanup-header:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary);\n outline-offset: -2px;\n}\n.dsh-rewind-cleanup-head-text {\n flex: 1;\n min-width: 0;\n display: flex;\n flex-direction: column;\n gap: 4px;\n}\n.dsh-rewind-cleanup-name {\n font-size: 15px;\n font-weight: 600;\n line-height: 1.4;\n color: var(--dsw-alias-label-primary);\n}\n.dsh-rewind-cleanup-desc {\n font-size: 13px;\n line-height: 1.5;\n color: var(--dsw-alias-label-tertiary);\n}\n.dsh-rewind-cleanup-chevron {\n flex: none;\n color: var(--dsw-alias-label-tertiary);\n transition: transform .16s;\n}\n.dsh-rewind-cleanup-chevron-open {\n transform: rotate(180deg);\n}\n.dsh-rewind-cleanup-pending {\n flex: none;\n border-radius: 999px;\n padding: 1px 8px;\n font-size: 11px;\n line-height: 17px;\n font-weight: 500;\n white-space: nowrap;\n background: var(--dsw-alias-bg-module-platform);\n color: var(--dsw-alias-label-secondary);\n}\n.dsh-rewind-cleanup-body {\n border-top: 1px solid var(--dsw-alias-border-l2);\n margin: 0 16px;\n padding: 4px 0 8px;\n}\n.dsh-rewind-cleanup-readonly {\n margin: 12px 0 0;\n font-size: 12px;\n line-height: 1.5;\n color: var(--dsw-alias-label-tertiary);\n}\n.dsh-rewind-cleanup-permission {\n display: grid;\n gap: 6px;\n padding: 12px 0;\n}\n.dsh-rewind-cleanup-field {\n display: flex;\n flex-direction: column;\n gap: 6px;\n padding: 12px 0;\n}\n.dsh-rewind-cleanup-field + .dsh-rewind-cleanup-field {\n border-top: 1px solid var(--dsw-alias-border-l2);\n}\n.dsh-rewind-cleanup-head {\n display: flex;\n align-items: center;\n gap: 8px;\n}\n.dsh-rewind-cleanup-label {\n flex: 1;\n min-width: 0;\n font-size: 13px;\n font-weight: 500;\n line-height: 1.5;\n color: var(--dsw-alias-label-primary);\n}\n.dsh-rewind-cleanup-hint {\n margin: 0;\n font-size: 12px;\n line-height: 1.5;\n color: var(--dsw-alias-label-tertiary);\n}\n.dsh-rewind-cleanup-error {\n margin: 0;\n font-size: 12px;\n line-height: 1.5;\n color: var(--dsw-alias-label-error);\n}\n/* Switch row: label left, role=switch button right, hint below (Subagent module). */\n.dsh-rewind-cleanup-toggle-row {\n display: flex;\n align-items: flex-start;\n justify-content: space-between;\n gap: 16px;\n font-size: 13px;\n line-height: 1.5;\n color: var(--dsw-alias-label-primary);\n}\n.dsh-rewind-cleanup-toggle-label {\n flex: 1;\n min-width: 0;\n}\n.dsh-rewind-cleanup-switch {\n box-sizing: border-box;\n position: relative;\n flex: 0 0 auto;\n width: 36px;\n height: 20px;\n padding: 2px;\n border: 0;\n border-radius: 10px;\n background: var(--dsw-alias-border-l3);\n cursor: pointer;\n}\n.dsh-rewind-cleanup-switch-on {\n background: var(--dsw-alias-brand-primary);\n}\n.dsh-rewind-cleanup-switch:disabled {\n cursor: default;\n opacity: 0.5;\n}\n.dsh-rewind-cleanup-switch:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary);\n outline-offset: 2px;\n}\n.dsh-rewind-cleanup-thumb {\n display: block;\n width: 16px;\n height: 16px;\n border-radius: 50%;\n background: var(--dsw-alias-label-primary-foreground);\n transition: transform 120ms ease;\n}\n.dsh-rewind-cleanup-switch-on .dsh-rewind-cleanup-thumb {\n transform: translateX(16px);\n}\n.dsh-rewind-cleanup-input {\n box-sizing: border-box;\n height: 34px;\n padding: 0 12px;\n border: 1px solid var(--dsw-alias-border-l2);\n border-radius: 8px;\n background: var(--dsw-alias-bg-layer-3);\n font: inherit;\n font-size: 13px;\n line-height: 1.5;\n color: var(--dsw-alias-label-primary);\n}\n.dsh-rewind-cleanup-input:focus-visible {\n outline: none;\n border-color: var(--dsw-alias-brand-primary);\n}\n.dsh-rewind-cleanup-input:disabled {\n color: var(--dsw-alias-label-tertiary);\n cursor: default;\n}\n.dsh-rewind-cleanup-input-invalid {\n border-color: var(--dsw-alias-label-error);\n}\n.dsh-rewind-cleanup-footer {\n display: flex;\n align-items: center;\n justify-content: flex-end;\n gap: 8px;\n padding: 12px 0 4px;\n border-top: 1px solid var(--dsw-alias-border-l2);\n}\n.dsh-rewind-cleanup-failed {\n flex: 1;\n min-width: 0;\n margin: 0;\n font-size: 12px;\n line-height: 1.5;\n color: var(--dsw-alias-label-error);\n}\n.dsh-rewind-cleanup-discard,\n.dsh-rewind-cleanup-save {\n appearance: none;\n border: 1px solid transparent;\n border-radius: 8px;\n padding: 5px 14px;\n font: inherit;\n font-size: 13px;\n line-height: 1.5;\n cursor: pointer;\n}\n.dsh-rewind-cleanup-discard {\n border-color: var(--dsw-alias-border-l2);\n background: none;\n color: var(--dsw-alias-label-secondary);\n}\n.dsh-rewind-cleanup-discard:hover:not(:disabled) {\n color: var(--dsw-alias-label-primary);\n border-color: var(--dsw-alias-label-dimmed);\n}\n.dsh-rewind-cleanup-save {\n background: var(--dsw-alias-label-primary);\n color: var(--dsw-alias-bg-layer-3);\n}\n.dsh-rewind-cleanup-discard:disabled,\n.dsh-rewind-cleanup-save:disabled {\n opacity: 0.4;\n cursor: default;\n}\n.dsh-rewind-cleanup-discard:focus-visible,\n.dsh-rewind-cleanup-save:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary);\n outline-offset: 1px;\n}\n";
|
|
26
|
+
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: 1px solid var(--dsw-alias-border-l2);\n border-radius: 12px;\n background: var(--dsw-specific-menu, var(--dsw-alias-bg-layer-3));\n box-shadow: var(--dsw-shadow-lv3);\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:disabled {\n opacity: 0.5;\n cursor: default;\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-button-primary-fill);\n color: var(--dsw-alias-label-primary-foreground);\n}\n.dsh-rewind-popover-primary:hover:not(:disabled) {\n background: var(--dsw-alias-button-primary-hover);\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: 1px solid var(--dsw-alias-border-l2);\n border-radius: 10px;\n background: var(--dsw-specific-menu, var(--dsw-alias-bg-layer-3));\n box-shadow: var(--dsw-shadow-lv3);\n font-size: 13px;\n line-height: 18px;\n color: var(--dsw-alias-label-primary);\n pointer-events: none;\n}\n\n/* ---- Snapshot-cleanup settings card (mirrors the harness PluginCard look) ---- */\n.dsh-rewind-cleanup-card {\n list-style: none;\n border: 1px solid var(--dsw-alias-border-l2);\n border-radius: 12px;\n background: var(--dsw-alias-bg-layer-3);\n transition: border-color .16s, background .16s;\n}\n.dsh-rewind-cleanup-card:hover {\n border-color: var(--dsw-alias-label-dimmed);\n}\n.dsh-rewind-cleanup-card-open {\n background: var(--dsw-alias-bg-layer-2);\n border-color: var(--dsw-alias-label-dimmed);\n}\n.dsh-rewind-cleanup-header {\n width: 100%;\n appearance: none;\n border: 0;\n background: none;\n font: inherit;\n color: inherit;\n text-align: left;\n cursor: pointer;\n display: flex;\n align-items: center;\n gap: 12px;\n padding: 14px 16px;\n border-radius: 12px;\n}\n.dsh-rewind-cleanup-header:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary);\n outline-offset: -2px;\n}\n.dsh-rewind-cleanup-head-text {\n flex: 1;\n min-width: 0;\n display: flex;\n flex-direction: column;\n gap: 4px;\n}\n.dsh-rewind-cleanup-name {\n font-size: 15px;\n font-weight: 600;\n line-height: 1.4;\n color: var(--dsw-alias-label-primary);\n}\n.dsh-rewind-cleanup-desc {\n font-size: 13px;\n line-height: 1.5;\n color: var(--dsw-alias-label-tertiary);\n}\n.dsh-rewind-cleanup-chevron {\n flex: none;\n color: var(--dsw-alias-label-tertiary);\n transition: transform .16s;\n}\n.dsh-rewind-cleanup-chevron-open {\n transform: rotate(180deg);\n}\n.dsh-rewind-cleanup-pending {\n flex: none;\n border-radius: 999px;\n padding: 1px 8px;\n font-size: 11px;\n line-height: 17px;\n font-weight: 500;\n white-space: nowrap;\n background: var(--dsw-alias-bg-module-platform);\n color: var(--dsw-alias-label-secondary);\n}\n.dsh-rewind-cleanup-body {\n border-top: 1px solid var(--dsw-alias-border-l2);\n margin: 0 16px;\n padding: 4px 0 8px;\n}\n.dsh-rewind-cleanup-readonly {\n margin: 12px 0 0;\n font-size: 12px;\n line-height: 1.5;\n color: var(--dsw-alias-label-tertiary);\n}\n.dsh-rewind-cleanup-permission {\n display: grid;\n gap: 6px;\n padding: 12px 0;\n}\n.dsh-rewind-cleanup-field {\n display: flex;\n flex-direction: column;\n gap: 6px;\n padding: 12px 0;\n}\n.dsh-rewind-cleanup-field + .dsh-rewind-cleanup-field {\n border-top: 1px solid var(--dsw-alias-border-l2);\n}\n.dsh-rewind-cleanup-head {\n display: flex;\n align-items: center;\n gap: 8px;\n}\n.dsh-rewind-cleanup-label {\n flex: 1;\n min-width: 0;\n font-size: 13px;\n font-weight: 500;\n line-height: 1.5;\n color: var(--dsw-alias-label-primary);\n}\n.dsh-rewind-cleanup-hint {\n margin: 0;\n font-size: 12px;\n line-height: 1.5;\n color: var(--dsw-alias-label-tertiary);\n}\n.dsh-rewind-cleanup-error {\n margin: 0;\n font-size: 12px;\n line-height: 1.5;\n color: var(--dsw-alias-label-error);\n}\n/* Switch row: label left, role=switch button right, hint below (Subagent module). */\n.dsh-rewind-cleanup-toggle-row {\n display: flex;\n align-items: flex-start;\n justify-content: space-between;\n gap: 16px;\n font-size: 13px;\n line-height: 1.5;\n color: var(--dsw-alias-label-primary);\n}\n.dsh-rewind-cleanup-toggle-label {\n flex: 1;\n min-width: 0;\n}\n.dsh-rewind-cleanup-switch {\n box-sizing: border-box;\n position: relative;\n flex: 0 0 auto;\n width: 36px;\n height: 20px;\n padding: 2px;\n border: 0;\n border-radius: 10px;\n background: var(--dsw-alias-border-l3);\n cursor: pointer;\n}\n.dsh-rewind-cleanup-switch-on {\n background: var(--dsw-alias-brand-primary);\n}\n.dsh-rewind-cleanup-switch:disabled {\n cursor: default;\n opacity: 0.5;\n}\n.dsh-rewind-cleanup-switch:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary);\n outline-offset: 2px;\n}\n.dsh-rewind-cleanup-thumb {\n display: block;\n width: 16px;\n height: 16px;\n border-radius: 50%;\n corner-shape: round;\n background: var(--dsw-alias-label-primary-foreground);\n transition: transform 120ms ease;\n}\n.dsh-rewind-cleanup-switch-on .dsh-rewind-cleanup-thumb {\n transform: translateX(16px);\n}\n.dsh-rewind-cleanup-input {\n box-sizing: border-box;\n height: 34px;\n padding: 0 12px;\n border: 1px solid var(--dsw-alias-border-l2);\n border-radius: 8px;\n background: var(--dsw-alias-bg-layer-3);\n font: inherit;\n font-size: 13px;\n line-height: 1.5;\n color: var(--dsw-alias-label-primary);\n}\n.dsh-rewind-cleanup-input:focus-visible {\n outline: none;\n border-color: var(--dsw-alias-brand-primary);\n}\n.dsh-rewind-cleanup-input:disabled {\n color: var(--dsw-alias-label-tertiary);\n cursor: default;\n}\n.dsh-rewind-cleanup-input-invalid {\n border-color: var(--dsw-alias-label-error);\n}\n.dsh-rewind-cleanup-footer {\n display: flex;\n align-items: center;\n justify-content: flex-end;\n gap: 8px;\n padding: 12px 0 4px;\n border-top: 1px solid var(--dsw-alias-border-l2);\n}\n.dsh-rewind-cleanup-failed {\n flex: 1;\n min-width: 0;\n margin: 0;\n font-size: 12px;\n line-height: 1.5;\n color: var(--dsw-alias-label-error);\n}\n.dsh-rewind-cleanup-discard,\n.dsh-rewind-cleanup-save {\n appearance: none;\n border: 1px solid transparent;\n border-radius: 8px;\n padding: 5px 14px;\n font: inherit;\n font-size: 13px;\n line-height: 1.5;\n cursor: pointer;\n}\n.dsh-rewind-cleanup-discard {\n border-color: var(--dsw-alias-border-l2);\n background: none;\n color: var(--dsw-alias-label-secondary);\n}\n.dsh-rewind-cleanup-discard:hover:not(:disabled) {\n color: var(--dsw-alias-label-primary);\n border-color: var(--dsw-alias-label-dimmed);\n}\n.dsh-rewind-cleanup-save {\n background: var(--dsw-alias-label-primary);\n color: var(--dsw-alias-bg-layer-3);\n}\n.dsh-rewind-cleanup-discard:disabled,\n.dsh-rewind-cleanup-save:disabled {\n opacity: 0.4;\n cursor: default;\n}\n.dsh-rewind-cleanup-discard:focus-visible,\n.dsh-rewind-cleanup-save:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary);\n outline-offset: 1px;\n}\n";
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cross-channel session event-log reader.
|
|
3
|
+
*
|
|
4
|
+
* Harness 0.1.1-rc.2 exposes the full log as `Session.events`
|
|
5
|
+
* (`get events(): readonly SessionEvent[]`), while 0.1.2-alpha.4 removed that
|
|
6
|
+
* member and replaced it with the on-demand APIs `snapshotEvents(from?, to?)`
|
|
7
|
+
* (half-open range, internally cached), `eventAt(seq)`, `ownEvents()` and
|
|
8
|
+
* `seq`. This module reads the full log through whichever channel the host
|
|
9
|
+
* exposes, so the plugin keeps one code path across both.
|
|
10
|
+
*
|
|
11
|
+
* `snapshotEvents()` is deliberately preferred over `ownEvents()`:
|
|
12
|
+
* `snapshotEvents()` defaults to the whole log (including any fork-inherited
|
|
13
|
+
* prefix), which is exactly what `Session.events` returned on rc.2 — the
|
|
14
|
+
* rewind semantics are preserved for fork-seeded sessions. `ownEvents()`
|
|
15
|
+
* would drop the inherited prefix and change behaviour.
|
|
16
|
+
*
|
|
17
|
+
* @module dsh-rewind/session-events
|
|
18
|
+
*/
|
|
19
|
+
import type { Session, SessionEvent } from '@deepseek-ai/dsh-session';
|
|
20
|
+
/**
|
|
21
|
+
* Read the full event log of a session, transparently across harness channels:
|
|
22
|
+
* - 0.1.1-rc.2 (`Session.events`): the live immutable log array.
|
|
23
|
+
* - 0.1.2-alpha.4 (`Session.snapshotEvents()`): a deep-frozen snapshot, cached
|
|
24
|
+
* by the session until the next append.
|
|
25
|
+
*
|
|
26
|
+
* Falls back to an empty log when the session exposes neither member (a future
|
|
27
|
+
* harness shape) rather than throwing — callers already treat the empty log as
|
|
28
|
+
* "no candidates".
|
|
29
|
+
*
|
|
30
|
+
* @param session - the session to read.
|
|
31
|
+
* @returns the full event log in log order (the same shape rc.2's `events`
|
|
32
|
+
* returned, so existing consumers are unchanged).
|
|
33
|
+
*/
|
|
34
|
+
export declare function eventsOf(session: Session): readonly SessionEvent[];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-rewind-plugin",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.2",
|
|
4
4
|
"description": "DSH 插件:真正便捷无感的同窗口内对话回退,从不新建分支;自带轻量工作区备份,可一并还原文件(完整 Claude Code /rewind 语义)。 · DSH plugin: genuinely effortless in-window conversation rewind — never forking a new session; ships a lightweight workspace backup that restores files together with the rewind (full Claude Code /rewind semantics).",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"deepseek-harness",
|