dsh-diff-approval 0.30.1 → 0.32.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.
Files changed (36) hide show
  1. package/README.md +11 -23
  2. package/README.zh.md +9 -21
  3. package/assets/fonts/README.md +10 -8
  4. package/assets/fonts/manifest.json +1 -1
  5. package/lib/client.js +2791 -894
  6. package/lib/index.js +1755 -269
  7. package/lib/types/annotate-skill.d.ts +1 -1
  8. package/lib/types/annotate-tool.d.ts +11 -0
  9. package/lib/types/annotate.d.ts +28 -0
  10. package/lib/types/client/PendingPanel.d.ts +31 -21
  11. package/lib/types/client/category-color.d.ts +62 -0
  12. package/lib/types/client/chords.d.ts +70 -17
  13. package/lib/types/client/discussion.d.ts +10 -3
  14. package/lib/types/client/dock.d.ts +8 -3
  15. package/lib/types/client/dsh-icons.d.ts +39 -1
  16. package/lib/types/client/header-entry.d.ts +8 -7
  17. package/lib/types/client/locales.d.ts +52 -12
  18. package/lib/types/client/panel-memory.d.ts +24 -6
  19. package/lib/types/client/path-match.d.ts +50 -0
  20. package/lib/types/client/port.d.ts +29 -2
  21. package/lib/types/client/produced-diff.d.ts +83 -6
  22. package/lib/types/client/reference.d.ts +0 -2
  23. package/lib/types/client/remap-sync.d.ts +18 -1
  24. package/lib/types/client/session-seat.d.ts +47 -7
  25. package/lib/types/client/settings.d.ts +40 -14
  26. package/lib/types/client/slots.d.ts +115 -3
  27. package/lib/types/client/store.d.ts +50 -0
  28. package/lib/types/client/whole-file-diff.d.ts +42 -0
  29. package/lib/types/comment-ask.d.ts +47 -20
  30. package/lib/types/comments.d.ts +141 -33
  31. package/lib/types/index.d.ts +1 -1
  32. package/lib/types/pending.d.ts +30 -4
  33. package/lib/types/types.d.ts +186 -4
  34. package/lib/types/vcs.d.ts +35 -1
  35. package/lib/types/version.d.ts +162 -0
  36. package/package.json +4 -2
@@ -204,32 +204,58 @@ export declare function setSplitMode(value: boolean): void;
204
204
  export declare function mdPreviewEnabled(): boolean;
205
205
  /** Persist the Markdown-preview default preference. */
206
206
  export declare function setMdPreviewEnabled(value: boolean): void;
207
+ /**
208
+ * Whether this plugin MOUNTS its own file menu at all — the press bridge that takes over a file press in
209
+ * the shell's lists, a file link in a message, and a relative link in the Markdown preview.
210
+ *
211
+ * Default ON, and the default is the interesting half: only an explicit `'0'` turns it off, so a reader who
212
+ * never touches the setting keeps the menu they have always had (and a storage that cannot be read — a
213
+ * privacy mode, a cleared profile — leaves the plugin working rather than silently disabled).
214
+ *
215
+ * Off means the bridge does not take the press over AT ALL: no menu, and the shell's own behaviour runs
216
+ * exactly as if this plugin were not installed. That includes this plugin's own preview, where a relative
217
+ * link therefore goes back to the browser's own navigation — which is the honest consequence of asking this
218
+ * plugin to keep its hands off, not a bug to special-case.
219
+ *
220
+ * @returns whether the file menu is enabled.
221
+ */
222
+ export declare function chipMenuEnabled(): boolean;
223
+ /** Persist the file-menu preference (see {@link chipMenuEnabled}). */
224
+ export declare function setChipMenuEnabled(value: boolean): void;
207
225
  /**
208
226
  * Whether commenting on a diff range is offered: the selection frame's comment button,
209
- * and the chord that stands for it. Defaults to off while the mode is a preview (see
210
- * {@link COMMENT_MODE_PREVIEW_KEY}); only an explicit `'1'` enables it. This gates
211
- * STARTING a thread, not the threads themselves — one already written keeps rendering,
212
- * so turning the mode off never hides work or interrupts an answer on its way back.
227
+ * and the chord that stands for it. **Defaults to ON** — comment mode is the mode now,
228
+ * not a preview — so only an explicit `'0'` turns it off (the `chipMenuEnabled` idiom a
229
+ * few lines up), and an absent field means on for every profile that predates the flip.
230
+ * This gates STARTING a thread, not the threads themselves — one already written keeps
231
+ * rendering, so turning the mode off never hides work or interrupts an answer on its way
232
+ * back.
213
233
  * @returns whether comment mode is on.
214
234
  */
215
235
  export declare function commentModeEnabled(): boolean;
216
- /** Window event: comment mode was toggled (see {@link setCommentModeEnabled}). */
217
- export declare const COMMENT_MODE_CHANGED_EVENT = "diff-approval:comment-mode";
218
- /** Persist the comment-mode preference. */
236
+ /** Window event: comment mode was toggled (see {@link setCommentModeEnabled}). A DIFFERENT string from
237
+ * {@link COMMENT_MODE_KEY} on purpose: a key literal and an event literal that read the same make every
238
+ * grep a guess (the hazard this file already names for the file-menu key at the top). */
239
+ export declare const COMMENT_MODE_CHANGED_EVENT = "diff-approval:comment-mode-changed";
240
+ /** Persist the comment-mode preference (`'1'` on, `'0'` off — see {@link commentModeEnabled}). */
219
241
  export declare function setCommentModeEnabled(value: boolean): void;
220
242
  /**
221
243
  * Whether the diff's code uses the font this plugin bundles.
222
244
  *
223
- * Defaults to **off**: the bundled face is JetBrains Maple Mono subset and
224
- * sliced into 138 woff2 files, and a page pulls the slices its characters land
225
- * in — tens of kilobytes for a comment, up to ~120 KB for a rare character. That
226
- * is real traffic, so it is the reader's decision, not a default they discover.
227
- * Only an explicit `'1'` enables it.
245
+ * **Defaults to ON**, like comment mode: absent means on, and only an explicit `'0'` turns
246
+ * it off. The old field's answer is deliberately NOT carried over (see the constant): a
247
+ * reader who had turned the bundled face off before this change comes back with it on, which
248
+ * is the one behaviour the reader asked for. The cost is real and worth naming where the
249
+ * switch is: the face is a JetBrains Maple Mono subset sliced into 138 woff2 files, and a
250
+ * page pulls only the slices its characters land in — tens of kilobytes for a comment, up to
251
+ * ~120 KB for a rare character — downloaded on demand rather than shipped.
228
252
  * @returns whether the bundled code font is enabled.
229
253
  */
230
254
  export declare function codeFontEnabled(): boolean;
231
- /** Window event: the bundled code font was turned on or off (see {@link setCodeFontEnabled}). */
232
- export declare const CODE_FONT_CHANGED_EVENT = "diff-approval:code-font";
255
+ /** Window event: the bundled code font was turned on or off (see {@link setCodeFontEnabled}). A DIFFERENT
256
+ * string from {@link CODE_FONT_KEY} on purpose — they used to be the same literal, which made a grep for
257
+ * either one a guess (the hazard this file names for the file-menu key at the top). */
258
+ export declare const CODE_FONT_CHANGED_EVENT = "diff-approval:code-font-changed";
233
259
  /** Persist the bundled-code-font preference. */
234
260
  export declare function setCodeFontEnabled(value: boolean): void;
235
261
  /**
@@ -4,7 +4,7 @@ import type { SessionId } from '@deepseek-ai/dsh-client-connection/client';
4
4
  import type { DockSnapshot } from './dock.tsx';
5
5
  import type { CommentDraft } from './port.ts';
6
6
  import type { CommentLineRange } from '../comment-lines.ts';
7
- import type { CommentRecord, DiffApprovalAddValue, DiffApprovalBlockRange, DiffApprovalBrowseValue, DiffApprovalCommentAddValue, DiffApprovalCommentAskValue, DiffApprovalCommentRemoveManyValue, DiffApprovalCommentRemoveValue, DiffApprovalOpenAction, DiffApprovalRefreshValue, PendingFileDiff, VcsImportValue } from '../types.ts';
7
+ import type { CommentRecord, DiffApprovalAddValue, DiffApprovalBlockRange, DiffApprovalBrowseValue, DiffApprovalCommentAddValue, DiffApprovalCommentAskValue, DiffApprovalCommentRemoveManyValue, DiffApprovalCommentRemoveValue, DiffApprovalOpenAction, DiffApprovalRefreshValue, DiffApprovalUpdateValue, PendingFileDiff, VcsImportValue } from '../types.ts';
8
8
  /** What the panel reads and drives: the pending list plus in-flight entries. */
9
9
  export interface PendingDiffSnapshot {
10
10
  /** Whether a list read has completed at least once. */
@@ -32,6 +32,21 @@ export interface PendingDiffSnapshot {
32
32
  commentAnswers: Record<string, string>;
33
33
  /** A read failure's message; absent while the latest read succeeded. */
34
34
  error?: string;
35
+ /**
36
+ * How many rows the host says this session has, from the LIGHT read (`list-count`) — the badge's
37
+ * question, answered without shipping any file content.
38
+ *
39
+ * It is the number a full read would have carried, but it is a separate fact with a separate age: a
40
+ * count lands on ticks that ask only for a count (when nothing is showing), and a FULL read clears it,
41
+ * because that read carries the same fact in `files` and is newer. So a reader's rule is
42
+ * `count ?? files.length` — the fallback covers both "no count has arrived" and "the list is fresher",
43
+ * and a count that FAILED leaves the previous number in place rather than showing zero.
44
+ *
45
+ * Absent on a host that predates the endpoint: there the store keeps doing full reads, the value stays
46
+ * absent, and every reader stays on the fallback — today's behaviour, at today's cost (see
47
+ * `PendingDiffStore.refreshCount`).
48
+ */
49
+ count?: number | undefined;
35
50
  /** Entry ids whose keep/revert is in flight; their controls are disabled. */
36
51
  busy: ReadonlySet<string>;
37
52
  /** Entry ids whose last keep/revert failed, mapped to the error message; the panel surfaces these inline (row tag + detail banner) instead of hiding the list. */
@@ -69,8 +84,81 @@ export interface PendingDiffSnapshot {
69
84
  * panel is closed) via a bottom-right notice. */
70
85
  redoCleared?: boolean;
71
86
  }
87
+ /** One filter over one session's view of the pending list; the shape every view hook takes. */
88
+ export type PendingViewSelector<T> = (view: PendingDiffSnapshot) => T;
89
+ /**
90
+ * The number a badge, a header entry or a dock chip shows for one session: the light count when one has
91
+ * arrived, else the length of the list that read carried.
92
+ *
93
+ * The fallback is not a nicety — it is the whole older-host story and the state before the first count
94
+ * lands. `count` is absent when no count has arrived (the page just loaded, or the host predates the
95
+ * endpoint, where the store keeps doing full reads) and it is cleared by every full read, whose `files`
96
+ * are the same fact and newer. So `count` is only ever shown while it is the FRESHEST number the client
97
+ * has; otherwise the list speaks. See `PendingDiffSnapshot.count`.
98
+ * @param view - one session's view.
99
+ * @returns how many rows it holds.
100
+ */
101
+ export declare function countOf(view: PendingDiffSnapshot): number;
102
+ /**
103
+ * The view a seat draws when it has NO reviewable session: a session that is absent, or one the shell has
104
+ * told us is blank — a brand-new session with nothing said in it yet.
105
+ *
106
+ * This exists because the page-wide fallback is the wrong answer for such a seat. `viewFor(undefined)`
107
+ * and `getSnapshot()` deliberately answer the newest session read on the page (the whole-page contract the
108
+ * remap follows), and with the store's slots keyed only by session id and no workspace comparison anywhere
109
+ * in this client, "the newest read" can be another session's list — even another workspace's. A seat that
110
+ * has nothing of its own to show must draw NOTHING, and this is the nothing: no rows (so `countOf` is 0),
111
+ * no comments, no workspace, and `read: false` because nothing has been read FOR this seat.
112
+ *
113
+ * Shared rather than re-spelled per seat: the panel's badge and body, the header entry's count and the
114
+ * dock chip all have to agree that "no reviewable session" means zero, and a second literal here is how
115
+ * they would drift. Frozen because it is handed to readers as a snapshot: nothing may write through it.
116
+ */
117
+ export declare const EMPTY_PENDING_VIEW: PendingDiffSnapshot;
118
+ /**
119
+ * The view a seat DRAWS when there is nothing to review: the same emptiness as {@link EMPTY_PENDING_VIEW},
120
+ * but READ.
121
+ *
122
+ * The difference is what the panel draws: an unread view is a session whose list is still coming, so it
123
+ * shows the loading state, while this one says "no pending changes" — the truth for a session that is
124
+ * absent or blank. Using the unread view here would leave a docked panel spinning forever behind an entry
125
+ * that is disabled and can never be opened.
126
+ */
127
+ export declare const NOTHING_TO_REVIEW_VIEW: PendingDiffSnapshot;
128
+ /** Function shape of {@link PendingViewHooks.pendingView}; named so a seat can call it directly. */
129
+ export type PendingViewReader = <T>(sessionId: SessionId | undefined, select: PendingViewSelector<T>) => T;
130
+ /**
131
+ * How a seat reads pending state for the session it is actually about.
132
+ *
133
+ * The page runs several seats at once — the sidebar footer's badge, the Session header's entry, the
134
+ * docked tab's chip and body — and they can belong to different sessions. Reading the ONE page-wide
135
+ * snapshot is what let a badge show another session's count, so a seat that knows its session reads
136
+ * that session's own view through {@link PendingViewHooks.pendingView} and nothing else.
137
+ *
138
+ * This lives BESIDE the face's `hooks`, not inside it: that compartment is a map of observables
139
+ * (`HooksSources`), and the framework binds each one onto the component props as a `use<Name>` selector
140
+ * hook. Folding a non-observable in there would stop the whole compartment from matching, and the panel
141
+ * would lose `usePending` and `useDock` with it. `pendingView` is a read, not an observable: the panel's
142
+ * `usePending` subscription is what re-renders it.
143
+ *
144
+ * The hook is optional because the shapes are read structurally: a host (or a test) that offers only
145
+ * `pending` still renders, with the panel falling back to the page-wide snapshot.
146
+ */
147
+ export interface PendingViewHooks {
148
+ /**
149
+ * Read one session's own view, with `select` applied inside the subscription.
150
+ *
151
+ * `sessionId === undefined` means "I have no session of my own": the answer is then the page-wide
152
+ * view (the newest session read), which is what a whole-page caller wants. A session that has never
153
+ * been read answers an unread empty view — never another session's list.
154
+ * @param sessionId - the session whose view to read.
155
+ * @param select - what to take out of that view.
156
+ * @returns the selected value.
157
+ */
158
+ pendingView?: PendingViewReader | undefined;
159
+ }
72
160
  /** The injected face the panel component receives from the plugin body. */
73
- export interface PendingPanelFace {
161
+ export interface PendingPanelFace extends PendingViewHooks {
74
162
  hooks: {
75
163
  /** Live pending-diff snapshot for the current page. */
76
164
  pending: HostObservable<PendingDiffSnapshot>;
@@ -91,8 +179,26 @@ export interface PendingPanelFace {
91
179
  /** Close the docked tab now, when one exists (the quick-summon chord's job
92
180
  * while the panel lives in the sidebar). */
93
181
  closeDock?: (() => void) | undefined;
182
+ /**
183
+ * Ask the host whether a newer release of this plugin is published.
184
+ *
185
+ * Optional, and deliberately so: a seat that is not given one (an older shell wiring, or a
186
+ * harness that does not stage it) simply draws no update chip. The panel calls it ONCE, after
187
+ * mount, and never waits on it — a host that cannot answer, or has no answer to give, leaves the
188
+ * chip's site exactly as it was. The answer is drawn in ONE place (the open file's status bar, at
189
+ * the panel's bottom edge) out of one piece of panel state.
190
+ */
191
+ onCheckUpdate?: (() => Promise<DiffApprovalUpdateValue>) | undefined;
94
192
  /** Read the pending list for the current session into the snapshot. */
95
193
  onRefresh: (sessionId: SessionId | undefined) => void;
194
+ /**
195
+ * The badge's tick, for a seat with nothing on screen: ask how many rows the session has, with no file
196
+ * content on the wire. Optional so a face built against an older shape still renders — the panel then
197
+ * keeps the full read, which is what such a page did before the verb existed.
198
+ */
199
+ onRefreshCount?: ((sessionId: SessionId | undefined) => void) | undefined;
200
+ /** The reader is looking at this file now: its row's unseen dot goes out. */
201
+ onMarkSeen?: ((sessionId: SessionId | undefined, id: string) => void) | undefined;
96
202
  /** Keep one operation. `keepListed` leaves the resolved entry in the list. */
97
203
  onKeep: (sessionId: SessionId, id: string, keepListed?: boolean) => Promise<void>;
98
204
  /** Revert one operation (restore its prior content, or remove a created file).
@@ -149,7 +255,7 @@ export interface PendingPanelFace {
149
255
  /**
150
256
  * Drop several annotations in one host call. Ending a pick of comments is one action the reader
151
257
  * asked for, so it goes to the host as one request and comes back as one read — see
152
- * `DiffApprovalStore.commentRemoveMany`.
258
+ * `PendingDiffStore.commentRemoveMany`.
153
259
  */
154
260
  onCommentRemoveMany: (sessionId: SessionId, ids: readonly string[]) => Promise<DiffApprovalCommentRemoveManyValue>;
155
261
  /**
@@ -159,6 +265,12 @@ export interface PendingPanelFace {
159
265
  * question so the thread can draw what was written.
160
266
  */
161
267
  onCommentAsk: (sessionId: SessionId, id: string, prompt: string, text: string) => Promise<DiffApprovalCommentAskValue>;
268
+ /**
269
+ * The reader has this comment's card in view: its unseen dot goes out, and the answers on it become
270
+ * the baseline a later rewrite is measured against. A reader action, so it is its own call rather
271
+ * than something a read does — a read that cleared attention would make the dot impossible to see.
272
+ */
273
+ onCommentSeen?: ((sessionId: SessionId, id: string) => void) | undefined;
162
274
  /** Acknowledge the redo-cleared notice so it is only surfaced once. */
163
275
  onAckRedoCleared: () => void;
164
276
  /** Acknowledge a refused-undo notice once the panel has said it (see `undoNotice`). */
@@ -12,8 +12,51 @@ import type { PendingDiffSnapshot } from './slots.ts';
12
12
  import type { CommentDraft, DiffApprovalPort } from './port.ts';
13
13
  /** The observable the panel reads and the plugin body drives. */
14
14
  export interface PendingDiffStore extends HostObservable<PendingDiffSnapshot> {
15
+ /**
16
+ * One session's own view of the pending list.
17
+ *
18
+ * The page runs several mounts of the panel at once — the footer entry, the Session header entry and
19
+ * the docked tab — and they can be about DIFFERENT sessions (the reader is in one, while the docked
20
+ * tab was opened from another). One page-wide snapshot made every badge read whichever session's read
21
+ * landed last, which is the cross-talk the reader reported: a badge showing another session's count,
22
+ * and a session with a real list showing zero. So each session's read publishes only into its own
23
+ * slot, and every consumer takes its own slot, by the session it is actually about.
24
+ *
25
+ * A session that has never been read answers an unread empty view — never another session's list.
26
+ * `sessionId === undefined` answers the most recently published view — exactly what
27
+ * {@link PendingDiffStore.getSnapshot} answers — because that is what a caller with no session of its
28
+ * own (a whole-page concern) is asking for.
29
+ * @param sessionId - the session whose view to read.
30
+ * @returns that session's snapshot, identity-stable until it is next published.
31
+ */
32
+ viewFor: (sessionId: SessionId | undefined) => PendingDiffSnapshot;
33
+ /**
34
+ * Every session's view this page is HOLDING, one per session it has read.
35
+ *
36
+ * This is the reader a press needs (see `produced-diff.ts`): a shell press names a file and asks whether
37
+ * ANY session this page has read holds it, and the press cannot wait for a poll. The slots are where the
38
+ * fresh paths live — the light read's `paths` and the last full read's `files` — so answering from the
39
+ * slots is what keeps a file edited since the last full read from being a press nothing recognises.
40
+ * @returns the held views; a session never read contributes nothing (it has no slot).
41
+ */
42
+ views: () => readonly PendingDiffSnapshot[];
15
43
  /** Re-read one session's pending list (an absent session empties the view). */
16
44
  refresh: (sessionId: SessionId | undefined) => Promise<void>;
45
+ /**
46
+ * Ask how many rows this session has, THE LIGHT WAY: one number, no file content.
47
+ *
48
+ * This is what a seat polls while it is showing nothing but its badge. It publishes into the same
49
+ * per-session slot as `refresh` — as `count`, never as `files` — so a badge keeps reading the view it
50
+ * already subscribes to, and it deliberately does NOT set `read` (a count is not a list: a session
51
+ * whose list has never been read stays unread, and its list stays empty rather than looking loaded).
52
+ *
53
+ * Failure keeps the LAST GOOD number: a count that failed knows nothing, and showing 0 or a blank
54
+ * badge for a list that may well be full is worse than showing the previous figure. A host that
55
+ * predates the endpoint (`unknown endpoint "list-count"`) is detected here ONCE and then this call
56
+ * degrades to `refresh`, which is exactly the old behaviour at exactly the old cost — one full read
57
+ * per tick — and an absent session has no count to ask for, so it returns without a call.
58
+ */
59
+ refreshCount: (sessionId: SessionId | undefined) => Promise<void>;
17
60
  /** Keep one operation. `keepListed` leaves the resolved entry in the list. */
18
61
  keep: (sessionId: SessionId, id: string, keepListed?: boolean) => Promise<void>;
19
62
  /** Revert one operation. `keepListed` leaves the resolved entry in the list. */
@@ -35,6 +78,11 @@ export interface PendingDiffStore extends HostObservable<PendingDiffSnapshot> {
35
78
  browse: (sessionId: SessionId, path?: string) => Promise<DiffApprovalBrowseValue>;
36
79
  /** Add one named path to the list, then refresh; resolves to what the scan did. */
37
80
  addPath: (sessionId: SessionId, path: string, includeUnchanged: boolean, exact?: boolean) => Promise<DiffApprovalAddValue>;
81
+ /**
82
+ * The reader is looking at this file now: its row's unseen dot goes out. Deliberately NOT followed by a
83
+ * re-read — see the note at the implementation.
84
+ */
85
+ markSeen: (sessionId: SessionId, id: string) => Promise<void>;
38
86
  /** Open one file with its default application or reveal it in the folder. */
39
87
  open: (sessionId: SessionId, id: string, action: DiffApprovalOpenAction) => Promise<void>;
40
88
  /** Keep every pending entry of one session in a single host call, then refresh. */
@@ -70,6 +118,8 @@ export interface PendingDiffStore extends HostObservable<PendingDiffSnapshot> {
70
118
  commentRemoveMany: (sessionId: SessionId, ids: readonly string[]) => Promise<DiffApprovalCommentRemoveManyValue>;
71
119
  /** Ask one stored comment as its own turn, then refresh (the answer is derived on read). */
72
120
  commentAsk: (sessionId: SessionId, id: string, prompt: string, text: string) => Promise<DiffApprovalCommentAskValue>;
121
+ /** The reader is looking at a comment: its card's dot goes out, then the list is re-read. */
122
+ commentSeen: (sessionId: SessionId, id: string) => Promise<void>;
73
123
  /** Drop every local fact (used on connection reset). */
74
124
  reset: () => void;
75
125
  /** Acknowledge a redo-cleared notice so the panel surfaces it only once. */
@@ -33,6 +33,21 @@ export interface ChangeBlock {
33
33
  }
34
34
  /** Split a row list into maximal runs of non-context rows. */
35
35
  export declare function changeBlocksOf(diff: WholeFileDiff): ChangeBlock[];
36
+ /**
37
+ * The change region a row range sits in, widened to whole runs: from the first row of the run `start`
38
+ * belongs to, to the last row of the run `end` belongs to. A row range that names one side of a change —
39
+ * one line of a replacement, or the additions alone — grows to cover that change's removals as well, which
40
+ * is what a reader pointing at it means. Rows that are context stand alone (a run is a maximal non-context
41
+ * run), so a range that touches no change is returned as it is.
42
+ * @param rows - the whole-file diff rows.
43
+ * @param start - the range's first row.
44
+ * @param end - the range's last row.
45
+ * @returns the widened row range.
46
+ */
47
+ export declare function changeRunSpanOf(rows: readonly WholeFileDiffRow[], start: number, end: number): {
48
+ start: number;
49
+ end: number;
50
+ };
36
51
  /**
37
52
  * One diff block's old/new line ranges, 1-based inclusive, for block-level
38
53
  * keep/revert. A side with no lines (a pure addition or deletion) is empty; its
@@ -46,6 +61,33 @@ export declare function changeBlocksOf(diff: WholeFileDiff): ChangeBlock[];
46
61
  * @returns the old and new line ranges.
47
62
  */
48
63
  export declare function blockRangesOf(rows: readonly WholeFileDiffRow[], block: ChangeBlock): DiffApprovalBlockRange;
64
+ /** One side-by-side pair: the whole-file row index each side holds, when it has one. */
65
+ export interface DiffPairRows {
66
+ old?: number;
67
+ new?: number;
68
+ }
69
+ /**
70
+ * One side-by-side pair's old/new line ranges, 1-based inclusive, for pair-level keep/revert. A pair is
71
+ * one old line against one new line, so each side contributes a single line; a side the pair does not
72
+ * have (an unpaired insertion or removal) is the empty range at that side's insertion point, read off the
73
+ * row before the pair — the same rule `blockRangesOf` applies to a side with no lines at all.
74
+ * @param rows - the whole-file diff rows.
75
+ * @param pair - the rows the pair holds.
76
+ * @returns the old and new line ranges, or undefined for a pair that holds no row.
77
+ */
78
+ export declare function pairRangeOf(rows: readonly WholeFileDiffRow[], pair: DiffPairRows): DiffApprovalBlockRange | undefined;
79
+ /**
80
+ * The old/new ranges keep/revert act on for the pairs a side-by-side selection names, in the order they
81
+ * must be sent. Pairs of context rows are dropped (nothing to act on). When the changed pairs occupy one
82
+ * contiguous span of the row list with no other changed row inside it, ONE range covers them all: one
83
+ * host call, one undo step, and the same range a whole-block action would send. Otherwise each pair gets
84
+ * its own range, ordered from the BOTTOM of the file up, so a splice never moves the line numbers of a
85
+ * pair still to be applied.
86
+ * @param rows - the whole-file diff rows.
87
+ * @param pairs - the pairs the selection names, in row order.
88
+ * @returns one range, several, or none when the selection carries no change.
89
+ */
90
+ export declare function pairsRangeOf(rows: readonly WholeFileDiffRow[], pairs: readonly DiffPairRows[]): DiffApprovalBlockRange[];
49
91
  /**
50
92
  * Compute the whole-file view between two contents. The context budget is the
51
93
  * side lengths, so every hunk covers the file and unchanged lines survive as
@@ -19,8 +19,8 @@
19
19
  */
20
20
  import type { Context } from '@deepseek-ai/cordis';
21
21
  import type { SessionId } from '@deepseek-ai/dsh-session';
22
- import type { DiffApprovalCommentAskValue } from './types.ts';
23
- import type { CommentStore } from './comments.ts';
22
+ import type { DiffApprovalCommentAskValue, CommentRecord } from './types.ts';
23
+ import type { CommentScope, CommentStore } from './comments.ts';
24
24
  /** One derived transcript message, narrowed to what answering a comment needs. */
25
25
  interface DerivedMessage {
26
26
  role?: unknown;
@@ -75,6 +75,14 @@ export interface AskRead {
75
75
  /** The claiming turn is over (`turn/end` in the log), whatever it did or did not write. */
76
76
  ended: boolean;
77
77
  }
78
+ /**
79
+ * One request's already-read transcripts, keyed by the session read.
80
+ *
81
+ * `undefined` is cached as a value (hence `has`, not a truthy check): a session whose log cannot be
82
+ * read must not be asked for again within the same read, or a folded-over lineage would pay for the
83
+ * same failure once per transcript.
84
+ */
85
+ export type TranscriptCache = Map<SessionId, readonly LogEvent[] | undefined>;
78
86
  /**
79
87
  * The answer to one submitted comment, read from the session's own event log.
80
88
  *
@@ -99,13 +107,6 @@ export interface AskRead {
99
107
  * @returns the claiming turn, its answer text, and whether the turn is over.
100
108
  */
101
109
  export declare function answerForRequest(events: readonly LogEvent[], requestId: string): AskRead;
102
- /**
103
- * Read every question one session has asked, keyed by its request id.
104
- * @param events - the session's event log, in sequence order.
105
- * @param requestIds - the questions' request ids.
106
- * @returns the read per request id; a question the log holds nothing for reads as nothing.
107
- */
108
- export declare function answersIn(events: readonly LogEvent[], requestIds: readonly string[]): Record<string, AskRead>;
109
110
  /**
110
111
  * Submits comments as turns of their session, and reads their answers back.
111
112
  *
@@ -140,24 +141,44 @@ export declare class CommentAsker {
140
141
  * arrives while the submission is still in flight already finds its comment. A
141
142
  * submission that rejects is marked dropped rather than left looking pending.
142
143
  *
143
- * @param sessionId - the session the comment belongs to.
144
+ * @param sessionId - the session the question is asked FROM: the prompt goes into it, so its
145
+ * transcript is where the answer will appear.
144
146
  * @param commentId - the comment to ask.
145
147
  * @param prompt - the prompt text to send verbatim.
146
148
  * @param text - the reader's own words, which the prompt wraps: the thread keeps these.
147
149
  * @param signal - aborts the submission.
150
+ * @param inScope - which comment authors the caller's lineage covers, so a thread another seat
151
+ * wrote can be asked from this one.
148
152
  * @returns what the request did.
149
153
  */
150
- ask(sessionId: SessionId, commentId: string, prompt: string, text: string, signal: AbortSignal): Promise<DiffApprovalCommentAskValue>;
154
+ ask(sessionId: SessionId, commentId: string, prompt: string, text: string, signal: AbortSignal, inScope: CommentScope): Promise<DiffApprovalCommentAskValue>;
155
+ /**
156
+ * One scope's questions, grouped by THE TRANSCRIPT each was submitted into.
157
+ *
158
+ * A lineage's threads may hold questions asked from several seats, and an answer lives in the
159
+ * transcript of the session that was asked — so the read is per transcript, not per thread. Grouping
160
+ * here (rather than reading each thread separately) is what keeps a poll to ONE log walk per
161
+ * transcript, however many threads asked there.
162
+ *
163
+ * The legacy case is why `askTranscript` exists: a question asked before the asking session was
164
+ * recorded has only the comment's own session to go on, which is where it went.
165
+ * @param visible - the comments this read may see (already scoped by the caller).
166
+ * @returns the request ids per transcript; empty when nothing was ever asked.
167
+ */
168
+ answerGroups(visible: readonly CommentRecord[]): Map<SessionId, string[]>;
151
169
  /**
152
- * Every question of one session and what the transcript says about it, keyed by the
170
+ * Every question submitted into ONE transcript and what it says about them, keyed by the
153
171
  * question's request id (which is what `CommentAsk` records and what a client matches
154
- * against). Derived from the session's own event log on every call and never stored:
155
- * the log is the one source of truth for what the agent said, so a stored copy could
156
- * only ever disagree with it.
157
- * @param sessionId - the session whose comments to read.
172
+ * against). Derived from that session's event log on every call and never stored: the log
173
+ * is the one source of truth for what the agent said, so a stored copy could only ever
174
+ * disagree with it.
175
+ * @param sessionId - the transcript to read.
176
+ * @param requestIds - the questions that were submitted into it.
177
+ * @param cache - this request's already-read logs, so the answer fold and the turn-end read
178
+ * below share ONE walk of a log that can hold >100k events.
158
179
  * @returns the read per question; empty when the session holds no questions.
159
180
  */
160
- answers(sessionId: SessionId): Record<string, AskRead>;
181
+ answersFor(sessionId: SessionId, requestIds: readonly string[], cache?: TranscriptCache): Record<string, AskRead>;
161
182
  /**
162
183
  * The turns the session's log shows as over, for the questions it holds.
163
184
  *
@@ -167,10 +188,11 @@ export declare class CommentAsker {
167
188
  * a client that connected afterwards, a deployment whose agent events cannot be
168
189
  * subscribed at all — because `turn/end` is durable and the event is not.
169
190
  *
170
- * @param sessionId - the session whose turns to check.
191
+ * @param sessionId - the transcript whose turns to check.
192
+ * @param cache - this request's already-read logs (see `answersFor`).
171
193
  * @returns the turns the log closed, or empty when the log cannot be read.
172
194
  */
173
- endedTurns(sessionId: SessionId): number[];
195
+ endedTurns(sessionId: SessionId, cache?: TranscriptCache): number[];
174
196
  /**
175
197
  * One session's event log, as the reads above take it.
176
198
  *
@@ -180,7 +202,12 @@ export declare class CommentAsker {
180
202
  * and `snapshotEvents` hands back a frozen, sequence-ordered copy of it, so walking it
181
203
  * is a plain read that cannot disturb the session.
182
204
  *
205
+ * The cache is per REQUEST, not per session: one list read asks this twice for every transcript
206
+ * (the answer fold and the turn-end read), and the copy is the expensive part of a long session's
207
+ * log. `has` rather than a truthy check, so "cannot be read" is cached as such instead of being
208
+ * asked again.
183
209
  * @param sessionId - the session to read.
210
+ * @param cache - this request's already-read logs, when the caller is doing several reads.
184
211
  * @returns its events, or undefined when no live session can be read.
185
212
  */
186
213
  private sessionEvents;
@@ -200,7 +227,7 @@ export declare class CommentAsker {
200
227
  * released first, so re-arming is not a leak: one set of handlers per live agent,
201
228
  * not one more per identity change.
202
229
  *
203
- * Called on every ask AND on every poll read (`answers`), because the agent can be
230
+ * Called on every ask AND on every poll read (`answersFor`), because the agent can be
204
231
  * replaced while a question is still pending — the case where nothing asks again.
205
232
  * @param sessionId - the session whose agent to watch.
206
233
  */