dsh-diff-approval 0.26.0 → 0.27.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
@@ -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,12 @@ 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;
137
158
  } & import("react").RefAttributes<SplitDiffHandle>>;
138
159
  /**
139
160
  * Reconstruct the plain text of the current selection so auto-wrap's visual
@@ -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
  }
@@ -60,6 +60,11 @@ export declare const zh: {
60
60
  'panel.discussionRoundsDesc': string;
61
61
  'panel.quickSummonDesc': string;
62
62
  'panel.recordShortcut': string;
63
+ 'panel.key.goto': string;
64
+ 'action.goto': string;
65
+ 'action.gotoGo': string;
66
+ 'action.gotoCancel': string;
67
+ 'panel.gotoMissing': 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;
@@ -284,6 +292,11 @@ export declare const en: {
284
292
  'panel.discussionRoundsDesc': string;
285
293
  'panel.quickSummonDesc': string;
286
294
  'panel.recordShortcut': string;
295
+ 'panel.key.goto': string;
296
+ 'action.goto': string;
297
+ 'action.gotoGo': string;
298
+ 'action.gotoCancel': string;
299
+ 'panel.gotoMissing': 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,32 @@ 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
+ * Whether the reader asked not to be asked again about removing one file.
52
+ * @param sessionId - the session the panel is reviewing; nothing is quiet without one.
53
+ * @param fileId - the pending entry the question was about.
54
+ * @returns true when the action should run without the question.
55
+ */
56
+ export declare function removalAskQuiet(sessionId: string | undefined, fileId: string): boolean;
57
+ /**
58
+ * Stop asking whether one file should leave the list, for the rest of this page.
59
+ * @param sessionId - the session the panel is reviewing; nothing is recorded without one.
60
+ * @param fileId - the pending entry the question was about.
61
+ */
62
+ export declare function quietenRemovalAsk(sessionId: string | undefined, fileId: string): void;
37
63
  /**
38
64
  * Remember that a session's panel was left showing one file, and where in it.
39
65
  * @param sessionId - the session the panel was reviewing; nothing is recorded
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-diff-approval",
3
- "version": "0.26.0",
3
+ "version": "0.27.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",