dsh-diff-approval 0.9.0 → 0.11.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
@@ -694,6 +694,35 @@ const inject = [
694
694
  ];
695
695
  /** The connection RPC channel this plugin serves. */
696
696
  const DIFF_APPROVAL_CHANNEL = "/diff-approval";
697
+ /** The dominant line ending of `text`: CRLF when `\r\n` meets or beats lone
698
+ * `\n`, else LF; a text with no line breaks falls back to LF.
699
+ * @param text - the content to inspect.
700
+ * @returns the dominant line-ending sequence.
701
+ */
702
+ function detectEol(text) {
703
+ let crlf = 0;
704
+ let lf = 0;
705
+ for (let i = 0; i < text.length; i++) {
706
+ const c = text.charCodeAt(i);
707
+ if (c === 13) {
708
+ if (i + 1 < text.length && text.charCodeAt(i + 1) === 10) {
709
+ crlf++;
710
+ i++;
711
+ }
712
+ } else if (c === 10) lf++;
713
+ }
714
+ if (crlf === 0 && lf === 0) return "\n";
715
+ return crlf >= lf ? "\r\n" : "\n";
716
+ }
717
+ /** Normalize any line ending (`\r\n`, `\r`) to `\n`. */
718
+ function normalizeEol(text) {
719
+ return text.replace(/\r\n?/g, "\n");
720
+ }
721
+ /** Re-encode `text`'s line endings to `eol` (its content is unchanged). */
722
+ function reencodeEol(text, eol) {
723
+ const normalized = normalizeEol(text);
724
+ return eol === "\n" ? normalized : normalized.replace(/\n/g, "\r\n");
725
+ }
697
726
  /**
698
727
  * Narrow a successful `edit` result value to an operation outcome. The edit
699
728
  * tool's output schema declares exactly `{ path, before, after }`; anything
@@ -1275,7 +1304,8 @@ function apply(ctx, config) {
1275
1304
  if (entry.kind === "create") await rm(ctx.fs.processPath(resolved), { force: true });
1276
1305
  else {
1277
1306
  const preWrite = await ctx.fs.readText(resolved, void 0) ?? entry.newText;
1278
- await writeRevert(resolved, entry.oldText, target.sessionId, signal);
1307
+ const content = reencodeEol(entry.oldText, detectEol(entry.newText));
1308
+ await writeRevert(resolved, content, target.sessionId, signal);
1279
1309
  undo = {
1280
1310
  before: {
1281
1311
  id: entry.id,
@@ -1287,7 +1317,7 @@ function apply(ctx, config) {
1287
1317
  id: entry.id,
1288
1318
  path: entry.path,
1289
1319
  entry: void 0,
1290
- fileText: entry.oldText
1320
+ fileText: content
1291
1321
  }
1292
1322
  };
1293
1323
  }
@@ -1353,25 +1383,26 @@ function apply(ctx, config) {
1353
1383
  };
1354
1384
  const restored = contentRangeOf(entry.oldText, blockTarget.block.oldStart, blockTarget.block.oldEnd);
1355
1385
  const updatedNew = replaceContentLines(entry.newText, blockTarget.block.newStart, blockTarget.block.newEnd, restored);
1386
+ const content = reencodeEol(updatedNew, detectEol(entry.newText));
1356
1387
  let afterEntry;
1357
- if (updatedNew === entry.oldText) {
1388
+ if (normalizeEol(updatedNew) === normalizeEol(entry.oldText)) {
1358
1389
  store.remove(blockTarget.sessionId, blockTarget.id);
1359
1390
  afterEntry = void 0;
1360
1391
  } else {
1361
- store.update(blockTarget.sessionId, blockTarget.id, { newText: updatedNew });
1392
+ store.update(blockTarget.sessionId, blockTarget.id, { newText: content });
1362
1393
  afterEntry = {
1363
1394
  ...entry,
1364
- newText: updatedNew,
1395
+ newText: content,
1365
1396
  updatedAt: Date.now()
1366
1397
  };
1367
1398
  }
1368
1399
  let undo;
1369
1400
  try {
1370
1401
  const resolved = await ctx.fs.resolve(entry.path, { signal });
1371
- if (entry.kind === "create" && updatedNew === "") await rm(ctx.fs.processPath(resolved), { force: true });
1402
+ if (entry.kind === "create" && content === "") await rm(ctx.fs.processPath(resolved), { force: true });
1372
1403
  else {
1373
1404
  const preWrite = await ctx.fs.readText(resolved, void 0) ?? entry.newText;
1374
- await writeRevert(resolved, updatedNew, blockTarget.sessionId, signal);
1405
+ await writeRevert(resolved, content, blockTarget.sessionId, signal);
1375
1406
  undo = {
1376
1407
  before: {
1377
1408
  id: entry.id,
@@ -1383,7 +1414,7 @@ function apply(ctx, config) {
1383
1414
  id: entry.id,
1384
1415
  path: entry.path,
1385
1416
  entry: afterEntry,
1386
- fileText: updatedNew
1417
+ fileText: content
1387
1418
  }
1388
1419
  };
1389
1420
  }
@@ -1,6 +1,7 @@
1
1
  /** Sidebar-foot pending-edit review action and the split review panel it opens. */
2
2
  import type { InjectFace, PropsLocale, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots';
3
3
  import type { PendingPanelFace } from './slots.ts';
4
+ export declare function wrapInto(text: string, widthPx: number, measure: (t: string) => number, tabPx: number): string[];
4
5
  /** Full panel props composed by the sidebar footer-action slot. */
5
6
  export type PendingPanelProps = PropsRuntime<'sidebar.footer.action'> & InjectFace<PendingPanelFace> & PropsLocale<'diff-approval'>;
6
7
  /**
@@ -3,9 +3,10 @@ import type { PropsLocale, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots
3
3
  /** Full component props assembled by the Settings slot renderer. */
4
4
  export type DiffApprovalSettingsTabProps = PropsRuntime<'settings.section'> & PropsLocale<'diff-approval'>;
5
5
  /**
6
- * The plugin's preferences: auto-paste a copied reference into the input, and
7
- * whether importing workspace VCS changes includes untracked files. Each row
8
- * mirrors the harness's Agent-preset row (title + description on the left, a
9
- * pill picker on the right) with the two boolean states 打开 / 关闭.
6
+ * The plugin's preferences: auto-paste a copied reference into the input,
7
+ * whether importing workspace VCS changes includes untracked files, and the
8
+ * diff's tab width. Each row mirrors the harness's Agent-preset row (title +
9
+ * description on the left, a pill picker on the right); the tab-width row
10
+ * offers 2 / 4 / 8 spaces.
10
11
  */
11
12
  export declare function DiffApprovalSettingsTab({ t }: DiffApprovalSettingsTabProps): import("react").JSX.Element;
@@ -22,6 +22,8 @@ export declare const zh: {
22
22
  'panel.pasteOnCopyDesc': string;
23
23
  'panel.importUntracked': string;
24
24
  'panel.importUntrackedDesc': string;
25
+ 'panel.tabWidth': string;
26
+ 'panel.tabWidthDesc': string;
25
27
  'settings.tabLabel': string;
26
28
  'row.create': string;
27
29
  'row.failed': string;
@@ -36,11 +38,14 @@ export declare const zh: {
36
38
  'action.busy': string;
37
39
  'action.prevDiff': string;
38
40
  'action.nextDiff': string;
41
+ 'action.showFileList': string;
42
+ 'action.hideFileList': string;
39
43
  'action.copyHint': string;
40
44
  'action.copied': string;
41
45
  'action.langAuto': string;
42
46
  'action.langAutoDetected': string;
43
47
  'action.langSelect': string;
48
+ 'action.wrap': string;
44
49
  'action.toggleOn': string;
45
50
  'action.toggleOff': string;
46
51
  'action.importVcs': string;
@@ -88,6 +93,8 @@ export declare const en: {
88
93
  'panel.pasteOnCopyDesc': string;
89
94
  'panel.importUntracked': string;
90
95
  'panel.importUntrackedDesc': string;
96
+ 'panel.tabWidth': string;
97
+ 'panel.tabWidthDesc': string;
91
98
  'settings.tabLabel': string;
92
99
  'row.create': string;
93
100
  'row.failed': string;
@@ -102,11 +109,14 @@ export declare const en: {
102
109
  'action.busy': string;
103
110
  'action.prevDiff': string;
104
111
  'action.nextDiff': string;
112
+ 'action.showFileList': string;
113
+ 'action.hideFileList': string;
105
114
  'action.copyHint': string;
106
115
  'action.copied': string;
107
116
  'action.langAuto': string;
108
117
  'action.langAutoDetected': string;
109
118
  'action.langSelect': string;
119
+ 'action.wrap': string;
110
120
  'action.toggleOn': string;
111
121
  'action.toggleOff': string;
112
122
  'action.importVcs': string;
@@ -17,3 +17,22 @@ export declare function setPasteOnCopyEnabled(value: boolean): void;
17
17
  export declare function includeUntrackedEnabled(): boolean;
18
18
  /** Persist the import-untracked preference. */
19
19
  export declare function setIncludeUntrackedEnabled(value: boolean): void;
20
+ /**
21
+ * Whether lines wrap (auto-wrap) in the diff for one highlight language.
22
+ * Defaults to off; only an explicit `'1'` enables it. Stored per language, so
23
+ * a language's preference never leaks into another's.
24
+ * @param lang - the highlight language (or `''` for the auto/default bucket).
25
+ * @returns whether lines wrap.
26
+ */
27
+ export declare function wrapEnabled(lang: string): boolean;
28
+ /** Persist the per-language auto-wrap preference. */
29
+ export declare function setWrapEnabled(lang: string, value: boolean): void;
30
+ /**
31
+ * The diff's tab width in spaces. Defaults to 4; the settings UI offers 2/4/8,
32
+ * but any positive integer is accepted. This drives both the rendered
33
+ * `tab-size` and the wrapped-line tab measurement, so they always agree.
34
+ * @returns the number of spaces one tab advances.
35
+ */
36
+ export declare function tabWidth(): number;
37
+ /** Persist the diff's tab width (in spaces). */
38
+ export declare function setTabWidth(value: number): void;
@@ -29,7 +29,10 @@ export interface WholeFileDiff {
29
29
  * Compute the whole-file view between two contents. The context budget is the
30
30
  * side lengths, so every hunk covers the file and unchanged lines survive as
31
31
  * context rows; the `` patch marker is annotation
32
- * and never becomes a row.
32
+ * and never becomes a row. Both sides are line-ending normalized (`\r\n?` →
33
+ * `\n`) first, so a repo baseline stored with one EOL and a worktree with
34
+ * another never show as a whole-file delete+add — the diff is about approved
35
+ * content, not line-ending noise.
33
36
  * @param oldText - the file content before the pending change.
34
37
  * @param newText - the file content after the pending change.
35
38
  * @returns the complete row list with totals.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-diff-approval",
3
- "version": "0.9.0",
3
+ "version": "0.11.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",