dsh-diff-approval 0.18.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,7 +1445,7 @@ function apply(ctx, config) {
1325
1445
  fileText: void 0
1326
1446
  });
1327
1447
  persistSession();
1328
- const fullyResolved = updatedOld === entry.newText;
1448
+ const fullyResolved = contentEqual(updatedOld, entry.newText);
1329
1449
  if (fullyResolved && blockTarget.removeWhenResolved === true) {
1330
1450
  store.remove(blockTarget.id);
1331
1451
  pushUndo(blockTarget.sessionId, {
@@ -1394,7 +1514,7 @@ function apply(ctx, config) {
1394
1514
  }
1395
1515
  if (undo !== void 0) pushUndo(blockTarget.sessionId, undo.before, undo.after);
1396
1516
  persistSession();
1397
- const fullyResolved = normalizeEol(updatedNew) === normalizeEol(entry.oldText);
1517
+ const fullyResolved = contentEqual(updatedNew, entry.oldText);
1398
1518
  if (fullyResolved && blockTarget.removeWhenResolved === true) {
1399
1519
  store.remove(blockTarget.id);
1400
1520
  pushUndo(blockTarget.sessionId, {
@@ -1581,6 +1701,29 @@ function apply(ctx, config) {
1581
1701
  value: { outcome: "opened" }
1582
1702
  };
1583
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
+ }
1584
1727
  default: return rpcError(`unknown endpoint ${JSON.stringify(endpoint)}`);
1585
1728
  }
1586
1729
  };
@@ -1681,6 +1824,18 @@ function targetOf(payload) {
1681
1824
  id
1682
1825
  };
1683
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
+ }
1684
1839
  /** Narrow a wire payload to one open target: the keep/revert pair plus the action. */
1685
1840
  function openTargetOf(payload) {
1686
1841
  const target = targetOf(payload);
@@ -64,7 +64,7 @@ interface RowRange {
64
64
  /** Imperative surface the parent uses to drive block navigation from the
65
65
  * shared toolbar/keyboard in split mode (its own `focus` is private here). */
66
66
  export interface SplitDiffHandle {
67
- jump: (direction: -1 | 1, wrapGuard?: boolean) => void;
67
+ jump: (direction: -1 | 1, wrapGuard?: boolean, singleToast?: boolean) => void;
68
68
  openSearch: () => void;
69
69
  searchNext: (direction: -1 | 1) => boolean;
70
70
  }
@@ -93,5 +93,5 @@ export declare const SplitDiff: import("react").ForwardRefExoticComponent<{
93
93
  */
94
94
  export declare function selectedPlainText(): string | undefined;
95
95
  /** Render the pending-edit review panel and its unified footer action. */
96
- export declare function PendingPanel({ wide, useSessions, usePending, onRefresh, onKeep, onRevert, onBlockKeep, onBlockRevert, onOpen, onPasteReference, onUndo, onRedo, onImportVcs, onAckRedoCleared, 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;
97
97
  export {};
@@ -36,6 +36,10 @@ export declare const zh: {
36
36
  'panel.diffFontSizeDesc': string;
37
37
  'panel.diffLineHeight': string;
38
38
  'panel.diffLineHeightDesc': string;
39
+ 'panel.mdPreview': string;
40
+ 'panel.mdPreviewDesc': string;
41
+ 'panel.mdMaxWidth': string;
42
+ 'panel.mdMaxWidthDesc': string;
39
43
  'panel.diffAddColor': string;
40
44
  'panel.diffAddColorDesc': string;
41
45
  'panel.diffDelColor': string;
@@ -79,6 +83,13 @@ export declare const zh: {
79
83
  'action.hideFileList': string;
80
84
  'action.viewSplit': string;
81
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;
82
93
  'action.copyHint': string;
83
94
  'action.copied': string;
84
95
  'action.langAuto': string;
@@ -150,6 +161,10 @@ export declare const en: {
150
161
  'panel.diffFontSizeDesc': string;
151
162
  'panel.diffLineHeight': string;
152
163
  'panel.diffLineHeightDesc': string;
164
+ 'panel.mdPreview': string;
165
+ 'panel.mdPreviewDesc': string;
166
+ 'panel.mdMaxWidth': string;
167
+ 'panel.mdMaxWidthDesc': string;
153
168
  'panel.diffAddColor': string;
154
169
  'panel.diffAddColorDesc': string;
155
170
  'panel.diffDelColor': string;
@@ -193,6 +208,13 @@ export declare const en: {
193
208
  'action.hideFileList': string;
194
209
  'action.viewSplit': string;
195
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;
196
218
  'action.copyHint': string;
197
219
  'action.copied': string;
198
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. */
@@ -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.
@@ -13,6 +13,11 @@ export declare const DIFF_LINE_HEIGHT_MAX = 36;
13
13
  export declare const DIFF_FONT_SCALE_DEFAULT = 100;
14
14
  export declare const DIFF_FONT_SCALE_MIN = 50;
15
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;
16
21
  /**
17
22
  * Whether copying a reference should also paste it into the chat input and
18
23
  * focus it. Defaults to on; only an explicit `'0'` disables it.
@@ -87,6 +92,24 @@ export declare function currentDiffDelColor(): string;
87
92
  export declare function splitMode(): boolean;
88
93
  /** Persist the split-view preference. */
89
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;
90
113
  /**
91
114
  * How many rows of lead the diff block jump leaves above the jumped-to block,
92
115
  * and how far the anchored navigation scans. Defaults to 2; an out-of-range or
@@ -39,6 +39,9 @@ export interface PendingPanelFace {
39
39
  onBlockRevert: (sessionId: SessionId, id: string, block: DiffApprovalBlockRange, removeWhenResolved?: boolean) => Promise<void>;
40
40
  /** Open one file with its default application or reveal it in the folder. */
41
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>;
42
45
  /** Paste a copied reference into the session's chat input and focus it. */
43
46
  onPasteReference: (sessionId: SessionId, reference: string) => void;
44
47
  /** Undo the session's last keep/revert, then refresh the list; resolves to the affected entry id when it is still pending. */
@@ -47,6 +50,10 @@ export interface PendingPanelFace {
47
50
  onRedo: (sessionId: SessionId) => Promise<string | undefined>;
48
51
  /** Import the workspace's local VCS changes as pending entries (detection included). */
49
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>;
50
57
  /** Acknowledge the redo-cleared notice so it is only surfaced once. */
51
58
  onAckRedoCleared: () => void;
52
59
  /** Collapse the DSH sidebar (no-op when already collapsed) before the modal
@@ -30,6 +30,12 @@ export interface PendingDiffStore extends HostObservable<PendingDiffSnapshot> {
30
30
  importVcs: (sessionId: SessionId, includeUntracked: boolean) => Promise<VcsImportValue>;
31
31
  /** Open one file with its default application or reveal it in the folder. */
32
32
  open: (sessionId: SessionId, id: string, action: DiffApprovalOpenAction) => Promise<void>;
33
+ /** Keep every pending entry of one session in a single host call, then refresh. */
34
+ keepAll: (sessionId: SessionId) => Promise<void>;
35
+ /** Revert every pending entry of one session in a single host call, then refresh. */
36
+ revertAll: (sessionId: SessionId) => Promise<void>;
37
+ /** Inline one workspace image as a base64 data URI (empty when unreadable). */
38
+ previewImage: (sessionId: SessionId, path: string) => Promise<string | undefined>;
33
39
  /** Drop every local fact (used on connection reset). */
34
40
  reset: () => void;
35
41
  /** Acknowledge a redo-cleared notice so the panel surfaces it only once. */
@@ -67,6 +67,21 @@ export interface IntraRun {
67
67
  text: string;
68
68
  kind: 'same' | 'del' | 'add';
69
69
  }
70
+ /**
71
+ * Compute the intra-line runs for one del/add line pair, or `undefined` when
72
+ * the pair should not be annotated: identical lines (no internal change) or
73
+ * lines too dissimilar to be a modification (a rewrite, not an edit).
74
+ * Word-based via the `diff` package's `diffArrays` over `tokenizeLine`'s
75
+ * tokens, so an English edit highlights whole changed words and a CJK edit
76
+ * highlights the individual changed characters.
77
+ * @param oldText - the removed side's line text.
78
+ * @param newText - the added side's line text.
79
+ * @returns the del-side and add-side runs, or `undefined` to skip annotation.
80
+ */
81
+ export declare function intraRunsOf(oldText: string, newText: string): {
82
+ del: IntraRun[];
83
+ add: IntraRun[];
84
+ } | undefined;
70
85
  /** One matched del→add row pair within a change block. */
71
86
  interface BlockPair {
72
87
  delIndex: number;
@@ -105,6 +105,24 @@ export interface VcsImportValue {
105
105
  /** Whether a VCS root was found at all (false when the workspace is not in a git/svn/p4 checkout). */
106
106
  detected: boolean;
107
107
  }
108
+ /** Target of one preview-image read. */
109
+ export interface DiffApprovalPreviewImageTarget {
110
+ sessionId: SessionId;
111
+ /** The backend resolution path of the image (workspace-relative or absolute display path). */
112
+ path: string;
113
+ }
114
+ /** Value returned by the channel's preview-image endpoint. */
115
+ export interface DiffApprovalPreviewImageValue {
116
+ /** The image's base64 data URI (MIME from the file's extension), or `undefined`
117
+ * when the host could not read the file (absent, outside the workspace, or
118
+ * unreadable) — a missing value leaves the image unresolved in the preview. */
119
+ dataUri?: string | undefined;
120
+ }
121
+ /** Value returned by the channel's keep-all/revert-all endpoints. */
122
+ export interface DiffApprovalBulkValue {
123
+ /** How many pending entries were kept/reverted (0 when the session had none). */
124
+ affected: number;
125
+ }
108
126
  /** Value returned by the channel's keep and revert endpoints. */
109
127
  export interface DiffApprovalActionValue {
110
128
  /** What the request did; `missing` means no pending entry existed. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-diff-approval",
3
- "version": "0.18.0",
3
+ "version": "0.19.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",
@@ -60,6 +60,8 @@
60
60
  "@deepseek-ai/dsh-home-paths": "^0.1.0-rc.5",
61
61
  "@shikijs/langs": "^4.4.3",
62
62
  "diff": "^9.0.0",
63
+ "dompurify": "^3.4.15",
64
+ "marked": "^18.0.11",
63
65
  "shiki": "^4.4.3"
64
66
  },
65
67
  "peerDependencies": {