dsh-diff-approval 0.29.0 → 0.30.1

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.
@@ -7,6 +7,7 @@
7
7
  *
8
8
  * @module dsh-diff-approval/client/discussion
9
9
  */
10
+ import type { CommentQuoteLine } from '../types.ts';
10
11
  /** One row range in the current diff model (inclusive, 0-based row indices). */
11
12
  export interface DiscussionAnchor {
12
13
  /** First row of the range, in the model this block is currently drawn against. */
@@ -97,23 +98,8 @@ export interface Discussion {
97
98
  lost?: boolean;
98
99
  /** The answer being streamed for the question in flight. */
99
100
  reply?: string;
100
- /**
101
- * Where the session's transcript stood when the last question was sent — its node count.
102
- *
103
- * Kept with the block rather than only in the panel's refs, so a question still waiting
104
- * for its answer can be picked up again after the panel is closed and reopened: the search
105
- * for our own prompt starts here, which is what keeps an older, identical-looking prompt
106
- * from being mistaken for it.
107
- */
108
- baseline?: number;
109
101
  /** The turn for the last question is running. */
110
102
  asking?: boolean;
111
- /**
112
- * The question is sent but still behind another turn of the session, so nothing
113
- * streamed yet belongs to this discussion. The block says it is waiting instead
114
- * of showing the other turn's answer.
115
- */
116
- queued?: boolean;
117
103
  /** The answer failed (the session recorded a prompt error). */
118
104
  failed?: boolean;
119
105
  /**
@@ -234,6 +220,25 @@ export declare function discussionRows(discussion: Discussion): number;
234
220
  * @returns a map from row index to the rows added below it.
235
221
  */
236
222
  export declare function discussionRowExtras(discussions: readonly Discussion[], rowCount: number): Map<number, number>;
223
+ /**
224
+ * Where each block's own box starts, in rows, measured from the BASE of the stack it hangs in.
225
+ *
226
+ * Several blocks can end on one row, and then they are drawn one below the other under it, in the
227
+ * order they are held: `discussionRowExtras` charges the row the sum of them all, and the row stream
228
+ * (and the split view's card layer) draws one reserving box per block, in that same order. So the
229
+ * first block of a stack starts at the base, the second one box down, and so on — and a jump that
230
+ * names a block has to add that many boxes to the base, or it lands the top of the stack whatever
231
+ * block the reader asked for.
232
+ *
233
+ * Nothing is reordered here and no height changes: this is a reading of the drawing order the views
234
+ * already use, keyed by block id so a jump can ask for the box it was sent for.
235
+ *
236
+ * @param discussions - the blocks currently attached to this file, in the order they are drawn.
237
+ * @param anchorOf - the row the block hangs under, clamped exactly as the caller's row stream clamps
238
+ * it (its own height table and its own idea of the last row are what the base below is measured in).
239
+ * @returns a map from block id to the block's own start, in whole rows below its stack's base.
240
+ */
241
+ export declare function discussionStackOffsets(discussions: readonly Discussion[], anchorOf: (discussion: Discussion) => number): Map<string, number>;
237
242
  /**
238
243
  * The discussion attached to exactly this range, if there is one.
239
244
  *
@@ -271,7 +276,12 @@ export declare function discussionOverlapping(discussions: readonly Discussion[]
271
276
  * is what says whether they still hold the code the comment was written about. When they do
272
277
  * not — the lines were edited under it, or the hunk moved — the quote is looked for elsewhere
273
278
  * in the model and the thread follows it, nearest occurrence first when the same code shows up
274
- * more than once. When nothing matches, the block keeps the rows it last matched and is marked
279
+ * more than once. A match farther from the stored rows than `REANCHOR_MAX_LINES` is not that code
280
+ * having moved but another place in the file that happens to read the same — a look-alike
281
+ * declaration, which the recorded ±1 context cannot tell apart — so it is refused and the block is
282
+ * marked outdated instead. That is the one bound the host half applies in lines as well, so the
283
+ * figure the list ships and the place this half draws the card cannot come from two rules. When
284
+ * nothing matches, the block keeps the rows it last matched and is marked
275
285
  * outdated: it says so and keeps its quote for the reader, rather than being silently moved
276
286
  * onto whatever took those lines. Only when the whole range is gone from the model does the
277
287
  * block move, and then by line number, back to where the range used to be (see `gone`). The mark
@@ -301,6 +311,49 @@ export declare function remapDiscussion(discussion: Discussion, lineOf: (row: nu
301
311
  * @returns the re-anchored blocks, in the order they came in.
302
312
  */
303
313
  export declare function remapDiscussions(discussions: readonly Discussion[], lineOf: (row: number) => number | undefined, textOf: (row: number) => string, rowCount: number, hasNewLine?: (row: number) => boolean): readonly Discussion[];
314
+ /**
315
+ * The frame the reader drew, as a block of quoted lines for the question to carry.
316
+ *
317
+ * A reference is one number, and one number cannot say what the comment is about: a reader annotating
318
+ * a DELETED line produced `a.txt:2`, and the agent read line 2 of the file — which is another line
319
+ * entirely, because a removed line has no number in the file as it reads now (its `2` is the old
320
+ * file's). This block says what the number cannot: every row of the frame, in order, with the side it
321
+ * was on (`-` removed, `+` added, a space for context) and the number THAT side's file gave it.
322
+ *
323
+ * `quote` and `quoteLines` are what the record already holds — one entry per frame row, in row order —
324
+ * so nothing new is recorded for this: the question is built from the same snapshot the re-anchor uses.
325
+ * A record from before `quoteLines` was kept (or one the agent annotated, which names current lines
326
+ * only) has the text alone, and the block is that text with no numbers rather than a made-up side.
327
+ *
328
+ * @param quote - the anchored lines as they read when the comment was written.
329
+ * @param quoteLines - their gutter numbers and sides, in the same order.
330
+ * @returns the block, or `''` when there is nothing to show.
331
+ */
332
+ export declare function quotedFrame(quote: string | undefined, quoteLines: readonly DiscussionQuoteLine[] | undefined): string;
333
+ /**
334
+ * Whether a frame's numbers all come from the OLD file.
335
+ *
336
+ * A frame of removed rows alone names no line the file still has, so the numbers its record carries
337
+ * (`anchor`, and the line a jump is handed) are the old file's. Everything that would otherwise
338
+ * present them as current lines asks this first: the label a card and a list item wear, and the
339
+ * landing a jump to that comment makes. A record that kept no sides answers false — what it holds is
340
+ * read exactly as it always was.
341
+ *
342
+ * @param quoteLines - the frame's gutter numbers and sides, when the record kept them.
343
+ * @returns true when the frame holds at least one row and none of them has a new-file line.
344
+ */
345
+ export declare function frameNamesNoCurrentLine(quoteLines: readonly DiscussionQuoteLine[] | readonly CommentQuoteLine[] | undefined): boolean;
346
+ /**
347
+ * Whether a frame covers a line the file no longer has.
348
+ *
349
+ * The one thing a frame can hide behind its numbers: `anchor` is documented as new-file lines, but a
350
+ * frame of removed rows alone has none of those, so the numbers it carries are the old file's. A
351
+ * caller that must not present them as current lines asks this.
352
+ *
353
+ * @param quoteLines - the frame's gutter numbers and sides, when the record kept them.
354
+ * @returns true when at least one row of the frame is a removed line.
355
+ */
356
+ export declare function frameCoversRemovedRow(quoteLines: readonly DiscussionQuoteLine[] | undefined): boolean;
304
357
  /** What the selection frame shows for the current selection. */
305
358
  export interface SelectionFrameActions {
306
359
  /** Whether keep/revert apply (the selection fully covers change blocks). */
@@ -133,11 +133,6 @@ export interface DiffDockBodyProps extends PropsLocale<'diff-approval'> {
133
133
  readonly onDockClose?: ((close: (() => void) | undefined) => void) | undefined;
134
134
  [key: string]: unknown;
135
135
  }
136
- /** One live dock adapter: the face, and the teardown that unsubscribes. */
137
- export interface DiffDock {
138
- readonly face: DockFace;
139
- dispose(): void;
140
- }
141
136
  /** The cordis pieces this adapter uses, structurally: optional service lookup and
142
137
  * a scoped effect. Both are what the plugin sandbox always forwards — its
143
138
  * whitelist covers `effect`/`on`/timers but NOT cordis's dynamic `inject`, and it
@@ -0,0 +1,60 @@
1
+ /**
2
+ * The shell's own icons and controls, fetched by every name the shells have given them.
3
+ *
4
+ * 0.1.7 renamed the whole product icon set: `IconCloseOutline16` and `IconChevronDownOutline14` became
5
+ * `IconCloseOutlineMedium` and `IconChevronDownOutlineMedium` (the `Medium`/`Regular` stroke pair,
6
+ * where the numbers used to say the size). A static import of the old name survives compilation and
7
+ * then hands React `undefined` to render, which is a crash of the whole mount — React #130, measured
8
+ * here on 0.1.7-rc.2: `DiffApprovalHeaderEntry` took the panel down with it, and nothing in the plugin
9
+ * worked until the shell was changed again.
10
+ *
11
+ * So each glyph is resolved in two steps, and neither step can throw:
12
+ *
13
+ * 1. the NEW name, looked up on the module namespace at run time — present on 0.1.7 and later, and
14
+ * absent (harmlessly) anywhere else;
15
+ * 2. the legacy name, imported statically, which is what every shell before 0.1.7 exports.
16
+ *
17
+ * A glyph no shell offers renders nothing rather than failing. That is the point of this module: a
18
+ * rename in the shell becomes a shell that draws fewer icons, not a plugin that dies. The plugin's
19
+ * peer floor stays `0.1.0-rc.5`.
20
+ *
21
+ * The exports carry the names this plugin's code already uses (`IconCloseOutline16` and the rest), so
22
+ * a caller changes only where it imports from.
23
+ *
24
+ * @module dsh-diff-approval/client/dsh-icons
25
+ */
26
+ import * as primitives from '@deepseek-ai/dsh-client-ui-primitives';
27
+ /** The pending-changes glyph: the sidebar's badge and the header entry carry it. */
28
+ export declare const IconListPenOutline16: ({ size, className }: primitives.IconProps) => import("react").JSX.Element;
29
+ /** The list's fold chevrons. */
30
+ export declare const IconChevronDownOutline14: ({ size, className }: primitives.IconProps) => import("react").JSX.Element;
31
+ export declare const IconChevronUpOutline14: ({ size, className }: primitives.IconProps) => import("react").JSX.Element;
32
+ /** The path picker's tree disclosure. */
33
+ export declare const IconChevronRightOutline14: ({ size, className }: primitives.IconProps) => import("react").JSX.Element;
34
+ /** Closing a card, a dialog or a search bar. */
35
+ export declare const IconCloseOutline16: ({ size, className }: primitives.IconProps) => import("react").JSX.Element;
36
+ /** The thread's own overflow menu. */
37
+ export declare const IconEllipsisOutline16: ({ size, className }: primitives.IconProps) => import("react").JSX.Element;
38
+ /** The add-path dialog's browse button. */
39
+ export declare const IconBrowseOutline16: ({ size, className }: primitives.IconProps) => import("react").JSX.Element;
40
+ /** A folder in the picker's tree, open and closed. */
41
+ export declare const IconFolderOpenOutline16: ({ size, className }: primitives.IconProps) => import("react").JSX.Element;
42
+ export declare const IconFolderOpen16: ({ size, className }: primitives.IconProps) => import("react").JSX.Element;
43
+ export declare const IconFolderClose16: ({ size, className }: primitives.IconProps) => import("react").JSX.Element;
44
+ /** The presentation switch: dock the panel in the sidebar. */
45
+ export declare const IconPanelLeftOutline16: ({ size, className }: primitives.IconProps) => import("react").JSX.Element;
46
+ /** The settings tab's refresh, which the older set spelled with the small size. */
47
+ export declare const IconRefreshOutline14: ({ size, className }: primitives.IconProps) => import("react").JSX.Element;
48
+ /** Adding a path, the diff refresh, the search box, and Settings. */
49
+ export declare const IconPlusOutline16: ({ size, className }: primitives.IconProps) => import("react").JSX.Element;
50
+ export declare const IconRefreshOutline16: ({ size, className }: primitives.IconProps) => import("react").JSX.Element;
51
+ export declare const IconSearchOutline16: ({ size, className }: primitives.IconProps) => import("react").JSX.Element;
52
+ export declare const IconSettingsOutline16: ({ size, className }: primitives.IconProps) => import("react").JSX.Element;
53
+ /**
54
+ * The package's own menus, toasts and clipboard helper.
55
+ *
56
+ * Re-exported so one module names everything this plugin takes from the shell's primitives: these
57
+ * names have survived both releases, and a caller has no reason to reach past this file.
58
+ */
59
+ export { Menu, Toast, Tooltip, writeClipboard } from '@deepseek-ai/dsh-client-ui-primitives';
60
+ export type { MenuEntry } from '@deepseek-ai/dsh-client-ui-primitives';
@@ -38,15 +38,30 @@ export interface HeaderEntryFace {
38
38
  export interface HeaderEntrySeat {
39
39
  /** The selected session and its rows; the entry is inert without one. */
40
40
  useSessions?: ((select: (state: HeaderEntrySessions) => boolean) => boolean) | undefined;
41
+ /**
42
+ * The session this seat is about, composed by the shell.
43
+ *
44
+ * This is where 0.1.7 hands it over: the session-list store it gives `useSessions` carries
45
+ * `{ ids, byId, phase, projectionsBySession }` and no selected session at all, while the shell's
46
+ * own session-scoped seats take the id from their props and ask the store about that row
47
+ * (`useSessions(s => s.byId[sessionId]?.blank)`). Older shells put `current` in the store, which is
48
+ * why both are read (see `session-seat.ts`).
49
+ */
50
+ sessionId?: SessionId | undefined;
41
51
  }
42
- /** The slice of the session list this entry reads. */
52
+ /** The slice of the session list this entry reads: whichever of these a shell carries. */
43
53
  export interface HeaderEntrySessions {
44
- /** The session the app is showing. */
54
+ /** The session the app is showing, on the shells that keep it here (0.1.5 and earlier). */
45
55
  current?: SessionId | undefined;
46
56
  /** Known sessions by id, for the blank-session check. */
47
- byId: Record<string, {
57
+ byId?: Record<string, {
48
58
  blank?: boolean;
49
- } | undefined>;
59
+ } | undefined> | undefined;
60
+ /** A blank flag carried by the STATE itself on the shells that place it there. */
61
+ blank?: boolean | undefined;
62
+ /** Every other field a shell may carry: this plugin reads none of them, and a shape it does not
63
+ * know must not disable the entry (see `session-seat.ts`). */
64
+ [field: string]: unknown;
50
65
  }
51
66
  /** Full props of the header entry: the injected face's hooks bound as
52
67
  * `usePending`/`useDock`, the framework seats, and the locale `t`. */
@@ -56,4 +71,4 @@ export type DiffApprovalHeaderEntryProps = InjectFace<HeaderEntryFace> & HeaderE
56
71
  * @param props - the bound hooks, the session seat, and the translator.
57
72
  * @returns the icon button, with the pending count while there is one.
58
73
  */
59
- export declare function DiffApprovalHeaderEntry({ usePending, useSessions, useDock, t }: DiffApprovalHeaderEntryProps): ReactNode;
74
+ export declare function DiffApprovalHeaderEntry({ usePending, useSessions, useDock, sessionId, t }: DiffApprovalHeaderEntryProps): ReactNode;
@@ -0,0 +1,18 @@
1
+ /**
2
+ * A fresh identifier for a record the browser half mints.
3
+ *
4
+ * `crypto.randomUUID` is a SECURE CONTEXT API. Reaching this host from a phone or a tablet over the
5
+ * LAN means plain HTTP, where `crypto.randomUUID` is undefined and calling it throws a TypeError —
6
+ * synchronously, before the code around it does anything. That is how writing a comment died on a
7
+ * tablet: the composer stayed open with the reader's words in it, nothing was sent, and no failure was
8
+ * shown, because the throw happened before the request and outside the `try` that would have said so.
9
+ * The desktop was never affected: `localhost` IS a secure context.
10
+ *
11
+ * `crypto.getRandomValues` carries no such restriction, so the fallback builds the id from it — a
12
+ * version-4 UUID, the shape the host and the client both treat as opaque but which reads as one
13
+ * everywhere it is printed. `randomUUID` is still preferred when it is there: it is the same thing
14
+ * done by the platform.
15
+ *
16
+ * @returns an identifier, unique for practical purposes.
17
+ */
18
+ export declare function newId(): string;
@@ -9,7 +9,6 @@ export declare const zh: {
9
9
  'panel.dockFailed': string;
10
10
  'panel.loading': string;
11
11
  'panel.readFailed': string;
12
- 'panel.group.current': string;
13
12
  'panel.group.others': string;
14
13
  'panel.tabs': string;
15
14
  'panel.tab.pending': string;
@@ -22,7 +21,9 @@ export declare const zh: {
22
21
  'panel.blockAtEnd': string;
23
22
  'panel.blockAtStart': string;
24
23
  'panel.blockSingle': string;
25
- 'panel.viewDiff': string;
24
+ 'chip.openDefault': string;
25
+ 'chip.reviewInPanel': string;
26
+ 'chip.gone': string;
26
27
  'panel.fileNotPending': string;
27
28
  'panel.selectHint': string;
28
29
  'panel.searchPlaceholder': string;
@@ -67,6 +68,8 @@ export declare const zh: {
67
68
  'action.gotoCancel': string;
68
69
  'panel.gotoMissing': string;
69
70
  'panel.persistFailed': string;
71
+ 'panel.commentPersistFailed': string;
72
+ 'panel.undoFailed': string;
70
73
  'settings.keybindings': string;
71
74
  'settings.keybindingsDesc': string;
72
75
  'settings.comment': string;
@@ -101,6 +104,20 @@ export declare const zh: {
101
104
  'row.keepListed': string;
102
105
  'row.keepRemove': string;
103
106
  'row.revertRemove': string;
107
+ 'panel.batchKeepAsk': string;
108
+ 'panel.batchKeepRemoveAsk': string;
109
+ 'panel.batchRevertAsk': string;
110
+ 'panel.batchRevertRemoveAsk': string;
111
+ 'panel.batchKeepAllAsk': string;
112
+ 'panel.batchRevertAllAsk': string;
113
+ 'panel.batchCloseAsk': string;
114
+ 'panel.batchDeletes': string;
115
+ 'panel.batchAndMore': string;
116
+ 'panel.batchGo': string;
117
+ 'panel.removeOneAsk': string;
118
+ 'panel.removeCommentsOne': string;
119
+ 'panel.removeCommentsMany': string;
120
+ 'panel.removeCommentsMore': string;
104
121
  'row.added': string;
105
122
  'row.removed': string;
106
123
  'action.keep': string;
@@ -110,15 +127,17 @@ export declare const zh: {
110
127
  'action.discussionCollapse': string;
111
128
  'discussion.placeholder': string;
112
129
  'discussion.marker': string;
130
+ 'discussion.frameLegend': string;
131
+ 'discussion.deletedLabel': string;
113
132
  'discussion.promptRule': string;
114
133
  'discussion.promptRuleSkill': string;
115
134
  'discussion.thinking': string;
116
- 'discussion.queued': string;
117
- 'discussion.answering': string;
118
135
  'discussion.outdatedQuote': string;
119
136
  'discussion.hidden': string;
120
137
  'discussion.failed': string;
121
138
  'discussion.stopped': string;
139
+ 'discussion.addMissing': string;
140
+ 'discussion.askMissing': string;
122
141
  'action.revert': string;
123
142
  'action.delete': string;
124
143
  'action.discussionEnd': string;
@@ -138,10 +157,6 @@ export declare const zh: {
138
157
  'action.viewUnified': string;
139
158
  'action.viewPreview': string;
140
159
  'action.viewSource': string;
141
- 'action.viewBefore': string;
142
- 'action.viewAfter': string;
143
- 'action.viewSingle': string;
144
- 'action.viewDouble': string;
145
160
  'panel.mdPreviewFailed': string;
146
161
  'action.copyHint': string;
147
162
  'action.copied': string;
@@ -161,7 +176,6 @@ export declare const zh: {
161
176
  'panel.importFailed': string;
162
177
  'action.search': string;
163
178
  'action.settings': string;
164
- 'action.presentation': string;
165
179
  'action.presentationCurrent': string;
166
180
  'action.cover': string;
167
181
  'cover.left': string;
@@ -186,13 +200,10 @@ export declare const zh: {
186
200
  'action.summonHint': string;
187
201
  'action.retry': string;
188
202
  'panel.crashed': string;
189
- 'status.kept': string;
190
- 'status.reverted': string;
191
- 'status.missing': string;
192
203
  'panel.resolvedAsk': string;
193
204
  'panel.keepInList': string;
194
- 'panel.removalQuiet': string;
195
- 'panel.removalQuietHint': string;
205
+ 'panel.keepInListQuiet': string;
206
+ 'panel.keepInListQuietHint': string;
196
207
  'panel.fileKeptAsk': string;
197
208
  'panel.fileRevertedAsk': string;
198
209
  'panel.confirmFileRemove': string;
@@ -247,7 +258,6 @@ export declare const en: {
247
258
  'panel.dockFailed': string;
248
259
  'panel.loading': string;
249
260
  'panel.readFailed': string;
250
- 'panel.group.current': string;
251
261
  'panel.group.others': string;
252
262
  'panel.tabs': string;
253
263
  'panel.tab.pending': string;
@@ -260,7 +270,9 @@ export declare const en: {
260
270
  'panel.blockAtEnd': string;
261
271
  'panel.blockAtStart': string;
262
272
  'panel.blockSingle': string;
263
- 'panel.viewDiff': string;
273
+ 'chip.openDefault': string;
274
+ 'chip.reviewInPanel': string;
275
+ 'chip.gone': string;
264
276
  'panel.fileNotPending': string;
265
277
  'panel.selectHint': string;
266
278
  'panel.searchPlaceholder': string;
@@ -305,6 +317,8 @@ export declare const en: {
305
317
  'action.gotoCancel': string;
306
318
  'panel.gotoMissing': string;
307
319
  'panel.persistFailed': string;
320
+ 'panel.commentPersistFailed': string;
321
+ 'panel.undoFailed': string;
308
322
  'settings.keybindings': string;
309
323
  'settings.keybindingsDesc': string;
310
324
  'settings.comment': string;
@@ -339,6 +353,20 @@ export declare const en: {
339
353
  'row.keepListed': string;
340
354
  'row.keepRemove': string;
341
355
  'row.revertRemove': string;
356
+ 'panel.batchKeepAsk': string;
357
+ 'panel.batchKeepRemoveAsk': string;
358
+ 'panel.batchRevertAsk': string;
359
+ 'panel.batchRevertRemoveAsk': string;
360
+ 'panel.batchKeepAllAsk': string;
361
+ 'panel.batchRevertAllAsk': string;
362
+ 'panel.batchCloseAsk': string;
363
+ 'panel.batchDeletes': string;
364
+ 'panel.batchAndMore': string;
365
+ 'panel.batchGo': string;
366
+ 'panel.removeOneAsk': string;
367
+ 'panel.removeCommentsOne': string;
368
+ 'panel.removeCommentsMany': string;
369
+ 'panel.removeCommentsMore': string;
342
370
  'row.added': string;
343
371
  'row.removed': string;
344
372
  'action.keep': string;
@@ -348,15 +376,17 @@ export declare const en: {
348
376
  'action.discussionCollapse': string;
349
377
  'discussion.placeholder': string;
350
378
  'discussion.marker': string;
379
+ 'discussion.frameLegend': string;
380
+ 'discussion.deletedLabel': string;
351
381
  'discussion.promptRule': string;
352
382
  'discussion.promptRuleSkill': string;
353
383
  'discussion.thinking': string;
354
- 'discussion.queued': string;
355
- 'discussion.answering': string;
356
384
  'discussion.outdatedQuote': string;
357
385
  'discussion.hidden': string;
358
386
  'discussion.failed': string;
359
387
  'discussion.stopped': string;
388
+ 'discussion.addMissing': string;
389
+ 'discussion.askMissing': string;
360
390
  'action.revert': string;
361
391
  'action.delete': string;
362
392
  'action.discussionEnd': string;
@@ -376,10 +406,6 @@ export declare const en: {
376
406
  'action.viewUnified': string;
377
407
  'action.viewPreview': string;
378
408
  'action.viewSource': string;
379
- 'action.viewBefore': string;
380
- 'action.viewAfter': string;
381
- 'action.viewSingle': string;
382
- 'action.viewDouble': string;
383
409
  'panel.mdPreviewFailed': string;
384
410
  'action.copyHint': string;
385
411
  'action.copied': string;
@@ -399,7 +425,6 @@ export declare const en: {
399
425
  'panel.importFailed': string;
400
426
  'action.search': string;
401
427
  'action.settings': string;
402
- 'action.presentation': string;
403
428
  'action.presentationCurrent': string;
404
429
  'action.cover': string;
405
430
  'cover.left': string;
@@ -424,13 +449,10 @@ export declare const en: {
424
449
  'action.summonHint': string;
425
450
  'action.retry': string;
426
451
  'panel.crashed': string;
427
- 'status.kept': string;
428
- 'status.reverted': string;
429
- 'status.missing': string;
430
452
  'panel.resolvedAsk': string;
431
453
  'panel.keepInList': string;
432
- 'panel.removalQuiet': string;
433
- 'panel.removalQuietHint': string;
454
+ 'panel.keepInListQuiet': string;
455
+ 'panel.keepInListQuietHint': string;
434
456
  'panel.fileKeptAsk': string;
435
457
  'panel.fileRevertedAsk': string;
436
458
  'panel.confirmFileRemove': string;
@@ -1,5 +1,6 @@
1
1
  /**
2
- * Where the review panel was left, for this page's lifetime.
2
+ * Where the review panel was left, and what the reader has typed but not sent, for
3
+ * this page's lifetime.
3
4
  *
4
5
  * Closing the panel is not "done reviewing": reopening it should put the reader
5
6
  * back on the file they were in, at the offset they were at. That is a fact about
@@ -13,52 +14,116 @@
13
14
  *
14
15
  * @module dsh-diff-approval/client/panel-memory
15
16
  */
16
- import type { Discussion } from './discussion.ts';
17
+ import type { Discussion, DiscussionQuoteLine } from './discussion.ts';
17
18
  /**
18
- * Fired on `window` whenever a session's threads change. The threads are written by the file
19
- * detail (which owns them) and read by the list pane (whose comments tab shows them all), so the
20
- * event is what keeps the two in step: a reader who posts a comment sees it in the list at once
21
- * instead of on the next poll.
19
+ * What the panel keeps about one thread that is NOT the host's to keep: what the reader is
20
+ * typing, whether the block is folded, and the body height the panel last measured for it.
21
+ *
22
+ * A thread itself is the host's record now (see `pending.comments`), so nothing here may
23
+ * hold a turn: the panel renders the snapshot, and this only remembers the page-local
24
+ * presentation of it. That split is what makes a poll harmless — a poll replaces the
25
+ * snapshot, and a draft typed a moment ago is not in it, so keeping the draft here is what
26
+ * stops the poll from clearing the field under the reader's hands.
22
27
  */
23
- export declare const COMMENTS_CHANGED_EVENT = "dsh-diff-approval:comments-changed";
28
+ export interface ThreadLocal {
29
+ /** What the reader has typed into the compose row (not sent yet). */
30
+ draft: string;
31
+ /** Folded to the one-row header. */
32
+ collapsed: boolean;
33
+ /**
34
+ * The last send of this thread was refused (no live agent, or the ask itself failed). It is a
35
+ * page-local fact because nothing was stored: the host has no question to record, so the block
36
+ * could only say so from here. Cleared by the next send.
37
+ */
38
+ failed?: boolean | undefined;
39
+ /**
40
+ * How many rows the block's body was last measured to occupy, or absent before the first
41
+ * measurement. The reservation follows the drawing rather than the other way round (see
42
+ * the panel's read-back), and it is kept here so a mount that comes back draws the thread
43
+ * at the size it was already measured at instead of reserving the model's estimate again.
44
+ */
45
+ bodyRows?: number | undefined;
46
+ }
24
47
  /**
25
- * The comment threads a session's panel is holding.
48
+ * The page-local thread state a session's panel is holding.
26
49
  * @param sessionId - the session, if any.
27
- * @returns the threads by pending entry id; empty when there are none.
50
+ * @returns the state by comment id; empty when there is none.
28
51
  */
29
- export declare function rememberedDiscussions(sessionId: string | undefined): Readonly<Record<string, readonly Discussion[]>>;
52
+ export declare function rememberedThreads(sessionId: string | undefined): Readonly<Record<string, ThreadLocal>>;
30
53
  /**
31
- * Remember the comment threads a session's panel holds. Called on every change, so a mount
32
- * that appears later (another presentation, or the panel reopened) starts from them.
54
+ * Remember one session's page-local thread state. Called on every change, so a mount that
55
+ * appears later starts from it.
33
56
  * @param sessionId - the session the panel is reviewing; nothing is recorded without one.
34
- * @param byFile - the threads, by pending entry id.
57
+ * @param threads - the state by comment id.
58
+ */
59
+ export declare function rememberThreads(sessionId: string | undefined, threads: Readonly<Record<string, ThreadLocal>>): void;
60
+ /**
61
+ * A block the reader has PLACED on a file's lines but has not written down yet.
62
+ *
63
+ * It has no host record (see `sendDiscussion`), so nothing else on this page holds it: without this
64
+ * the reader's complaint was right — placing a comment, typing a first thought and closing the panel
65
+ * threw the whole block away, because only the words were remembered and a comment id to hang them
66
+ * under does not exist until the block is sent. Everything a mount needs to draw the block again is
67
+ * here: the entry it was placed on, the lines, the quote it was placed against, and the words.
68
+ */
69
+ export interface PlacedThread {
70
+ /**
71
+ * The block's page-local id, minted when it was placed. The comment's own id is minted when the
72
+ * block is written (see `sendDiscussion`), and this is only ever the name of an unsent block.
73
+ */
74
+ id: string;
75
+ /** The pending entry whose rows it was placed on. */
76
+ fileId: string;
77
+ /**
78
+ * The rows it was made on, plus the new-file lines that survive a rebuild. The lines are what a
79
+ * mount re-anchors from (see `remapDiscussions`); the row indices are the placement's own hint,
80
+ * stale the moment the model moves.
81
+ */
82
+ anchor: Discussion['anchor'];
83
+ /** The anchored lines as they read when it was placed (the re-anchor fingerprint). */
84
+ quote: string;
85
+ /** `quote` with one line of context on each side, as it read then. */
86
+ quoteContext?: string | undefined;
87
+ /** The gutter numbers of `quote`'s lines, in the same order. */
88
+ quoteLines?: readonly DiscussionQuoteLine[] | undefined;
89
+ /** What the reader had typed into it. */
90
+ draft: string;
91
+ }
92
+ /**
93
+ * The placed-but-unsent blocks a session's panel is holding.
94
+ * @param sessionId - the session, if any.
95
+ * @returns the blocks, oldest placement first; empty when there are none.
35
96
  */
36
- export declare function rememberDiscussions(sessionId: string | undefined, byFile: Readonly<Record<string, readonly Discussion[]>>): void;
97
+ export declare function rememberedPlacedThreads(sessionId: string | undefined): readonly PlacedThread[];
37
98
  /**
38
- * End one comment, wherever it came from.
99
+ * Remember one session's placed-but-unsent blocks. Called on every change, so a mount that comes
100
+ * back draws them again on the same lines with the same words.
39
101
  *
40
- * The threads belong to the file detail, which writes them here on every change — but the list
41
- * pane shows them all and ends them too, and it has no way to reach into another component's
42
- * state. So the list asks the memory, and the detail reads the memory back on the event this
43
- * fires (see `COMMENTS_CHANGED_EVENT`): one record, one action, whichever pane the reader used.
102
+ * A list that says the same thing as the one held is not stored: the caller rebuilds it per render,
103
+ * and only a real change (a placement, a draft, a block written or discarded) may republish it.
44
104
  *
45
- * @param sessionId - the session the panel is reviewing; nothing is forgotten without one.
46
- * @param fileId - the pending entry the thread hangs in.
47
- * @param discussionId - the thread to end; an id that is not there changes nothing.
105
+ * @param sessionId - the session the panel is reviewing; nothing is recorded without one.
106
+ * @param threads - the placements, oldest first.
48
107
  */
49
- export declare function forgetDiscussion(sessionId: string | undefined, fileId: string, discussionId: string): void;
108
+ export declare function rememberPlacedThreads(sessionId: string | undefined, threads: readonly PlacedThread[]): void;
50
109
  /**
51
- * Drop every thread whose file the list no longer holds.
110
+ * Drop every placed block whose file the list no longer holds.
111
+ *
112
+ * A placement is a block of rows in ONE file's diff, so a file the reader has kept or
113
+ * reverted out of the list leaves nothing for its block to hang on: it goes with the file,
114
+ * exactly as the comments on those rows do (the host drops those; see `comments.ts`). The
115
+ * entry ids ARE paths, so a path that comes back later is the same id again — without this
116
+ * the placement was resurrected onto a file the reader had already dealt with, and
117
+ * `discussionOverlapping` went on refusing a fresh comment on those very rows because a
118
+ * block that is not on screen still counted as theirs.
52
119
  *
53
- * A thread is a block of rows in one file's diff, so a file the reader has kept or reverted out of the
54
- * list leaves nothing for its comments to hang on: they go with it, and the same path coming back later
55
- * in this visit starts clean. A new object is published only when something actually goes, so the panes
56
- * that adopt this record (see `COMMENTS_CHANGED_EVENT`) do not re-adopt an equal object on every poll.
120
+ * Nothing is published when nothing is dropped, so a poll that changes no file is not a
121
+ * change to the page's memory (see the identity discipline in `rememberPlacedThreads`).
57
122
  *
58
123
  * @param sessionId - the session the panel is reviewing; nothing is dropped without one.
59
124
  * @param listed - the pending entry ids the list is holding.
60
125
  */
61
- export declare function forgetDiscussionsNotIn(sessionId: string | undefined, listed: readonly string[]): void;
126
+ export declare function forgetPlacedThreadsNotIn(sessionId: string | undefined, listed: readonly string[]): void;
62
127
  /**
63
128
  * Whether the reader asked not to be asked again about removing one file.
64
129
  * @param sessionId - the session the panel is reviewing; nothing is quiet without one.