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/lib/index.js CHANGED
@@ -208,14 +208,11 @@ function planRewind(events, surface, target) {
208
208
  }
209
209
 
210
210
  // src/session-cwd.ts
211
- import { canonicalPath } from "@deepseek-ai/dsh-sandbox";
212
- var PARENT_PATH_SEGMENT = /(?:^|[\\/])\.\.(?:[\\/]|$)/;
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, requestedPath) {
218
- return sessionCwd(exec.agent?.session.header.cwd, requestedPath);
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/session-start`), so a session that never records is
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 (used by tests and diagnostics). */
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/session-start`
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/session-start` — after DSH has migrated/loaded
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, path);
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
- await runAutoCleanupCheck({
2601
- pruner: store,
2602
- readConfig: () => readCleanupPolicy(),
2603
- statePath: resolveCleanupStatePath(dshHome),
2604
- log: (msg) => ctx.logger.warn(msg)
2605
- }, sessionId);
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
- cleanupStore = settingsCleanupStore(cleanupScope);
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/session-start", ({ 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. On the 0.1.2-rc.1 line (the
28
- * plugin's single baseline) the chat is served by the `uiConversation`
29
- * service's named "chat" view (contributed by dsh-client-ui-chat through the
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 0.1.2-rc.1 `uiConversation` "chat"
38
- * view's own `subscribe` is the chat-update signal; `cb` fires whenever the
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). Unknown/absent text degrades to
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.discard': string;
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.discard': string;
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 pending-steering bubble
3
- * rows with the session's transient queue mirror rows (`placement === 'steering'`).
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 derive from the host's next-step inbox order the ChatView
6
- * renders `pendingSteering` in array order and the queue mirror keeps the same
7
- * host order so index-primary matching is reliable. Text equality is still
8
- * verified as a per-row cross-check, and a row that fails (or a row with no
9
- * mirror item, or a mirror item with no row) is skipped INDIVIDUALLY: one bad
10
- * row never takes down the other rows' buttons. The matching text is the
11
- * bubble's message text WITHOUT its actions container — the harness copy
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 steering occurrence from the session queue mirror. */
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 queue items in host order (== render order).
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 queue items in host order (== render order).
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 0.1.2-rc.1 `uiConversation` "chat" view: the durable
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` and `data-time-hover-root` attributes plus the
30
- * `anchorSeq` field read in `client/hidden.ts`. Those are harness-internal and
31
- * may change with the UI; this module (and `hidden.ts`) must be re-adapted to
32
- * follow, and is the migration target when a first-class user-action slot or
33
- * an official renderer hook surface appears. The coupling is accepted
34
- * deliberately because a standards-conformant alternative does not exist
35
- * today; it is not a defect to be removed while the DOM-portal approach stands.
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 0.1.2-rc.1 `uiConversation` "chat" view: every chat
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
- readonly currentSessionId: () => string | undefined;
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 0.1.2-rc.1
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 0.1.2-rc.1 session input facade's write face (structural, so the plugin
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 0.1.2-rc.1 `contenteditable`
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` (0.1.2-rc.1, correct
122
- * whole-draft replace), then degrade to the DOM `fillComposer` (0.1.2-rc.1
123
- * contenteditable). A facade that throws (session teardown) is treated as
124
- * absent so the DOM path still restores the text. Never throws.
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 0.1.2-rc.1 session input draft writer, when reachable.
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', currentSessionId: () => string | undefined, chatOf: ChatOf, watchChat: ChatWatch, setComposerText: (sessionId: string, text: string) => boolean): Promise<void>;
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
- * On the 0.1.2-rc.1 line the `data-time-hover-root` marker lives only on the
149
- * per-turn tail footer (`TurnTailNodeView`), and the user action row is
150
- * revealed via CSS `:has()`. The container is located structurally: the direct
151
- * holder of the copy `<button>` (the `MessageIconActions` container, which
152
- * mounts that button as a direct child — `MessageIconActions.tsx:83,86`).
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, currentSessionId, watchChat, t, subscribeLocale, setComposerText }: RewindPortalsProps): ReactNode;
211
- /** The current composer draft: the 0.1.2-rc.1 contenteditable `textContent`.
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;