dsh-diff-approval 0.19.2 → 0.19.3

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
@@ -440,6 +440,18 @@ function isPathInside(absolutePath, root) {
440
440
  if (path === base) return true;
441
441
  return path.startsWith(base + sep);
442
442
  }
443
+ /**
444
+ * Whether one changed path belongs to the scan: inside the workspace, and inside
445
+ * the narrowed `scope` when the caller set one.
446
+ * @param absolutePath - the changed file's absolute path.
447
+ * @param workspaceRoot - the session's workspace root.
448
+ * @param scope - one path to restrict the scan to, or undefined for all of it.
449
+ * @returns whether the change is in scope.
450
+ */
451
+ function inScanScope(absolutePath, workspaceRoot, scope) {
452
+ if (!isPathInside(absolutePath, workspaceRoot)) return false;
453
+ return scope === void 0 || isPathInside(absolutePath, scope);
454
+ }
443
455
  /** The VCS marker of one directory, or undefined when it holds none. */
444
456
  function markerOf(directory) {
445
457
  if (existsSync(resolve(directory, ".git"))) return "git";
@@ -511,12 +523,12 @@ function parseGitPorcelainZ(output) {
511
523
  }
512
524
  /** Enumerate the workspace's local changes in a git checkout. */
513
525
  async function gitChanges(input) {
514
- const { root, workspaceRoot, includeUntracked, shell, readText, signal } = input;
526
+ const { root, workspaceRoot, includeUntracked, scope, shell, readText, signal } = input;
515
527
  const stdout = await runShell(shell, "git -c status.renames=false status --porcelain=v1 -z --untracked-files=all", root, signal);
516
528
  const changes = [];
517
529
  for (const { xy, rel } of parseGitPorcelainZ(stdout)) {
518
530
  const absolute = resolve(root, rel);
519
- if (!isPathInside(absolute, workspaceRoot)) continue;
531
+ if (!inScanScope(absolute, workspaceRoot, scope)) continue;
520
532
  if (xy === "??") {
521
533
  if (!includeUntracked) continue;
522
534
  const newText = await readText(absolute) ?? "";
@@ -559,7 +571,7 @@ function xmlUnescape(value) {
559
571
  }
560
572
  /** Enumerate the workspace's local changes in an svn working copy. */
561
573
  async function svnChanges(input) {
562
- const { root, workspaceRoot, includeUntracked, shell, readText, signal } = input;
574
+ const { root, workspaceRoot, includeUntracked, scope, shell, readText, signal } = input;
563
575
  const stdout = await runShell(shell, "svn status --xml", root, signal);
564
576
  const changes = [];
565
577
  const entryPattern = /<entry[^>]*path="([^"]*)"[^>]*>\s*<wc-status[^>]*item="([^"]*)"/g;
@@ -568,7 +580,7 @@ async function svnChanges(input) {
568
580
  const rel = xmlUnescape(match[1]);
569
581
  const item = match[2];
570
582
  const absolute = resolve(root, rel);
571
- if (!isPathInside(absolute, workspaceRoot)) continue;
583
+ if (!inScanScope(absolute, workspaceRoot, scope)) continue;
572
584
  if (item === "modified" || item === "deleted") {
573
585
  let oldText = "";
574
586
  try {
@@ -623,7 +635,7 @@ function p4ChangeOf(line) {
623
635
  * not yet opened for add; off keeps to already-opened files (`p4 opened`) so
624
636
  * the scan — which can be slow — is skipped. */
625
637
  async function p4Changes(input) {
626
- const { root, workspaceRoot, includeUntracked, shell, readText, signal } = input;
638
+ const { root, workspaceRoot, includeUntracked, scope, shell, readText, signal } = input;
627
639
  const stdout = await runShell(shell, includeUntracked ? "p4 status" : "p4 opened", root, signal);
628
640
  const changes = [];
629
641
  for (const line of stdout.split("\n")) {
@@ -632,7 +644,7 @@ async function p4Changes(input) {
632
644
  const local = (await runShell(shell, `p4 where ${shq(opened.depot)}`, root, signal)).trim().split(/\s+/).pop();
633
645
  if (local === void 0 || local.length === 0) continue;
634
646
  const absolute = resolve(local);
635
- if (!isPathInside(absolute, workspaceRoot)) continue;
647
+ if (!inScanScope(absolute, workspaceRoot, scope)) continue;
636
648
  const deleted = opened.action === "delete" || opened.action === "move/delete";
637
649
  const created = opened.action === "add" || opened.action === "move/add";
638
650
  const newText = deleted ? "" : await readText(absolute) ?? "";
@@ -1298,6 +1310,33 @@ function apply(ctx, config) {
1298
1310
  value: { outcome: "missing" }
1299
1311
  };
1300
1312
  }
1313
+ if (payload.keepListed === true) {
1314
+ store.update(target.id, { oldText: entry.newText });
1315
+ const afterEntry = {
1316
+ ...entry,
1317
+ oldText: entry.newText,
1318
+ updatedAt: Date.now()
1319
+ };
1320
+ pushUndo(target.sessionId, {
1321
+ id: entry.path,
1322
+ path: entry.path,
1323
+ entry,
1324
+ fileText: void 0
1325
+ }, {
1326
+ id: entry.path,
1327
+ path: entry.path,
1328
+ entry: afterEntry,
1329
+ fileText: void 0
1330
+ });
1331
+ persistSession(true);
1332
+ return {
1333
+ ok: true,
1334
+ value: {
1335
+ outcome: "kept",
1336
+ resolved: true
1337
+ }
1338
+ };
1339
+ }
1301
1340
  store.remove(target.id);
1302
1341
  pushUndo(target.sessionId, {
1303
1342
  id: entry.path,
@@ -1334,6 +1373,34 @@ function apply(ctx, config) {
1334
1373
  } catch (error) {
1335
1374
  return rpcError(`revert failed: ${errorMessage(error)}`);
1336
1375
  }
1376
+ if (payload.keepListed === true) {
1377
+ const content = undo?.after.fileText ?? "";
1378
+ store.update(target.id, { newText: content });
1379
+ const afterEntry = {
1380
+ ...entry,
1381
+ newText: content,
1382
+ updatedAt: Date.now()
1383
+ };
1384
+ if (undo !== void 0) pushUndo(target.sessionId, {
1385
+ id: entry.path,
1386
+ path: entry.path,
1387
+ entry,
1388
+ fileText: undo.before.fileText
1389
+ }, {
1390
+ id: entry.path,
1391
+ path: entry.path,
1392
+ entry: afterEntry,
1393
+ fileText: content
1394
+ });
1395
+ persistSession(true);
1396
+ return {
1397
+ ok: true,
1398
+ value: {
1399
+ outcome: "reverted",
1400
+ resolved: true
1401
+ }
1402
+ };
1403
+ }
1337
1404
  store.remove(target.id);
1338
1405
  if (undo !== void 0) pushUndo(target.sessionId, undo.before, undo.after);
1339
1406
  persistSession(true);
@@ -1687,6 +1754,74 @@ function apply(ctx, config) {
1687
1754
  }
1688
1755
  };
1689
1756
  }
1757
+ case "vcs-refresh": {
1758
+ const target = targetOf(payload);
1759
+ if (target === void 0) return rpcError("sessionId and id must be non-empty strings");
1760
+ await ensureLoaded();
1761
+ const entry = store.get(target.id);
1762
+ if (entry === void 0) return {
1763
+ ok: true,
1764
+ value: { outcome: "missing" }
1765
+ };
1766
+ const workspace = workspaceOf(target.sessionId);
1767
+ if (workspace === void 0) return rpcError("refresh unavailable: the session has no workspace");
1768
+ const root = detectVcsRoot(workspace.path);
1769
+ if (root === void 0) return {
1770
+ ok: true,
1771
+ value: { outcome: "no-vcs" }
1772
+ };
1773
+ const shell = ctx.get("shell");
1774
+ if (shell === void 0) return rpcError("refresh unavailable: the deployment has no shell executor");
1775
+ let changes;
1776
+ try {
1777
+ changes = await listVcsChanges({
1778
+ kind: root.kind,
1779
+ root: root.root,
1780
+ workspaceRoot: workspace.path,
1781
+ includeUntracked: payload.includeUntracked === true,
1782
+ scope: entry.path,
1783
+ shell,
1784
+ readText: (path) => readFile(path, "utf8").catch(() => void 0),
1785
+ signal
1786
+ });
1787
+ } catch (error) {
1788
+ return rpcError(`refresh failed: ${errorMessage(error)}`);
1789
+ }
1790
+ const folded = (value) => process.platform === "win32" ? resolve(value).toLowerCase() : resolve(value);
1791
+ const change = changes.find((candidate) => folded(candidate.path) === folded(entry.path));
1792
+ if (change === void 0) return {
1793
+ ok: true,
1794
+ value: { outcome: "no-change" }
1795
+ };
1796
+ if (change.kind === entry.kind && change.oldText === entry.oldText && change.newText === entry.newText) return {
1797
+ ok: true,
1798
+ value: { outcome: "unchanged" }
1799
+ };
1800
+ const refreshed = {
1801
+ ...entry,
1802
+ kind: change.kind,
1803
+ oldText: change.oldText,
1804
+ newText: change.newText,
1805
+ updatedAt: Date.now()
1806
+ };
1807
+ store.restore(refreshed);
1808
+ pushUndo(target.sessionId, {
1809
+ id: entry.path,
1810
+ path: entry.path,
1811
+ entry,
1812
+ fileText: void 0
1813
+ }, {
1814
+ id: entry.path,
1815
+ path: entry.path,
1816
+ entry: refreshed,
1817
+ fileText: void 0
1818
+ });
1819
+ persistSession(true);
1820
+ return {
1821
+ ok: true,
1822
+ value: { outcome: "refreshed" }
1823
+ };
1824
+ }
1690
1825
  case "open": {
1691
1826
  const target = openTargetOf(payload);
1692
1827
  if (target === void 0) return rpcError("sessionId, id, and action must be valid");
@@ -66,7 +66,11 @@ interface RowRange {
66
66
  export interface SplitDiffHandle {
67
67
  jump: (direction: -1 | 1, wrapGuard?: boolean, singleToast?: boolean) => void;
68
68
  openSearch: () => void;
69
+ toggleSearch: () => void;
70
+ closeSearch: () => boolean;
69
71
  searchNext: (direction: -1 | 1) => boolean;
72
+ toggleMatchCase: () => boolean;
73
+ toggleMatchWholeWord: () => boolean;
70
74
  }
71
75
  /** The two-column (side-by-side) whole-file diff view. */
72
76
  export declare const SplitDiff: import("react").ForwardRefExoticComponent<{
@@ -93,5 +97,5 @@ export declare const SplitDiff: import("react").ForwardRefExoticComponent<{
93
97
  */
94
98
  export declare function selectedPlainText(): string | undefined;
95
99
  /** 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, onPreviewImage, onPasteReference, onUndo, onRedo, onImportVcs, onKeepAll, onRevertAll, onAckRedoCleared, collapseSidebar, t, }: PendingPanelProps): import("react").JSX.Element;
100
+ export declare function PendingPanel({ wide, useSessions, usePending, onRefresh, onKeep, onRevert, onBlockKeep, onBlockRevert, onOpen, onPreviewImage, onPasteReference, onUndo, onRedo, onImportVcs, onRefreshVcs, onKeepAll, onRevertAll, onAckRedoCleared, collapseSidebar, t, }: PendingPanelProps): import("react").JSX.Element;
97
101
  export {};
@@ -59,6 +59,8 @@ export declare const zh: {
59
59
  'panel.key.openSearch': string;
60
60
  'panel.key.searchNext': string;
61
61
  'panel.key.searchPrev': string;
62
+ 'panel.key.matchCase': string;
63
+ 'panel.key.matchWholeWord': string;
62
64
  'panel.key.undo': string;
63
65
  'panel.key.redo': string;
64
66
  'panel.key.cycleNext': string;
@@ -116,6 +118,18 @@ export declare const zh: {
116
118
  'status.missing': string;
117
119
  'panel.resolvedAsk': string;
118
120
  'panel.keepInList': string;
121
+ 'panel.fileKeptAsk': string;
122
+ 'panel.fileRevertedAsk': string;
123
+ 'panel.confirmFileRemove': string;
124
+ 'panel.confirmFileRemoveDesc': string;
125
+ 'action.refreshVcs': string;
126
+ 'action.matchCase': string;
127
+ 'action.matchWholeWord': string;
128
+ 'panel.refreshDone': string;
129
+ 'panel.refreshUnchanged': string;
130
+ 'panel.refreshNone': string;
131
+ 'panel.refreshUntrackedHint': string;
132
+ 'panel.refreshFailed': string;
119
133
  };
120
134
  /** Translation keys owned by the pending-edit review namespace. */
121
135
  export type DiffApprovalKey = keyof typeof zh;
@@ -184,6 +198,8 @@ export declare const en: {
184
198
  'panel.key.openSearch': string;
185
199
  'panel.key.searchNext': string;
186
200
  'panel.key.searchPrev': string;
201
+ 'panel.key.matchCase': string;
202
+ 'panel.key.matchWholeWord': string;
187
203
  'panel.key.undo': string;
188
204
  'panel.key.redo': string;
189
205
  'panel.key.cycleNext': string;
@@ -241,4 +257,16 @@ export declare const en: {
241
257
  'status.missing': string;
242
258
  'panel.resolvedAsk': string;
243
259
  'panel.keepInList': string;
260
+ 'panel.fileKeptAsk': string;
261
+ 'panel.fileRevertedAsk': string;
262
+ 'panel.confirmFileRemove': string;
263
+ 'panel.confirmFileRemoveDesc': string;
264
+ 'action.refreshVcs': string;
265
+ 'action.matchCase': string;
266
+ 'action.matchWholeWord': string;
267
+ 'panel.refreshDone': string;
268
+ 'panel.refreshUnchanged': string;
269
+ 'panel.refreshNone': string;
270
+ 'panel.refreshUntrackedHint': string;
271
+ 'panel.refreshFailed': string;
244
272
  };
@@ -5,17 +5,17 @@
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, DiffApprovalBulkValue, DiffApprovalListValue, DiffApprovalOpenAction, DiffApprovalOpenValue, DiffApprovalPreviewImageValue, VcsImportValue } from '../types.ts';
8
+ import type { DiffApprovalActionValue, DiffApprovalBlockRange, DiffApprovalBulkValue, DiffApprovalListValue, DiffApprovalOpenAction, DiffApprovalOpenValue, DiffApprovalPreviewImageValue, DiffApprovalRefreshValue, 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. */
12
12
  export interface DiffApprovalPort {
13
13
  /** Read one session's pending entries (plus its workspace root), oldest capture first. */
14
14
  list(sessionId: SessionId): Promise<DiffApprovalListValue>;
15
- /** Keep one operation. */
16
- keep(sessionId: SessionId, id: string): Promise<DiffApprovalActionValue>;
17
- /** Revert one operation. */
18
- revert(sessionId: SessionId, id: string): Promise<DiffApprovalActionValue>;
15
+ /** Keep one operation. `keepListed` leaves the resolved entry in the list. */
16
+ keep(sessionId: SessionId, id: string, keepListed?: boolean): Promise<DiffApprovalActionValue>;
17
+ /** Revert one operation. `keepListed` leaves the resolved entry in the list. */
18
+ revert(sessionId: SessionId, id: string, keepListed?: boolean): Promise<DiffApprovalActionValue>;
19
19
  /** Keep one diff block (accept its change into the tracked baseline). */
20
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). */
@@ -26,6 +26,8 @@ export interface DiffApprovalPort {
26
26
  redo(sessionId: SessionId): Promise<DiffApprovalActionValue>;
27
27
  /** Import the workspace's local VCS changes as pending entries. */
28
28
  importVcs(sessionId: SessionId, includeUntracked: boolean): Promise<VcsImportValue>;
29
+ /** Replace one entry's diff with the file's current local VCS change. */
30
+ refreshVcs(sessionId: SessionId, id: string, includeUntracked: boolean): Promise<DiffApprovalRefreshValue>;
29
31
  /** Open one file with its default application or reveal it in the folder. */
30
32
  open(sessionId: SessionId, id: string, action: DiffApprovalOpenAction): Promise<DiffApprovalOpenValue>;
31
33
  /** Keep every pending entry of one session in a single host call (one batch). */
@@ -0,0 +1,23 @@
1
+ /**
2
+ * The in-file search matcher, shared by the unified diff and the split view so
3
+ * both highlight and count the same occurrences.
4
+ * @module dsh-diff-approval/client/search
5
+ */
6
+ /** Which occurrences an in-file search accepts. */
7
+ export interface SearchOptions {
8
+ /** Match the query's letter case exactly. */
9
+ caseSensitive: boolean;
10
+ /** Accept only whole words (no identifier character touching either end). */
11
+ wholeWord: boolean;
12
+ }
13
+ /** The plain search: case-insensitive, substring. */
14
+ export declare const DEFAULT_SEARCH_OPTIONS: SearchOptions;
15
+ /**
16
+ * Character ranges of every occurrence of `query` in `text` that the options
17
+ * accept. Case-insensitive and substring-based unless narrowed.
18
+ * @param text - the line to search.
19
+ * @param query - the search text; empty matches nothing.
20
+ * @param options - case and whole-word narrowing.
21
+ * @returns `[start, end)` ranges in ascending order.
22
+ */
23
+ export declare function matchRangesOf(text: string, query: string, options?: SearchOptions): [number, number][];
@@ -36,6 +36,31 @@ export declare function setPasteOnCopyEnabled(value: boolean): void;
36
36
  export declare function includeUntrackedEnabled(): boolean;
37
37
  /** Persist the import-untracked preference. */
38
38
  export declare function setIncludeUntrackedEnabled(value: boolean): void;
39
+ /**
40
+ * Whether a whole-file keep/revert asks before dropping the resolved file from
41
+ * the list. Defaults to on; only an explicit `'0'` disables it, which then
42
+ * removes the file straight away.
43
+ * @returns whether the remove prompt is enabled.
44
+ */
45
+ export declare function confirmFileRemoveEnabled(): boolean;
46
+ /** Persist the whole-file remove-prompt preference. */
47
+ export declare function setConfirmFileRemoveEnabled(value: boolean): void;
48
+ /**
49
+ * Whether the in-file search matches letter case exactly. Defaults to off, so a
50
+ * plain query keeps matching either case; only an explicit `'1'` turns it on.
51
+ * @returns whether the search is case-sensitive.
52
+ */
53
+ export declare function searchCaseSensitive(): boolean;
54
+ /** Persist the search case-sensitivity preference. */
55
+ export declare function setSearchCaseSensitive(value: boolean): void;
56
+ /**
57
+ * Whether the in-file search only matches whole words. Defaults to off; only an
58
+ * explicit `'1'` enables it.
59
+ * @returns whether the search matches whole words only.
60
+ */
61
+ export declare function searchWholeWord(): boolean;
62
+ /** Persist the search whole-word preference. */
63
+ export declare function setSearchWholeWord(value: boolean): void;
39
64
  /**
40
65
  * Whether lines wrap (auto-wrap) in the diff for one highlight language.
41
66
  * Defaults to off; only an explicit `'1'` enables it. Stored per language, so
@@ -1,7 +1,7 @@
1
1
  /** The panel's injected business face and its observable snapshot. */
2
2
  import type { HostObservable } from '@deepseek-ai/dsh-client-ui-slots';
3
3
  import type { SessionId } from '@deepseek-ai/dsh-client-connection/client';
4
- import type { DiffApprovalBlockRange, DiffApprovalOpenAction, PendingFileDiff, VcsImportValue } from '../types.ts';
4
+ import type { DiffApprovalBlockRange, DiffApprovalOpenAction, DiffApprovalRefreshValue, PendingFileDiff, VcsImportValue } from '../types.ts';
5
5
  /** What the panel reads and drives: the pending list plus in-flight entries. */
6
6
  export interface PendingDiffSnapshot {
7
7
  /** Whether a list read has completed at least once. */
@@ -29,10 +29,11 @@ export interface PendingPanelFace {
29
29
  };
30
30
  /** Read the pending list for the current session into the snapshot. */
31
31
  onRefresh: (sessionId: SessionId | undefined) => void;
32
- /** Keep one operation (remove it from the pending list). */
33
- onKeep: (sessionId: SessionId, id: string) => Promise<void>;
34
- /** Revert one operation (restore its prior content, or remove a created file). */
35
- onRevert: (sessionId: SessionId, id: string) => Promise<void>;
32
+ /** Keep one operation. `keepListed` leaves the resolved entry in the list. */
33
+ onKeep: (sessionId: SessionId, id: string, keepListed?: boolean) => Promise<void>;
34
+ /** Revert one operation (restore its prior content, or remove a created file).
35
+ * `keepListed` leaves the resolved entry in the list. */
36
+ onRevert: (sessionId: SessionId, id: string, keepListed?: boolean) => Promise<void>;
36
37
  /** Keep one diff block (accept its change into the tracked baseline). */
37
38
  onBlockKeep: (sessionId: SessionId, id: string, block: DiffApprovalBlockRange, removeWhenResolved?: boolean) => Promise<void>;
38
39
  /** Revert one diff block (restore its old lines in the file). */
@@ -50,6 +51,9 @@ export interface PendingPanelFace {
50
51
  onRedo: (sessionId: SessionId) => Promise<string | undefined>;
51
52
  /** Import the workspace's local VCS changes as pending entries (detection included). */
52
53
  onImportVcs: (sessionId: SessionId, includeUntracked: boolean) => Promise<VcsImportValue>;
54
+ /** Replace one entry's diff with the file's current local VCS change, then
55
+ * refresh the list; resolves to what the scan found. */
56
+ onRefreshVcs: (sessionId: SessionId, id: string, includeUntracked: boolean) => Promise<DiffApprovalRefreshValue>;
53
57
  /** Keep every pending entry of one session in a single host call (bulk). */
54
58
  onKeepAll: (sessionId: SessionId) => Promise<void>;
55
59
  /** Revert every pending entry of one session in a single host call (bulk). */
@@ -8,6 +8,7 @@
8
8
  * @module dsh-diff-approval/client/split-diff
9
9
  */
10
10
  import type { WholeFileDiffRow } from './whole-file-diff.ts';
11
+ import type { SearchOptions } from './search.ts';
11
12
  /** One side of a split pair: text plus its 1-based line number (absent on a pure add). */
12
13
  export interface SplitSide {
13
14
  /** The side's source line content, without the terminating newline. */
@@ -43,12 +44,13 @@ export interface SplitDiff {
43
44
  */
44
45
  export declare function computeSideBySideDiff(rows: readonly WholeFileDiffRow[], alignBySimilarity?: boolean): SplitDiff;
45
46
  /**
46
- * Pair indices whose left or right text contains the query (case-insensitive).
47
- * A pair counts once however many times the query appears, so split search
48
- * highlights the whole pair on both columns and a single "current" pair is
49
- * stepped through — not each individual left/right occurrence.
47
+ * Pair indices whose left or right text matches the query. A pair counts once
48
+ * however many times the query appears, so split search highlights the whole pair
49
+ * on both columns and a single "current" pair is stepped through — not each
50
+ * individual left/right occurrence.
50
51
  * @param pairs - the split pairs.
51
52
  * @param query - the query text; an empty query matches nothing.
53
+ * @param options - case and whole-word narrowing.
52
54
  * @returns matching pair indices, in file order.
53
55
  */
54
- export declare function searchPairs(pairs: readonly SplitPair[], query: string): number[];
56
+ export declare function searchPairs(pairs: readonly SplitPair[], query: string, options?: SearchOptions): number[];
@@ -7,17 +7,17 @@
7
7
  */
8
8
  import type { SessionId } from '@deepseek-ai/dsh-client-connection/client';
9
9
  import type { HostObservable } from '@deepseek-ai/dsh-client-ui-slots';
10
- import type { DiffApprovalBlockRange, DiffApprovalOpenAction, VcsImportValue } from '../types.ts';
10
+ import type { DiffApprovalBlockRange, DiffApprovalOpenAction, DiffApprovalRefreshValue, VcsImportValue } from '../types.ts';
11
11
  import type { PendingDiffSnapshot } from './slots.ts';
12
12
  import type { DiffApprovalPort } from './port.ts';
13
13
  /** The observable the panel reads and the plugin body drives. */
14
14
  export interface PendingDiffStore extends HostObservable<PendingDiffSnapshot> {
15
15
  /** Re-read one session's pending list (an absent session empties the view). */
16
16
  refresh: (sessionId: SessionId | undefined) => Promise<void>;
17
- /** Keep one operation. */
18
- keep: (sessionId: SessionId, id: string) => Promise<void>;
19
- /** Revert one operation. */
20
- revert: (sessionId: SessionId, id: string) => Promise<void>;
17
+ /** Keep one operation. `keepListed` leaves the resolved entry in the list. */
18
+ keep: (sessionId: SessionId, id: string, keepListed?: boolean) => Promise<void>;
19
+ /** Revert one operation. `keepListed` leaves the resolved entry in the list. */
20
+ revert: (sessionId: SessionId, id: string, keepListed?: boolean) => Promise<void>;
21
21
  /** Keep one diff block, then refresh so the entry's diff reflects the accept. */
22
22
  blockKeep: (sessionId: SessionId, id: string, block: DiffApprovalBlockRange, removeWhenResolved?: boolean) => Promise<void>;
23
23
  /** Revert one diff block, then refresh so the entry's diff reflects the undo. */
@@ -28,6 +28,9 @@ export interface PendingDiffStore extends HostObservable<PendingDiffSnapshot> {
28
28
  redo: (sessionId: SessionId) => Promise<string | undefined>;
29
29
  /** Import the workspace's local VCS changes as pending entries, then refresh. */
30
30
  importVcs: (sessionId: SessionId, includeUntracked: boolean) => Promise<VcsImportValue>;
31
+ /** Replace one entry's diff with the file's current local VCS change, then
32
+ * refresh; resolves to what the scan found. */
33
+ refreshVcs: (sessionId: SessionId, id: string, includeUntracked: boolean) => Promise<DiffApprovalRefreshValue>;
31
34
  /** Open one file with its default application or reveal it in the folder. */
32
35
  open: (sessionId: SessionId, id: string, action: DiffApprovalOpenAction) => Promise<void>;
33
36
  /** Keep every pending entry of one session in a single host call, then refresh. */
@@ -35,7 +35,7 @@
35
35
  */
36
36
  import { Context } from '@deepseek-ai/cordis';
37
37
  import type { DiffApprovalOpenAction } from './types.ts';
38
- export type { DiffApprovalActionOutcome, DiffApprovalActionValue, DiffApprovalBlockRange, DiffApprovalBlockTarget, DiffApprovalListValue, DiffApprovalOpenAction, DiffApprovalOpenValue, PendingEntry, PendingEntryKind, PendingFileDiff, } from './types.ts';
38
+ export type { DiffApprovalActionOutcome, DiffApprovalActionValue, DiffApprovalBlockRange, DiffApprovalBlockTarget, DiffApprovalListValue, DiffApprovalOpenAction, DiffApprovalOpenValue, DiffApprovalRefreshOutcome, DiffApprovalRefreshValue, PendingEntry, PendingEntryKind, PendingFileDiff, } from './types.ts';
39
39
  export { PendingDiffStore } from './pending.ts';
40
40
  export { PendingPersistence, defaultStorageDir } from './persist.ts';
41
41
  export { defaultOpenPath } from './open.ts';
@@ -123,6 +123,22 @@ export interface DiffApprovalBulkValue {
123
123
  /** How many pending entries were kept/reverted (0 when the session had none). */
124
124
  affected: number;
125
125
  }
126
+ /** What one file's VCS refresh found. */
127
+ export type DiffApprovalRefreshOutcome =
128
+ /** The tracked diff was replaced with the file's current VCS change. */
129
+ 'refreshed'
130
+ /** A VCS change exists but matches what the entry already tracks. */
131
+ | 'unchanged'
132
+ /** The file has no local VCS change (untracked-and-excluded, or already clean). */
133
+ | 'no-change'
134
+ /** No pending entry existed for the id. */
135
+ | 'missing'
136
+ /** The workspace is not inside a git/svn/p4 checkout. */
137
+ | 'no-vcs';
138
+ /** Value returned by the channel's vcs-refresh endpoint. */
139
+ export interface DiffApprovalRefreshValue {
140
+ outcome: DiffApprovalRefreshOutcome;
141
+ }
126
142
  /** Value returned by the channel's keep and revert endpoints. */
127
143
  export interface DiffApprovalActionValue {
128
144
  /** What the request did; `missing` means no pending entry existed. */
@@ -68,6 +68,12 @@ export interface VcsImportInput {
68
68
  workspaceRoot: string;
69
69
  /** Whether new/untracked files are imported (git `??`, svn `?`). */
70
70
  includeUntracked: boolean;
71
+ /**
72
+ * Restrict the scan to one absolute path (a file, or a directory's subtree).
73
+ * Absent scans the whole workspace, which is what an import does; the review
74
+ * panel's per-file refresh passes the file so a large tree is not rescanned.
75
+ */
76
+ scope?: string | undefined;
71
77
  shell: ShellExecutorLike;
72
78
  /** Reads working-file content (the host passes a node fs reader). */
73
79
  readText: VcsFileReader;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-diff-approval",
3
- "version": "0.19.2",
3
+ "version": "0.19.3",
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",