dsh-rewind-plugin 0.1.0

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/lib/client.js ADDED
@@ -0,0 +1,469 @@
1
+ /* dsh-rewind client bundle — generated by scripts/build.mjs from src/client/ */
2
+ window.__ModuleLoader__.load({
3
+ id: "dsh-rewind-plugin",
4
+ factory: (require) => {
5
+ var module = { exports: {} };
6
+ var exports = module.exports;
7
+ "use strict";
8
+ var __defProp = Object.defineProperty;
9
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
10
+ var __getOwnPropNames = Object.getOwnPropertyNames;
11
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
12
+ var __export = (target, all) => {
13
+ for (var name2 in all)
14
+ __defProp(target, name2, { get: all[name2], enumerable: true });
15
+ };
16
+ var __copyProps = (to, from, except, desc) => {
17
+ if (from && typeof from === "object" || typeof from === "function") {
18
+ for (let key of __getOwnPropNames(from))
19
+ if (!__hasOwnProp.call(to, key) && key !== except)
20
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
21
+ }
22
+ return to;
23
+ };
24
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
25
+
26
+ // src/client/index.ts
27
+ var index_exports = {};
28
+ __export(index_exports, {
29
+ apply: () => apply,
30
+ inject: () => inject,
31
+ name: () => name
32
+ });
33
+ module.exports = __toCommonJS(index_exports);
34
+
35
+ // src/client/locales.ts
36
+ var zh = {
37
+ "button.aria": "\u56DE\u9000\u5230\u6B64\u6D88\u606F",
38
+ "button.title": "\u56DE\u9000",
39
+ "popover.title": "\u56DE\u9000\u5230\u8FD9\u6761\u6D88\u606F",
40
+ "popover.target": "seq {seq} \xB7 {time}",
41
+ "popover.chat": "\u4EC5\u56DE\u9000\u5BF9\u8BDD",
42
+ "popover.chat.hint": "\u53EA\u56DE\u9000\u6A21\u578B\u4E0A\u4E0B\u6587\uFF0C\u4E0D\u52A8\u5DE5\u4F5C\u533A\u6587\u4EF6",
43
+ "popover.both": "\u56DE\u9000\u5BF9\u8BDD\u548C\u4EE3\u7801",
44
+ "popover.both.hint": "\u5BF9\u8BDD\u56DE\u9000\u5E76\u8FD8\u539F\u5DE5\u4F5C\u533A\u6587\u4EF6",
45
+ "popover.cancel": "\u53D6\u6D88",
46
+ "popover.impact.loading": "\u6B63\u5728\u83B7\u53D6\u5F71\u54CD\u6E05\u5355\u2026",
47
+ "popover.impact.failed": "\u65E0\u6CD5\u83B7\u53D6\u5F71\u54CD\u6E05\u5355\uFF1A{message}",
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
+ "popover.confirm": "\u786E\u8BA4\u56DE\u9000",
50
+ "popover.back": "\u8FD4\u56DE"
51
+ };
52
+ var en = {
53
+ "button.aria": "Rewind to this message",
54
+ "button.title": "Rewind",
55
+ "popover.title": "Rewind to this message",
56
+ "popover.target": "seq {seq} \xB7 {time}",
57
+ "popover.chat": "Rewind conversation only",
58
+ "popover.chat.hint": "Cut the model context only; workspace files stay untouched",
59
+ "popover.both": "Rewind conversation and code",
60
+ "popover.both.hint": "Cut the context and restore workspace files",
61
+ "popover.cancel": "Cancel",
62
+ "popover.impact.loading": "Fetching impact list\u2026",
63
+ "popover.impact.failed": "Could not fetch the impact list: {message}",
64
+ "popover.impact.none": "No ledger-recorded file changes after the target; nothing to restore.",
65
+ "popover.confirm": "Confirm rewind",
66
+ "popover.back": "Back"
67
+ };
68
+
69
+ // src/client/styles.ts
70
+ var REWIND_ATTACHED = "data-dsh-rewind-attached";
71
+ var CLASS = {
72
+ button: "dsh-rewind-btn",
73
+ popover: "dsh-rewind-popover",
74
+ popoverTitle: "dsh-rewind-popover-title",
75
+ popoverTarget: "dsh-rewind-popover-target",
76
+ popoverOption: "dsh-rewind-popover-option",
77
+ popoverOptionLabel: "dsh-rewind-popover-option-label",
78
+ popoverOptionHint: "dsh-rewind-popover-option-hint",
79
+ popoverImpact: "dsh-rewind-popover-impact",
80
+ popoverActions: "dsh-rewind-popover-actions",
81
+ popoverPrimary: "dsh-rewind-popover-primary",
82
+ popoverGhost: "dsh-rewind-popover-ghost"
83
+ };
84
+ var REWIND_ICON_SVG = [
85
+ '<svg width="16" height="16" viewBox="0 0 16 16" fill="none" aria-hidden="true">',
86
+ ' <path d="M6.5 2.5 2.5 6.5l4 4" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>',
87
+ ' <path d="M2.5 6.5h7a4 4 0 0 1 4 4v1.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>',
88
+ "</svg>"
89
+ ].join("");
90
+ var STYLE = `
91
+ .dsh-rewind-btn {
92
+ display: inline-flex;
93
+ align-items: center;
94
+ justify-content: center;
95
+ width: 28px;
96
+ height: 28px;
97
+ padding: 6px;
98
+ border: none;
99
+ border-radius: 28px;
100
+ background: transparent;
101
+ color: var(--dsw-alias-label-tertiary);
102
+ cursor: pointer;
103
+ }
104
+ .dsh-rewind-btn:hover {
105
+ background: var(--dsw-alias-interactive-bg-hover);
106
+ color: var(--dsw-alias-label-secondary);
107
+ }
108
+
109
+ .dsh-rewind-popover {
110
+ position: fixed;
111
+ z-index: 1000;
112
+ width: 288px;
113
+ padding: 12px;
114
+ border-radius: 12px;
115
+ background: var(--dsw-specific-surface-1, var(--dsw-alias-surface-1, #1f2127));
116
+ box-shadow: 0 8px 28px rgba(0, 0, 0, 0.32);
117
+ font-size: 14px;
118
+ line-height: 20px;
119
+ color: var(--dsw-alias-label-primary);
120
+ }
121
+ .dsh-rewind-popover-title {
122
+ font-size: 14px;
123
+ font-weight: 600;
124
+ line-height: 20px;
125
+ }
126
+ .dsh-rewind-popover-target {
127
+ margin: 4px 0 10px;
128
+ font-size: 12px;
129
+ line-height: 16px;
130
+ color: var(--dsw-alias-label-tertiary);
131
+ word-break: break-all;
132
+ }
133
+ .dsh-rewind-popover-option {
134
+ display: flex;
135
+ flex-direction: column;
136
+ gap: 2px;
137
+ width: 100%;
138
+ margin: 0 0 6px;
139
+ padding: 8px 10px;
140
+ border: 1px solid transparent;
141
+ border-radius: 8px;
142
+ background: transparent;
143
+ color: inherit;
144
+ font: inherit;
145
+ text-align: left;
146
+ cursor: pointer;
147
+ }
148
+ .dsh-rewind-popover-option:hover {
149
+ background: var(--dsw-alias-interactive-bg-hover);
150
+ }
151
+ .dsh-rewind-popover-option-label {
152
+ font-weight: 500;
153
+ }
154
+ .dsh-rewind-popover-option-hint {
155
+ font-size: 12px;
156
+ line-height: 16px;
157
+ color: var(--dsw-alias-label-tertiary);
158
+ }
159
+ .dsh-rewind-popover-impact {
160
+ margin: 4px 0 10px;
161
+ padding: 8px 10px;
162
+ border-radius: 8px;
163
+ background: var(--dsw-alias-interactive-bg-hover);
164
+ font-size: 12px;
165
+ line-height: 16px;
166
+ color: var(--dsw-alias-label-secondary);
167
+ white-space: pre-wrap;
168
+ max-height: 160px;
169
+ overflow: auto;
170
+ }
171
+ .dsh-rewind-popover-actions {
172
+ display: flex;
173
+ justify-content: flex-end;
174
+ gap: 8px;
175
+ }
176
+ .dsh-rewind-popover-primary,
177
+ .dsh-rewind-popover-ghost {
178
+ padding: 5px 12px;
179
+ border: none;
180
+ border-radius: 8px;
181
+ font: inherit;
182
+ font-size: 13px;
183
+ line-height: 18px;
184
+ cursor: pointer;
185
+ }
186
+ .dsh-rewind-popover-primary {
187
+ background: var(--dsw-alias-accent, var(--dsw-accent, #5b8cff));
188
+ color: var(--dsw-alias-on-accent, #fff);
189
+ }
190
+ .dsh-rewind-popover-primary:disabled {
191
+ opacity: 0.5;
192
+ cursor: default;
193
+ }
194
+ .dsh-rewind-popover-ghost {
195
+ background: transparent;
196
+ color: var(--dsw-alias-label-secondary);
197
+ }
198
+ .dsh-rewind-popover-ghost:hover {
199
+ background: var(--dsw-alias-interactive-bg-hover);
200
+ }
201
+ `;
202
+
203
+ // src/client/popover.ts
204
+ var popoverEl = null;
205
+ var disposeOutside = null;
206
+ function closePopover() {
207
+ if (popoverEl !== null) {
208
+ popoverEl.remove();
209
+ popoverEl = null;
210
+ }
211
+ if (disposeOutside !== null) {
212
+ disposeOutside();
213
+ disposeOutside = null;
214
+ }
215
+ }
216
+ function formatTarget(seq, time, preview) {
217
+ const d = new Date(time);
218
+ const hh = String(d.getHours()).padStart(2, "0");
219
+ const mm = String(d.getMinutes()).padStart(2, "0");
220
+ const previewText = preview.length > 0 ? preview : "(no text)";
221
+ return `seq ${seq} \xB7 ${hh}:${mm} \xB7 ${previewText}`;
222
+ }
223
+ function findCommand(snapshot, match) {
224
+ let found;
225
+ for (const key of snapshot.chat.order) {
226
+ const node = snapshot.chat.nodes.get(key);
227
+ if (node !== void 0 && node.kind === "command") {
228
+ const command = node.data;
229
+ if (match(command)) found = command;
230
+ }
231
+ }
232
+ return found;
233
+ }
234
+ function waitForCommand(session, match, timeoutMs = 8e3) {
235
+ return new Promise((resolve) => {
236
+ let settled = false;
237
+ const settle = (value) => {
238
+ if (settled) return;
239
+ settled = true;
240
+ clearTimeout(timer);
241
+ unsubscribe();
242
+ resolve(value);
243
+ };
244
+ const check = () => {
245
+ const node = findCommand(session.getSnapshot(), match);
246
+ if (node?.outcome !== null && node?.outcome !== void 0) {
247
+ settle({ kind: node.outcome.kind, text: node.outcome.text });
248
+ }
249
+ };
250
+ const unsubscribe = session.subscribe(check);
251
+ const timer = setTimeout(() => settle(null), timeoutMs);
252
+ check();
253
+ });
254
+ }
255
+ function el(tag, className, text) {
256
+ const node = document.createElement(tag);
257
+ node.className = className;
258
+ if (text !== void 0) node.textContent = text;
259
+ return node;
260
+ }
261
+ function modeOption(label, hint, onClick) {
262
+ const button = document.createElement("button");
263
+ button.type = "button";
264
+ button.className = CLASS.popoverOption;
265
+ const labelEl = el("span", CLASS.popoverOptionLabel, label);
266
+ const hintEl = el("span", CLASS.popoverOptionHint, hint);
267
+ button.append(labelEl, hintEl);
268
+ button.addEventListener("click", onClick);
269
+ return button;
270
+ }
271
+ function renderImpactStep(root, opts, back) {
272
+ const { session, seq, t } = opts;
273
+ const impact = el("div", CLASS.popoverImpact, t("popover.impact.loading"));
274
+ const actions = el("div", CLASS.popoverActions);
275
+ const backButton = document.createElement("button");
276
+ backButton.type = "button";
277
+ backButton.className = CLASS.popoverGhost;
278
+ backButton.textContent = t("popover.back");
279
+ backButton.addEventListener("click", back);
280
+ actions.append(backButton);
281
+ const confirm = document.createElement("button");
282
+ confirm.type = "button";
283
+ confirm.className = CLASS.popoverPrimary;
284
+ confirm.textContent = t("popover.confirm");
285
+ confirm.disabled = true;
286
+ actions.append(confirm);
287
+ root.replaceChildren(impact, actions);
288
+ let outcome = null;
289
+ void (async () => {
290
+ const result = await session.command(`/rewind preview @${seq} both`);
291
+ if (!result.ok || result.value?.matched !== true) {
292
+ impact.textContent = t("popover.impact.failed", {
293
+ message: result.ok ? "command not matched" : result.error?.message ?? "unknown error"
294
+ });
295
+ return;
296
+ }
297
+ outcome = await waitForCommand(
298
+ session,
299
+ (node) => {
300
+ const args = node.args ?? "";
301
+ return node.name === "rewind" && args.includes("preview") && args.includes(`@${seq}`);
302
+ }
303
+ );
304
+ if (outcome === null) {
305
+ impact.textContent = t("popover.impact.failed", { message: "timeout" });
306
+ return;
307
+ }
308
+ if (outcome.kind === "error") {
309
+ impact.textContent = t("popover.impact.failed", { message: outcome.text ?? "unknown error" });
310
+ return;
311
+ }
312
+ impact.textContent = outcome.text ?? t("popover.impact.none");
313
+ confirm.disabled = false;
314
+ confirm.addEventListener("click", () => {
315
+ closePopover();
316
+ void session.command(`/rewind @${seq} both`);
317
+ });
318
+ })().catch(() => {
319
+ impact.textContent = t("popover.impact.failed", { message: "unexpected error" });
320
+ });
321
+ }
322
+ function openPopover(opts) {
323
+ closePopover();
324
+ const { session, seq, time, preview, anchor, t } = opts;
325
+ const root = el("div", CLASS.popover);
326
+ root.setAttribute("role", "dialog");
327
+ root.setAttribute("aria-label", t("popover.title"));
328
+ root.append(
329
+ el("div", CLASS.popoverTitle, t("popover.title")),
330
+ el("div", CLASS.popoverTarget, formatTarget(seq, time, preview))
331
+ );
332
+ const renderModes = () => {
333
+ root.replaceChildren(
334
+ el("div", CLASS.popoverTitle, t("popover.title")),
335
+ el("div", CLASS.popoverTarget, formatTarget(seq, time, preview)),
336
+ modeOption(t("popover.chat"), t("popover.chat.hint"), () => {
337
+ closePopover();
338
+ void session.command(`/rewind @${seq} chat`);
339
+ }),
340
+ modeOption(t("popover.both"), t("popover.both.hint"), () => {
341
+ renderImpactStep(root, opts, renderModes);
342
+ }),
343
+ (() => {
344
+ const actions = el("div", CLASS.popoverActions);
345
+ const cancel = document.createElement("button");
346
+ cancel.type = "button";
347
+ cancel.className = CLASS.popoverGhost;
348
+ cancel.textContent = t("popover.cancel");
349
+ cancel.addEventListener("click", closePopover);
350
+ actions.append(cancel);
351
+ return actions;
352
+ })()
353
+ );
354
+ };
355
+ renderModes();
356
+ document.body.append(root);
357
+ const rect = anchor.getBoundingClientRect();
358
+ const gap = 4;
359
+ const height = root.offsetHeight;
360
+ const top = rect.bottom + gap + height <= window.innerHeight - 8 ? rect.bottom + gap : Math.max(8, rect.top - gap - height);
361
+ root.style.top = `${Math.round(top)}px`;
362
+ root.style.left = `${Math.round(Math.min(rect.right, window.innerWidth - 8 - root.offsetWidth))}px`;
363
+ popoverEl = root;
364
+ const onPointerDown = (event) => {
365
+ const target = event.target;
366
+ if (root.contains(target) || anchor.contains(target)) return;
367
+ closePopover();
368
+ };
369
+ const onKeyDown = (event) => {
370
+ if (event.key === "Escape") closePopover();
371
+ };
372
+ const deferred = setTimeout(() => {
373
+ document.addEventListener("pointerdown", onPointerDown);
374
+ document.addEventListener("keydown", onKeyDown);
375
+ }, 0);
376
+ disposeOutside = () => {
377
+ clearTimeout(deferred);
378
+ document.removeEventListener("pointerdown", onPointerDown);
379
+ document.removeEventListener("keydown", onKeyDown);
380
+ };
381
+ }
382
+
383
+ // src/client/index.ts
384
+ var name = "dsh-rewind";
385
+ var inject = ["sessions", "locale"];
386
+ var NS = "rewind";
387
+ var USER_SEAT_SELECTOR = '[data-chat-flow-kind="user"]';
388
+ var ACTIONS_ROOT_SELECTOR = "[data-time-hover-root]";
389
+ function messagePreviewOf(node) {
390
+ return node.content.map((block) => block.type === "text" && typeof block.text === "string" ? block.text : "").join("").replace(/\s+/g, " ").trim().slice(0, 80);
391
+ }
392
+ function apply(ctx) {
393
+ ctx.effect(function* () {
394
+ yield ctx.locale.register(NS, { zh, en });
395
+ const t = ctx.locale.bind(NS);
396
+ const style = document.createElement("style");
397
+ style.dataset.plugin = "dsh-rewind";
398
+ style.textContent = STYLE;
399
+ document.head.appendChild(style);
400
+ const attached = /* @__PURE__ */ new WeakSet();
401
+ const buttons = /* @__PURE__ */ new Map();
402
+ let observer = null;
403
+ const sessionFor = () => {
404
+ const sessionId = ctx.sessions.list.getSnapshot().current;
405
+ if (sessionId === void 0) return void 0;
406
+ return ctx.sessions.binding(sessionId)?.session;
407
+ };
408
+ const userNodeFor = (key, session) => {
409
+ const node = session.getSnapshot().chat.nodes.get(key);
410
+ if (node === void 0 || node.kind !== "user") return void 0;
411
+ return node.data;
412
+ };
413
+ const attach = (seat) => {
414
+ const key = seat.dataset.chatAnchorKey;
415
+ if (key === void 0 || attached.has(seat)) return;
416
+ const hoverRoot = seat.querySelector(ACTIONS_ROOT_SELECTOR);
417
+ const actions = hoverRoot?.lastElementChild;
418
+ if (!(actions instanceof HTMLElement) || actions.querySelector("button") === null) return;
419
+ attached.add(seat);
420
+ const button = document.createElement("button");
421
+ button.type = "button";
422
+ button.className = CLASS.button;
423
+ button.setAttribute("aria-label", t("button.aria"));
424
+ button.title = t("button.title");
425
+ button.innerHTML = REWIND_ICON_SVG;
426
+ button.addEventListener("click", (event) => {
427
+ event.stopPropagation();
428
+ const session = sessionFor();
429
+ if (session === void 0) {
430
+ console.warn("[dsh-rewind] rewind button clicked with no current session");
431
+ return;
432
+ }
433
+ const node = userNodeFor(key, session);
434
+ if (node === void 0) return;
435
+ openPopover({
436
+ session,
437
+ seq: node.seq,
438
+ time: node.time,
439
+ preview: messagePreviewOf(node),
440
+ anchor: button,
441
+ t
442
+ });
443
+ });
444
+ actions.appendChild(button);
445
+ buttons.set(key, button);
446
+ seat.setAttribute(REWIND_ATTACHED, "");
447
+ };
448
+ const scan = () => {
449
+ for (const [key, button] of buttons) {
450
+ if (!button.isConnected) buttons.delete(key);
451
+ }
452
+ for (const seat of document.querySelectorAll(USER_SEAT_SELECTOR)) attach(seat);
453
+ };
454
+ observer = new MutationObserver(scan);
455
+ observer.observe(document.body, { childList: true, subtree: true });
456
+ scan();
457
+ yield () => {
458
+ observer?.disconnect();
459
+ for (const button of buttons.values()) button.remove();
460
+ buttons.clear();
461
+ closePopover();
462
+ style.remove();
463
+ };
464
+ }, "dsh-rewind client lifecycle");
465
+ }
466
+
467
+ return module.exports;
468
+ }
469
+ });