dsh-diff-approval 0.26.0 → 0.28.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/index.js CHANGED
@@ -1114,9 +1114,20 @@ async function writeJson(file, value) {
1114
1114
  throw error;
1115
1115
  }
1116
1116
  }
1117
+ /**
1118
+ * The directory a path names, in the platform's own spelling.
1119
+ *
1120
+ * `node:path` rather than a scan for the last `/`: on Windows the storage root is
1121
+ * `C:\Users\<user>\.dsh\diff-approval\workspaces`, which contains no forward slash at all — the
1122
+ * scan answered `.`, the `mkdir` before every write became a no-op, and the write itself threw
1123
+ * ENOENT, so no pending change was ever persisted there (the failure only reached a logger
1124
+ * warning). Both separators have to be understood, and only the platform knows which they are.
1125
+ *
1126
+ * @param file - the file whose directory is wanted.
1127
+ * @returns that directory, or `.` for a bare file name.
1128
+ */
1117
1129
  function dirName(file) {
1118
- const index = file.lastIndexOf("/");
1119
- return index < 0 ? "." : file.slice(0, index);
1130
+ return dirname(file);
1120
1131
  }
1121
1132
  /**
1122
1133
  * File-backed pending entries, one global file keyed by path.
@@ -1928,7 +1939,7 @@ function apply(ctx, config) {
1928
1939
  if (entries.length > 0) store.hydrate(entries);
1929
1940
  if (migratedLegacy) await persistence.save(store.all());
1930
1941
  } catch (error) {
1931
- ctx.logger.warn(`diff-approval: loading persisted state failed: ${errorMessage(error)}`);
1942
+ ctx.logger.error(`diff-approval: loading persisted state failed, so the list starts empty: ${errorMessage(error)}`);
1932
1943
  }
1933
1944
  })();
1934
1945
  return loadPromise;
@@ -2396,6 +2407,9 @@ function apply(ctx, config) {
2396
2407
  let persistScheduled = false;
2397
2408
  let lastPersistAt = 0;
2398
2409
  let persistTimer;
2410
+ /** The last persist failure reported, so a writer that keeps failing says so once (cleared by a
2411
+ * write that works, which is what makes a later, different failure report again). */
2412
+ let persistFailureReported;
2399
2413
  /** Actually write the (dirty) store to disk; one coalesced write. */
2400
2414
  async function flushPersist() {
2401
2415
  if (!persistDirty) return;
@@ -2404,8 +2418,13 @@ function apply(ctx, config) {
2404
2418
  try {
2405
2419
  await ensureLoaded();
2406
2420
  await persistence.save(store.all());
2421
+ persistFailureReported = void 0;
2407
2422
  } catch (error) {
2408
- ctx.logger.warn(`diff-approval: persisting pending changes failed: ${errorMessage(error)}`);
2423
+ const message = errorMessage(error);
2424
+ if (persistFailureReported !== message) {
2425
+ persistFailureReported = message;
2426
+ ctx.logger.error(`diff-approval: persisting pending changes failed, so the list will not survive a restart: ${message}`);
2427
+ }
2409
2428
  }
2410
2429
  }
2411
2430
  /** Mark the store dirty and schedule one throttled, coalesced write. Pass
@@ -2463,7 +2482,8 @@ function apply(ctx, config) {
2463
2482
  files,
2464
2483
  workspacePath: workspaceOf(sessionId)?.path,
2465
2484
  redoCleared: redoCleared || void 0,
2466
- commentSkill: commentSkill()
2485
+ commentSkill: commentSkill(),
2486
+ persistError: persistFailureReported
2467
2487
  }
2468
2488
  };
2469
2489
  }
@@ -1,13 +1,27 @@
1
1
  /** Sidebar-foot pending-edit review action and the split review panel it opens. */
2
+ import type { ReactNode } from 'react';
2
3
  import type { SessionId } from '@deepseek-ai/dsh-client-connection/client';
3
4
  import type { InjectFace, PropsLocale, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots';
4
5
  import type { DiffApprovalBlockRange, PendingFileDiff } from '../types.ts';
5
6
  import type { PendingPanelFace } from './slots.ts';
6
7
  import type { Translator } from './locales.ts';
7
8
  import { computeWholeFileDiff } from './whole-file-diff.ts';
9
+ import type { Discussion } from './discussion.ts';
8
10
  import type { ChangeBlock, IntraRun } from './whole-file-diff.ts';
9
11
  import type { HighlightSides } from './highlight.ts';
10
12
  import type { VisibleLines } from './windowed-highlight.ts';
13
+ /**
14
+ * File-list pane width bounds for the manual split drag, in px.
15
+ *
16
+ * The floor is the width the pane's bulk footer needs to keep its three labels on ONE line
17
+ * (全部保留 / 全部回退 / 添加). Each is a `.action` — 12px text, 12px of side padding and a
18
+ * hairline, so 26px of chrome — laid out in `.bulkActions` (6px gaps, and its own 8px right inset)
19
+ * inside `.fileList`'s 8px side padding and 1px border: 74 + 6 + 74 + 6 + 66 + 8 + 17 = 251px, and
20
+ * a hair of slack on top. Narrower than that the row shrinks its buttons until the labels wrap onto
21
+ * two lines, which makes the pane's most-used controls two rows tall in the narrowest case — so the
22
+ * floor is not just a drag limit, it is the width at which the footer is still itself.
23
+ */
24
+ export declare const MIN_LIST_WIDTH_PX = 260;
11
25
  /** Normalize a path for comparison: forward slashes, no trailing slash. */
12
26
  export declare function normalizeDiffPath(p: string): string;
13
27
  /** Whether a produced-file chip path and a pending file path refer to the same
@@ -109,6 +123,7 @@ interface RowRange {
109
123
  * shared toolbar/keyboard in split mode (its own `focus` is private here). */
110
124
  export interface SplitDiffHandle {
111
125
  jump: (direction: -1 | 1, wrapGuard?: boolean, singleToast?: boolean) => void;
126
+ land: (row: number, toCard?: boolean, flash?: boolean) => void;
112
127
  openSearch: () => void;
113
128
  toggleSearch: () => void;
114
129
  closeSearch: () => boolean;
@@ -134,6 +149,16 @@ export declare const SplitDiff: import("react").ForwardRefExoticComponent<{
134
149
  /** Report which source lines this view is showing, so the parent's windowed
135
150
  * highlighter follows this view's own scroller (it has its own virtual window). */
136
151
  onVisibleLines: (visible: VisibleLines) => void;
152
+ /** The threads this file carries, laid out: each hangs under the pair its anchor ends in. */
153
+ discussions?: readonly Discussion[];
154
+ /** Draws one thread's card at the width this view gives it (see `DiscussionBlock`). */
155
+ renderDiscussion?: (discussion: Discussion, bodyWidth: number, split: boolean) => ReactNode;
156
+ /** The comment action for the current selection, when the panel offers one for it. */
157
+ selectionComment?: ReactNode;
158
+ /** The changed-line runs the overview ruler draws, in whole-file row indices. */
159
+ rulerRuns: readonly RulerRun[];
160
+ /** The go-to popup, which this view centres on its own box (see `gotoDialog`). */
161
+ gotoDialog?: ReactNode;
137
162
  } & import("react").RefAttributes<SplitDiffHandle>>;
138
163
  /**
139
164
  * Reconstruct the plain text of the current selection so auto-wrap's visual
@@ -143,6 +168,12 @@ export declare const SplitDiff: import("react").ForwardRefExoticComponent<{
143
168
  * without a newline while the real newline between diff rows is kept.
144
169
  */
145
170
  export declare function selectedPlainText(): string | undefined;
171
+ /** One maximal run of same-kind changed rows: what both views' rulers are built from. */
172
+ interface RulerRun {
173
+ start: number;
174
+ end: number;
175
+ kind: 'del' | 'add';
176
+ }
146
177
  /** Render the pending-edit review panel and its unified footer action. */
147
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;
148
179
  export {};
@@ -70,6 +70,17 @@ export interface Discussion {
70
70
  * outdated and the quote is what the reader is shown to know what it had been about.
71
71
  */
72
72
  quote?: string;
73
+ /**
74
+ * The quoted rows with one row of context on each side, as they read when the thread was created.
75
+ *
76
+ * The fingerprint a later rebuild matches the thread against. `quote` alone is too weak to say
77
+ * whether the code is still there: a comment on a common line — a closing brace, a blank line —
78
+ * found that line somewhere else in the file, followed it, and stayed live however thoroughly the
79
+ * code under its numbers had been rewritten. The context is what tells a genuine move from that
80
+ * coincidence. Absent on threads recorded before it was kept, which then falls back to matching
81
+ * the quote alone.
82
+ */
83
+ quoteContext?: string;
73
84
  /**
74
85
  * The gutter numbers of `quote`'s lines, in the same order, so an outdated block can lay the
75
86
  * quote out exactly as the file lays code out. See `DiscussionQuoteLine`.
@@ -118,6 +129,16 @@ export interface Discussion {
118
129
  * sets the code's line height to.
119
130
  */
120
131
  bodyRows?: number;
132
+ /**
133
+ * The wrap setting `bodyRows` was counted with, carried by the caller so the quote the
134
+ * block draws reads the very value its rows were reserved from. Set together with
135
+ * `bodyRows` and never on its own; an outdated block reserves the quote's rows in code
136
+ * rows scaled to the thread's grid (see `layoutDiscussion`), and a quote that wrapped
137
+ * while its rows were counted unwrapped — or the other way round — leaves the block
138
+ * exactly as tall as the difference, air that the bottom-anchored writing row shows as
139
+ * a hole in the middle of the thread.
140
+ */
141
+ quoteWrap?: boolean;
121
142
  /** How many older messages the row cap left out of the render. */
122
143
  hidden?: number;
123
144
  }
@@ -263,7 +284,7 @@ export declare function discussionOverlapping(discussions: readonly Discussion[]
263
284
  * @param rowCount - how many rows the current model has.
264
285
  * @returns the re-anchored discussion (the same object when nothing moved).
265
286
  */
266
- export declare function remapDiscussion(discussion: Discussion, lineOf: (row: number) => number | undefined, textOf: (row: number) => string, rowCount: number): Discussion;
287
+ export declare function remapDiscussion(discussion: Discussion, lineOf: (row: number) => number | undefined, textOf: (row: number) => string, rowCount: number, hasNewLine?: (row: number) => boolean): Discussion;
267
288
  /**
268
289
  * Re-anchor a whole file's blocks in one pass.
269
290
  *
@@ -279,7 +300,7 @@ export declare function remapDiscussion(discussion: Discussion, lineOf: (row: nu
279
300
  * @param rowCount - how many rows the current model has.
280
301
  * @returns the re-anchored blocks, in the order they came in.
281
302
  */
282
- export declare function remapDiscussions(discussions: readonly Discussion[], lineOf: (row: number) => number | undefined, textOf: (row: number) => string, rowCount: number): readonly Discussion[];
303
+ export declare function remapDiscussions(discussions: readonly Discussion[], lineOf: (row: number) => number | undefined, textOf: (row: number) => string, rowCount: number, hasNewLine?: (row: number) => boolean): readonly Discussion[];
283
304
  /** What the selection frame shows for the current selection. */
284
305
  export interface SelectionFrameActions {
285
306
  /** Whether keep/revert apply (the selection fully covers change blocks). */
@@ -95,6 +95,16 @@ export interface TabRegistryFace {
95
95
  order: number;
96
96
  title: () => string;
97
97
  description?: () => string;
98
+ /**
99
+ * Glyph drawn before the title. The package types this as
100
+ * `ComponentType<IconProps>`; declared structurally so the build stays off
101
+ * that package's version line — the shape is what the seat reads, and
102
+ * leaving it out is why this panel's guide row had no mark of its own.
103
+ */
104
+ icon?: (props: {
105
+ size?: number;
106
+ className?: string;
107
+ }) => unknown;
98
108
  }[];
99
109
  }): () => void;
100
110
  }
@@ -16,7 +16,6 @@ export declare const zh: {
16
16
  'panel.tab.comments': string;
17
17
  'panel.commentsEmpty': string;
18
18
  'panel.commentOutdated': string;
19
- 'panel.commentEmptyTitle': string;
20
19
  'panel.aria': string;
21
20
  'panel.stats': string;
22
21
  'panel.blockPosition': string;
@@ -60,6 +59,12 @@ export declare const zh: {
60
59
  'panel.discussionRoundsDesc': string;
61
60
  'panel.quickSummonDesc': string;
62
61
  'panel.recordShortcut': string;
62
+ 'panel.key.goto': string;
63
+ 'action.goto': string;
64
+ 'action.gotoGo': string;
65
+ 'action.gotoCancel': string;
66
+ 'panel.gotoMissing': string;
67
+ 'panel.persistFailed': string;
63
68
  'settings.keybindings': string;
64
69
  'settings.keybindingsDesc': string;
65
70
  'settings.comment': string;
@@ -104,6 +109,7 @@ export declare const zh: {
104
109
  'discussion.promptRuleSkill': string;
105
110
  'discussion.thinking': string;
106
111
  'discussion.queued': string;
112
+ 'discussion.answering': string;
107
113
  'discussion.outdatedQuote': string;
108
114
  'discussion.hidden': string;
109
115
  'discussion.failed': string;
@@ -180,6 +186,8 @@ export declare const zh: {
180
186
  'status.missing': string;
181
187
  'panel.resolvedAsk': string;
182
188
  'panel.keepInList': string;
189
+ 'panel.removalQuiet': string;
190
+ 'panel.removalQuietHint': string;
183
191
  'panel.fileKeptAsk': string;
184
192
  'panel.fileRevertedAsk': string;
185
193
  'panel.confirmFileRemove': string;
@@ -240,7 +248,6 @@ export declare const en: {
240
248
  'panel.tab.comments': string;
241
249
  'panel.commentsEmpty': string;
242
250
  'panel.commentOutdated': string;
243
- 'panel.commentEmptyTitle': string;
244
251
  'panel.aria': string;
245
252
  'panel.stats': string;
246
253
  'panel.blockPosition': string;
@@ -284,6 +291,12 @@ export declare const en: {
284
291
  'panel.discussionRoundsDesc': string;
285
292
  'panel.quickSummonDesc': string;
286
293
  'panel.recordShortcut': string;
294
+ 'panel.key.goto': string;
295
+ 'action.goto': string;
296
+ 'action.gotoGo': string;
297
+ 'action.gotoCancel': string;
298
+ 'panel.gotoMissing': string;
299
+ 'panel.persistFailed': string;
287
300
  'settings.keybindings': string;
288
301
  'settings.keybindingsDesc': string;
289
302
  'settings.comment': string;
@@ -328,6 +341,7 @@ export declare const en: {
328
341
  'discussion.promptRuleSkill': string;
329
342
  'discussion.thinking': string;
330
343
  'discussion.queued': string;
344
+ 'discussion.answering': string;
331
345
  'discussion.outdatedQuote': string;
332
346
  'discussion.hidden': string;
333
347
  'discussion.failed': string;
@@ -404,6 +418,8 @@ export declare const en: {
404
418
  'status.missing': string;
405
419
  'panel.resolvedAsk': string;
406
420
  'panel.keepInList': string;
421
+ 'panel.removalQuiet': string;
422
+ 'panel.removalQuietHint': string;
407
423
  'panel.fileKeptAsk': string;
408
424
  'panel.fileRevertedAsk': string;
409
425
  'panel.confirmFileRemove': string;
@@ -34,6 +34,44 @@ export declare function rememberedDiscussions(sessionId: string | undefined): Re
34
34
  * @param byFile - the threads, by pending entry id.
35
35
  */
36
36
  export declare function rememberDiscussions(sessionId: string | undefined, byFile: Readonly<Record<string, readonly Discussion[]>>): void;
37
+ /**
38
+ * End one comment, wherever it came from.
39
+ *
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.
44
+ *
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.
48
+ */
49
+ export declare function forgetDiscussion(sessionId: string | undefined, fileId: string, discussionId: string): void;
50
+ /**
51
+ * Drop every thread whose file the list no longer holds.
52
+ *
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.
57
+ *
58
+ * @param sessionId - the session the panel is reviewing; nothing is dropped without one.
59
+ * @param listed - the pending entry ids the list is holding.
60
+ */
61
+ export declare function forgetDiscussionsNotIn(sessionId: string | undefined, listed: readonly string[]): void;
62
+ /**
63
+ * Whether the reader asked not to be asked again about removing one file.
64
+ * @param sessionId - the session the panel is reviewing; nothing is quiet without one.
65
+ * @param fileId - the pending entry the question was about.
66
+ * @returns true when the action should run without the question.
67
+ */
68
+ export declare function removalAskQuiet(sessionId: string | undefined, fileId: string): boolean;
69
+ /**
70
+ * Stop asking whether one file should leave the list, for the rest of this page.
71
+ * @param sessionId - the session the panel is reviewing; nothing is recorded without one.
72
+ * @param fileId - the pending entry the question was about.
73
+ */
74
+ export declare function quietenRemovalAsk(sessionId: string | undefined, fileId: string): void;
37
75
  /**
38
76
  * Remember that a session's panel was left showing one file, and where in it.
39
77
  * @param sessionId - the session the panel was reviewing; nothing is recorded
@@ -24,6 +24,12 @@ export interface PendingDiffSnapshot {
24
24
  * same rules inline when it is not.
25
25
  */
26
26
  commentSkill?: string | undefined;
27
+ /**
28
+ * The last failure to write the pending state to disk, or absent while writes are working. The
29
+ * panel says so once: the file is what makes the list survive a restart, so a reader who is never
30
+ * told finds the list gone later with nothing to explain it (see the wire type).
31
+ */
32
+ persistError?: string | undefined;
27
33
  /** Latched when an external change created a fresh undo checkpoint that
28
34
  * superseded the redo history; the panel surfaces it once (deferred if the
29
35
  * panel is closed) via a bottom-right notice. */
@@ -66,6 +66,12 @@ export interface DiffApprovalListValue {
66
66
  * is set, and carries the same rules inline when it is not.
67
67
  */
68
68
  commentSkill?: string | undefined;
69
+ /**
70
+ * The last failure to write the pending state to disk, or absent while writes are working. The
71
+ * panel says so once: that file is what makes the list survive a restart, so a reader who is not
72
+ * told about it simply finds the list gone later, with nothing to explain it (issue #6).
73
+ */
74
+ persistError?: string | undefined;
69
75
  }
70
76
  /** What the open endpoint asks the OS to do with a file. */
71
77
  export type DiffApprovalOpenAction = 'open' | 'reveal';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-diff-approval",
3
- "version": "0.26.0",
3
+ "version": "0.28.0",
4
4
  "description": "DeepSeek Harness plugin: pending-edit review with whole-file diff and Keep/Revert",
5
5
  "packageManager": "pnpm@11.21.0",
6
6
  "author": "Wu Zhiwei",