dsh-diff-approval 0.29.1 → 0.31.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 (40) hide show
  1. package/README.md +7 -4
  2. package/README.zh.md +7 -4
  3. package/assets/fonts/README.md +10 -8
  4. package/assets/fonts/manifest.json +1 -1
  5. package/lib/client.js +5161 -1972
  6. package/lib/index.js +4331 -442
  7. package/lib/types/annotate-skill.d.ts +34 -0
  8. package/lib/types/annotate-tool.d.ts +227 -0
  9. package/lib/types/annotate.d.ts +186 -0
  10. package/lib/types/atomic-write.d.ts +77 -0
  11. package/lib/types/client/PendingPanel.d.ts +92 -24
  12. package/lib/types/client/category-color.d.ts +62 -0
  13. package/lib/types/client/chords.d.ts +70 -17
  14. package/lib/types/client/discussion.d.ts +76 -16
  15. package/lib/types/client/dock.d.ts +8 -8
  16. package/lib/types/client/dsh-icons.d.ts +98 -0
  17. package/lib/types/client/header-entry.d.ts +27 -11
  18. package/lib/types/client/ids.d.ts +18 -0
  19. package/lib/types/client/locales.d.ts +88 -36
  20. package/lib/types/client/panel-memory.d.ts +118 -35
  21. package/lib/types/client/path-match.d.ts +50 -0
  22. package/lib/types/client/port.d.ts +77 -1
  23. package/lib/types/client/presentation-menu.d.ts +1 -1
  24. package/lib/types/client/produced-diff.d.ts +136 -22
  25. package/lib/types/client/reference.d.ts +0 -2
  26. package/lib/types/client/remap-sync.d.ts +18 -1
  27. package/lib/types/client/session-seat.d.ts +93 -0
  28. package/lib/types/client/settings.d.ts +40 -14
  29. package/lib/types/client/slots.d.ts +184 -12
  30. package/lib/types/client/store.d.ts +85 -2
  31. package/lib/types/comment-ask.d.ts +236 -0
  32. package/lib/types/comment-lines.d.ts +131 -0
  33. package/lib/types/comments.d.ts +465 -0
  34. package/lib/types/index.d.ts +7 -2
  35. package/lib/types/pending.d.ts +53 -4
  36. package/lib/types/types.d.ts +356 -4
  37. package/lib/types/vcs.d.ts +53 -26
  38. package/lib/types/version.d.ts +162 -0
  39. package/package.json +7 -2
  40. package/lib/types/client/chat-bridge.d.ts +0 -70
@@ -7,7 +7,7 @@ import type { PendingPanelFace } from './slots.ts';
7
7
  import type { Translator } from './locales.ts';
8
8
  import { computeWholeFileDiff } from './whole-file-diff.ts';
9
9
  import type { Discussion } from './discussion.ts';
10
- import type { ChangeBlock, IntraRun } from './whole-file-diff.ts';
10
+ import type { ChangeBlock, IntraRun, WholeFileDiffRow } from './whole-file-diff.ts';
11
11
  import type { HighlightSides } from './highlight.ts';
12
12
  import type { VisibleLines } from './windowed-highlight.ts';
13
13
  /**
@@ -22,15 +22,12 @@ import type { VisibleLines } from './windowed-highlight.ts';
22
22
  * floor is not just a drag limit, it is the width at which the footer is still itself.
23
23
  */
24
24
  export declare const MIN_LIST_WIDTH_PX = 260;
25
- /** Normalize a path for comparison: forward slashes, no trailing slash. */
26
- export declare function normalizeDiffPath(p: string): string;
27
- /** Whether a produced-file chip path and a pending file path refer to the same
28
- * file, tolerant of separator style (\\ vs /) and of a workspace-relative vs
29
- * absolute form. `chipPath` is typically the harness's workspace-relative
30
- * forward-slash path; `filePath` is the host's absolute native-separator path.
31
- * Matching is case-insensitive so a Windows drive/segment case difference does
32
- * not miss the file the user clicked. */
33
- export declare function diffPathsMatch(chipPath: string, filePath: string, workspacePath: string | undefined): boolean;
25
+ /**
26
+ * Path comparison, re-exported from its own module so every existing importer keeps working: the panel's
27
+ * own jump, the plugin's press bridge and the store reader all ask the same question, and they must not
28
+ * each carry their own answer (see `path-match.ts`).
29
+ */
30
+ export { diffPathsMatch, normalizeDiffPath } from './path-match.ts';
34
31
  /** The dsh shell's sidebar auto-collapse breakpoint (ui-layout columns.ts):
35
32
  * below it the sidebar auto-collapses, and the file list floats on the same
36
33
  * breakpoint so the two stay consistent. */
@@ -61,7 +58,17 @@ export declare function wrapChipRows(line: string, room: number, measure: (text:
61
58
  /** Create a measurer bound to `font`; falls back to a rough char estimate. */
62
59
  export declare function makeMeasurer(font: string | undefined): ((text: string) => number) | undefined;
63
60
  /** Full panel props composed by the sidebar footer-action slot. */
64
- export type PendingPanelProps = PropsRuntime<'sidebar.footer.action'> & InjectFace<PendingPanelFace> & PropsLocale<'diff-approval'> & PendingPanelDockProps;
61
+ export type PendingPanelProps = PropsRuntime<'sidebar.footer.action'> & InjectFace<PendingPanelFace> & PropsLocale<'diff-approval'> & PendingPanelDockProps & PendingPanelSeat;
62
+ /**
63
+ * The session this mount is for, when the shell composes one into the seat.
64
+ *
65
+ * Session-scoped seats are handed it on 0.1.7; the root-scoped footer slot that hosts this panel is
66
+ * not, and neither is the dock tab. The other two sources are read where this is absent (see
67
+ * `session-seat.ts`).
68
+ */
69
+ export interface PendingPanelSeat {
70
+ sessionId?: SessionId | undefined;
71
+ }
65
72
  /** How the panel is hosted when it is not the footer's floating overlay: the
66
73
  * right sidebar's tab renders it docked, filling the tab and portaling its
67
74
  * content into `dockHost` (the element the tab body owns). */
@@ -72,17 +79,27 @@ export interface PendingPanelDockProps {
72
79
  docked?: boolean;
73
80
  /** The element the docked panel portals its content into. */
74
81
  dockHost?: HTMLElement;
82
+ /**
83
+ * Whether this seat is SHOWING anything, and so whether it should read the host at all.
84
+ *
85
+ * A seat that is not on screen — a docked tab the reader has switched away from — draws nothing, so
86
+ * its poll reads a list nobody is looking at, once a second, for a session the reader may not even be
87
+ * in. `false` stops that read; the seat keeps what it last read and starts again the moment it is
88
+ * shown. Absent means "always showing", which is what the footer entry is: its badge is the reader's
89
+ * at-a-glance count, and a badge that never read would simply show nothing.
90
+ */
91
+ showing?: boolean;
92
+ /**
93
+ * Ask the host how many rows this session has, WITHOUT reading the list (`list-count`).
94
+ *
95
+ * This is what the tick asks while this seat has no surface on screen: the badge is the thing that has
96
+ * to keep working while the panel is shut, and the full read is what makes it slow (it ships every
97
+ * visible entry's whole old and new text). A seat that IS on screen keeps the full read, and so does a
98
+ * face that offers no counter at all — absent means "this face cannot count", which is today's
99
+ * behaviour: one full read per tick.
100
+ */
101
+ onRefreshCount?: ((sessionId: SessionId | undefined) => void) | undefined;
75
102
  }
76
- /**
77
- * Open the DSH settings dialog and switch to this plugin's section. The
78
- * settings shell keeps its open state and the active section id as
79
- * component-local viewing state with no cross-plugin service, so the dialog
80
- * is opened by clicking the sidebar's settings trigger and then the nav cell
81
- * for this section. `element.click()` still fires React's delegated click
82
- * handlers, reaching the same state transitions a user gesture would.
83
- * @param sectionLabel - the nav label of this plugin's settings section.
84
- */
85
- export declare function openSettingsSection(sectionLabel: string): void;
86
103
  /**
87
104
  * How much of the window the app itself occupies on each side of the conversation
88
105
  * — how far the floating panel has to start from an edge to leave that part
@@ -103,6 +120,27 @@ export declare function frameInsets(): {
103
120
  left: number;
104
121
  right: number;
105
122
  };
123
+ /**
124
+ * The model row that holds one new-file line, or the last row that reads before it.
125
+ *
126
+ * The list's comments tab names a comment by the LINES it was written on (it draws no rows, so it has
127
+ * no row index to give), and a jump has to land the way every other jump does. A line the model no
128
+ * longer holds — an outdated thread — lands on the last row before it, which is exactly where
129
+ * `remapDiscussion` hangs a thread whose range is gone, so the box the reader came for is on screen.
130
+ *
131
+ * A row the file STILL HAS wins over one it is offering to take away, because a modified file numbers
132
+ * both sides and the same number is often carried by each: the row for old line 378 sits in the row
133
+ * stream just above the row for new line 378 (that is what a four-line deletion above the line looks
134
+ * like), and reading `newLine ?? oldLine` in one pass answers with the DELETED row. Landing there put
135
+ * the view above the code the number names — which is a comment's box with its top edge cut off by the
136
+ * viewport, the report this answers. The deleted row is still the right answer when the line exists
137
+ * nowhere else, and the last row before it when the file has moved past the number altogether.
138
+ *
139
+ * @param rows - the current model's rows.
140
+ * @param line - the new-file line to find.
141
+ * @returns the row index, or undefined when no row reads before that line.
142
+ */
143
+ export declare function rowOfLine(rows: readonly WholeFileDiffRow[], line: number, oldSide?: boolean): number | undefined;
106
144
  /** One file's synchronous view: diff rows and change blocks. */
107
145
  interface RowModel {
108
146
  diff: ReturnType<typeof computeWholeFileDiff>;
@@ -123,7 +161,7 @@ interface RowRange {
123
161
  * shared toolbar/keyboard in split mode (its own `focus` is private here). */
124
162
  export interface SplitDiffHandle {
125
163
  jump: (direction: -1 | 1, wrapGuard?: boolean, singleToast?: boolean) => void;
126
- land: (row: number, toCard?: boolean, flash?: boolean) => void;
164
+ land: (row: number, toCard?: boolean, flash?: boolean, comment?: string | undefined) => void;
127
165
  openSearch: () => void;
128
166
  toggleSearch: () => void;
129
167
  closeSearch: () => boolean;
@@ -134,6 +172,10 @@ export interface SplitDiffHandle {
134
172
  /** The two-column (side-by-side) whole-file diff view. */
135
173
  export declare const SplitDiff: import("react").ForwardRefExoticComponent<{
136
174
  file: PendingFileDiff;
175
+ /** The session the panel is VIEWING, which owns every decision made in this view (see
176
+ * `PendingDiffProps.sessionId`). Not `file.sessionId`, which names the session that most
177
+ * recently touched the file. */
178
+ sessionId: SessionId;
137
179
  model: RowModel;
138
180
  runs: HighlightSides | undefined;
139
181
  langWrap: boolean;
@@ -174,6 +216,32 @@ interface RulerRun {
174
216
  end: number;
175
217
  kind: 'del' | 'add';
176
218
  }
219
+ /**
220
+ * How many leading toolbar items fit in `available` px, with `gap` between neighbours.
221
+ *
222
+ * The decisions at the right end of the row (Keep / Revert) are what the row is for, so the
223
+ * informational buttons on its left are the ones that give way. An item that has not been measured
224
+ * yet costs nothing here and is settled by the next pass.
225
+ *
226
+ * @param widths - each item's own width in px, in the order they are drawn.
227
+ * @param available - the width the items may occupy.
228
+ * @param gap - the space between two neighbouring items.
229
+ * @returns the number of leading items that fit; 0 when even the first one does not.
230
+ */
231
+ export declare function fittingItems(widths: readonly number[], available: number, gap: number): number;
232
+ /**
233
+ * How many leading items the toolbar draws inline, given the room the group has.
234
+ *
235
+ * When they do not all fit, room has to be made for the overflow button that holds the rest — which
236
+ * can push one more item out — so the answer is asked twice, the second time with that button's
237
+ * width reserved. Reserving space can only shrink the count, so two passes settle it.
238
+ *
239
+ * @param widths - each item's own width in px, in the order they are drawn.
240
+ * @param available - the width the group may occupy.
241
+ * @param gap - the space between two neighbouring items.
242
+ * @param overflowWidth - the overflow button's width, reserved only when something is left over.
243
+ * @returns how many leading items to draw inline.
244
+ */
245
+ export declare function inlineItemCount(widths: readonly number[], available: number, gap: number, overflowWidth: number): number;
177
246
  /** Render the pending-edit review panel and its unified footer action. */
178
- export declare function PendingPanel({ wide, useSessions, usePending, onRefresh, onKeep, onRevert, onBlockKeep, onBlockRevert, onOpen, onPreviewImage, onPasteReference, onAskAgent, watchChat, onUndo, onRedo, onImportVcs, onRefreshVcs, onBrowse, onAddPath, onKeepAll, onRevertAll, onAckRedoCleared, collapseSidebar, t, docked, dockHost, onOpenDock, closeDock, useDock, }: PendingPanelProps): import("react").JSX.Element;
179
- export {};
247
+ export declare function PendingPanel({ wide, useSessions, sessionId, usePending, pendingView, showing, onRefresh, onRefreshCount, onMarkSeen, onKeep, onRevert, onBlockKeep, onBlockRevert, onOpen, onPreviewImage, onPasteReference, onCommentAdd, onCommentRemove, onCommentRemoveMany, onCommentAsk, onCommentSeen, onUndo, onRedo, onImportVcs, onRefreshVcs, onBrowse, onAddPath, onKeepAll, onKeepMany, onRevertMany, onAckRedoCleared, onAckUndoNotice, collapseSidebar, t, docked, dockHost, onOpenDock, closeDock, useDock, onCheckUpdate, }: PendingPanelProps): import("react").JSX.Element;
@@ -0,0 +1,62 @@
1
+ /**
2
+ * The colour a comment's CATEGORY is drawn as: one round dot per class of annotation.
3
+ *
4
+ * An agent annotating several passes over the same review (a call-chain walk, then the findings, then a
5
+ * second round of questions) can pass the same `category` id for every annotation of one kind and a
6
+ * different id for the next — and the reader then sees at a glance which class each comment belongs to,
7
+ * without reading a word. The id itself is opaque here: this module never parses it, only hashes it.
8
+ *
9
+ * The mapping is a plain hash into a fixed palette, so it is the SAME colour on any host and after any
10
+ * reload, with nothing persisted beyond the id the record already carries. Two ids can collide on one
11
+ * colour — 12 buckets cannot promise otherwise — which is why the reader also gets the id in words
12
+ * (`title`/`aria-label`): colour is a fast cue, never the only one.
13
+ *
14
+ * @module dsh-diff-approval/src/client/category-color
15
+ */
16
+ /**
17
+ * The twelve colours, in the order the hash indexes them — and THE ORDER IS PART OF THE CONTRACT, because
18
+ * the index is the bucket (see `categoryColorIndex`).
19
+ *
20
+ * The SET was chosen by CIEDE2000, not by eye and not by hue angle: it is a coordinate-ascent result over
21
+ * twelve colour families, and its MINIMUM pairwise ΔE00 is **18.82** — owned by the light blue and the grey
22
+ * — with every entry between L* 42 and 70, so each reads as a mark on the panel's light and dark surfaces
23
+ * alike. An earlier palette looked well spaced on the hue wheel and was not: indigo `#6E56CF` against
24
+ * purple `#8E4EC6` sat 20° apart at the same lightness and chroma — ΔE00 **6.94** on a 3px mark — and a
25
+ * near-duplicate green `#57a751` would have passed a hue-angle floor at ΔE00 2.65.
26
+ *
27
+ * THE ORDER was then solved for SEPARATELY (2026-10-06), because the set's spacing says nothing about the
28
+ * spacing of NEIGHBOURING buckets: the array used to be a hue ramp, so two categories whose hashes landed
29
+ * on adjacent indices — 2 times out of 12, which is the reader's own pair at indices 7 and 8 — differed by
30
+ * as little as ΔE00 19.28. The sequence below is the bottleneck Hamiltonian path over those same twelve
31
+ * colours: the order that MAXIMISES the MINIMUM ΔE00 over the eleven consecutive pairs, proved optimal by
32
+ * a DP over (mask, last) in the scratch script that chose it. Its consecutive minimum is **32.7475** — the
33
+ * full value is 32.74748843826888, which is why the test's floor is 32.7474 and not the rounded figure —
34
+ * owned by the grey and the red, against 19.2835 for the shipped order, and the set itself is byte-identical,
35
+ * so the whole-palette floor above is still 18.82. `tests/category-color.client.spec.ts` asserts BOTH numbers
36
+ * (whole-set and consecutive) plus the L* band, so a thirteenth colour or a reshuffle fails a test rather
37
+ * than shipping.
38
+ *
39
+ * Two categories can still collide EXACTLY — twelve buckets cannot promise otherwise, and `alpha`/`beta` in
40
+ * the spec do — so the promise this order keeps is "adjacent buckets differ a lot", never "different ids
41
+ * always differ"; colour is a fast cue, and the id in the mark's own `title`/`aria-label` is the exact one.
42
+ */
43
+ export declare const CATEGORY_COLORS: readonly ["#7F888E", "#C33D41", "#6A5CA4", "#7AAB83", "#B8A3CA", "#1D6F66", "#DC54B9", "#80A5EB", "#8D711B", "#13A9BF", "#577F11", "#C19784"];
44
+ /**
45
+ * Which palette entry a category id maps to: FNV-1a (32-bit) over the id's UTF-16 code units, mod 12.
46
+ *
47
+ * FNV-1a because it is four lines, has no dependencies, and spreads uuid-shaped ids (the reader's own
48
+ * suggestion: "passing a uuid per call") well enough that a handful of categories rarely collide. It is
49
+ * pinned by a test with a fixed id, so changing the hash is a visible decision rather than a silent
50
+ * recolouring of every existing annotation.
51
+ *
52
+ * @param category - the category id, as it was stored (already trimmed and capped by the tool rule).
53
+ * @returns the index into {@link CATEGORY_COLORS}, 0..11.
54
+ */
55
+ export declare function categoryColorIndex(category: string): number;
56
+ /**
57
+ * The colour one category is drawn as.
58
+ *
59
+ * @param category - the category id.
60
+ * @returns a `#RRGGBB` string from {@link CATEGORY_COLORS}.
61
+ */
62
+ export declare function categoryColor(category: string): string;
@@ -9,13 +9,6 @@
9
9
  * @module dsh-diff-approval/client/chords
10
10
  */
11
11
  import type { Translator } from './locales.ts';
12
- /**
13
- * One stored chord as a hint renders it: modifiers as written, arrow keys as
14
- * glyphs (`Ctrl+ArrowUp` → `Ctrl+↑`).
15
- * @param chord - the stored chord.
16
- * @returns the hint text; `''` for an unbound action.
17
- */
18
- export declare function chordHint(chord: string): string;
19
12
  /**
20
13
  * One action's chord as a hint.
21
14
  * @param action - the keybinding action id (see `DEFAULT_KEYBINDINGS`).
@@ -31,19 +24,79 @@ export declare function chordLabel(action: string): string;
31
24
  * @returns the label, with ` (chord)` appended when one is configured.
32
25
  */
33
26
  export declare function withChord(label: string, action: string): string;
27
+ /** One chord in both the forms a tooltip needs, read from a single setting so the two cannot drift. */
28
+ export interface ChordShortcut {
29
+ /**
30
+ * The keycaps, in order, with a literal `'+'` between the parts: the shell renders one `<kbd>` per
31
+ * entry and groups a combination into a single keycap when the array carries a separator (see the
32
+ * primitives' `ShortcutKeys`).
33
+ */
34
+ keys: string[];
35
+ /** The `aria-keyshortcuts` value: the same chord spelled in DOM key names. */
36
+ aria: string;
37
+ }
38
+ /**
39
+ * One action's shortcut, as a tooltip's `shortcutKeys` and the anchor's `aria-keyshortcuts`.
40
+ * @param action - the keybinding action id (see `DEFAULT_KEYBINDINGS`).
41
+ * @returns the two forms, or `undefined` when the action is unbound.
42
+ */
43
+ export declare function shortcutOf(action: string): ChordShortcut | undefined;
44
+ /**
45
+ * The quick-summon chord: its own setting rather than one of `DEFAULT_KEYBINDINGS`, since it is the
46
+ * one chord the panel is opened BY rather than one it answers inside.
47
+ * @returns the two forms, or `undefined` when the chord is unbound.
48
+ */
49
+ export declare function summonShortcut(): ChordShortcut | undefined;
50
+ /**
51
+ * Escape alone: the key the INNERMOST dismissible answers to (the search bar, a dialog), which no
52
+ * panel chord stands behind — the quick-summon chord closes the panel, not the bar inside it.
53
+ * @returns the keycaps and the aria form.
54
+ */
55
+ export declare function escapeShortcut(): ChordShortcut;
56
+ /**
57
+ * The panel's close button: Escape always, plus the quick-summon chord while one is bound.
58
+ *
59
+ * This is the one place the shell's one-chord model needs a judgment: these are two WAYS out, not a
60
+ * sequence, so they stay two keycaps (`['Esc', 'Ctrl+D']`) and two space-separated aria shortcuts —
61
+ * grouping them into one cap with separators would read as "press Esc, then Ctrl+D".
62
+ * @returns the keycaps and the aria form.
63
+ */
64
+ export declare function closeShortcut(): ChordShortcut;
65
+ /** What one tooltip site hands the shell, decided here so that no call site ever branches. */
66
+ export interface TooltipChord {
67
+ /**
68
+ * The label for the shell's `Tooltip`: the action's own name where the shell draws keycaps, and the
69
+ * label this plugin spelled before the primitives' 0.1.7-rc.2 — chord glued in — where it cannot.
70
+ */
71
+ label: string;
72
+ /** The keycaps to pass, or `undefined` when there is no chord or this host cannot draw one. */
73
+ shortcutKeys: readonly string[] | undefined;
74
+ /** The anchor's `aria-keyshortcuts`: plain text, and the same on every host. */
75
+ aria: string | undefined;
76
+ }
77
+ /**
78
+ * A tooltip for one action's chord.
79
+ * @param label - the translated action label.
80
+ * @param action - the keybinding action id, or `undefined` for a control that has no chord at all.
81
+ * @returns the label, keycaps and aria; an unbound or absent action keeps the plain label.
82
+ */
83
+ export declare function actionTooltip(label: string, action: string | undefined): TooltipChord;
84
+ /**
85
+ * The entry buttons that open the panel (the session header's, the footer badge): what it holds, and
86
+ * the chord that does the same. With the chord unbound the plain label is the whole hint.
87
+ * @param t - the panel's translator.
88
+ * @returns the label, keycaps and aria.
89
+ */
90
+ export declare function summonTooltip(t: Translator): TooltipChord;
34
91
  /**
35
- * How the panel's close button names its two ways out: Escape, and the
36
- * quick-summon chord when the user still has one bound. Unbinding the chord
37
- * leaves Escape as the only answer, so the hint then says just that.
92
+ * The panel's close button: Escape, plus the quick-summon chord while one is bound.
38
93
  * @param t - the panel's translator.
39
- * @returns the tooltip label.
94
+ * @returns the label, keycaps and aria.
40
95
  */
41
- export declare function closeHint(t: Translator): string;
96
+ export declare function closeTooltip(t: Translator): TooltipChord;
42
97
  /**
43
- * How an entry button names what it opens: the pending-changes panel, plus the
44
- * chord that does the same. With the chord unbound the plain label is the whole
45
- * hint — never an empty parenthetical.
98
+ * A control only Escape dismisses — the search bar — which no panel chord stands behind.
46
99
  * @param t - the panel's translator.
47
- * @returns the tooltip label.
100
+ * @returns the label, keycaps and aria.
48
101
  */
49
- export declare function summonHint(t: Translator): string;
102
+ export declare function escapeTooltip(t: Translator): TooltipChord;
@@ -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
  /**
@@ -141,6 +127,13 @@ export interface Discussion {
141
127
  quoteWrap?: boolean;
142
128
  /** How many older messages the row cap left out of the render. */
143
129
  hidden?: number;
130
+ /**
131
+ * Something the reader has not looked at yet arrived in this thread — an agent's own annotation, or
132
+ * an answer to one of its questions — so the card wears the dot. Host truth, carried through from
133
+ * the stored record (see `CommentRecord.unseen`): the block draws it and the card's own visibility
134
+ * is what asks the host to take it down.
135
+ */
136
+ unseen?: boolean;
144
137
  }
145
138
  /** Rows a folded block occupies: its header line. */
146
139
  export declare const DISCUSSION_HEADER_ROWS = 1;
@@ -234,6 +227,25 @@ export declare function discussionRows(discussion: Discussion): number;
234
227
  * @returns a map from row index to the rows added below it.
235
228
  */
236
229
  export declare function discussionRowExtras(discussions: readonly Discussion[], rowCount: number): Map<number, number>;
230
+ /**
231
+ * Where each block's own box starts, in rows, measured from the BASE of the stack it hangs in.
232
+ *
233
+ * Several blocks can end on one row, and then they are drawn one below the other under it, in the
234
+ * order they are held: `discussionRowExtras` charges the row the sum of them all, and the row stream
235
+ * (and the split view's card layer) draws one reserving box per block, in that same order. So the
236
+ * first block of a stack starts at the base, the second one box down, and so on — and a jump that
237
+ * names a block has to add that many boxes to the base, or it lands the top of the stack whatever
238
+ * block the reader asked for.
239
+ *
240
+ * Nothing is reordered here and no height changes: this is a reading of the drawing order the views
241
+ * already use, keyed by block id so a jump can ask for the box it was sent for.
242
+ *
243
+ * @param discussions - the blocks currently attached to this file, in the order they are drawn.
244
+ * @param anchorOf - the row the block hangs under, clamped exactly as the caller's row stream clamps
245
+ * it (its own height table and its own idea of the last row are what the base below is measured in).
246
+ * @returns a map from block id to the block's own start, in whole rows below its stack's base.
247
+ */
248
+ export declare function discussionStackOffsets(discussions: readonly Discussion[], anchorOf: (discussion: Discussion) => number): Map<string, number>;
237
249
  /**
238
250
  * The discussion attached to exactly this range, if there is one.
239
251
  *
@@ -271,7 +283,12 @@ export declare function discussionOverlapping(discussions: readonly Discussion[]
271
283
  * is what says whether they still hold the code the comment was written about. When they do
272
284
  * not — the lines were edited under it, or the hunk moved — the quote is looked for elsewhere
273
285
  * 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
286
+ * more than once. A match farther from the stored rows than `REANCHOR_MAX_LINES` is not that code
287
+ * having moved but another place in the file that happens to read the same — a look-alike
288
+ * declaration, which the recorded ±1 context cannot tell apart — so it is refused and the block is
289
+ * marked outdated instead. That is the one bound the host half applies in lines as well, so the
290
+ * figure the list ships and the place this half draws the card cannot come from two rules. When
291
+ * nothing matches, the block keeps the rows it last matched and is marked
275
292
  * outdated: it says so and keeps its quote for the reader, rather than being silently moved
276
293
  * onto whatever took those lines. Only when the whole range is gone from the model does the
277
294
  * block move, and then by line number, back to where the range used to be (see `gone`). The mark
@@ -301,6 +318,49 @@ export declare function remapDiscussion(discussion: Discussion, lineOf: (row: nu
301
318
  * @returns the re-anchored blocks, in the order they came in.
302
319
  */
303
320
  export declare function remapDiscussions(discussions: readonly Discussion[], lineOf: (row: number) => number | undefined, textOf: (row: number) => string, rowCount: number, hasNewLine?: (row: number) => boolean): readonly Discussion[];
321
+ /**
322
+ * The frame the reader drew, as a block of quoted lines for the question to carry.
323
+ *
324
+ * A reference is one number, and one number cannot say what the comment is about: a reader annotating
325
+ * a DELETED line produced `a.txt:2`, and the agent read line 2 of the file — which is another line
326
+ * entirely, because a removed line has no number in the file as it reads now (its `2` is the old
327
+ * file's). This block says what the number cannot: every row of the frame, in order, with the side it
328
+ * was on (`-` removed, `+` added, a space for context) and the number THAT side's file gave it.
329
+ *
330
+ * `quote` and `quoteLines` are what the record already holds — one entry per frame row, in row order —
331
+ * so nothing new is recorded for this: the question is built from the same snapshot the re-anchor uses.
332
+ * A record from before `quoteLines` was kept (or one the agent annotated, which names current lines
333
+ * only) has the text alone, and the block is that text with no numbers rather than a made-up side.
334
+ *
335
+ * @param quote - the anchored lines as they read when the comment was written.
336
+ * @param quoteLines - their gutter numbers and sides, in the same order.
337
+ * @returns the block, or `''` when there is nothing to show.
338
+ */
339
+ export declare function quotedFrame(quote: string | undefined, quoteLines: readonly DiscussionQuoteLine[] | undefined): string;
340
+ /**
341
+ * Whether a frame's numbers all come from the OLD file.
342
+ *
343
+ * A frame of removed rows alone names no line the file still has, so the numbers its record carries
344
+ * (`anchor`, and the line a jump is handed) are the old file's. Everything that would otherwise
345
+ * present them as current lines asks this first: the label a card and a list item wear, and the
346
+ * landing a jump to that comment makes. A record that kept no sides answers false — what it holds is
347
+ * read exactly as it always was.
348
+ *
349
+ * @param quoteLines - the frame's gutter numbers and sides, when the record kept them.
350
+ * @returns true when the frame holds at least one row and none of them has a new-file line.
351
+ */
352
+ export declare function frameNamesNoCurrentLine(quoteLines: readonly DiscussionQuoteLine[] | readonly CommentQuoteLine[] | undefined): boolean;
353
+ /**
354
+ * Whether a frame covers a line the file no longer has.
355
+ *
356
+ * The one thing a frame can hide behind its numbers: `anchor` is documented as new-file lines, but a
357
+ * frame of removed rows alone has none of those, so the numbers it carries are the old file's. A
358
+ * caller that must not present them as current lines asks this.
359
+ *
360
+ * @param quoteLines - the frame's gutter numbers and sides, when the record kept them.
361
+ * @returns true when at least one row of the frame is a removed line.
362
+ */
363
+ export declare function frameCoversRemovedRow(quoteLines: readonly DiscussionQuoteLine[] | undefined): boolean;
304
364
  /** What the selection frame shows for the current selection. */
305
365
  export interface SelectionFrameActions {
306
366
  /** Whether keep/revert apply (the selection fully covers change blocks). */
@@ -14,7 +14,8 @@
14
14
  */
15
15
  import type { ReactNode } from 'react';
16
16
  import type { HostObservable, PropsLocale } from '@deepseek-ai/dsh-client-ui-slots';
17
- import type { PendingDiffSnapshot } from './slots.ts';
17
+ import type { SessionId } from '@deepseek-ai/dsh-client-connection/client';
18
+ import type { PendingDiffSnapshot, PendingViewHooks } from './slots.ts';
18
19
  /** The tab type's `kind`: what `openTab` names. */
19
20
  export declare const DIFF_DOCK_KIND = "diff-approval";
20
21
  /** The registration's `id`: the key its body and title register under. The
@@ -120,11 +121,15 @@ type TabInfoHook = () => {
120
121
  readonly visible: boolean;
121
122
  };
122
123
  };
123
- export interface DiffDockBodyProps extends PropsLocale<'diff-approval'> {
124
+ export interface DiffDockBodyProps extends PropsLocale<'diff-approval'>, PendingViewHooks {
124
125
  /** The seat's own tab hook (the sidebar framework injects it). */
125
126
  readonly useTabInfo?: TabInfoHook;
126
- /** This plugin's pending store hook, for the chip's count. */
127
+ /** This plugin's pending store hook, for the chip's count where no per-session reader reaches it. */
127
128
  readonly usePending?: (select: (snapshot: PendingDiffSnapshot) => unknown) => unknown;
129
+ /** The session seat, when this host composes one into the tab (see `session-seat.ts`). */
130
+ readonly useSessions?: ((select: (state: unknown) => unknown) => unknown) | undefined;
131
+ /** The session this dock tab belongs to, when the shell composes one. */
132
+ readonly sessionId?: SessionId | undefined;
128
133
  /** Whether the panel renders as the docked body (set by the tab seat's face). */
129
134
  readonly docked?: boolean;
130
135
  /** Report this body's visibility to the dock state (see {@link DockFace}). */
@@ -133,11 +138,6 @@ export interface DiffDockBodyProps extends PropsLocale<'diff-approval'> {
133
138
  readonly onDockClose?: ((close: (() => void) | undefined) => void) | undefined;
134
139
  [key: string]: unknown;
135
140
  }
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
141
  /** The cordis pieces this adapter uses, structurally: optional service lookup and
142
142
  * a scoped effect. Both are what the plugin sandbox always forwards — its
143
143
  * whitelist covers `effect`/`on`/timers but NOT cordis's dynamic `inject`, and it
@@ -0,0 +1,98 @@
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
+ import type { ReactElement } from 'react';
28
+ /** The pending-changes glyph: the sidebar's badge and the header entry carry it. */
29
+ export declare const IconListPenOutline16: ({ size, className }: primitives.IconProps) => import("react").JSX.Element;
30
+ /** The list's fold chevrons. */
31
+ export declare const IconChevronDownOutline14: ({ size, className }: primitives.IconProps) => import("react").JSX.Element;
32
+ export declare const IconChevronUpOutline14: ({ size, className }: primitives.IconProps) => import("react").JSX.Element;
33
+ /** The path picker's tree disclosure. */
34
+ export declare const IconChevronRightOutline14: ({ size, className }: primitives.IconProps) => import("react").JSX.Element;
35
+ /** Closing a card, a dialog or a search bar. */
36
+ export declare const IconCloseOutline16: ({ size, className }: primitives.IconProps) => import("react").JSX.Element;
37
+ /** The thread's own overflow menu. */
38
+ export declare const IconEllipsisOutline16: ({ size, className }: primitives.IconProps) => import("react").JSX.Element;
39
+ /** The add-path dialog's browse button. */
40
+ export declare const IconBrowseOutline16: ({ size, className }: primitives.IconProps) => import("react").JSX.Element;
41
+ /** A folder in the picker's tree, open and closed. */
42
+ export declare const IconFolderOpenOutline16: ({ size, className }: primitives.IconProps) => import("react").JSX.Element;
43
+ export declare const IconFolderOpen16: ({ size, className }: primitives.IconProps) => import("react").JSX.Element;
44
+ export declare const IconFolderClose16: ({ size, className }: primitives.IconProps) => import("react").JSX.Element;
45
+ /** The presentation switch: dock the panel in the sidebar. */
46
+ export declare const IconPanelLeftOutline16: ({ size, className }: primitives.IconProps) => import("react").JSX.Element;
47
+ /** The settings tab's refresh, which the older set spelled with the small size. */
48
+ export declare const IconRefreshOutline14: ({ size, className }: primitives.IconProps) => import("react").JSX.Element;
49
+ /** Adding a path, the diff refresh, the search box, and Settings. */
50
+ export declare const IconPlusOutline16: ({ size, className }: primitives.IconProps) => import("react").JSX.Element;
51
+ export declare const IconRefreshOutline16: ({ size, className }: primitives.IconProps) => import("react").JSX.Element;
52
+ export declare const IconSearchOutline16: ({ size, className }: primitives.IconProps) => import("react").JSX.Element;
53
+ export declare const IconSettingsOutline16: ({ size, className }: primitives.IconProps) => import("react").JSX.Element;
54
+ /**
55
+ * The package's own menus, toasts and clipboard helper.
56
+ *
57
+ * Re-exported so one module names everything this plugin takes from the shell's primitives: these
58
+ * names have survived both releases, and a caller has no reason to reach past this file.
59
+ */
60
+ export { Menu, Toast, writeClipboard } from '@deepseek-ai/dsh-client-ui-primitives';
61
+ export type { MenuEntry } from '@deepseek-ai/dsh-client-ui-primitives';
62
+ /** The tooltip props this plugin uses, including the one the installed types do not know yet. */
63
+ export interface ShellTooltipProps {
64
+ label: string;
65
+ side?: 'right' | 'bottom' | 'top' | undefined;
66
+ delayMs?: number | undefined;
67
+ maxWidth?: number | undefined;
68
+ disabled?: boolean | undefined;
69
+ /**
70
+ * The keycaps the bubble renders AFTER its label, one `<kbd>` per entry, with a literal `'+'`
71
+ * between the parts so the shell groups a combination into a single keycap. `undefined` (or an
72
+ * empty array) renders no keycaps at all — which is what an unbound action passes.
73
+ */
74
+ shortcutKeys?: readonly string[] | undefined;
75
+ children: ReactElement;
76
+ }
77
+ /**
78
+ * The primitives' `Tooltip`, typed with the props above.
79
+ *
80
+ * `shortcutKeys` arrived in the primitives' 0.1.7-rc.2 — the release the host runs — while what this
81
+ * repo COMPILES against is 0.1.0-rc.6, whose `Tooltip.d.ts` predates it. Passing the prop at a call
82
+ * site is therefore a type error even though the running component accepts it (and an older one simply
83
+ * ignores an unknown prop). The cast lives here, once, at the same boundary this module already uses
84
+ * to bridge shell versions, rather than at every call site.
85
+ */
86
+ export declare const Tooltip: (props: ShellTooltipProps) => ReactElement;
87
+ /**
88
+ * Whether the running shell can draw a shortcut beside a tooltip's label.
89
+ *
90
+ * `ShortcutKeys` and the `Tooltip.shortcutKeys` prop shipped together in the primitives'
91
+ * **0.1.7-rc.2**: across all 24 installed versions this plugin is tested against, the component is
92
+ * absent from every release up to and including 0.1.7-rc.1 (`lib/index.js` does not contain the name
93
+ * at all, and `Tooltip.d.ts` takes no `shortcutKeys`) and present from 0.1.7-rc.2 on (`function
94
+ * ShortcutKeys(...)`, exported, and `shortcutKeys?: readonly string[]` on `Tooltip`) — the same
95
+ * release the prop arrived in, so the two can never disagree. An older host ignores an unknown prop
96
+ * silently, which is why a caller must not rely on the prop alone (see `chords.ts`).
97
+ */
98
+ export declare const SHELL_DRAWS_SHORTCUTS: boolean;