dsh-diff-approval 0.17.0 → 0.19.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
@@ -1,5 +1,5 @@
1
1
  import { mkdir, readFile, readdir, rename, rm, writeFile } from "node:fs/promises";
2
- import { dirname, join, resolve, sep } from "node:path";
2
+ import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
3
3
  import { dshHomePath, expandHomePath } from "@deepseek-ai/dsh-home-paths";
4
4
  import { SessionId } from "@deepseek-ai/dsh-session";
5
5
  import { spawn } from "node:child_process";
@@ -738,6 +738,12 @@ function detectEol(text) {
738
738
  function normalizeEol(text) {
739
739
  return text.replace(/\r\n?/g, "\n");
740
740
  }
741
+ /** Whether two contents are equal ignoring line endings and a trailing-newline
742
+ * difference — the same tolerance the whole-file diff uses, so a file that the
743
+ * diff view shows as "no pending diff" is treated as fully resolved here too. */
744
+ function contentEqual(a, b) {
745
+ return contentLinesOf(normalizeEol(a)).join("\n") === contentLinesOf(normalizeEol(b)).join("\n");
746
+ }
741
747
  /** Re-encode `text`'s line endings to `eol` (its content is unchanged). */
742
748
  function reencodeEol(text, eol) {
743
749
  const normalized = normalizeEol(text);
@@ -813,6 +819,26 @@ function sessionOfAgent(agent) {
813
819
  return typeof id === "string" && id.length > 0 ? SessionId(id) : void 0;
814
820
  }
815
821
  /**
822
+ * The MIME type for an image path by its lowercased extension. An unknown or
823
+ * non-image extension falls back to the generic binary type, which browsers
824
+ * still render when the bytes decode; the common Markdown image formats are
825
+ * covered so a preview inlines as the right content type.
826
+ * @param path - the image's OS path.
827
+ * @returns the MIME type.
828
+ */
829
+ function imageMimeOf(path) {
830
+ const lower = path.toLowerCase();
831
+ if (lower.endsWith(".png")) return "image/png";
832
+ if (lower.endsWith(".jpg") || lower.endsWith(".jpeg")) return "image/jpeg";
833
+ if (lower.endsWith(".gif")) return "image/gif";
834
+ if (lower.endsWith(".webp")) return "image/webp";
835
+ if (lower.endsWith(".svg")) return "image/svg+xml";
836
+ if (lower.endsWith(".avif")) return "image/avif";
837
+ if (lower.endsWith(".bmp")) return "image/bmp";
838
+ if (lower.endsWith(".ico")) return "image/x-icon";
839
+ return "application/octet-stream";
840
+ }
841
+ /**
816
842
  * Build one channel error in the closed RPC error vocabulary. `internal` is
817
843
  * the catch-all: business misses ride the success branch as `outcome: 'missing'`.
818
844
  * @param message - the handler-side description.
@@ -1102,6 +1128,40 @@ function apply(ctx, config) {
1102
1128
  const policy = sandboxPolicyOf(sessionId);
1103
1129
  return policy === void 0 ? ctx.fs.writeText(target, content, void 0, signal) : ctx.fs.writeText(target, content, void 0, signal, policy);
1104
1130
  }
1131
+ /**
1132
+ * Revert one entry's file back to its baseline (the shared per-entry logic
1133
+ * behind both the single `revert` endpoint and the bulk `revert-all`). A
1134
+ * created file's revert deletes it (not undoable: the file is gone), an edit
1135
+ * writes the baseline back and yields the before/after undo snapshot.
1136
+ * @param entry - the entry to revert.
1137
+ * @param sessionId - the entry's session (for the per-session write policy).
1138
+ * @param signal - aborts before atomic publication takes effect.
1139
+ * @returns the undo snapshot, or `undefined` when the revert is not undoable.
1140
+ */
1141
+ async function revertEntryContent(entry, sessionId, signal) {
1142
+ const resolved = await ctx.fs.resolve(entry.path, { signal });
1143
+ if (entry.kind === "create") {
1144
+ await rm(ctx.fs.processPath(resolved), { force: true });
1145
+ return;
1146
+ }
1147
+ const preWrite = await ctx.fs.readText(resolved, void 0) ?? entry.newText;
1148
+ const content = reencodeEol(entry.oldText, detectEol(entry.newText));
1149
+ await writeRevert(resolved, content, sessionId, signal);
1150
+ return {
1151
+ before: {
1152
+ id: entry.id,
1153
+ path: entry.path,
1154
+ entry,
1155
+ fileText: preWrite
1156
+ },
1157
+ after: {
1158
+ id: entry.id,
1159
+ path: entry.path,
1160
+ entry: void 0,
1161
+ fileText: content
1162
+ }
1163
+ };
1164
+ }
1105
1165
  const undoStack = [];
1106
1166
  const redoStack = [];
1107
1167
  function pushUndo(sessionId, before, after) {
@@ -1264,27 +1324,7 @@ function apply(ctx, config) {
1264
1324
  }
1265
1325
  let undo;
1266
1326
  try {
1267
- const resolved = await ctx.fs.resolve(entry.path, { signal });
1268
- if (entry.kind === "create") await rm(ctx.fs.processPath(resolved), { force: true });
1269
- else {
1270
- const preWrite = await ctx.fs.readText(resolved, void 0) ?? entry.newText;
1271
- const content = reencodeEol(entry.oldText, detectEol(entry.newText));
1272
- await writeRevert(resolved, content, target.sessionId, signal);
1273
- undo = {
1274
- before: {
1275
- id: entry.path,
1276
- path: entry.path,
1277
- entry,
1278
- fileText: preWrite
1279
- },
1280
- after: {
1281
- id: entry.path,
1282
- path: entry.path,
1283
- entry: void 0,
1284
- fileText: content
1285
- }
1286
- };
1287
- }
1327
+ undo = await revertEntryContent(entry, target.sessionId, signal);
1288
1328
  } catch (error) {
1289
1329
  return rpcError(`revert failed: ${errorMessage(error)}`);
1290
1330
  }
@@ -1296,6 +1336,86 @@ function apply(ctx, config) {
1296
1336
  value: { outcome: "reverted" }
1297
1337
  };
1298
1338
  }
1339
+ case "keep-all": {
1340
+ const sessionId = sessionOf(payload);
1341
+ if (sessionId === void 0) return rpcError("sessionId must be a non-empty string");
1342
+ await ensureLoaded();
1343
+ const entries = store.list(sessionId);
1344
+ const before = [];
1345
+ const after = [];
1346
+ for (const entry of entries) {
1347
+ before.push({
1348
+ id: entry.id,
1349
+ path: entry.path,
1350
+ entry,
1351
+ fileText: void 0
1352
+ });
1353
+ after.push({
1354
+ id: entry.id,
1355
+ path: entry.path,
1356
+ entry: void 0,
1357
+ fileText: void 0
1358
+ });
1359
+ store.remove(entry.id);
1360
+ }
1361
+ if (before.length > 0) pushUndo(sessionId, {
1362
+ id: before[0].id,
1363
+ path: before[0].path,
1364
+ entry: void 0,
1365
+ fileText: void 0,
1366
+ batch: before
1367
+ }, {
1368
+ id: after[0].id,
1369
+ path: after[0].path,
1370
+ entry: void 0,
1371
+ fileText: void 0,
1372
+ batch: after
1373
+ });
1374
+ persistSession(true);
1375
+ return {
1376
+ ok: true,
1377
+ value: { affected: before.length }
1378
+ };
1379
+ }
1380
+ case "revert-all": {
1381
+ const sessionId = sessionOf(payload);
1382
+ if (sessionId === void 0) return rpcError("sessionId must be a non-empty string");
1383
+ await ensureLoaded();
1384
+ const entries = store.list(sessionId);
1385
+ const batchBefore = [];
1386
+ const batchAfter = [];
1387
+ for (const entry of entries) {
1388
+ let undo;
1389
+ try {
1390
+ undo = await revertEntryContent(entry, sessionId, signal);
1391
+ } catch {
1392
+ return rpcError(`revert-all failed for ${entry.path}`);
1393
+ }
1394
+ if (undo !== void 0) {
1395
+ batchBefore.push(undo.before);
1396
+ batchAfter.push(undo.after);
1397
+ }
1398
+ store.remove(entry.id);
1399
+ }
1400
+ if (batchBefore.length > 0) pushUndo(sessionId, {
1401
+ id: batchBefore[0].id,
1402
+ path: batchBefore[0].path,
1403
+ entry: void 0,
1404
+ fileText: void 0,
1405
+ batch: batchBefore
1406
+ }, {
1407
+ id: batchAfter[0].id,
1408
+ path: batchAfter[0].path,
1409
+ entry: void 0,
1410
+ fileText: void 0,
1411
+ batch: batchAfter
1412
+ });
1413
+ persistSession(true);
1414
+ return {
1415
+ ok: true,
1416
+ value: { affected: entries.length }
1417
+ };
1418
+ }
1299
1419
  case "block-keep": {
1300
1420
  const blockTarget = blockTargetOf(payload);
1301
1421
  if (blockTarget === void 0) return rpcError("sessionId, id, and block must be valid");
@@ -1325,9 +1445,25 @@ function apply(ctx, config) {
1325
1445
  fileText: void 0
1326
1446
  });
1327
1447
  persistSession();
1448
+ const fullyResolved = contentEqual(updatedOld, entry.newText);
1449
+ if (fullyResolved && blockTarget.removeWhenResolved === true) {
1450
+ store.remove(blockTarget.id);
1451
+ pushUndo(blockTarget.sessionId, {
1452
+ id: entry.id,
1453
+ path: entry.path,
1454
+ entry: afterEntry,
1455
+ fileText: void 0
1456
+ }, {
1457
+ id: entry.id,
1458
+ path: entry.path,
1459
+ entry: void 0,
1460
+ fileText: void 0
1461
+ });
1462
+ persistSession(true);
1463
+ }
1328
1464
  return {
1329
1465
  ok: true,
1330
- value: updatedOld === entry.newText ? {
1466
+ value: fullyResolved ? {
1331
1467
  outcome: "kept",
1332
1468
  resolved: true
1333
1469
  } : { outcome: "kept" }
@@ -1378,9 +1514,25 @@ function apply(ctx, config) {
1378
1514
  }
1379
1515
  if (undo !== void 0) pushUndo(blockTarget.sessionId, undo.before, undo.after);
1380
1516
  persistSession();
1517
+ const fullyResolved = contentEqual(updatedNew, entry.oldText);
1518
+ if (fullyResolved && blockTarget.removeWhenResolved === true) {
1519
+ store.remove(blockTarget.id);
1520
+ pushUndo(blockTarget.sessionId, {
1521
+ id: entry.id,
1522
+ path: entry.path,
1523
+ entry: afterEntry,
1524
+ fileText: void 0
1525
+ }, {
1526
+ id: entry.id,
1527
+ path: entry.path,
1528
+ entry: void 0,
1529
+ fileText: void 0
1530
+ });
1531
+ persistSession(true);
1532
+ }
1381
1533
  return {
1382
1534
  ok: true,
1383
- value: normalizeEol(updatedNew) === normalizeEol(entry.oldText) ? {
1535
+ value: fullyResolved ? {
1384
1536
  outcome: "reverted",
1385
1537
  resolved: true
1386
1538
  } : { outcome: "reverted" }
@@ -1549,6 +1701,29 @@ function apply(ctx, config) {
1549
1701
  value: { outcome: "opened" }
1550
1702
  };
1551
1703
  }
1704
+ case "preview-image": {
1705
+ const image = previewImageTargetOf(payload);
1706
+ if (image === void 0) return rpcError("sessionId and path must be valid");
1707
+ await ensureLoaded();
1708
+ const workspace = workspaceOf(image.sessionId);
1709
+ if (workspace === void 0) return rpcError("image unavailable: the session has no workspace");
1710
+ let dataUri;
1711
+ try {
1712
+ const target = await ctx.fs.resolve(image.path, { signal });
1713
+ const workspaceTarget = await ctx.fs.resolve(workspace.path, {});
1714
+ const osPath = ctx.fs.processPath(target);
1715
+ const workspaceOs = ctx.fs.processPath(workspaceTarget);
1716
+ const inside = relative(workspaceOs, osPath);
1717
+ if (inside !== "" && !inside.startsWith("..") && !isAbsolute(inside)) {
1718
+ const bytes = await readFile(osPath);
1719
+ if (bytes.length > 0) dataUri = `data:${imageMimeOf(osPath)};base64,${bytes.toString("base64")}`;
1720
+ }
1721
+ } catch {}
1722
+ return {
1723
+ ok: true,
1724
+ value: { dataUri }
1725
+ };
1726
+ }
1552
1727
  default: return rpcError(`unknown endpoint ${JSON.stringify(endpoint)}`);
1553
1728
  }
1554
1729
  };
@@ -1625,6 +1800,7 @@ function blockTargetOf(payload) {
1625
1800
  newStart,
1626
1801
  newEnd
1627
1802
  ].every((value) => typeof value === "number" && Number.isFinite(value))) return void 0;
1803
+ const removeWhenResolved = payload.removeWhenResolved;
1628
1804
  return {
1629
1805
  ...target,
1630
1806
  block: {
@@ -1632,7 +1808,8 @@ function blockTargetOf(payload) {
1632
1808
  oldEnd,
1633
1809
  newStart,
1634
1810
  newEnd
1635
- }
1811
+ },
1812
+ removeWhenResolved: typeof removeWhenResolved === "boolean" ? removeWhenResolved : void 0
1636
1813
  };
1637
1814
  }
1638
1815
  /** Narrow a wire payload to one keep/revert target. */
@@ -1647,6 +1824,18 @@ function targetOf(payload) {
1647
1824
  id
1648
1825
  };
1649
1826
  }
1827
+ /** Narrow a wire payload to one preview-image target. */
1828
+ function previewImageTargetOf(payload) {
1829
+ const sessionId = sessionOf(payload);
1830
+ if (sessionId === void 0) return void 0;
1831
+ if (typeof payload !== "object" || payload === null || Array.isArray(payload)) return void 0;
1832
+ const path = payload.path;
1833
+ if (typeof path !== "string" || path.length === 0) return void 0;
1834
+ return {
1835
+ sessionId,
1836
+ path
1837
+ };
1838
+ }
1650
1839
  /** Narrow a wire payload to one open target: the keep/revert pair plus the action. */
1651
1840
  function openTargetOf(payload) {
1652
1841
  const target = targetOf(payload);
@@ -0,0 +1,23 @@
1
+ /** A compact HSV color picker (saturation/value square + hue bar + hex/RGB
2
+ * input). Avoids the browser-native color dialog, which cannot be themed, so
3
+ * it matches DSH. Pure helpers are exported for testing. */
4
+ /** Parse `#rrggbb` / `#rgb` / `rrggbb` / `rgb(r,g,b)` into a normalised
5
+ * `#rrggbb`, or `undefined` when the text is not a color. */
6
+ export declare function parseColor(input: string): string | undefined;
7
+ /** Convert a `#rrggbb` hex to HSV (h 0-360, s 0-1, v 0-1). */
8
+ export declare function hexToHsv(hex: string): {
9
+ h: number;
10
+ s: number;
11
+ v: number;
12
+ };
13
+ /** Convert HSV (h 0-360, s 0-1, v 0-1) to a `#rrggbb` hex. */
14
+ export declare function hsvToHex(h: number, s: number, v: number): string;
15
+ /** A draggable HSV picker: saturation/value square + hue bar + a hex/RGB text
16
+ * input. `onChange` reports a normalised `#rrggbb`; `onClose` fires after a
17
+ * committed text value so the caller can close the popover. */
18
+ export declare function ColorPicker({ value, onChange, onClose, ariaLabel, }: {
19
+ value: string;
20
+ onChange: (hex: string) => void;
21
+ onClose?: () => void;
22
+ ariaLabel: string;
23
+ }): import("react").JSX.Element;
@@ -7,6 +7,15 @@ import type { DiffApprovalKey } from './locales.ts';
7
7
  import { computeWholeFileDiff } from './whole-file-diff.ts';
8
8
  import type { IntraRun } from './whole-file-diff.ts';
9
9
  import { highlightLines } from './highlight.ts';
10
+ /** Normalize a path for comparison: forward slashes, no trailing slash. */
11
+ export declare function normalizeDiffPath(p: string): string;
12
+ /** Whether a produced-file chip path and a pending file path refer to the same
13
+ * file, tolerant of separator style (\\ vs /) and of a workspace-relative vs
14
+ * absolute form. `chipPath` is typically the harness's workspace-relative
15
+ * forward-slash path; `filePath` is the host's absolute native-separator path.
16
+ * Matching is case-insensitive so a Windows drive/segment case difference does
17
+ * not miss the file the user clicked. */
18
+ export declare function diffPathsMatch(chipPath: string, filePath: string, workspacePath: string | undefined): boolean;
10
19
  /** The dsh shell's sidebar auto-collapse breakpoint (ui-layout columns.ts):
11
20
  * below it the sidebar auto-collapses, and the file list floats on the same
12
21
  * breakpoint so the two stay consistent. */
@@ -55,7 +64,7 @@ interface RowRange {
55
64
  /** Imperative surface the parent uses to drive block navigation from the
56
65
  * shared toolbar/keyboard in split mode (its own `focus` is private here). */
57
66
  export interface SplitDiffHandle {
58
- jump: (direction: -1 | 1, wrapGuard?: boolean) => void;
67
+ jump: (direction: -1 | 1, wrapGuard?: boolean, singleToast?: boolean) => void;
59
68
  openSearch: () => void;
60
69
  searchNext: (direction: -1 | 1) => boolean;
61
70
  }
@@ -84,5 +93,5 @@ export declare const SplitDiff: import("react").ForwardRefExoticComponent<{
84
93
  */
85
94
  export declare function selectedPlainText(): string | undefined;
86
95
  /** Render the pending-edit review panel and its unified footer action. */
87
- export declare function PendingPanel({ wide, useSessions, usePending, onRefresh, onKeep, onRevert, onBlockKeep, onBlockRevert, onOpen, onPasteReference, onUndo, onRedo, onImportVcs, onAckRedoCleared, onAckJustResolved, collapseSidebar, t, }: PendingPanelProps): import("react").JSX.Element;
96
+ export declare function PendingPanel({ wide, useSessions, usePending, onRefresh, onKeep, onRevert, onBlockKeep, onBlockRevert, onOpen, onPreviewImage, onPasteReference, onUndo, onRedo, onImportVcs, onKeepAll, onRevertAll, onAckRedoCleared, collapseSidebar, t, }: PendingPanelProps): import("react").JSX.Element;
88
97
  export {};
@@ -32,10 +32,26 @@ export declare const zh: {
32
32
  'panel.splitModeDesc': string;
33
33
  'panel.navLeadRows': string;
34
34
  'panel.navLeadRowsDesc': string;
35
+ 'panel.diffFontSize': string;
36
+ 'panel.diffFontSizeDesc': string;
37
+ 'panel.diffLineHeight': string;
38
+ 'panel.diffLineHeightDesc': string;
39
+ 'panel.mdPreview': string;
40
+ 'panel.mdPreviewDesc': string;
41
+ 'panel.mdMaxWidth': string;
42
+ 'panel.mdMaxWidthDesc': string;
43
+ 'panel.diffAddColor': string;
44
+ 'panel.diffAddColorDesc': string;
45
+ 'panel.diffDelColor': string;
46
+ 'panel.diffDelColorDesc': string;
35
47
  'panel.quickSummon': string;
36
48
  'panel.quickSummonDesc': string;
37
49
  'panel.recordShortcut': string;
38
50
  'settings.keybindings': string;
51
+ 'settings.keybindingsDesc': string;
52
+ 'settings.diffView': string;
53
+ 'settings.diffViewDesc': string;
54
+ 'settings.diffPreview': string;
39
55
  'panel.keyDesc': string;
40
56
  'panel.key.jumpUp': string;
41
57
  'panel.key.jumpDown': string;
@@ -67,6 +83,13 @@ export declare const zh: {
67
83
  'action.hideFileList': string;
68
84
  'action.viewSplit': string;
69
85
  'action.viewUnified': string;
86
+ 'action.viewPreview': string;
87
+ 'action.viewSource': string;
88
+ 'action.viewBefore': string;
89
+ 'action.viewAfter': string;
90
+ 'action.viewSingle': string;
91
+ 'action.viewDouble': string;
92
+ 'panel.mdPreviewFailed': string;
70
93
  'action.copyHint': string;
71
94
  'action.copied': string;
72
95
  'action.langAuto': string;
@@ -134,10 +157,26 @@ export declare const en: {
134
157
  'panel.splitModeDesc': string;
135
158
  'panel.navLeadRows': string;
136
159
  'panel.navLeadRowsDesc': string;
160
+ 'panel.diffFontSize': string;
161
+ 'panel.diffFontSizeDesc': string;
162
+ 'panel.diffLineHeight': string;
163
+ 'panel.diffLineHeightDesc': string;
164
+ 'panel.mdPreview': string;
165
+ 'panel.mdPreviewDesc': string;
166
+ 'panel.mdMaxWidth': string;
167
+ 'panel.mdMaxWidthDesc': string;
168
+ 'panel.diffAddColor': string;
169
+ 'panel.diffAddColorDesc': string;
170
+ 'panel.diffDelColor': string;
171
+ 'panel.diffDelColorDesc': string;
137
172
  'panel.quickSummon': string;
138
173
  'panel.quickSummonDesc': string;
139
174
  'panel.recordShortcut': string;
140
175
  'settings.keybindings': string;
176
+ 'settings.keybindingsDesc': string;
177
+ 'settings.diffView': string;
178
+ 'settings.diffViewDesc': string;
179
+ 'settings.diffPreview': string;
141
180
  'panel.keyDesc': string;
142
181
  'panel.key.jumpUp': string;
143
182
  'panel.key.jumpDown': string;
@@ -169,6 +208,13 @@ export declare const en: {
169
208
  'action.hideFileList': string;
170
209
  'action.viewSplit': string;
171
210
  'action.viewUnified': string;
211
+ 'action.viewPreview': string;
212
+ 'action.viewSource': string;
213
+ 'action.viewBefore': string;
214
+ 'action.viewAfter': string;
215
+ 'action.viewSingle': string;
216
+ 'action.viewDouble': string;
217
+ 'panel.mdPreviewFailed': string;
172
218
  'action.copyHint': string;
173
219
  'action.copied': string;
174
220
  'action.langAuto': string;
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Post-render local-image resolver for the Markdown preview. The preview body
3
+ * is rendered with `innerHTML` from the file's Markdown, so a local `<img
4
+ * src="details/foo.png">` stays a relative path that a browser cannot load on its
5
+ * own (the document's base URL is the DSH shell, not the file's folder). This
6
+ * pass rewrites those srcs to inline base64 data URIs through the host RPC,
7
+ * leaving absolute URLs and already-inlined data URIs untouched.
8
+ *
9
+ * Security: only the host reads the file, and the host confines reads to the
10
+ * session's workspace (see the `preview-image` endpoint). This module only
11
+ * computes the path to ask for — it never reads a file itself.
12
+ * @module dsh-diff-approval/client/markdown-images
13
+ */
14
+ /**
15
+ * Resolve every local `<img src>` in a rendered Markdown preview to an inline
16
+ * data URI. Absolute URLs, fragments, and already-inlined data URIs are left as
17
+ * written; a reference the host cannot read keeps its original src.
18
+ * @param container - the preview body element (recently rendered).
19
+ * @param markdownPath - the Markdown file's backend path (for relative references).
20
+ * @param workspacePath - the session's workspace root, when known.
21
+ * @param resolveImage - the host call: maps a backend path to a data URI.
22
+ * @returns resolution once every image has been considered.
23
+ */
24
+ export declare function resolvePreviewImages(container: HTMLElement, markdownPath: string, workspacePath: string | undefined, resolveImage: (path: string) => Promise<string | undefined>): Promise<void>;
@@ -0,0 +1,20 @@
1
+ /** Self-contained Markdown preview-diff renderer.
2
+ *
3
+ * Renders the whole-file text diff at the Markdown level: the source diff is
4
+ * computed first, then each added/removed/context run is rendered through
5
+ * `marked` and presented single-column (merged, like the unified source diff)
6
+ * or double-column (before | after). Every rendered block carries its run's
7
+ * add/remove/context status so it can be tinted with the diff colors.
8
+ */
9
+ /**
10
+ * Render the Markdown preview as sanitized HTML.
11
+ * @param oldText - the file's tracked baseline (rendered as "before").
12
+ * @param newText - the file's current content (rendered as "after").
13
+ * @param mode - 'single' merges add/remove/context (unified-like); 'double'
14
+ * shows the before and after renderings aligned row by row, each change block
15
+ * tinted (deleted on the before side, added on the after side). Comparable
16
+ * edits get word-level highlights on the changed words.
17
+ * @returns sanitized HTML for the preview body. Local images keep their src so
18
+ * a later pass can resolve them to data URIs (see the preview image resolver).
19
+ */
20
+ export declare function renderMarkdownPreview(oldText: string, newText: string, mode: 'single' | 'double'): string;
@@ -5,7 +5,7 @@
5
5
  * @module dsh-diff-approval/client/port
6
6
  */
7
7
  import type { ClientConnectionRpc, SessionId } from '@deepseek-ai/dsh-client-connection/client';
8
- import type { DiffApprovalActionValue, DiffApprovalBlockRange, DiffApprovalListValue, DiffApprovalOpenAction, DiffApprovalOpenValue, VcsImportValue } from '../types.ts';
8
+ import type { DiffApprovalActionValue, DiffApprovalBlockRange, DiffApprovalBulkValue, DiffApprovalListValue, DiffApprovalOpenAction, DiffApprovalOpenValue, DiffApprovalPreviewImageValue, VcsImportValue } from '../types.ts';
9
9
  /** The channel the host half registers and this port calls. */
10
10
  export declare const DIFF_APPROVAL_CHANNEL = "/diff-approval";
11
11
  /** This package's business verbs over the review channel. */
@@ -17,9 +17,9 @@ export interface DiffApprovalPort {
17
17
  /** Revert one operation. */
18
18
  revert(sessionId: SessionId, id: string): Promise<DiffApprovalActionValue>;
19
19
  /** Keep one diff block (accept its change into the tracked baseline). */
20
- blockKeep(sessionId: SessionId, id: string, block: DiffApprovalBlockRange): Promise<DiffApprovalActionValue>;
20
+ blockKeep(sessionId: SessionId, id: string, block: DiffApprovalBlockRange, removeWhenResolved?: boolean): Promise<DiffApprovalActionValue>;
21
21
  /** Revert one diff block (restore its old lines in the file). */
22
- blockRevert(sessionId: SessionId, id: string, block: DiffApprovalBlockRange): Promise<DiffApprovalActionValue>;
22
+ blockRevert(sessionId: SessionId, id: string, block: DiffApprovalBlockRange, removeWhenResolved?: boolean): Promise<DiffApprovalActionValue>;
23
23
  /** Undo the session's last keep/revert (restore the before state). */
24
24
  undo(sessionId: SessionId): Promise<DiffApprovalActionValue>;
25
25
  /** Redo the session's last undone keep/revert (re-apply the after state). */
@@ -28,6 +28,12 @@ export interface DiffApprovalPort {
28
28
  importVcs(sessionId: SessionId, includeUntracked: boolean): Promise<VcsImportValue>;
29
29
  /** Open one file with its default application or reveal it in the folder. */
30
30
  open(sessionId: SessionId, id: string, action: DiffApprovalOpenAction): Promise<DiffApprovalOpenValue>;
31
+ /** Keep every pending entry of one session in a single host call (one batch). */
32
+ keepAll(sessionId: SessionId): Promise<DiffApprovalBulkValue>;
33
+ /** Revert every pending entry of one session in a single host call (one batch). */
34
+ revertAll(sessionId: SessionId): Promise<DiffApprovalBulkValue>;
35
+ /** Read one workspace image and inline it as a base64 data URI (for the Markdown preview). */
36
+ previewImage(sessionId: SessionId, path: string): Promise<DiffApprovalPreviewImageValue>;
31
37
  }
32
38
  /** Build the port over one generic RPC caller.
33
39
  * @param rpc - the connection's channel caller.
@@ -3,6 +3,21 @@
3
3
  export declare const NAV_LEAD_ROWS_DEFAULT = 2;
4
4
  export declare const NAV_LEAD_ROWS_MIN = 0;
5
5
  export declare const NAV_LEAD_ROWS_MAX = 10;
6
+ /** The code block's fixed row height used by the virtual window and jump math.
7
+ * A hardcoded px base; the settings UI offers 10–36 (a low floor would clip the
8
+ * code text, so the range is loose but never degenerate). */
9
+ export declare const DIFF_LINE_HEIGHT_DEFAULT = 22;
10
+ export declare const DIFF_LINE_HEIGHT_MIN = 10;
11
+ export declare const DIFF_LINE_HEIGHT_MAX = 36;
12
+ /** Font-size scale range: a percentage of the current value, stepped by 10. */
13
+ export declare const DIFF_FONT_SCALE_DEFAULT = 100;
14
+ export declare const DIFF_FONT_SCALE_MIN = 50;
15
+ export declare const DIFF_FONT_SCALE_MAX = 200;
16
+ /** Markdown-preview content max width (single column): a comfortable reading
17
+ * width for mainstream 1080p+ displays. The double-column view is 2x this. */
18
+ export declare const MD_MAX_WIDTH_DEFAULT = 800;
19
+ export declare const MD_MAX_WIDTH_MIN = 480;
20
+ export declare const MD_MAX_WIDTH_MAX = 1600;
6
21
  /**
7
22
  * Whether copying a reference should also paste it into the chat input and
8
23
  * focus it. Defaults to on; only an explicit `'0'` disables it.
@@ -40,6 +55,34 @@ export declare function setWrapEnabled(lang: string, value: boolean): void;
40
55
  export declare function tabWidth(): number;
41
56
  /** Persist the diff's tab width (in spaces). */
42
57
  export declare function setTabWidth(value: number): void;
58
+ /**
59
+ * The diff code font size as a percentage of the current (theme) size. Defaults
60
+ * to 100 (the current look); the settings UI steps it by ±10.
61
+ * @returns the font-size scale, as a percentage (e.g. 100, 110, 90).
62
+ */
63
+ export declare function diffFontScale(): number;
64
+ /** Persist the diff's code font-size scale (a percentage). */
65
+ export declare function setDiffFontScale(value: number): void;
66
+ /**
67
+ * The diff code line height in px (the fixed row height the virtual window and
68
+ * jump math are keyed to). Defaults to 22 (the pre-customization value).
69
+ * @returns the line height in px.
70
+ */
71
+ export declare function diffLineHeight(): number;
72
+ /** Persist the diff's code line height (px). */
73
+ export declare function setDiffLineHeight(value: number): void;
74
+ /** The added-line base color the user chose, or `undefined` (theme color). */
75
+ export declare function diffAddColor(): string | undefined;
76
+ /** Persist the added-line base color (a hex like `#22c55e`). */
77
+ export declare function setDiffAddColor(value: string): void;
78
+ /** The removed-line base color the user chose, or `undefined` (theme color). */
79
+ export declare function diffDelColor(): string | undefined;
80
+ /** Persist the removed-line base color (a hex like `#ef4444`). */
81
+ export declare function setDiffDelColor(value: string): void;
82
+ /** The theme's added-line base color, for the settings swatch default. */
83
+ export declare function currentDiffAddColor(): string;
84
+ /** The theme's removed-line base color, for the settings swatch default. */
85
+ export declare function currentDiffDelColor(): string;
43
86
  /**
44
87
  * Whether the whole-file diff view uses the two-column (side-by-side) layout.
45
88
  * Default off (single column): the unified diff. Only an explicit `'1'` enables
@@ -49,6 +92,24 @@ export declare function setTabWidth(value: number): void;
49
92
  export declare function splitMode(): boolean;
50
93
  /** Persist the split-view preference. */
51
94
  export declare function setSplitMode(value: boolean): void;
95
+ /**
96
+ * Whether the rendered Markdown preview is shown by default (for Markdown files).
97
+ * Default off: the source diff is shown unless the panel toggle is used. Only an
98
+ * explicit `'1'` enables the preview default.
99
+ * @returns whether the Markdown preview default is enabled.
100
+ */
101
+ export declare function mdPreviewEnabled(): boolean;
102
+ /** Persist the Markdown-preview default preference. */
103
+ export declare function setMdPreviewEnabled(value: boolean): void;
104
+ /**
105
+ * The Markdown-preview content max width, in pixels (single column). Defaults to
106
+ * 800; the double-column view totals 2x this. An out-of-range or non-integer
107
+ * value falls back to the default.
108
+ * @returns the max width in pixels.
109
+ */
110
+ export declare function mdMaxWidth(): number;
111
+ /** Persist the Markdown-preview content max width. */
112
+ export declare function setMdMaxWidth(value: number): void;
52
113
  /**
53
114
  * How many rows of lead the diff block jump leaves above the jumped-to block,
54
115
  * and how far the anchored navigation scans. Defaults to 2; an out-of-range or
@@ -20,9 +20,6 @@ export interface PendingDiffSnapshot {
20
20
  * superseded the redo history; the panel surfaces it once (deferred if the
21
21
  * panel is closed) via a bottom-right notice. */
22
22
  redoCleared?: boolean;
23
- /** Latched file id whose last block just resolved; the panel prompts once to
24
- * remove-or-keep it, then acknowledges to clear the latch. */
25
- justResolved?: string | undefined;
26
23
  }
27
24
  /** The injected face the panel component receives from the plugin body. */
28
25
  export interface PendingPanelFace {
@@ -37,11 +34,14 @@ export interface PendingPanelFace {
37
34
  /** Revert one operation (restore its prior content, or remove a created file). */
38
35
  onRevert: (sessionId: SessionId, id: string) => Promise<void>;
39
36
  /** Keep one diff block (accept its change into the tracked baseline). */
40
- onBlockKeep: (sessionId: SessionId, id: string, block: DiffApprovalBlockRange) => Promise<void>;
37
+ onBlockKeep: (sessionId: SessionId, id: string, block: DiffApprovalBlockRange, removeWhenResolved?: boolean) => Promise<void>;
41
38
  /** Revert one diff block (restore its old lines in the file). */
42
- onBlockRevert: (sessionId: SessionId, id: string, block: DiffApprovalBlockRange) => Promise<void>;
39
+ onBlockRevert: (sessionId: SessionId, id: string, block: DiffApprovalBlockRange, removeWhenResolved?: boolean) => Promise<void>;
43
40
  /** Open one file with its default application or reveal it in the folder. */
44
41
  onOpen: (sessionId: SessionId, id: string, action: DiffApprovalOpenAction) => Promise<void>;
42
+ /** Inline one workspace image as a base64 data URI for the Markdown preview
43
+ * (empty when the host cannot read it). */
44
+ onPreviewImage: (sessionId: SessionId, path: string) => Promise<string | undefined>;
45
45
  /** Paste a copied reference into the session's chat input and focus it. */
46
46
  onPasteReference: (sessionId: SessionId, reference: string) => void;
47
47
  /** Undo the session's last keep/revert, then refresh the list; resolves to the affected entry id when it is still pending. */
@@ -50,10 +50,12 @@ export interface PendingPanelFace {
50
50
  onRedo: (sessionId: SessionId) => Promise<string | undefined>;
51
51
  /** Import the workspace's local VCS changes as pending entries (detection included). */
52
52
  onImportVcs: (sessionId: SessionId, includeUntracked: boolean) => Promise<VcsImportValue>;
53
+ /** Keep every pending entry of one session in a single host call (bulk). */
54
+ onKeepAll: (sessionId: SessionId) => Promise<void>;
55
+ /** Revert every pending entry of one session in a single host call (bulk). */
56
+ onRevertAll: (sessionId: SessionId) => Promise<void>;
53
57
  /** Acknowledge the redo-cleared notice so it is only surfaced once. */
54
58
  onAckRedoCleared: () => void;
55
- /** Acknowledge the just-resolved prompt so it is only surfaced once. */
56
- onAckJustResolved: () => void;
57
59
  /** Collapse the DSH sidebar (no-op when already collapsed) before the modal
58
60
  * opens or fullscreens, so an expanded sidebar can't overlap the modal. */
59
61
  collapseSidebar: () => void;