dsh-rewind-plugin 0.20.0 → 0.21.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/README.en.md +2 -2
- package/README.md +2 -2
- package/SECURITY.md +80 -183
- package/lib/client.js +417 -387
- package/lib/types/client/action.d.ts +40 -0
- package/lib/types/client/hidden.d.ts +2 -2
- package/lib/types/client/index.d.ts +14 -11
- package/lib/types/client/locales.d.ts +17 -21
- package/lib/types/client/log.d.ts +1 -1
- package/lib/types/client/mount.d.ts +79 -0
- package/lib/types/client/pending-mount.d.ts +36 -0
- package/lib/types/client/pending.d.ts +43 -57
- package/lib/types/client/popover.d.ts +19 -19
- package/lib/types/client/retract.d.ts +32 -0
- package/lib/types/client/rewind-flow.d.ts +86 -0
- package/lib/types/client/styles.d.ts +5 -14
- package/lib/types/snapshot.d.ts +1 -1
- package/package.json +1 -1
- package/lib/types/client/portals.d.ts +0 -301
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-rewind flow half: the executed rewind command and its composer refill.
|
|
3
|
+
*
|
|
4
|
+
* This is the BEHAVIOR layer, deliberately free of any mounting decision. Both
|
|
5
|
+
* the `/rewind` text flow (command decoration → mode popover) and, in A2, the
|
|
6
|
+
* per-message action call {@link runRewindAndFill}; nothing here portals,
|
|
7
|
+
* observes rows, or knows where the trigger sits.
|
|
8
|
+
*
|
|
9
|
+
* The refill is event-driven: it runs only when THIS page performed the
|
|
10
|
+
* rewind (the user confirmed moments ago). It must NEVER scan loaded history
|
|
11
|
+
* for rewind commands — a session window opens with only the tail page, so
|
|
12
|
+
* "a command already in the snapshot" cannot be told apart from "executed in
|
|
13
|
+
* this page", and the old baseline heuristic refilled withdrawn text into the
|
|
14
|
+
* composer after switching sessions or restarting dsh.
|
|
15
|
+
*
|
|
16
|
+
* @module dsh-rewind/client/rewind-flow
|
|
17
|
+
*/
|
|
18
|
+
import type { SessionFace } from '@deepseek-ai/dsh-api-session-controller/client';
|
|
19
|
+
import { type ChatOf, type ChatWatch, type MessageImageRef } from './hidden.ts';
|
|
20
|
+
/**
|
|
21
|
+
* The session input facade's write face (structural, so the plugin
|
|
22
|
+
* never imports the conversation UI package). `setDraft` replaces the whole
|
|
23
|
+
* composer draft through the harness's own Lexical editor — the correct way
|
|
24
|
+
* to restore the withdrawn text.
|
|
25
|
+
*/
|
|
26
|
+
interface ComposerDraftWriter {
|
|
27
|
+
setDraft(text: string): void;
|
|
28
|
+
/**
|
|
29
|
+
* Return the keyboard to the composer with the caret it last held (the
|
|
30
|
+
* facade's own `focus`). Optional so the DOM fallback and older faces still
|
|
31
|
+
* restore the text.
|
|
32
|
+
*/
|
|
33
|
+
focus?(): void;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Fill the dsh composer with `text` through the `contenteditable`
|
|
37
|
+
* DOM path. Used by `setComposerText` (the harness-facade-aware writer) as the
|
|
38
|
+
* last-resort and by `runRewindAndFill` to put the withdrawn target message
|
|
39
|
+
* back into the composer after a rewind. Best-effort — no composer match means
|
|
40
|
+
* false, never a throw.
|
|
41
|
+
*/
|
|
42
|
+
export declare function fillComposer(text: string): boolean;
|
|
43
|
+
/**
|
|
44
|
+
* Composer write: prefer the harness facade's `setDraft` (correct whole-draft
|
|
45
|
+
* replace), then degrade to the DOM `fillComposer` (contenteditable). A facade
|
|
46
|
+
* that throws (session teardown) is treated as absent so the DOM path still
|
|
47
|
+
* restores the text. Never throws.
|
|
48
|
+
* @param text - the withdrawn target message text.
|
|
49
|
+
* @param facade - the session input draft writer, when reachable.
|
|
50
|
+
* @returns whether a channel applied the text.
|
|
51
|
+
*/
|
|
52
|
+
export declare function writeComposer(text: string, facade: ComposerDraftWriter | undefined): boolean;
|
|
53
|
+
/**
|
|
54
|
+
* Execute one rewind and, when it settles successfully, put the withdrawn
|
|
55
|
+
* target message's text back into the composer so the user can edit and
|
|
56
|
+
* re-send.
|
|
57
|
+
*/
|
|
58
|
+
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, restoreImages?: (sessionId: string, images: readonly MessageImageRef[]) => Promise<void>): Promise<void>;
|
|
59
|
+
/** The current composer draft: the contenteditable `textContent`.
|
|
60
|
+
* Empty when the composer is absent. Exported as a test seam (the
|
|
61
|
+
* empty-composer guard in `runRewindAndFill`). */
|
|
62
|
+
export declare function composerText(): string;
|
|
63
|
+
/**
|
|
64
|
+
* The session-kind slice the durable-target gate reads — the session
|
|
65
|
+
* snapshot's `subagent` cell, the Harness's own runtime signal for a
|
|
66
|
+
* direct-subagent (child) session. Typed structurally so the plugin never
|
|
67
|
+
* imports the session-controller snapshot contract.
|
|
68
|
+
*/
|
|
69
|
+
export interface SessionKindLike {
|
|
70
|
+
/** Non-null exactly while this Session is addressed as a subagent child. */
|
|
71
|
+
readonly subagent: unknown;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Whether a session is rewind-inert: a direct-subagent (child) session.
|
|
75
|
+
*
|
|
76
|
+
* The Harness refuses every generic Session RPC for a subagent-owned identity
|
|
77
|
+
* (`session/agent-busy`, "use subagent delivery for this child session"), so
|
|
78
|
+
* `/rewind` can never execute there — the command's own admission is the
|
|
79
|
+
* refusal, before any handler runs. A wired-up action in such a session would
|
|
80
|
+
* therefore be dead UI (SiriLee/dsh-rewind#26). This mirrors the Harness's own
|
|
81
|
+
* slash-command directory (which returns no commands for an addressed child)
|
|
82
|
+
* and the Host's `isSubagentSession` skips: a child session gets no rewind
|
|
83
|
+
* surface and records no snapshot.
|
|
84
|
+
*/
|
|
85
|
+
export declare function isRewindInertSession(snapshot: SessionKindLike): boolean;
|
|
86
|
+
export {};
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Client plugin styling: one injected `<style>` tag (scoped class names),
|
|
3
|
-
* following the dsh design tokens (`--dsw-*`) so the
|
|
4
|
-
*
|
|
3
|
+
* following the dsh design tokens (`--dsw-*`) so the per-message action, the
|
|
4
|
+
* mode popover, the failure toast and the settings card blend with the
|
|
5
|
+
* conversation chrome.
|
|
5
6
|
*
|
|
6
7
|
* @module dsh-rewind/client/styles
|
|
7
8
|
*/
|
|
8
9
|
/** Class names shared between the injected DOM and the stylesheet. */
|
|
9
10
|
export declare const CLASS: {
|
|
10
|
-
readonly
|
|
11
|
+
readonly action: 'dsh-rewind-action';
|
|
11
12
|
readonly popover: 'dsh-rewind-popover';
|
|
12
13
|
readonly popoverTitle: 'dsh-rewind-popover-title';
|
|
13
14
|
readonly popoverTarget: 'dsh-rewind-popover-target';
|
|
@@ -21,15 +22,5 @@ export declare const CLASS: {
|
|
|
21
22
|
readonly popoverGhost: 'dsh-rewind-popover-ghost';
|
|
22
23
|
readonly guardHint: 'dsh-rewind-guard-hint';
|
|
23
24
|
};
|
|
24
|
-
/**
|
|
25
|
-
* The ↶ glyph, drawn inline so the bundle stays dependency-free.
|
|
26
|
-
*
|
|
27
|
-
* Size follows the 0.1.7 product-icon set (15px, as the neighbouring copy/branch
|
|
28
|
-
* actions in `MessageIconActions`). Stroke is 1.25 rather than upstream's
|
|
29
|
-
* `Regular` 1: ↶ is a single open stroke next to a dense outline-plus-fill copy
|
|
30
|
-
* glyph, so at 1 it reads thinned out. Rounded caps/joins stay — upstream keeps
|
|
31
|
-
* them on curved artwork.
|
|
32
|
-
*/
|
|
33
|
-
export declare const REWIND_ICON_SVG: string;
|
|
34
25
|
/** One injected stylesheet (scoped under `.dsh-rewind-*`). */
|
|
35
|
-
export declare const STYLE = "\n.dsh-rewind-
|
|
26
|
+
export declare const STYLE = "\n/* The official action chrome, value for value: the action is Copy's peer in the\n same 28px row, so a different box, glyph scale, or resting tier would read as\n a different kind of control. */\n.dsh-rewind-action {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: calc(28px + var(--dsh-content-font-delta, 0px));\n height: calc(28px + var(--dsh-content-font-delta, 0px));\n padding: 6px;\n border: none;\n border-radius: var(--dsw-radius-sm, 8px);\n background: transparent;\n color: var(--dsw-alias-label-tertiary);\n cursor: pointer;\n}\n.dsh-rewind-action svg {\n width: calc(15px + var(--dsh-content-font-delta, 0px));\n height: calc(15px + var(--dsh-content-font-delta, 0px));\n}\n@media (hover: hover) {\n .dsh-rewind-action:hover {\n background: var(--dsw-alias-interactive-bg-hover);\n color: var(--dsw-alias-label-secondary);\n }\n}\n\n.dsh-rewind-popover {\n position: fixed;\n z-index: 1100;\n box-sizing: border-box;\n width: min(300px, calc(100vw - 24px));\n padding: 16px;\n border: 0;\n border-radius: var(--dsw-radius-lg);\n background: var(--dsw-specific-menu, var(--dsw-alias-bg-layer-3));\n backdrop-filter: var(--dsw-menu-backdrop-filter);\n isolation: isolate;\n --dsw-elevation-stroke-color: var(--dsw-alias-border-l1);\n /* Elevated surface: match the menu cards' l2 scrollbar tokens. */\n --dsh-scrollbar-thumb: var(--dsw-alias-scrollbar-bg-l2);\n --dsh-scrollbar-thumb-hover: var(--dsw-alias-scrollbar-hover-l2);\n box-shadow: var(--dsw-elevation-prominent);\n font-size: 12px;\n line-height: 18px;\n color: var(--dsw-alias-label-primary);\n}\n.dsh-rewind-popover-title {\n font-size: 12px;\n font-weight: 500;\n line-height: 18px;\n}\n.dsh-rewind-popover-target {\n margin: 2px 0 10px;\n font-size: 12px;\n line-height: 18px;\n color: var(--dsw-alias-label-tertiary);\n word-break: break-all;\n /* The full message sits above the popover; two lines are enough context. */\n display: -webkit-box;\n -webkit-box-orient: vertical;\n -webkit-line-clamp: 2;\n overflow: hidden;\n}\n.dsh-rewind-popover-option {\n display: flex;\n flex-direction: column;\n gap: 2px;\n width: 100%;\n margin: 0 0 2px;\n padding: 6px 8px;\n border: 0;\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/* Arrow navigation moves real focus, so the focused row carries the pointer's\n fill and the browser ring is suppressed: in dark mode the UA two-tone ring\n draws its dark half against the filled row and reads as a residue edge (the\n harness Menu suppresses it the same way). */\n.dsh-rewind-popover-option:focus-visible:not(:disabled) {\n background: var(--dsw-alias-interactive-bg-hover);\n outline: none;\n}\n.dsh-rewind-popover-option:disabled {\n opacity: 0.4;\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-note {\n margin-top: 6px;\n font-size: 11px;\n line-height: 15px;\n color: var(--dsw-alias-label-tertiary);\n}\n.dsh-rewind-popover-actions {\n display: flex;\n justify-content: flex-end;\n gap: 8px;\n margin-top: 8px;\n}\n.dsh-rewind-popover-primary,\n.dsh-rewind-popover-ghost {\n height: 28px;\n padding: 0 10px;\n border: none;\n border-radius: 8px;\n font: inherit;\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.4;\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.dsh-rewind-popover-ghost:active {\n background: var(--dsw-alias-interactive-bg-active);\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: 0;\n border-radius: var(--dsw-radius-md);\n background: var(--dsw-specific-menu, var(--dsw-alias-bg-layer-3));\n backdrop-filter: var(--dsw-menu-backdrop-filter);\n isolation: isolate;\n --dsw-elevation-stroke-color: var(--dsw-alias-border-l1);\n box-shadow: var(--dsw-elevation-panel);\n font-size: 13px;\n line-height: 18px;\n color: var(--dsw-alias-label-primary);\n pointer-events: none;\n}\n\n/* ---- Plugin configuration form ---- */\n/* Only the card's own controls: the two switch rows, their hints, and the divider. */\n.dsh-rewind-settings-divider {\n border-top: 1px solid var(--dsw-alias-border-l2);\n}\n.dsh-rewind-settings-section {\n display: grid;\n gap: 6px;\n padding: 12px 0;\n}\n.dsh-rewind-settings-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-settings-toggle-label {\n flex: 1;\n min-width: 0;\n}\n.dsh-rewind-settings-hint {\n margin: 0;\n font-size: 12px;\n line-height: 1.5;\n color: var(--dsw-alias-label-tertiary);\n}\n";
|
package/lib/types/snapshot.d.ts
CHANGED
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
* kept), and restores read/write the real file system with plain `node:fs`
|
|
23
23
|
* — independent of the fs service. The atomic-commit and journaled-restore
|
|
24
24
|
* guarantees, the security model and the in-store layout are specified in
|
|
25
|
-
* `
|
|
25
|
+
* `docs/security-model.md` and `docs/format.md`.
|
|
26
26
|
*
|
|
27
27
|
* Restore semantics (identical to Claude Code): for every path with entries
|
|
28
28
|
* anchored at or after the target message, apply the EARLIEST entry — write
|
package/package.json
CHANGED
|
@@ -1,301 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* dsh-rewind portal half: the per-user-message ↶ rewind button, rendered as a
|
|
3
|
-
* React portal inside the message's `MessageIconActions` row.
|
|
4
|
-
*
|
|
5
|
-
* Why portals (aligned with the copy button's own rendering): the copy button
|
|
6
|
-
* is a React child of the actions row, painted in the same commit as the
|
|
7
|
-
* bubble. A pure-DOM `appendChild` (the earlier approach) lands one microtask
|
|
8
|
-
* later and re-runs a full-transcript scan on EVERY mutation, which can push
|
|
9
|
-
* the paint of a newly sent bubble — the "occasional hiccup before the bubble
|
|
10
|
-
* shows". Portals let React own the button lifecycle (mount/unmount with the
|
|
11
|
-
* row, no orphaned buttons, no manual re-attach after harness re-renders),
|
|
12
|
-
* and the target collection is coalesced (one refresh per mutation batch) and
|
|
13
|
-
* diffed (no setState churn when nothing changed).
|
|
14
|
-
*
|
|
15
|
-
* Mount point: the plugin registers a session-scoped bridge into the harness's
|
|
16
|
-
* `conversation.session.header.actions` list slot. The bridge renders NO
|
|
17
|
-
* header UI — it only portals buttons into the user rows of the session the
|
|
18
|
-
* harness mounts it for. That slot is the harness-native way to get a
|
|
19
|
-
* per-session React mount without touching any source; the registration is
|
|
20
|
-
* typed structurally (see `SlotsLike`) so the plugin never imports the
|
|
21
|
-
* conversation UI package's types and survives its version drift.
|
|
22
|
-
*
|
|
23
|
-
* COUPLING NOTE — HIGH. There is no harness interface for a per-user-message
|
|
24
|
-
* action (see docs/compat/audit.md): `MessageIconActions.extraActions` and the
|
|
25
|
-
* `conversation.chat.assistant-actions` slot are wired for assistant messages
|
|
26
|
-
* only, and no per-user-message action slot exists. So this portal targets
|
|
27
|
-
* undocumented internal structure — the `data-chat-flow-kind`,
|
|
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.
|
|
40
|
-
*
|
|
41
|
-
* @module dsh-rewind/client/portals
|
|
42
|
-
*/
|
|
43
|
-
import { type ReactNode } from 'react';
|
|
44
|
-
import type { SessionFace } from '@deepseek-ai/dsh-api-session-controller/client';
|
|
45
|
-
import { type ChatOf, type ChatWatch, type HiddenChat, type MessageImageRef } from './hidden.ts';
|
|
46
|
-
import type { RewindKey } from './locales.ts';
|
|
47
|
-
type Translate = (key: RewindKey, params?: Record<string, unknown>) => string;
|
|
48
|
-
/** One portal target: the actions row of a user/steering seat + its durable node. */
|
|
49
|
-
export type PortalTarget = {
|
|
50
|
-
readonly kind: 'durable';
|
|
51
|
-
/** The seat's chat node key (React reconciliation + diff identity). */
|
|
52
|
-
readonly key: string;
|
|
53
|
-
/** The row's actions container (React portal target). */
|
|
54
|
-
readonly container: HTMLElement;
|
|
55
|
-
readonly seq: number;
|
|
56
|
-
readonly time: number;
|
|
57
|
-
readonly preview: string;
|
|
58
|
-
} | {
|
|
59
|
-
readonly kind: 'pending';
|
|
60
|
-
/** `pending:${itemId}` — stable per inbox occurrence. */
|
|
61
|
-
readonly key: string;
|
|
62
|
-
/** The row's actions container (React portal target). */
|
|
63
|
-
readonly container: HTMLElement;
|
|
64
|
-
/** The host inbox occurrence the retract button addresses. */
|
|
65
|
-
readonly itemId: string;
|
|
66
|
-
/** Complete editable text; null when the message contains no text block. */
|
|
67
|
-
readonly text: string | null;
|
|
68
|
-
readonly preview: string;
|
|
69
|
-
};
|
|
70
|
-
/** Capabilities the session-scoped bridge receives from the plugin apply(). */
|
|
71
|
-
export interface RewindBridgeDeps {
|
|
72
|
-
readonly sessionOf: (sessionId: string) => SessionFace | undefined;
|
|
73
|
-
/**
|
|
74
|
-
* Chat reader on the `uiConversation` "chat" view: every chat
|
|
75
|
-
* snapshot read goes through it. See `chatSnapshotOf` in hidden.ts.
|
|
76
|
-
*/
|
|
77
|
-
readonly chatOf: ChatOf;
|
|
78
|
-
/**
|
|
79
|
-
* Subscribe to one session's live chat-update signal, so the composer refill
|
|
80
|
-
* waiting on an executed rewind's chat node can be woken when the chat
|
|
81
|
-
* snapshot changes. Passed through to `waitForCommand`.
|
|
82
|
-
*/
|
|
83
|
-
readonly watchChat: ChatWatch;
|
|
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;
|
|
91
|
-
readonly t: Translate;
|
|
92
|
-
readonly subscribeLocale: (cb: () => void) => () => void;
|
|
93
|
-
/**
|
|
94
|
-
* Session-aware composer writer (see `writeComposer`): the
|
|
95
|
-
* `conversation.input` facade `setDraft` when reachable, else the DOM fill.
|
|
96
|
-
* Session-scoped so the refill only lands in the session that just rewound.
|
|
97
|
-
*/
|
|
98
|
-
readonly setComposerText: (sessionId: string, text: string) => boolean;
|
|
99
|
-
/**
|
|
100
|
-
* Session-aware image restorer (see `restoreImages` in index.ts): re-adds
|
|
101
|
-
* a withdrawn message's durable image references to that session's
|
|
102
|
-
* composer draft so a rewind of an image-bearing message does not lose
|
|
103
|
-
* the pictures. Optional — absent degrades to the text-only refill.
|
|
104
|
-
*/
|
|
105
|
-
readonly restoreImages?: (sessionId: string, images: readonly MessageImageRef[]) => Promise<void>;
|
|
106
|
-
}
|
|
107
|
-
/** Structural face of the runtime slot service (see the module doc). */
|
|
108
|
-
export interface SlotsLike {
|
|
109
|
-
inject(key: string, install: () => () => void): () => void;
|
|
110
|
-
/**
|
|
111
|
-
* One slot registration. `Face` is the injected business face the renderer
|
|
112
|
-
* decomposes (its `hooks` compartment is rebound as a `use<Name>` selector
|
|
113
|
-
* hook); `Props` is the COMPOSED props a component receives — the slot's
|
|
114
|
-
* runtime owner share, the locale seat, and that face — which is what the
|
|
115
|
-
* component is typed against, never the face alone.
|
|
116
|
-
*/
|
|
117
|
-
register<P, Face = P>(entry: {
|
|
118
|
-
readonly name: string;
|
|
119
|
-
readonly id?: string;
|
|
120
|
-
readonly order?: number;
|
|
121
|
-
readonly key?: string;
|
|
122
|
-
readonly locale?: string;
|
|
123
|
-
readonly inject?: () => Face;
|
|
124
|
-
}, component: (props: P) => ReactNode): () => void;
|
|
125
|
-
}
|
|
126
|
-
/**
|
|
127
|
-
* The session input facade's write face (structural, so the plugin
|
|
128
|
-
* never imports the conversation UI package). `setDraft` replaces the whole
|
|
129
|
-
* composer draft through the harness's own Lexical editor — the correct way
|
|
130
|
-
* to restore the withdrawn text.
|
|
131
|
-
*/
|
|
132
|
-
interface ComposerDraftWriter {
|
|
133
|
-
setDraft(text: string): void;
|
|
134
|
-
/**
|
|
135
|
-
* Return the keyboard to the composer with the caret it last held (the
|
|
136
|
-
* facade's own `focus`). Optional so the DOM fallback and older faces still
|
|
137
|
-
* restore the text.
|
|
138
|
-
*/
|
|
139
|
-
focus?(): void;
|
|
140
|
-
}
|
|
141
|
-
/**
|
|
142
|
-
* Fill the dsh composer with `text` through the `contenteditable`
|
|
143
|
-
* DOM path. Used by `setComposerText` (the harness-facade-aware writer) as the
|
|
144
|
-
* last-resort and by `runRewindAndFill` to put the withdrawn target message
|
|
145
|
-
* back into the composer after a rewind. Best-effort — no composer match means
|
|
146
|
-
* false, never a throw.
|
|
147
|
-
*/
|
|
148
|
-
export declare function fillComposer(text: string): boolean;
|
|
149
|
-
/**
|
|
150
|
-
* Composer write: prefer the harness facade's `setDraft` (correct whole-draft
|
|
151
|
-
* replace), then degrade to the DOM `fillComposer` (contenteditable). A facade
|
|
152
|
-
* that throws (session teardown) is treated as absent so the DOM path still
|
|
153
|
-
* restores the text. Never throws.
|
|
154
|
-
* @param text - the withdrawn target message text.
|
|
155
|
-
* @param facade - the session input draft writer, when reachable.
|
|
156
|
-
* @returns whether a channel applied the text.
|
|
157
|
-
*/
|
|
158
|
-
export declare function writeComposer(text: string, facade: ComposerDraftWriter | undefined): boolean;
|
|
159
|
-
/**
|
|
160
|
-
* Execute one rewind from the popover and, when it settles successfully,
|
|
161
|
-
* put the withdrawn target message's text back into the composer so the
|
|
162
|
-
* user can edit and re-send.
|
|
163
|
-
*
|
|
164
|
-
* THE COMPOSER FILL IS EVENT-DRIVEN: it runs only when THIS page performed
|
|
165
|
-
* the rewind (the user clicked confirm moments ago). It must NEVER scan
|
|
166
|
-
* loaded history for rewind commands: a session window opens with only
|
|
167
|
-
* the tail page and grows via loadOlder, so a "command already in the
|
|
168
|
-
* snapshot" cannot be told apart from "command executed in this page" —
|
|
169
|
-
* the old baseline heuristic refilled withdrawn text into the composer
|
|
170
|
-
* after switching sessions or restarting dsh.
|
|
171
|
-
*/
|
|
172
|
-
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, restoreImages?: (sessionId: string, images: readonly MessageImageRef[]) => Promise<void>): Promise<void>;
|
|
173
|
-
/**
|
|
174
|
-
* Locate the actions container of a user/steering seat row — the element the
|
|
175
|
-
* ↶ button portals into (the copy/branch IconActions row).
|
|
176
|
-
*
|
|
177
|
-
* The finder is STRUCTURAL on purpose: `MessageIconActions` marks this container
|
|
178
|
-
* with `data-clock`, which names clock placement rather than the action row (the
|
|
179
|
-
* old `data-time-hover-root` marker lived on the per-turn tail footer and is
|
|
180
|
-
* gone in 0.1.6-alpha.2), so the container is located as the direct holder of
|
|
181
|
-
* the copy `<button>` (the `MessageIconActions` container, which mounts that
|
|
182
|
-
* button as a direct child).
|
|
183
|
-
*
|
|
184
|
-
* Returns undefined when no qualifying container is found; the caller refuses
|
|
185
|
-
* to portal (never a crash, never a wrong attachment). Exported as a test seam
|
|
186
|
-
* (see `collectTargets`) so the row-shape finder is exercised directly for
|
|
187
|
-
* both durable and pending row shapes without a full React portal render.
|
|
188
|
-
*/
|
|
189
|
-
export declare function actionsContainerOf(row: HTMLElement | undefined): HTMLElement | undefined;
|
|
190
|
-
/**
|
|
191
|
-
* Collect the portal targets of one session: user rows × snapshot nodes.
|
|
192
|
-
* Exported as a test seam — the DOM→targets pairing that drives the ↶ button
|
|
193
|
-
* is otherwise only reachable through a full React portal render.
|
|
194
|
-
*/
|
|
195
|
-
export declare function collectTargets(chat: HiddenChat): readonly PortalTarget[];
|
|
196
|
-
/**
|
|
197
|
-
* The session-kind slice the durable-target gate reads — the session
|
|
198
|
-
* snapshot's `subagent` cell, the Harness's own runtime signal for a
|
|
199
|
-
* direct-subagent (child) session. Typed structurally so the plugin never
|
|
200
|
-
* imports the session-controller snapshot contract.
|
|
201
|
-
*/
|
|
202
|
-
export interface SessionKindLike {
|
|
203
|
-
/** Non-null exactly while this Session is addressed as a subagent child. */
|
|
204
|
-
readonly subagent: unknown;
|
|
205
|
-
}
|
|
206
|
-
/**
|
|
207
|
-
* Whether a session is rewind-inert: a direct-subagent (child) session.
|
|
208
|
-
*
|
|
209
|
-
* The Harness refuses every generic Session RPC for a subagent-owned identity
|
|
210
|
-
* (`session/agent-busy`, "use subagent delivery for this child session"), so
|
|
211
|
-
* `/rewind` can never execute there — the command's own admission is the
|
|
212
|
-
* refusal, before any handler runs. A wired-up ↶ button in such a session is
|
|
213
|
-
* therefore dead UI that closes its popover and does nothing
|
|
214
|
-
* (SiriLee/dsh-rewind#26). This mirrors the Harness's own slash-command
|
|
215
|
-
* directory (which returns no commands for an addressed child), the pending
|
|
216
|
-
* path's `collectPendingTargets` gate, and the Host's `isSubagentSession`
|
|
217
|
-
* skips: a child session gets no rewind surface and records no snapshot.
|
|
218
|
-
*/
|
|
219
|
-
export declare function isRewindInertSession(snapshot: SessionKindLike): boolean;
|
|
220
|
-
/**
|
|
221
|
-
* Collect the durable (sent-message) rewind targets of one session: none at all
|
|
222
|
-
* for a rewind-inert subagent session, otherwise `collectTargets`' DOM→target
|
|
223
|
-
* pairing. Kept separate from `collectTargets` so the session-kind gate is a
|
|
224
|
-
* pure, directly testable decision.
|
|
225
|
-
* @param snapshot - the session snapshot carrying the `subagent` cell.
|
|
226
|
-
* @param chat - the session's chat snapshot, or undefined while unavailable.
|
|
227
|
-
*/
|
|
228
|
-
export declare function collectDurableTargets(snapshot: SessionKindLike, chat: HiddenChat | undefined): readonly PortalTarget[];
|
|
229
|
-
/**
|
|
230
|
-
* The pending bubble row's compared text and its message-attachment block
|
|
231
|
-
* count, read from a CLONE (the live row is never touched).
|
|
232
|
-
*
|
|
233
|
-
* Both the actions container and the message-attachment block are excluded from
|
|
234
|
-
* the text: the copy button's Tooltip mounts its label inside that button, and
|
|
235
|
-
* the attachment block renders text of its own (a file name/size, a lone
|
|
236
|
-
* image's loading label) that no inbox row reproduces. The block is counted
|
|
237
|
-
* instead, as the attachment half of the match key (`matchPendingRows`).
|
|
238
|
-
*
|
|
239
|
-
* The clone's counterpart is taken by query position, not by position among the
|
|
240
|
-
* row's children: the harness renders the attachment block BEFORE the message
|
|
241
|
-
* bubble, so "drop the first child" would drop the message text.
|
|
242
|
-
*
|
|
243
|
-
* @param row - one rendered `[data-pending-steering]` row.
|
|
244
|
-
* @returns the compared text and the attachment block count.
|
|
245
|
-
*/
|
|
246
|
-
export declare function bubbleTextOf(row: HTMLElement): {
|
|
247
|
-
readonly text: string;
|
|
248
|
-
readonly attachments: number;
|
|
249
|
-
};
|
|
250
|
-
interface RewindPortalsProps extends RewindBridgeDeps {
|
|
251
|
-
readonly sessionId: string;
|
|
252
|
-
}
|
|
253
|
-
/**
|
|
254
|
-
* Session-scoped portal bridge: renders the ↶ button of every user message
|
|
255
|
-
* row of the session the harness mounts it for. The refresh is coalesced
|
|
256
|
-
* (one pass per microtask window via queueMicrotask) and diffed (setState is
|
|
257
|
-
* skipped when the target set is unchanged), so the plugin never runs a
|
|
258
|
-
* synchronous full-transcript scan inside a commit microtask.
|
|
259
|
-
*
|
|
260
|
-
* The refresh is driven by the three inputs its targets are made of: the chat
|
|
261
|
-
* snapshot's visible order, the session's `inbox` projection (the pending
|
|
262
|
-
* occurrences — see `syncInboxSubscription`), and the DOM (row/actions mounts,
|
|
263
|
-
* via the scoped observer).
|
|
264
|
-
*/
|
|
265
|
-
export declare function RewindPortals({ sessionId, sessionOf, chatOf, isMainViewSession, watchChat, t, subscribeLocale, setComposerText, restoreImages }: RewindPortalsProps): ReactNode;
|
|
266
|
-
/** The current composer draft: the contenteditable `textContent`.
|
|
267
|
-
* Empty when the composer is absent. Exported as a test seam (the
|
|
268
|
-
* empty-composer guard in `retractPending`). */
|
|
269
|
-
export declare function composerText(): string;
|
|
270
|
-
/**
|
|
271
|
-
* Withdraw one pre-sent (pending steering) message and every steering message
|
|
272
|
-
* after it (the rollback point's "future"), WITHOUT interrupting the run.
|
|
273
|
-
*
|
|
274
|
-
* The message is still in the agent's next-step inbox, so the loop has not
|
|
275
|
-
* claimed it into any request: removing it cannot change what the model is
|
|
276
|
-
* generating. Stopping the run first (the durable-rewind rule) would throw
|
|
277
|
-
* away in-flight work — a long tool call, a partial answer — for an edit the
|
|
278
|
-
* model can never observe (SiriLee/dsh-rewind#27). Only the durable rewind
|
|
279
|
-
* needs a stop, because it cuts the model-visible surface.
|
|
280
|
-
*
|
|
281
|
-
* 1. Remove the target and its future (steering only; queued stays) through
|
|
282
|
-
* the session's own `updateQueue` channel, oldest first.
|
|
283
|
-
* 2. Put the target's own images back, then its text when the composer is
|
|
284
|
-
* empty (the empty-composer guard, so a draft is never clobbered).
|
|
285
|
-
*
|
|
286
|
-
* The realistic failure is `queue-item-not-found`: a step boundary claimed
|
|
287
|
-
* the whole next-step batch a moment ago, so the message is now durable and
|
|
288
|
-
* the durable row's regular rewind button takes over with no gap. Nothing is
|
|
289
|
-
* removed then, and the composer is NOT refilled — refilling would invite a
|
|
290
|
-
* duplicate send of a message the model has already received.
|
|
291
|
-
*/
|
|
292
|
-
export declare function retractPending(session: SessionFace, itemId: string, text: string | null, setComposerText: (sessionId: string, text: string) => boolean, restoreImages?: (sessionId: string, images: readonly MessageImageRef[]) => Promise<void>): Promise<void>;
|
|
293
|
-
/**
|
|
294
|
-
* Build the slot-entry component for the plugin apply(): a tiny bridge that
|
|
295
|
-
* injects the apply-time capabilities (session resolution, locale, rewind
|
|
296
|
-
* runner) into the module-level `RewindPortals`.
|
|
297
|
-
*/
|
|
298
|
-
export declare function createRewindBridge(deps: RewindBridgeDeps): (props: {
|
|
299
|
-
readonly sessionId: string;
|
|
300
|
-
}) => ReactNode;
|
|
301
|
-
export {};
|