dsh-diff-approval 0.10.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/client.js CHANGED
@@ -643,15 +643,21 @@ window.__ModuleLoader__.load({
643
643
  * Compute the whole-file view between two contents. The context budget is the
644
644
  * side lengths, so every hunk covers the file and unchanged lines survive as
645
645
  * context rows; the `` patch marker is annotation
646
- * and never becomes a row.
646
+ * and never becomes a row. Both sides are line-ending normalized (`\r\n?` →
647
+ * `\n`) first, so a repo baseline stored with one EOL and a worktree with
648
+ * another never show as a whole-file delete+add — the diff is about approved
649
+ * content, not line-ending noise.
647
650
  * @param oldText - the file content before the pending change.
648
651
  * @param newText - the file content after the pending change.
649
652
  * @returns the complete row list with totals.
650
653
  */
651
654
  function computeWholeFileDiff(oldText, newText) {
655
+ oldText = oldText.replace(/\r\n?/g, "\n");
656
+ newText = newText.replace(/\r\n?/g, "\n");
652
657
  const oldLines = contentLines(oldText);
653
658
  const newLines = contentLines(newText);
654
- const patch = structuredPatch("", "", oldText, newText, void 0, void 0, { context: Math.max(1, oldLines.length, newLines.length) });
659
+ const context = Math.max(1, oldLines.length, newLines.length);
660
+ const patch = structuredPatch("", "", oldText, newText, void 0, void 0, { context });
655
661
  const rows = [];
656
662
  let removed = 0;
657
663
  let added = 0;
@@ -11692,99 +11698,99 @@ XID_Start XIDS`.split(/\s/).map((p) => [w(p), p]));
11692
11698
  document.head.appendChild(tag);
11693
11699
  }
11694
11700
  var PendingPanel_module_css_default = {
11695
- "badge": "F1KBNa_badge",
11696
- "panel": "F1KBNa_panel",
11697
- "settingsRow": "F1KBNa_settingsRow",
11698
- "searchBar": "F1KBNa_searchBar",
11701
+ "divergedHint": "F1KBNa_divergedHint",
11699
11702
  "detail": "F1KBNa_detail",
11700
- "diffActions": "F1KBNa_diffActions",
11703
+ "fullscreenBackdrop": "F1KBNa_fullscreenBackdrop",
11704
+ "diffBody": "F1KBNa_diffBody",
11705
+ "langSelect": "F1KBNa_langSelect",
11706
+ "note": "F1KBNa_note",
11707
+ "code": "F1KBNa_code",
11708
+ "diffHeader": "F1KBNa_diffHeader",
11709
+ "overviewRuler": "F1KBNa_overviewRuler",
11710
+ "readError": "F1KBNa_readError",
11711
+ "gutter": "F1KBNa_gutter",
11701
11712
  "markerAdd": "F1KBNa_markerAdd",
11713
+ "del": "F1KBNa_del",
11714
+ "fileList": "F1KBNa_fileList",
11715
+ "delCount": "F1KBNa_delCount",
11716
+ "diffBodyWrap": "F1KBNa_diffBodyWrap",
11717
+ "blockFlash": "F1KBNa_blockFlash",
11718
+ "context": "F1KBNa_context",
11719
+ "fileListFloat": "F1KBNa_fileListFloat",
11702
11720
  "diff": "F1KBNa_diff",
11703
- "gutter": "F1KBNa_gutter",
11721
+ "rowMeta": "F1KBNa_rowMeta",
11722
+ "panel": "F1KBNa_panel",
11723
+ "title": "F1KBNa_title",
11724
+ "expand": "F1KBNa_expand",
11725
+ "lines": "F1KBNa_lines",
11704
11726
  "missingHint": "F1KBNa_missingHint",
11727
+ "headerActions": "F1KBNa_headerActions",
11728
+ "settingsRowDesc": "F1KBNa_settingsRowDesc",
11729
+ "close": "F1KBNa_close",
11730
+ "iconAction": "F1KBNa_iconAction",
11731
+ "rail": "F1KBNa_rail",
11705
11732
  "wrap": "F1KBNa_wrap",
11706
- "diffHeader": "F1KBNa_diffHeader",
11707
- "context": "F1KBNa_context",
11733
+ "diffPath": "F1KBNa_diffPath",
11708
11734
  "importNote": "F1KBNa_importNote",
11709
- "divergedHint": "F1KBNa_divergedHint",
11710
- "listScroll": "F1KBNa_listScroll",
11711
- "statusBar": "F1KBNa_statusBar",
11712
- "readError": "F1KBNa_readError",
11713
- "settingsRowDesc": "F1KBNa_settingsRowDesc",
11714
- "settingsRowText": "F1KBNa_settingsRowText",
11735
+ "missing": "F1KBNa_missing",
11736
+ "footerButtons": "F1KBNa_footerButtons",
11737
+ "resizeHandle": "F1KBNa_resizeHandle",
11738
+ "line": "F1KBNa_line",
11739
+ "kindTag": "F1KBNa_kindTag",
11740
+ "bulkActions": "F1KBNa_bulkActions",
11741
+ "vSpacer": "F1KBNa_vSpacer",
11715
11742
  "emptyState": "F1KBNa_emptyState",
11716
- "langLabel": "F1KBNa_langLabel",
11717
- "expandExpanded": "F1KBNa_expandExpanded",
11718
- "noticeText": "F1KBNa_noticeText",
11719
- "addCount": "F1KBNa_addCount",
11720
- "subline": "F1KBNa_subline",
11743
+ "badgeLabel": "F1KBNa_badgeLabel",
11744
+ "rowFailed": "F1KBNa_rowFailed",
11745
+ "layer": "F1KBNa_layer",
11721
11746
  "badgeCount": "F1KBNa_badgeCount",
11722
- "line": "F1KBNa_line",
11747
+ "noteCentered": "F1KBNa_noteCentered",
11748
+ "settingsSelectorChevron": "F1KBNa_settingsSelectorChevron",
11749
+ "importButton": "F1KBNa_importButton",
11723
11750
  "header": "F1KBNa_header",
11724
- "close": "F1KBNa_close",
11725
- "blockActions": "F1KBNa_blockActions",
11751
+ "action": "F1KBNa_action",
11726
11752
  "actionError": "F1KBNa_actionError",
11727
- "rows": "F1KBNa_rows",
11728
- "actionPrimary": "F1KBNa_actionPrimary",
11729
- "searchCount": "F1KBNa_searchCount",
11730
- "wrapActive": "F1KBNa_wrapActive",
11731
- "rail": "F1KBNa_rail",
11732
- "overviewRuler": "F1KBNa_overviewRuler",
11733
- "diffBody": "F1KBNa_diffBody",
11734
- "vSpacer": "F1KBNa_vSpacer",
11735
- "title": "F1KBNa_title",
11736
- "rowMeta": "F1KBNa_rowMeta",
11737
- "diffStats": "F1KBNa_diffStats",
11753
+ "flexSpacer": "F1KBNa_flexSpacer",
11754
+ "settingsRowText": "F1KBNa_settingsRowText",
11755
+ "actionQuietDisabled": "F1KBNa_actionQuietDisabled",
11756
+ "blockActions": "F1KBNa_blockActions",
11757
+ "listScroll": "F1KBNa_listScroll",
11758
+ "hint": "F1KBNa_hint",
11759
+ "kindHint": "F1KBNa_kindHint",
11760
+ "diffActions": "F1KBNa_diffActions",
11761
+ "expandExpanded": "F1KBNa_expandExpanded",
11762
+ "settingsRow": "F1KBNa_settingsRow",
11738
11763
  "blockPosition": "F1KBNa_blockPosition",
11764
+ "statusAction": "F1KBNa_statusAction",
11765
+ "searchBar": "F1KBNa_searchBar",
11739
11766
  "searchInput": "F1KBNa_searchInput",
11740
- "bulkActions": "F1KBNa_bulkActions",
11741
- "noteCentered": "F1KBNa_noteCentered",
11742
- "blockFlash": "F1KBNa_blockFlash",
11743
- "actionQuietDisabled": "F1KBNa_actionQuietDisabled",
11744
- "code": "F1KBNa_code",
11745
- "langSelect": "F1KBNa_langSelect",
11746
- "kindTag": "F1KBNa_kindTag",
11747
- "diffFlash": "F1KBNa_diffFlash",
11748
- "footerButtons": "F1KBNa_footerButtons",
11749
- "missing": "F1KBNa_missing",
11750
- "layer": "F1KBNa_layer",
11751
- "noticeButton": "F1KBNa_noticeButton",
11752
- "fileList": "F1KBNa_fileList",
11753
- "add": "F1KBNa_add",
11754
- "split": "F1KBNa_split",
11767
+ "rows": "F1KBNa_rows",
11768
+ "statusBar": "F1KBNa_statusBar",
11769
+ "settingsPage": "F1KBNa_settingsPage",
11755
11770
  "overviewMarker": "F1KBNa_overviewMarker",
11756
- "resizeHandle": "F1KBNa_resizeHandle",
11757
- "settingsSelector": "F1KBNa_settingsSelector",
11758
- "badgeLabel": "F1KBNa_badgeLabel",
11759
- "fileListFloat": "F1KBNa_fileListFloat",
11760
- "expand": "F1KBNa_expand",
11761
- "diffBodyWrap": "F1KBNa_diffBodyWrap",
11762
- "lines": "F1KBNa_lines",
11763
- "action": "F1KBNa_action",
11764
- "statusAction": "F1KBNa_statusAction",
11771
+ "add": "F1KBNa_add",
11772
+ "langLabel": "F1KBNa_langLabel",
11765
11773
  "notice": "F1KBNa_notice",
11766
- "fullscreenBackdrop": "F1KBNa_fullscreenBackdrop",
11767
- "settingsSelectorChevron": "F1KBNa_settingsSelectorChevron",
11768
- "delCount": "F1KBNa_delCount",
11769
- "flexSpacer": "F1KBNa_flexSpacer",
11770
- "markerDel": "F1KBNa_markerDel",
11771
- "note": "F1KBNa_note",
11772
11774
  "states": "F1KBNa_states",
11773
- "rowFailed": "F1KBNa_rowFailed",
11774
- "settingsPage": "F1KBNa_settingsPage",
11775
- "rowHead": "F1KBNa_rowHead",
11776
- "headerActions": "F1KBNa_headerActions",
11777
- "group": "F1KBNa_group",
11778
- "settingsRowTitle": "F1KBNa_settingsRowTitle",
11779
11775
  "rowPath": "F1KBNa_rowPath",
11776
+ "searchCount": "F1KBNa_searchCount",
11777
+ "diffStats": "F1KBNa_diffStats",
11778
+ "badge": "F1KBNa_badge",
11779
+ "markerDel": "F1KBNa_markerDel",
11780
+ "addCount": "F1KBNa_addCount",
11781
+ "settingsSelector": "F1KBNa_settingsSelector",
11782
+ "diffFlash": "F1KBNa_diffFlash",
11783
+ "subline": "F1KBNa_subline",
11784
+ "split": "F1KBNa_split",
11780
11785
  "detailEmpty": "F1KBNa_detailEmpty",
11781
- "iconAction": "F1KBNa_iconAction",
11782
- "kindHint": "F1KBNa_kindHint",
11783
- "hint": "F1KBNa_hint",
11784
- "importButton": "F1KBNa_importButton",
11785
- "del": "F1KBNa_del",
11786
- "diffPath": "F1KBNa_diffPath",
11787
- "row": "F1KBNa_row"
11786
+ "noticeButton": "F1KBNa_noticeButton",
11787
+ "row": "F1KBNa_row",
11788
+ "wrapActive": "F1KBNa_wrapActive",
11789
+ "settingsRowTitle": "F1KBNa_settingsRowTitle",
11790
+ "group": "F1KBNa_group",
11791
+ "rowHead": "F1KBNa_rowHead",
11792
+ "actionPrimary": "F1KBNa_actionPrimary",
11793
+ "noticeText": "F1KBNa_noticeText"
11788
11794
  };
11789
11795
  //#endregion
11790
11796
  //#region lib/types/client/PendingPanel.js
@@ -14284,10 +14290,64 @@ XID_Start XIDS`.split(/\s/).map((p) => [w(p), p]));
14284
14290
  "sessions"
14285
14291
  ];
14286
14292
  /**
14293
+ * The dsh web-react renderer gives `div[data-slot="sidebar.footer.action"]` an
14294
+ * inline `display: contents`, so every plugin's footer entry root participates
14295
+ * directly in the `.footerActions` flex row — several plugins (e.g. this one
14296
+ * plus a file-browser) get crammed into one row and a full-width badge
14297
+ * overflows. Stack the slot entries vertically instead (the same fix the
14298
+ * dsh-footer-order plugin injects), so each plugin gets its own row.
14299
+ *
14300
+ * We defer to the dedicated dsh-footer-order plugin when it is present, so the
14301
+ * two never fight.
14302
+ *
14303
+ * Why we detect it by its injected stylesheet's `--dsh-footer-order-gap` custom
14304
+ * property rather than by `ctx.slots.entriesOfSlot('settings.plugin.item')`
14305
+ * (where footer-order registers its settings card, id `footer-order`): that slot
14306
+ * key is not in our SlotMap type (would need a cast) and, more importantly, it
14307
+ * may not be an active slot in this dsh version at all — footer-order targets a
14308
+ * newer dsh settings API, so the detection could miss footer-order and we would
14309
+ * then both inject, fighting over the same `!important` rule. The CSS marker is
14310
+ * footer-order's own, and its presence is exactly "footer-order's rule is really
14311
+ * applied", which is what decides the layout. So only when the marker is absent
14312
+ * do we inject our own rule.
14313
+ */
14314
+ function injectFooterStackStyle() {
14315
+ if (typeof document === "undefined") return;
14316
+ if (document.querySelector("style[data-diff-approval-footer-stack]") !== null) return;
14317
+ const CSS = "div[data-slot=\"sidebar.footer.action\"]{display:flex!important;flex-direction:column!important;flex:1 1 auto!important;align-items:stretch!important;}";
14318
+ const footerOrderInstalled = () => {
14319
+ for (const style of document.querySelectorAll("style")) if ((style.textContent ?? "").includes("--dsh-footer-order-gap")) return true;
14320
+ return false;
14321
+ };
14322
+ const inject = () => {
14323
+ if (document.querySelector("style[data-diff-approval-footer-stack]") !== null) return;
14324
+ if (footerOrderInstalled()) return;
14325
+ const style = document.createElement("style");
14326
+ style.setAttribute("data-diff-approval-footer-stack", "");
14327
+ style.textContent = CSS;
14328
+ document.head.appendChild(style);
14329
+ };
14330
+ if (document.querySelector("div[data-slot=\"sidebar.footer.action\"]") !== null) {
14331
+ inject();
14332
+ return;
14333
+ }
14334
+ const observer = new MutationObserver(() => {
14335
+ if (document.querySelector("div[data-slot=\"sidebar.footer.action\"]") !== null) {
14336
+ observer.disconnect();
14337
+ inject();
14338
+ }
14339
+ });
14340
+ observer.observe(document.documentElement, {
14341
+ childList: true,
14342
+ subtree: true
14343
+ });
14344
+ }
14345
+ /**
14287
14346
  * Mount the pending-edit review panel.
14288
14347
  * @param ctx - Client Cordis context carrying the wire and slot services.
14289
14348
  */
14290
14349
  function apply(ctx) {
14350
+ injectFooterStackStyle();
14291
14351
  ctx.effect(() => ctx.locale.register(NS, {
14292
14352
  zh,
14293
14353
  en
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
  }
@@ -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.10.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",