dsh-rewind-plugin 0.12.2 → 0.13.0-alpha.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/CONTRIBUTING.md +1 -1
- package/README.en.md +0 -3
- package/README.md +0 -3
- package/SECURITY.md +1 -1
- package/docs/README.md +1 -1
- package/docs/architecture.md +3 -3
- package/docs/compat/audit.md +30 -17
- package/docs/compat/diagnostics.md +1 -0
- package/docs/compat/diagnostics.zh.md +1 -0
- package/docs/contract/client-contract.md +1 -1
- package/docs/contract/client-contract.zh.md +1 -1
- package/docs/format.md +2 -2
- package/docs/harness-reference.md +6 -3
- package/docs/release/release.md +9 -9
- package/docs/release/release.zh.md +10 -10
- package/docs/rewind-fix.md +1 -1
- package/docs/rewind-fix.zh.md +1 -1
- package/lib/client.js +315 -346
- package/lib/index.js +56 -25
- package/lib/types/client/build-info.d.ts +6 -0
- package/lib/types/client/candidates.d.ts +0 -2
- package/lib/types/client/hidden.d.ts +6 -7
- package/lib/types/client/locales.d.ts +6 -14
- package/lib/types/client/pending.d.ts +57 -15
- package/lib/types/client/popover.d.ts +1 -1
- package/lib/types/client/portals.d.ts +37 -27
- package/lib/types/client/settings-card.d.ts +177 -48
- package/lib/types/client/styles.d.ts +1 -1
- package/lib/types/index.d.ts +1 -1
- package/lib/types/locales.d.ts +1 -1
- package/lib/types/rewind.d.ts +1 -1
- package/lib/types/session-cwd.d.ts +8 -10
- package/lib/types/snapshot-cleanup.d.ts +6 -4
- package/lib/types/snapshot.d.ts +4 -11
- package/package.json +54 -46
package/lib/index.js
CHANGED
|
@@ -208,14 +208,11 @@ function planRewind(events, surface, target) {
|
|
|
208
208
|
}
|
|
209
209
|
|
|
210
210
|
// src/session-cwd.ts
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
function sessionCwd(cwd, requestedPath) {
|
|
214
|
-
if (cwd === void 0 || !PARENT_PATH_SEGMENT.test(cwd) && !PARENT_PATH_SEGMENT.test(requestedPath)) return cwd;
|
|
215
|
-
return canonicalPath(cwd);
|
|
211
|
+
function sessionCwd(cwd) {
|
|
212
|
+
return cwd;
|
|
216
213
|
}
|
|
217
|
-
function execSessionCwd(exec
|
|
218
|
-
return sessionCwd(exec.agent?.session.header.cwd
|
|
214
|
+
function execSessionCwd(exec) {
|
|
215
|
+
return sessionCwd(exec.agent?.session.header.cwd);
|
|
219
216
|
}
|
|
220
217
|
|
|
221
218
|
// src/snapshot.ts
|
|
@@ -232,7 +229,6 @@ var LEGACY_JOURNAL_PREFIX = "restore-journal-";
|
|
|
232
229
|
var PENDING_MAX_AGE_MS = 24 * 60 * 60 * 1e3;
|
|
233
230
|
var COMPARE_CHUNK_BYTES = 64 * 1024;
|
|
234
231
|
var REPLACEMENT_CHAR = "\uFFFD";
|
|
235
|
-
var DEFAULT_SNAPSHOT_ROOT = join(resolveDshHome(), SNAPSHOT_DIR_NAME);
|
|
236
232
|
var SNAPSHOT_ROOT_ENV = "DSH_REWIND_SNAPSHOT_DIR";
|
|
237
233
|
var MAX_ANCHOR_GROUPS = 100;
|
|
238
234
|
var CURRENT_STORE_VERSION = 2;
|
|
@@ -668,7 +664,7 @@ var SnapshotStore = class _SnapshotStore {
|
|
|
668
664
|
/**
|
|
669
665
|
* Session-format version snapshots are anchored under, stamped into each
|
|
670
666
|
* session's `format` marker when an entry is recorded. `null` until the host
|
|
671
|
-
* sets it (from `agent/
|
|
667
|
+
* sets it (from `agent/created`), so a session that never records is
|
|
672
668
|
* never materialized and a marker is only written where snapshots exist.
|
|
673
669
|
*/
|
|
674
670
|
formatVersion = null;
|
|
@@ -1788,7 +1784,7 @@ var SnapshotStore = class _SnapshotStore {
|
|
|
1788
1784
|
}
|
|
1789
1785
|
}
|
|
1790
1786
|
}
|
|
1791
|
-
/** True when a path exists on disk
|
|
1787
|
+
/** True when a path exists on disk. */
|
|
1792
1788
|
async exists(path) {
|
|
1793
1789
|
try {
|
|
1794
1790
|
await stat(path);
|
|
@@ -2010,7 +2006,7 @@ var SnapshotStore = class _SnapshotStore {
|
|
|
2010
2006
|
}
|
|
2011
2007
|
/**
|
|
2012
2008
|
* Set the session-format version the store stamps onto every snapshot it
|
|
2013
|
-
* records. The host sets this once per process from `agent/
|
|
2009
|
+
* records. The host sets this once per process from `agent/created`
|
|
2014
2010
|
* (`agent.session.header.version`), so a marker is only materialized for a
|
|
2015
2011
|
* session that actually records a snapshot.
|
|
2016
2012
|
*/
|
|
@@ -2063,7 +2059,7 @@ var SnapshotStore = class _SnapshotStore {
|
|
|
2063
2059
|
* Session-format-version guard: clear a session's snapshot dir when the
|
|
2064
2060
|
* format its snapshots were anchored under differs from the current session
|
|
2065
2061
|
* format, so seq-anchored references can never survive a format migration
|
|
2066
|
-
* mis-mapped. Runs at `agent/
|
|
2062
|
+
* mis-mapped. Runs at `agent/created` — after DSH has migrated/loaded
|
|
2067
2063
|
* the session, so `sessionVersion` is the post-migration value.
|
|
2068
2064
|
*
|
|
2069
2065
|
* Conservative rule (per the "delete stale snapshots" policy): a session
|
|
@@ -2169,6 +2165,9 @@ async function saveLastSweepAt(path, ms) {
|
|
|
2169
2165
|
await writeFile2(tmp, JSON.stringify({ lastSweepAt: ms }), "utf8");
|
|
2170
2166
|
await rename2(tmp, path);
|
|
2171
2167
|
}
|
|
2168
|
+
function isAborted(signal) {
|
|
2169
|
+
return signal?.aborted === true;
|
|
2170
|
+
}
|
|
2172
2171
|
async function runAutoCleanupCheck(deps, sessionId) {
|
|
2173
2172
|
try {
|
|
2174
2173
|
const loaded = await deps.readConfig();
|
|
@@ -2177,8 +2176,11 @@ async function runAutoCleanupCheck(deps, sessionId) {
|
|
|
2177
2176
|
return;
|
|
2178
2177
|
}
|
|
2179
2178
|
if (!loaded.config.enabled) return;
|
|
2179
|
+
if (isAborted(deps.signal)) return;
|
|
2180
2180
|
if (!shouldRunAutoSweep(await loadLastSweepAt(deps.statePath), Date.now())) return;
|
|
2181
|
+
if (isAborted(deps.signal)) return;
|
|
2181
2182
|
await deps.pruner.pruneStale({ keepActiveId: sessionId, maxAgeDays: loaded.config.maxAgeDays });
|
|
2183
|
+
if (isAborted(deps.signal)) return;
|
|
2182
2184
|
await saveLastSweepAt(deps.statePath, Date.now());
|
|
2183
2185
|
} catch (error) {
|
|
2184
2186
|
deps.log(`[dsh-rewind] snapshot auto-cleanup failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
@@ -2313,7 +2315,7 @@ async function captureBefore(fs, store, exec, pending) {
|
|
|
2313
2315
|
if (session !== void 0 && isSubagentSession(session)) return;
|
|
2314
2316
|
const path = mutationPathOf(exec);
|
|
2315
2317
|
if (path === void 0) return;
|
|
2316
|
-
const cwd = execSessionCwd(exec
|
|
2318
|
+
const cwd = execSessionCwd(exec);
|
|
2317
2319
|
const target = await resolveTarget(fs, path, cwd, exec.signal);
|
|
2318
2320
|
if (target === void 0) return;
|
|
2319
2321
|
const info = await fs.stat(target, exec.signal).catch((error) => {
|
|
@@ -2594,15 +2596,23 @@ async function handleRewind(ctx, store, fs, invocation, inflight) {
|
|
|
2594
2596
|
return executeRewind(ctx, store, fs, invocation, target, mode, inflight);
|
|
2595
2597
|
}
|
|
2596
2598
|
var autoSweepChecked = false;
|
|
2597
|
-
async function maybeRunAutoCleanup(ctx, store, sessionId, dshHome) {
|
|
2598
|
-
if (autoSweepChecked) return;
|
|
2599
|
+
async function maybeRunAutoCleanup(ctx, store, sessionId, dshHome, signal) {
|
|
2600
|
+
if (autoSweepChecked || signal?.aborted === true) return;
|
|
2599
2601
|
autoSweepChecked = true;
|
|
2600
|
-
|
|
2601
|
-
|
|
2602
|
-
|
|
2603
|
-
|
|
2604
|
-
|
|
2605
|
-
|
|
2602
|
+
try {
|
|
2603
|
+
await runAutoCleanupCheck({
|
|
2604
|
+
pruner: store,
|
|
2605
|
+
readConfig: () => readCleanupPolicy(),
|
|
2606
|
+
statePath: resolveCleanupStatePath(dshHome),
|
|
2607
|
+
log: (msg) => ctx.logger.warn(msg),
|
|
2608
|
+
...signal === void 0 ? {} : { signal }
|
|
2609
|
+
}, sessionId);
|
|
2610
|
+
} catch (error) {
|
|
2611
|
+
try {
|
|
2612
|
+
ctx.logger.warn(`[dsh-rewind] snapshot auto-cleanup failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
2613
|
+
} catch {
|
|
2614
|
+
}
|
|
2615
|
+
}
|
|
2606
2616
|
}
|
|
2607
2617
|
async function readCleanupPolicy() {
|
|
2608
2618
|
if (cleanupStore === void 0) {
|
|
@@ -2713,15 +2723,28 @@ async function handleClearCurrent(store, invocation, apply2, trackedBySession) {
|
|
|
2713
2723
|
}
|
|
2714
2724
|
}
|
|
2715
2725
|
function apply(ctx, config) {
|
|
2726
|
+
activeLocale = "en";
|
|
2727
|
+
cleanupStore = void 0;
|
|
2728
|
+
autoSweepChecked = false;
|
|
2729
|
+
const lifecycle = new AbortController();
|
|
2730
|
+
ctx.effect(() => () => {
|
|
2731
|
+
lifecycle.abort();
|
|
2732
|
+
}, "dsh-rewind lifecycle abort");
|
|
2716
2733
|
const dshHome = config?.dshHome;
|
|
2717
2734
|
const store = new SnapshotStore(config?.snapshotDir, { dedup: config?.dedup, dshHome });
|
|
2718
2735
|
const pending = /* @__PURE__ */ new Map();
|
|
2736
|
+
ctx.effect(() => () => {
|
|
2737
|
+
const captures = [...pending.values()];
|
|
2738
|
+
pending.clear();
|
|
2739
|
+
return Promise.all(captures.map((capture) => discardCapture(capture))).then(() => void 0);
|
|
2740
|
+
}, "dsh-rewind dispose pending captures");
|
|
2719
2741
|
const anchorCache = /* @__PURE__ */ new WeakMap();
|
|
2720
2742
|
const inflight = /* @__PURE__ */ new Set();
|
|
2721
2743
|
const trackedBySession = /* @__PURE__ */ new Map();
|
|
2722
2744
|
let fsService;
|
|
2723
2745
|
ctx.inject(["settings"], (settingsCtx) => {
|
|
2724
2746
|
const settings = settingsCtx;
|
|
2747
|
+
activeLocale = "en";
|
|
2725
2748
|
const section = settings.settings.get("locale");
|
|
2726
2749
|
if (section?.preference === "zh" || section?.preference === "en") {
|
|
2727
2750
|
activeLocale = section.preference;
|
|
@@ -2731,7 +2754,11 @@ function apply(ctx, config) {
|
|
|
2731
2754
|
CleanupConfigSchema,
|
|
2732
2755
|
{ base: DEFAULT_CLEANUP_CONFIG }
|
|
2733
2756
|
);
|
|
2734
|
-
|
|
2757
|
+
const mountedStore = settingsCleanupStore(cleanupScope);
|
|
2758
|
+
cleanupStore = mountedStore;
|
|
2759
|
+
settingsCtx.effect(() => () => {
|
|
2760
|
+
if (cleanupStore === mountedStore) cleanupStore = void 0;
|
|
2761
|
+
}, "dsh-rewind cleanup store");
|
|
2735
2762
|
});
|
|
2736
2763
|
ctx.effect(function* () {
|
|
2737
2764
|
const rewindHandler = (invocation) => handleRewind(ctx, store, fsService, invocation, inflight);
|
|
@@ -2752,11 +2779,12 @@ function apply(ctx, config) {
|
|
|
2752
2779
|
handler: (invocation) => handleSnapshotCleanup(store, invocation, dshHome, trackedBySession)
|
|
2753
2780
|
});
|
|
2754
2781
|
}, "dsh-rewind command");
|
|
2755
|
-
ctx.on("agent/
|
|
2782
|
+
ctx.on("agent/created", ({ agent }) => {
|
|
2756
2783
|
const session = agent.session;
|
|
2757
2784
|
if (isSubagentSession(session)) return;
|
|
2758
2785
|
void (async () => {
|
|
2759
2786
|
try {
|
|
2787
|
+
if (lifecycle.signal.aborted) return;
|
|
2760
2788
|
store.setFormatVersion(session.header.version);
|
|
2761
2789
|
try {
|
|
2762
2790
|
await store.assertKnownStoreVersion(session.id);
|
|
@@ -2764,6 +2792,7 @@ function apply(ctx, config) {
|
|
|
2764
2792
|
ctx.logger.warn(`[dsh-rewind] file restore disabled for ${session.id}: ${error instanceof Error ? error.message : String(error)}`);
|
|
2765
2793
|
return;
|
|
2766
2794
|
}
|
|
2795
|
+
if (lifecycle.signal.aborted) return;
|
|
2767
2796
|
const result = await store.reconcileFormatVersion(session.id, session.header.version);
|
|
2768
2797
|
if (result.cleared) {
|
|
2769
2798
|
ctx.logger.warn(`[dsh-rewind] cleared snapshots for ${session.id}: session format changed (v${session.header.version})`);
|
|
@@ -2778,14 +2807,16 @@ function apply(ctx, config) {
|
|
|
2778
2807
|
if (isSubagentSession(session)) return;
|
|
2779
2808
|
void (async () => {
|
|
2780
2809
|
try {
|
|
2810
|
+
if (lifecycle.signal.aborted) return;
|
|
2781
2811
|
const sessionId = session.id;
|
|
2782
|
-
void maybeRunAutoCleanup(ctx, store, sessionId, dshHome);
|
|
2812
|
+
void maybeRunAutoCleanup(ctx, store, sessionId, dshHome, lifecycle.signal);
|
|
2783
2813
|
let tracked = trackedBySession.get(sessionId);
|
|
2784
2814
|
if (tracked === void 0) {
|
|
2785
2815
|
tracked = await store.trackedPaths(sessionId);
|
|
2786
2816
|
trackedBySession.set(sessionId, tracked);
|
|
2787
2817
|
}
|
|
2788
2818
|
if (tracked.size === 0) return;
|
|
2819
|
+
if (lifecycle.signal.aborted) return;
|
|
2789
2820
|
await reconcileTracked(store, sessionId, event.seq, tracked);
|
|
2790
2821
|
} catch (error) {
|
|
2791
2822
|
ctx.logger.warn(`[dsh-rewind] boundary re-check failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
@@ -2807,7 +2838,7 @@ function apply(ctx, config) {
|
|
|
2807
2838
|
try {
|
|
2808
2839
|
const session = exec.agent?.session;
|
|
2809
2840
|
if (session !== void 0 && !isSubagentSession(session)) {
|
|
2810
|
-
void maybeRunAutoCleanup(ctx, store, session.id, dshHome);
|
|
2841
|
+
void maybeRunAutoCleanup(ctx, store, session.id, dshHome, lifecycle.signal);
|
|
2811
2842
|
}
|
|
2812
2843
|
await commitEntry(store, pending, anchorCache, trackedBySession, exec, result);
|
|
2813
2844
|
} catch (error) {
|
|
@@ -15,5 +15,11 @@
|
|
|
15
15
|
*/
|
|
16
16
|
/** Plugin version baked in at build time (from `package.json`). */
|
|
17
17
|
export declare const PLUGIN_VERSION: string;
|
|
18
|
+
/**
|
|
19
|
+
* The bundle's package name — one identity for the client entry id, the
|
|
20
|
+
* `plugins.bundle.config` key and the `<style data-plugin>` marker the
|
|
21
|
+
* harness's owned-style fallback matches; a test pins it to `package.json`.
|
|
22
|
+
*/
|
|
23
|
+
export declare const PLUGIN_PACKAGE = "dsh-rewind-plugin";
|
|
18
24
|
/** Short content hash of the client source, for stale-bundle detection. */
|
|
19
25
|
export declare const BUILD_HASH: string;
|
|
@@ -80,8 +80,6 @@ export declare function rewindCandidatesOfChat(snap: CandidateChat): RewindCandi
|
|
|
80
80
|
* label/detail flex layout, with no recency numbers.
|
|
81
81
|
*/
|
|
82
82
|
export declare function rewindOptionsOf(snap: CandidateChat, t: Translate): SelectOption[];
|
|
83
|
-
/** Resolve one candidate by log seq (the mode popover's re-entry after a pick). */
|
|
84
|
-
export declare function candidateBySeq(snap: CandidateChat, seq: number): RewindCandidate | undefined;
|
|
85
83
|
/**
|
|
86
84
|
* Parse the host's candidate-list encoding (see `formatCandidateList` in
|
|
87
85
|
* src/rewind.ts) into typed candidates. Malformed lines are skipped; a
|
|
@@ -24,18 +24,17 @@ export interface HiddenChat {
|
|
|
24
24
|
};
|
|
25
25
|
}
|
|
26
26
|
/**
|
|
27
|
-
* Reader for one session's live chat snapshot
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
* uiSession slot hook).
|
|
27
|
+
* Reader for one session's live chat snapshot: the chat is served by the
|
|
28
|
+
* `uiConversation` service's named "chat" view (contributed by
|
|
29
|
+
* dsh-client-ui-chat through the uiSession slot hook).
|
|
31
30
|
*/
|
|
32
31
|
export type ChatOf = (session: {
|
|
33
32
|
readonly sessionId: string;
|
|
34
33
|
} | undefined) => HiddenChat | undefined;
|
|
35
34
|
/**
|
|
36
35
|
* Subscribe to one session's live chat-update signal, for waiting on a chat
|
|
37
|
-
* snapshot change without polling. The
|
|
38
|
-
*
|
|
36
|
+
* snapshot change without polling. The `uiConversation` "chat" view's own
|
|
37
|
+
* `subscribe` is the chat-update signal; `cb` fires whenever the
|
|
39
38
|
* chat snapshot invalidates.
|
|
40
39
|
*/
|
|
41
40
|
export type ChatWatch = (sessionId: string, cb: () => void) => () => void;
|
|
@@ -87,7 +86,7 @@ export declare function isExecutedRewindCommand(node: CommandNode, seq: number):
|
|
|
87
86
|
*
|
|
88
87
|
* Reads ONLY the machine-readable `impact=<n>` trailer the host appends to
|
|
89
88
|
* preview text. Older host output without the trailer is treated as having no
|
|
90
|
-
* changes (never guesses from human copy).
|
|
89
|
+
* changes (never guesses from human copy). Absent text (undefined) degrades to
|
|
91
90
|
* always-show so a working option is never hidden on a failed probe.
|
|
92
91
|
*/
|
|
93
92
|
export declare function hasFileImpact(text: string | undefined): boolean;
|
|
@@ -25,23 +25,19 @@ export declare const zh: {
|
|
|
25
25
|
'popover.impact.delete': string;
|
|
26
26
|
'popover.confirm': string;
|
|
27
27
|
'popover.back': string;
|
|
28
|
-
'cleanup.title': string;
|
|
29
|
-
'cleanup.desc': string;
|
|
30
|
-
'cleanup.expand': string;
|
|
31
|
-
'cleanup.collapse': string;
|
|
32
|
-
'cleanup.unsaved': string;
|
|
33
28
|
'cleanup.auto': string;
|
|
34
29
|
'cleanup.auto.on': string;
|
|
35
30
|
'cleanup.auto.off': string;
|
|
36
31
|
'cleanup.maxAge': string;
|
|
37
32
|
'cleanup.maxAge.hint': string;
|
|
38
33
|
'cleanup.invalid': string;
|
|
39
|
-
'cleanup.
|
|
34
|
+
'cleanup.overridden': string;
|
|
35
|
+
'cleanup.reset': string;
|
|
40
36
|
'cleanup.save': string;
|
|
41
37
|
'cleanup.saving': string;
|
|
42
|
-
'cleanup.saved': string;
|
|
43
38
|
'cleanup.saveFailed': string;
|
|
44
39
|
'cleanup.readonly': string;
|
|
40
|
+
'cleanup.unavailable': string;
|
|
45
41
|
};
|
|
46
42
|
/** The rewind namespace key union. */
|
|
47
43
|
export type RewindKey = keyof typeof zh;
|
|
@@ -77,21 +73,17 @@ export declare const en: {
|
|
|
77
73
|
'popover.impact.delete': string;
|
|
78
74
|
'popover.confirm': string;
|
|
79
75
|
'popover.back': string;
|
|
80
|
-
'cleanup.title': string;
|
|
81
|
-
'cleanup.desc': string;
|
|
82
|
-
'cleanup.expand': string;
|
|
83
|
-
'cleanup.collapse': string;
|
|
84
|
-
'cleanup.unsaved': string;
|
|
85
76
|
'cleanup.auto': string;
|
|
86
77
|
'cleanup.auto.on': string;
|
|
87
78
|
'cleanup.auto.off': string;
|
|
88
79
|
'cleanup.maxAge': string;
|
|
89
80
|
'cleanup.maxAge.hint': string;
|
|
90
81
|
'cleanup.invalid': string;
|
|
91
|
-
'cleanup.
|
|
82
|
+
'cleanup.overridden': string;
|
|
83
|
+
'cleanup.reset': string;
|
|
92
84
|
'cleanup.save': string;
|
|
93
85
|
'cleanup.saving': string;
|
|
94
|
-
'cleanup.saved': string;
|
|
95
86
|
'cleanup.saveFailed': string;
|
|
96
87
|
'cleanup.readonly': string;
|
|
88
|
+
'cleanup.unavailable': string;
|
|
97
89
|
};
|
|
@@ -1,17 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Pure pending-message matching: pairs the rendered
|
|
3
|
-
* rows with the session's
|
|
2
|
+
* Pure pending-message matching: pairs the rendered pre-admission steering
|
|
3
|
+
* bubble rows with the session's `next-step` inbox rows.
|
|
4
4
|
*
|
|
5
|
-
* Both sides
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* button's Tooltip mounts a label bubble inside that container on hover, so
|
|
13
|
-
* the full row textContent would flip between "message" and "message+Copy"
|
|
14
|
-
* with the mouse, flickering the button (see `bubbleTextOf` in portals.tsx).
|
|
5
|
+
* Both sides follow the same host order, so index-primary matching is reliable;
|
|
6
|
+
* text equality is still verified per row, and a row that fails (or has no
|
|
7
|
+
* counterpart) is skipped INDIVIDUALLY: one bad row never takes down the other
|
|
8
|
+
* rows' buttons. The compared text excludes the bubble's actions container —
|
|
9
|
+
* the copy button's Tooltip mounts a label bubble there on hover, so the row's
|
|
10
|
+
* full `textContent` would flip between "message" and "message+Copy" with the
|
|
11
|
+
* mouse (see `bubbleTextOf` in portals.tsx).
|
|
15
12
|
*
|
|
16
13
|
* The browser half lives in `portals.tsx`; this module stays DOM-free so the
|
|
17
14
|
* matching contract is unit-testable in a plain node environment.
|
|
@@ -23,16 +20,61 @@ export interface PendingRow {
|
|
|
23
20
|
/** The bubble's message text, excluding the actions container (see module doc). */
|
|
24
21
|
readonly text: string;
|
|
25
22
|
}
|
|
26
|
-
/** One
|
|
23
|
+
/** One wire content block of an inbox row (only the fields the derivation reads). */
|
|
24
|
+
export interface InboxBlockLike {
|
|
25
|
+
/** Wire block type (`text` | `image` | `file` | …). */
|
|
26
|
+
readonly type: string;
|
|
27
|
+
/** The block's text, on a `text` block. */
|
|
28
|
+
readonly text?: string;
|
|
29
|
+
}
|
|
30
|
+
/** One pending-inbox row (the alpha.2 `inbox` projection element). */
|
|
31
|
+
export interface InboxMessageLike {
|
|
32
|
+
/** Agent-owned inbox occurrence identity; the harness brands it as `MessageId`. */
|
|
33
|
+
readonly id: string;
|
|
34
|
+
/** Wire content blocks, in prompt order. */
|
|
35
|
+
readonly content: readonly InboxBlockLike[];
|
|
36
|
+
/** Message origin; only `kind: 'user'` rows are retractable (see `steeringItemsOf`). */
|
|
37
|
+
readonly source?: {
|
|
38
|
+
readonly kind?: string;
|
|
39
|
+
} | undefined;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* The alpha.2 `inbox` projection value: the agent's pending input folded per
|
|
43
|
+
* target order. Replaces the removed `SessionSnapshot.queue` mirror.
|
|
44
|
+
*/
|
|
45
|
+
export interface InboxLike {
|
|
46
|
+
/** Messages claimed at the next turn boundary. */
|
|
47
|
+
readonly 'next-turn': readonly InboxMessageLike[];
|
|
48
|
+
/** Messages injected at the next step boundary (user or plugin/command). */
|
|
49
|
+
readonly 'next-step': readonly InboxMessageLike[];
|
|
50
|
+
}
|
|
51
|
+
/** One steering occurrence derived from the session's inbox projection. */
|
|
27
52
|
export interface PendingSteeringItem {
|
|
28
53
|
readonly id: string;
|
|
29
54
|
/** Complete editable text; null when the message contains non-text blocks. */
|
|
30
55
|
readonly text: string | null;
|
|
56
|
+
/** Space-collapsed preview with image/file blocks excluded (the harness QueueDock rule). */
|
|
57
|
+
readonly preview: string;
|
|
31
58
|
}
|
|
59
|
+
/**
|
|
60
|
+
* Project the inbox `next-step` rows into the fields the retract path needs.
|
|
61
|
+
*
|
|
62
|
+
* ONLY user-sourced rows are steering: the host's deleted mapping
|
|
63
|
+
* (`queueItemsFromInbox`) called a `next-step` row `steering` for
|
|
64
|
+
* `source.kind === 'user'` and `context` otherwise, and this plugin has only
|
|
65
|
+
* ever retracted the former. An injected row must not get a button, and because
|
|
66
|
+
* `retractSpan` removes the target AND its future it must not be swept into a
|
|
67
|
+
* retract either; keeping it would also shift the positional match against the
|
|
68
|
+
* rendered submission echoes and cost every later row its button.
|
|
69
|
+
* @param nextStep - the inbox projection's `next-step` list (absent before the
|
|
70
|
+
* fold state exists, hence `undefined`).
|
|
71
|
+
* @returns one item per USER steering row, in host (FIFO) order.
|
|
72
|
+
*/
|
|
73
|
+
export declare function steeringItemsOf(nextStep: readonly InboxMessageLike[] | undefined): readonly PendingSteeringItem[];
|
|
32
74
|
/**
|
|
33
75
|
* Pair rows to steering items by index, verifying text equality per row.
|
|
34
76
|
* @param rows - pending bubble rows in DOM order (== render order).
|
|
35
|
-
* @param steering - steering
|
|
77
|
+
* @param steering - steering items in host order (== render order).
|
|
36
78
|
* @returns the item id for each row, or null for rows that cannot be matched
|
|
37
79
|
* safely (missing counterpart, text mismatch). A bad row never affects the
|
|
38
80
|
* other rows.
|
|
@@ -44,7 +86,7 @@ export declare function matchPendingRows(rows: readonly PendingRow[], steering:
|
|
|
44
86
|
* order. Queued (next-turn) messages are deliberately NOT included — the
|
|
45
87
|
* harness QueueDock already offers the user per-item edit/remove, so a rewind
|
|
46
88
|
* must not silently drop messages the user may still want to send.
|
|
47
|
-
* @param steering - steering
|
|
89
|
+
* @param steering - steering items in host order (== render order).
|
|
48
90
|
* @param targetId - the rewind target's inbox occurrence id.
|
|
49
91
|
* @returns the ids to remove, oldest-first; empty when the target is no
|
|
50
92
|
* longer pending (already claimed/consumed).
|
|
@@ -33,7 +33,7 @@ export interface PopoverOptions {
|
|
|
33
33
|
readonly onRetract?: () => void;
|
|
34
34
|
readonly preview: string;
|
|
35
35
|
/**
|
|
36
|
-
* Chat reader on the
|
|
36
|
+
* Chat reader on the `uiConversation` "chat" view: the durable
|
|
37
37
|
* variant's command probes scan the chat through it. Unused by the
|
|
38
38
|
* pending-retract variant.
|
|
39
39
|
*/
|
|
@@ -25,14 +25,18 @@
|
|
|
25
25
|
* `conversation.chat.assistant-actions` slot are wired for assistant messages
|
|
26
26
|
* only, and no per-user-message action slot exists. So this portal targets
|
|
27
27
|
* undocumented internal structure — the `data-chat-flow-kind`,
|
|
28
|
-
* `data-chat-anchor-key`, `data-composer-input`, `data-composer-card
|
|
29
|
-
* `data-pending-steering`
|
|
30
|
-
* `
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
28
|
+
* `data-chat-anchor-key`, `data-composer-input`, `data-composer-card` and
|
|
29
|
+
* `data-pending-steering` attributes plus the `anchorSeq` field read in
|
|
30
|
+
* `client/hidden.ts`. Those are harness-internal and may change with the UI;
|
|
31
|
+
* this module (and `hidden.ts`) must be re-adapted to follow, and is the
|
|
32
|
+
* migration target when a first-class user-action slot or an official renderer
|
|
33
|
+
* hook surface appears. The coupling is accepted deliberately because a
|
|
34
|
+
* standards-conformant alternative does not exist today; it is not a defect to
|
|
35
|
+
* be removed while the DOM-portal approach stands.
|
|
36
|
+
*
|
|
37
|
+
* Re-verified against 0.1.6-alpha.2: the five remaining anchors still exist (the
|
|
38
|
+
* anchor key and flow kind still come from `ChatNodeSeat`, values `user` /
|
|
39
|
+
* `steering`); only `data-time-hover-root` is gone, and nothing read it.
|
|
36
40
|
*
|
|
37
41
|
* @module dsh-rewind/client/portals
|
|
38
42
|
*/
|
|
@@ -67,7 +71,7 @@ export type PortalTarget = {
|
|
|
67
71
|
export interface RewindBridgeDeps {
|
|
68
72
|
readonly sessionOf: (sessionId: string) => SessionFace | undefined;
|
|
69
73
|
/**
|
|
70
|
-
* Chat reader on the
|
|
74
|
+
* Chat reader on the `uiConversation` "chat" view: every chat
|
|
71
75
|
* snapshot read goes through it. See `chatSnapshotOf` in hidden.ts.
|
|
72
76
|
*/
|
|
73
77
|
readonly chatOf: ChatOf;
|
|
@@ -77,11 +81,17 @@ export interface RewindBridgeDeps {
|
|
|
77
81
|
* snapshot changes. Passed through to `waitForCommand`.
|
|
78
82
|
*/
|
|
79
83
|
readonly watchChat: ChatWatch;
|
|
80
|
-
|
|
84
|
+
/**
|
|
85
|
+
* Whether a session is the one the MAIN VIEW currently retains — the alpha.2
|
|
86
|
+
* replacement for the removed `SessionListState.current` (see
|
|
87
|
+
* `isMainViewSession` in index.ts). A predicate rather than "the current
|
|
88
|
+
* session id", so a multi-window client can never refill the wrong composer.
|
|
89
|
+
*/
|
|
90
|
+
readonly isMainViewSession: (sessionId: string) => boolean;
|
|
81
91
|
readonly t: Translate;
|
|
82
92
|
readonly subscribeLocale: (cb: () => void) => () => void;
|
|
83
93
|
/**
|
|
84
|
-
* Session-aware composer writer (see `writeComposer`): the
|
|
94
|
+
* Session-aware composer writer (see `writeComposer`): the
|
|
85
95
|
* `conversation.input` facade `setDraft` when reachable, else the DOM fill.
|
|
86
96
|
* Session-scoped so the refill only lands in the session that just rewound.
|
|
87
97
|
*/
|
|
@@ -99,9 +109,8 @@ export interface SlotsLike {
|
|
|
99
109
|
readonly inject?: () => P;
|
|
100
110
|
}, component: (props: P) => ReactNode): () => void;
|
|
101
111
|
}
|
|
102
|
-
/** Join the text blocks of a user message into one plain preview. */
|
|
103
112
|
/**
|
|
104
|
-
* The
|
|
113
|
+
* The session input facade's write face (structural, so the plugin
|
|
105
114
|
* never imports the conversation UI package). `setDraft` replaces the whole
|
|
106
115
|
* composer draft through the harness's own Lexical editor — the correct way
|
|
107
116
|
* to restore the withdrawn text.
|
|
@@ -110,7 +119,7 @@ interface ComposerDraftWriter {
|
|
|
110
119
|
setDraft(text: string): void;
|
|
111
120
|
}
|
|
112
121
|
/**
|
|
113
|
-
* Fill the dsh composer with `text` through the
|
|
122
|
+
* Fill the dsh composer with `text` through the `contenteditable`
|
|
114
123
|
* DOM path. Used by `setComposerText` (the harness-facade-aware writer) as the
|
|
115
124
|
* last-resort and by `runRewindAndFill` to put the withdrawn target message
|
|
116
125
|
* back into the composer after a rewind. Best-effort — no composer match means
|
|
@@ -118,12 +127,12 @@ interface ComposerDraftWriter {
|
|
|
118
127
|
*/
|
|
119
128
|
export declare function fillComposer(text: string): boolean;
|
|
120
129
|
/**
|
|
121
|
-
* Composer write: prefer the harness facade's `setDraft` (
|
|
122
|
-
*
|
|
123
|
-
*
|
|
124
|
-
*
|
|
130
|
+
* Composer write: prefer the harness facade's `setDraft` (correct whole-draft
|
|
131
|
+
* replace), then degrade to the DOM `fillComposer` (contenteditable). A facade
|
|
132
|
+
* that throws (session teardown) is treated as absent so the DOM path still
|
|
133
|
+
* restores the text. Never throws.
|
|
125
134
|
* @param text - the withdrawn target message text.
|
|
126
|
-
* @param facade - the
|
|
135
|
+
* @param facade - the session input draft writer, when reachable.
|
|
127
136
|
* @returns whether a channel applied the text.
|
|
128
137
|
*/
|
|
129
138
|
export declare function writeComposer(text: string, facade: ComposerDraftWriter | undefined): boolean;
|
|
@@ -140,16 +149,17 @@ export declare function writeComposer(text: string, facade: ComposerDraftWriter
|
|
|
140
149
|
* the old baseline heuristic refilled withdrawn text into the composer
|
|
141
150
|
* after switching sessions or restarting dsh.
|
|
142
151
|
*/
|
|
143
|
-
export declare function runRewindAndFill(session: SessionFace, seq: number, mode: 'chat' | 'both',
|
|
152
|
+
export declare function runRewindAndFill(session: SessionFace, seq: number, mode: 'chat' | 'both', isMainViewSession: (sessionId: string) => boolean, chatOf: ChatOf, watchChat: ChatWatch, setComposerText: (sessionId: string, text: string) => boolean): Promise<void>;
|
|
144
153
|
/**
|
|
145
154
|
* Locate the actions container of a user/steering seat row — the element the
|
|
146
155
|
* ↶ button portals into (the copy/branch IconActions row).
|
|
147
156
|
*
|
|
148
|
-
*
|
|
149
|
-
*
|
|
150
|
-
*
|
|
151
|
-
* holder of the copy `<button>` (the `MessageIconActions`
|
|
152
|
-
* mounts that button as a direct child —
|
|
157
|
+
* The finder is STRUCTURAL on purpose: no harness attribute marks the actions
|
|
158
|
+
* row on the user seat (the old `data-time-hover-root` marker lived on the
|
|
159
|
+
* per-turn tail footer and is gone in 0.1.6-alpha.2), so the container is
|
|
160
|
+
* located as the direct holder of the copy `<button>` (the `MessageIconActions`
|
|
161
|
+
* container, which mounts that button as a direct child —
|
|
162
|
+
* `MessageIconActions.tsx:83,86`).
|
|
153
163
|
*
|
|
154
164
|
* Returns undefined when no qualifying container is found; the caller refuses
|
|
155
165
|
* to portal (never a crash, never a wrong attachment). Exported as a test seam
|
|
@@ -207,8 +217,8 @@ interface RewindPortalsProps extends RewindBridgeDeps {
|
|
|
207
217
|
* skipped when the target set is unchanged), so the plugin never runs a
|
|
208
218
|
* synchronous full-transcript scan inside a commit microtask.
|
|
209
219
|
*/
|
|
210
|
-
export declare function RewindPortals({ sessionId, sessionOf, chatOf,
|
|
211
|
-
/** The current composer draft: the
|
|
220
|
+
export declare function RewindPortals({ sessionId, sessionOf, chatOf, isMainViewSession, watchChat, t, subscribeLocale, setComposerText }: RewindPortalsProps): ReactNode;
|
|
221
|
+
/** The current composer draft: the contenteditable `textContent`.
|
|
212
222
|
* Empty when the composer is absent. Exported as a test seam (the
|
|
213
223
|
* empty-composer guard in `retractPending`). */
|
|
214
224
|
export declare function composerText(): string;
|